From 2361246f0bca75a3aff3880cd4ac827a2fe064f2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 14:23:35 +0200 Subject: [PATCH] LMAO Autocompletion Bar LMFAO --- src/api | 2 +- .../client/gui/util/GuiScriptTextArea.java | 116 +++- .../script/autocomplete/AutocompleteItem.java | 442 ++++++++++++++ .../autocomplete/AutocompleteManager.java | 561 ++++++++++++++++++ .../script/autocomplete/AutocompleteMenu.java | 488 +++++++++++++++ .../autocomplete/AutocompleteProvider.java | 63 ++ .../autocomplete/JSAutocompleteProvider.java | 288 +++++++++ .../JavaAutocompleteProvider.java | 287 +++++++++ .../script/interpreter/ScriptDocument.java | 47 +- .../interpreter/js_parser/JSTypeRegistry.java | 20 + .../client/key/impl/ScriptEditorKeys.java | 3 + 11 files changed, 2310 insertions(+), 7 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java diff --git a/src/api b/src/api index d003d6d87..305a3778c 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit d003d6d870db13d56fce223050a06faf15050eae +Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 55ab59f97..32aac7680 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -20,6 +20,7 @@ import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.hover.GutterIconRenderer; import noppes.npcs.client.gui.util.script.interpreter.hover.HoverState; import noppes.npcs.client.gui.util.script.interpreter.hover.TokenHoverRenderer; +import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteManager; import noppes.npcs.client.key.impl.ScriptEditorKeys; import noppes.npcs.util.ValueUtil; import org.lwjgl.input.Keyboard; @@ -120,6 +121,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== RENAME REFACTOR ==================== private final RenameRefactorHandler renameHandler = new RenameRefactorHandler(); + // ==================== AUTOCOMPLETE ==================== + private final AutocompleteManager autocompleteManager = new AutocompleteManager(); + // ==================== CONSTRUCTOR ==================== public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { @@ -461,6 +465,56 @@ public class GuiScriptTextArea extends GuiNpcTextField { return width - LINE_NUMBER_GUTTER_WIDTH - 8; // Account for gutter and scrollbar } }); + + // Initialize Autocomplete Manager with callback + autocompleteManager.setInsertCallback(new AutocompleteManager.InsertCallback() { + @Override + public void insertText(String text, int startPosition) { + // Replace text from startPosition to current cursor + int cursorPos = selection.getCursorPosition(); + String before = GuiScriptTextArea.this.text.substring(0, startPosition); + String after = GuiScriptTextArea.this.text.substring(cursorPos); + setText(before + text + after); + selection.reset(startPosition + text.length()); + scrollToCursor(); + } + + @Override + public int getCursorPosition() { + return selection.getCursorPosition(); + } + + @Override + public String getText() { + return GuiScriptTextArea.this.text; + } + + @Override + public int[] getCursorScreenPosition() { + // Calculate screen position of cursor for menu placement + int cursorLine = getCursorLineIndex(); + int cursorCol = 0; + if (container != null && container.lines != null && cursorLine < container.lines.size()) { + LineData ld = container.lines.get(cursorLine); + String lineText = ld.text; + int cursorOffset = selection.getCursorPosition() - ld.start; + cursorCol = ClientProxy.Font.width(lineText.substring(0, Math.min(cursorOffset, lineText.length()))); + } + + int screenX = GuiScriptTextArea.this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + cursorCol; + int lineY = cursorLine - scroll.getScrolledLine(); + int screenY = GuiScriptTextArea.this.y + lineY * (container != null ? container.lineHeight : 12); + + return new int[] { screenX, screenY }; + } + + @Override + public int[] getViewportDimensions() { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + return new int[] { sr.getScaledWidth(), sr.getScaledHeight() }; + } + }); } public boolean fullscreen() { @@ -507,9 +561,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); if (listener instanceof GuiNPCInterface) { ((GuiNPCInterface) listener).mouseScroll = wheelDelta; - boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; - if (wheelDelta != 0 && canScroll) - scroll.applyWheelScroll(wheelDelta, maxScroll); + + // Let autocomplete menu consume scroll first if visible + if (wheelDelta != 0 && autocompleteManager.isVisible() && autocompleteManager.mouseScrolled(xMouse, yMouse, wheelDelta)) { + // Autocomplete consumed the scroll + } else { + boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; + if (wheelDelta != 0 && canScroll) + scroll.applyWheelScroll(wheelDelta, maxScroll); + } } // Handle scrollbar dragging (delegated to ScrollState) @@ -901,6 +961,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw go to line dialog (overlays everything) goToLineDialog.draw(xMouse, yMouse); + + // Draw autocomplete menu (overlays code area) + autocompleteManager.draw(xMouse, yMouse); + KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); // Draw hover tooltips (on top of everything) @@ -1322,6 +1386,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { renameHandler.startRename(); } }); + + // AUTOCOMPLETE: Trigger autocomplete (Ctrl+Space) + KEYS.AUTOCOMPLETE.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + autocompleteManager.triggerExplicit(); + }); } public void unfocusAll() { @@ -1329,6 +1401,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (goToLineDialog.hasFocus()) goToLineDialog.unfocus(); if (renameHandler.isActive()) renameHandler.cancel(); + if (autocompleteManager.isVisible()) + autocompleteManager.dismiss(); } // ==================== KEYBOARD INPUT HANDLING ==================== @@ -1352,6 +1426,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Handle search bar input first if it has focus if (searchBar.isVisible() && searchBar.keyTyped(c, i)) return true; + + // Handle autocomplete navigation keys first when visible + if (autocompleteManager.isVisible()) { + if (autocompleteManager.keyPressed(i)) { + return true; + } + } if (!active) return false; @@ -1743,6 +1824,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(s + getSelectionAfterText()); selection.reset(selection.getStartSelection()); scrollToCursor(); + // Notify autocomplete of deletion + autocompleteManager.onDeleteKey(text, selection.getCursorPosition()); return true; } @@ -1897,6 +1980,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Move caret forward by one (skip over existing closer) selection.reset(before.length() + 1); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } @@ -1906,30 +1991,40 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(before + "\"\"" + after); selection.reset(before.length() + 1); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } if (c == '\'') { setText(before + "''" + after); selection.reset(before.length() + 1); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } if (c == '[') { setText(before + "[]" + after); selection.reset(before.length() + 1); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } if (c == '(') { setText(before + "()" + after); selection.reset(before.length() + 1); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } // Default insertion for printable characters: insert at caret (replacing selection) addText(Character.toString(c)); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } return false; @@ -1958,7 +2053,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } public boolean closeOnEsc(){ - return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); + return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive() && !autocompleteManager.isVisible(); } // ==================== KEYBOARD MODIFIERS ==================== @@ -2154,6 +2249,16 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== MOUSE HANDLING ==================== public void mouseClicked(int xMouse, int yMouse, int mouseButton) { + // Check autocomplete menu clicks first + if (autocompleteManager.isVisible() && autocompleteManager.mouseClicked(xMouse, yMouse, mouseButton)) { + return; + } + + // Dismiss autocomplete if clicking elsewhere + if (autocompleteManager.isVisible()) { + autocompleteManager.dismiss(); + } + // Check go to line dialog clicks first if (goToLineDialog.isVisible() && goToLineDialog.mouseClicked(xMouse, yMouse, mouseButton)) { return; @@ -2311,6 +2416,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Consider text changes user activity to pause caret blinking briefly selection.markActivity(); searchBar.updateMatches(); + + // Update autocomplete manager with current container + autocompleteManager.setContainer(this.container); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java new file mode 100644 index 000000000..79140da21 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -0,0 +1,442 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +/** + * Represents a single autocomplete suggestion. + * Unified representation for both Java and JavaScript completions. + */ +public class AutocompleteItem implements Comparable { + + /** + * The kind of completion item. + */ + public enum Kind { + METHOD(0), // Method or function + FIELD(1), // Field or property + VARIABLE(2), // Local variable or parameter + CLASS(3), // Class or interface type + ENUM(4), // Enum type + ENUM_CONSTANT(5), // Enum constant value + KEYWORD(6), // Language keyword + SNIPPET(7); // Code snippet + + private final int priority; + + Kind(int priority) { + this.priority = priority; + } + + public int getPriority() { + return priority; + } + } + + private final String name; // Display name (e.g., "getPlayer") + private final String insertText; // Text to insert (e.g., "getPlayer()") + private final Kind kind; // Type of completion + private final String typeLabel; // Type label (e.g., "IPlayer", "void") + private final String signature; // Full signature for methods + private final String documentation; // Documentation/description + private final Object sourceData; // Original source (MethodInfo, FieldInfo, etc.) + private final boolean deprecated; // Whether this item is deprecated + + // Match scoring + private int matchScore = 0; // How well this matches the query + private int[] matchIndices; // Indices of matched characters for highlighting + + private AutocompleteItem(String name, String insertText, Kind kind, String typeLabel, + String signature, String documentation, Object sourceData, boolean deprecated) { + this.name = name; + this.insertText = insertText; + this.kind = kind; + this.typeLabel = typeLabel; + this.signature = signature; + this.documentation = documentation; + this.sourceData = sourceData; + this.deprecated = deprecated; + } + + // ==================== FACTORY METHODS ==================== + + /** + * Create from a Java MethodInfo. + */ + public static AutocompleteItem fromMethod(MethodInfo method) { + String name = method.getName(); + StringBuilder insertText = new StringBuilder(name); + insertText.append("("); + + // Add placeholders for parameters if any + if (method.getParameterCount() > 0) { + // Just add () - user will fill in params + } + insertText.append(")"); + + String returnType = method.getReturnType() != null ? + method.getReturnType().getSimpleName() : "void"; + + String signature = buildMethodSignature(method); + + return new AutocompleteItem( + name, + insertText.toString(), + Kind.METHOD, + returnType, + signature, + method.getDocumentation(), + method, + false // TODO: Check for @Deprecated annotation + ); + } + + /** + * Create from a Java FieldInfo. + */ + public static AutocompleteItem fromField(FieldInfo field) { + String typeLabel = field.getTypeInfo() != null ? + field.getTypeInfo().getSimpleName() : "?"; + + Kind kind; + switch (field.getScope()) { + case PARAMETER: + kind = Kind.VARIABLE; + break; + case LOCAL: + kind = Kind.VARIABLE; + break; + case ENUM_CONSTANT: + kind = Kind.ENUM_CONSTANT; + break; + default: + kind = Kind.FIELD; + } + + return new AutocompleteItem( + field.getName(), + field.getName(), + kind, + typeLabel, + typeLabel + " " + field.getName(), + null, + field, + false + ); + } + + /** + * Create from a Java TypeInfo. + */ + public static AutocompleteItem fromType(TypeInfo type) { + Kind kind; + switch (type.getKind()) { + case INTERFACE: + kind = Kind.CLASS; + break; + case ENUM: + kind = Kind.ENUM; + break; + default: + kind = Kind.CLASS; + } + + return new AutocompleteItem( + type.getSimpleName(), + type.getSimpleName(), + kind, + type.getPackageName(), + type.getFullName(), + null, + type, + false + ); + } + + /** + * Create from a JavaScript JSMethodInfo. + */ + public static AutocompleteItem fromJSMethod(JSMethodInfo method) { + String name = method.getName(); + StringBuilder insertText = new StringBuilder(name); + insertText.append("("); + insertText.append(")"); + + return new AutocompleteItem( + name, + insertText.toString(), + Kind.METHOD, + method.getReturnType(), + method.getSignature(), + method.getDocumentation(), + method, + false + ); + } + + /** + * Create from a JavaScript JSFieldInfo. + */ + public static AutocompleteItem fromJSField(JSFieldInfo field) { + return new AutocompleteItem( + field.getName(), + field.getName(), + Kind.FIELD, + field.getType(), + field.toString(), + field.getDocumentation(), + field, + false + ); + } + + /** + * Create a keyword item. + */ + public static AutocompleteItem keyword(String keyword) { + return new AutocompleteItem( + keyword, + keyword, + Kind.KEYWORD, + "keyword", + null, + null, + null, + false + ); + } + + // ==================== HELPER METHODS ==================== + + private static String buildMethodSignature(MethodInfo method) { + StringBuilder sb = new StringBuilder(); + String returnType = method.getReturnType() != null ? + method.getReturnType().getSimpleName() : "void"; + sb.append(returnType).append(" ").append(method.getName()).append("("); + + for (int i = 0; i < method.getParameterCount(); i++) { + if (i > 0) sb.append(", "); + FieldInfo param = method.getParameters().get(i); + String paramType = param.getTypeInfo() != null ? + param.getTypeInfo().getSimpleName() : "?"; + sb.append(paramType).append(" ").append(param.getName()); + } + + sb.append(")"); + return sb.toString(); + } + + // ==================== MATCHING ==================== + + /** + * Calculate fuzzy match score against a query string. + * Returns -1 if no match, or a positive score (higher = better match). + */ + public int calculateMatchScore(String query) { + if (query == null || query.isEmpty()) { + matchScore = 100; // Everything matches empty query + matchIndices = new int[0]; + return matchScore; + } + + String lowerName = name.toLowerCase(); + String lowerQuery = query.toLowerCase(); + + // Exact prefix match is best + if (lowerName.startsWith(lowerQuery)) { + matchScore = 1000 - query.length(); // Shorter prefixes score higher + matchIndices = new int[query.length()]; + for (int i = 0; i < query.length(); i++) { + matchIndices[i] = i; + } + return matchScore; + } + + // Exact substring match + int subIndex = lowerName.indexOf(lowerQuery); + if (subIndex >= 0) { + matchScore = 500 - subIndex; // Earlier substrings score higher + matchIndices = new int[query.length()]; + for (int i = 0; i < query.length(); i++) { + matchIndices[i] = subIndex + i; + } + return matchScore; + } + + // Fuzzy match - characters must appear in order + int[] indices = new int[query.length()]; + int nameIdx = 0; + int queryIdx = 0; + int gaps = 0; + int consecutiveBonus = 0; + int lastMatchIdx = -2; + + while (queryIdx < query.length() && nameIdx < name.length()) { + if (Character.toLowerCase(name.charAt(nameIdx)) == lowerQuery.charAt(queryIdx)) { + indices[queryIdx] = nameIdx; + + // Bonus for consecutive matches + if (nameIdx == lastMatchIdx + 1) { + consecutiveBonus += 10; + } + + // Bonus for matching at word boundaries (camelCase) + if (nameIdx == 0 || !Character.isLetterOrDigit(name.charAt(nameIdx - 1)) || + (Character.isUpperCase(name.charAt(nameIdx)) && nameIdx > 0 && + Character.isLowerCase(name.charAt(nameIdx - 1)))) { + consecutiveBonus += 20; + } + + lastMatchIdx = nameIdx; + queryIdx++; + } else { + gaps++; + } + nameIdx++; + } + + if (queryIdx < query.length()) { + // Didn't match all characters + matchScore = -1; + matchIndices = null; + return -1; + } + + matchScore = 100 + consecutiveBonus - gaps; + matchIndices = indices; + return matchScore; + } + + // ==================== GETTERS ==================== + + public String getName() { return name; } + public String getInsertText() { return insertText; } + public Kind getKind() { return kind; } + public String getTypeLabel() { return typeLabel; } + public String getSignature() { return signature; } + public String getDocumentation() { return documentation; } + public Object getSourceData() { return sourceData; } + public boolean isDeprecated() { return deprecated; } + public int getMatchScore() { return matchScore; } + public int[] getMatchIndices() { return matchIndices; } + + /** + * Get icon identifier based on kind. + */ + public String getIconId() { + switch (kind) { + case METHOD: return "m"; + case FIELD: return "f"; + case VARIABLE: return "v"; + case CLASS: return "C"; + case ENUM: return "E"; + case ENUM_CONSTANT: return "e"; + case KEYWORD: return "k"; + case SNIPPET: return "s"; + default: return "?"; + } + } + + /** + * Get color for the kind icon. + */ + public int getIconColor() { + switch (kind) { + case METHOD: return 0xFFB877DB; // Purple for methods + case FIELD: return 0xFF79C0FF; // Blue for fields + case VARIABLE: return 0xFF7EE787; // Green for variables + case CLASS: return 0xFFFFA657; // Orange for classes + case ENUM: return 0xFFFFD866; // Yellow for enums + case ENUM_CONSTANT: return 0xFFFFD866; + case KEYWORD: return 0xFFFF7B72; // Red for keywords + case SNIPPET: return 0xFFCCCCCC; // Gray for snippets + default: return 0xFFCCCCCC; + } + } + + @Override + public int compareTo(AutocompleteItem other) { + // First by match score (descending) + if (this.matchScore != other.matchScore) { + return other.matchScore - this.matchScore; + } + // Then by kind priority + if (this.kind.getPriority() != other.kind.getPriority()) { + return this.kind.getPriority() - other.kind.getPriority(); + } + // Finally alphabetically + return this.name.compareToIgnoreCase(other.name); + } + + @Override + public String toString() { + return name + " (" + kind + ")"; + } + + // ==================== BUILDER ==================== + + /** + * Builder for creating AutocompleteItem instances without source data. + */ + public static class Builder { + private String name; + private String insertText; + private Kind kind = Kind.FIELD; + private String typeLabel = ""; + private String signature; + private String documentation; + private Object sourceData; + private boolean deprecated = false; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder insertText(String insertText) { + this.insertText = insertText; + return this; + } + + public Builder kind(Kind kind) { + this.kind = kind; + return this; + } + + public Builder typeLabel(String typeLabel) { + this.typeLabel = typeLabel; + return this; + } + + public Builder signature(String signature) { + this.signature = signature; + return this; + } + + public Builder documentation(String documentation) { + this.documentation = documentation; + return this; + } + + public Builder sourceData(Object sourceData) { + this.sourceData = sourceData; + return this; + } + + public Builder deprecated(boolean deprecated) { + this.deprecated = deprecated; + return this; + } + + public AutocompleteItem build() { + if (insertText == null) { + insertText = name; + } + return new AutocompleteItem(name, insertText, kind, typeLabel, + signature, documentation, sourceData, deprecated); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java new file mode 100644 index 000000000..0af1a3024 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -0,0 +1,561 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Manages autocomplete state and coordinates between providers and UI. + * This is the main entry point for autocomplete functionality. + */ +public class AutocompleteManager { + + // ==================== CONSTANTS ==================== + + /** Characters that trigger autocomplete */ + private static final String TRIGGER_CHARS = "."; + + /** Characters that should close autocomplete */ + private static final String CLOSE_CHARS = ";{}()[]<>,\"'`"; + + /** Minimum characters before showing suggestions (for non-dot triggers) */ + private static final int MIN_PREFIX_LENGTH = 1; + + /** Pattern for identifier characters */ + private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("[a-zA-Z_$][a-zA-Z0-9_$]*"); + + // ==================== STATE ==================== + + private final AutocompleteMenu menu; + private final JavaAutocompleteProvider javaProvider; + private final JSAutocompleteProvider jsProvider; + + private ScriptTextContainer container; + private ScriptDocument document; + + /** Whether autocomplete is currently active */ + private boolean active = false; + + /** The prefix being typed */ + private String currentPrefix = ""; + + /** Start position of the current prefix */ + private int prefixStartPosition = -1; + + /** Whether this was an explicit trigger (Ctrl+Space) */ + private boolean explicitTrigger = false; + + /** Callback for text insertion */ + private InsertCallback insertCallback; + + /** + * Callback interface for inserting autocomplete results. + */ + public interface InsertCallback { + /** + * Insert text, replacing from start to current cursor position. + * @param text Text to insert + * @param startPosition Position to start replacing from + */ + void insertText(String text, int startPosition); + + /** + * Get current cursor position. + */ + int getCursorPosition(); + + /** + * Get current document text. + */ + String getText(); + + /** + * Get cursor screen coordinates for menu positioning. + */ + int[] getCursorScreenPosition(); + + /** + * Get viewport dimensions. + */ + int[] getViewportDimensions(); + } + + // ==================== CONSTRUCTOR ==================== + + public AutocompleteManager() { + this.menu = new AutocompleteMenu(); + this.javaProvider = new JavaAutocompleteProvider(); + this.jsProvider = new JSAutocompleteProvider(); + + // Set up menu callback + menu.setCallback(new AutocompleteMenu.AutocompleteCallback() { + @Override + public void onItemSelected(AutocompleteItem item) { + handleItemSelected(item); + } + + @Override + public void onDismiss() { + active = false; + } + }); + } + + // ==================== CONFIGURATION ==================== + + /** + * Set the script container for type resolution. + */ + public void setContainer(ScriptTextContainer container) { + this.container = container; + if (container != null) { + this.document = container.getDocument(); + javaProvider.setDocument(document); + jsProvider.setDocument(document); + } + } + + /** + * Set the callback for text insertion. + */ + public void setInsertCallback(InsertCallback callback) { + this.insertCallback = callback; + } + + // ==================== TRIGGER LOGIC ==================== + + /** + * Called when a character is typed. + * Determines whether to show, update, or hide autocomplete. + */ + public void onCharTyped(char c, String text, int cursorPosition) { + // Check if we should close autocomplete + if (CLOSE_CHARS.indexOf(c) >= 0) { + dismiss(); + return; + } + + // Check if this is a trigger character + if (TRIGGER_CHARS.indexOf(c) >= 0) { + // Trigger after dot + triggerAfterDot(text, cursorPosition); + return; + } + + // Check if we're typing an identifier + if (Character.isJavaIdentifierPart(c)) { + if (active) { + // Update existing autocomplete + updatePrefix(text, cursorPosition); + } else if (Character.isJavaIdentifierStart(c)) { + // Potentially start new autocomplete + maybeStartAutocomplete(text, cursorPosition, false); + } + return; + } + + // Any other character closes autocomplete + if (active && !Character.isWhitespace(c)) { + dismiss(); + } + } + + /** + * Called when backspace/delete is pressed. + */ + public void onDeleteKey(String text, int cursorPosition) { + if (active) { + updatePrefix(text, cursorPosition); + + // Close if prefix is now empty and not after dot + if (currentPrefix.isEmpty() && !isAfterDot(text, cursorPosition)) { + dismiss(); + } + } + } + + /** + * Explicitly trigger autocomplete (Ctrl+Space). + */ + public void triggerExplicit() { + if (insertCallback == null) return; + + String text = insertCallback.getText(); + int cursorPosition = insertCallback.getCursorPosition(); + + explicitTrigger = true; + + // Check if after dot + if (isAfterDot(text, cursorPosition)) { + triggerAfterDot(text, cursorPosition); + } else { + maybeStartAutocomplete(text, cursorPosition, true); + } + } + + /** + * Trigger autocomplete after a dot is typed. + */ + private void triggerAfterDot(String text, int cursorPosition) { + // Find the receiver expression before the dot + int dotPos = cursorPosition - 1; + if (dotPos < 0 || text.charAt(dotPos) != '.') { + dotPos = text.lastIndexOf('.', cursorPosition - 1); + } + + if (dotPos < 0) return; + + String receiverExpr = findReceiverExpression(text, dotPos); + String prefix = findCurrentWord(text, cursorPosition); + + prefixStartPosition = dotPos + 1; + currentPrefix = prefix; + + showSuggestions(text, cursorPosition, prefix, prefixStartPosition, true, receiverExpr); + } + + /** + * Maybe start autocomplete based on current context. + */ + private void maybeStartAutocomplete(String text, int cursorPosition, boolean force) { + String prefix = findCurrentWord(text, cursorPosition); + int prefixStart = cursorPosition - prefix.length(); + + // Check minimum prefix length (unless forced) + if (!force && prefix.length() < MIN_PREFIX_LENGTH) { + return; + } + + // Don't auto-trigger if we're in the middle of a word + if (!force && prefixStart > 0 && Character.isJavaIdentifierPart(text.charAt(prefixStart - 1))) { + return; + } + + prefixStartPosition = prefixStart; + currentPrefix = prefix; + + showSuggestions(text, cursorPosition, prefix, prefixStart, false, null); + } + + /** + * Update the prefix and refresh suggestions. + */ + private void updatePrefix(String text, int cursorPosition) { + // Re-calculate prefix from the original start position + if (prefixStartPosition < 0 || prefixStartPosition > cursorPosition) { + dismiss(); + return; + } + + String newPrefix = text.substring(prefixStartPosition, cursorPosition); + + // Validate prefix (should be valid identifier or empty) + if (!newPrefix.isEmpty() && !isValidPrefix(newPrefix)) { + dismiss(); + return; + } + + currentPrefix = newPrefix; + + // Check if after dot + boolean isMemberAccess = prefixStartPosition > 0 && + text.charAt(prefixStartPosition - 1) == '.'; + + String receiverExpr = null; + if (isMemberAccess) { + receiverExpr = findReceiverExpression(text, prefixStartPosition - 1); + } + + showSuggestions(text, cursorPosition, currentPrefix, prefixStartPosition, + isMemberAccess, receiverExpr); + } + + // ==================== SUGGESTION LOGIC ==================== + + /** + * Show autocomplete suggestions. + */ + private void showSuggestions(String text, int cursorPosition, String prefix, + int prefixStart, boolean isMemberAccess, String receiverExpr) { + if (insertCallback == null || document == null) return; + + // Build context + int lineNumber = getLineNumber(text, cursorPosition); + String currentLine = getCurrentLine(text, cursorPosition); + int columnPosition = getColumnPosition(text, cursorPosition); + + AutocompleteProvider.Context context = new AutocompleteProvider.Context( + text, cursorPosition, lineNumber, columnPosition, currentLine, + prefix, prefixStart, isMemberAccess, receiverExpr, explicitTrigger + ); + + // Get suggestions from appropriate provider + AutocompleteProvider provider = document.isJavaScript() ? jsProvider : javaProvider; + List suggestions = provider.getSuggestions(context); + + // Show menu + if (suggestions.isEmpty()) { + if (explicitTrigger) { + // Show "No suggestions" message + suggestions.add(new AutocompleteItem.Builder() + .name("No suggestions") + .kind(AutocompleteItem.Kind.SNIPPET) + .typeLabel("") + .build()); + } else { + dismiss(); + return; + } + } + + // Get screen position for menu + int[] screenPos = insertCallback.getCursorScreenPosition(); + int[] viewport = insertCallback.getViewportDimensions(); + + menu.show(screenPos[0], screenPos[1] + 15, suggestions, viewport[0], viewport[1]); + active = true; + explicitTrigger = false; + } + + // ==================== KEY HANDLING ==================== + + /** + * Handle key press. + * @return true if the key was consumed + */ + public boolean keyPressed(int keyCode) { + if (!active || !menu.isVisible()) return false; + + switch (keyCode) { + case 200: // UP + menu.selectPrevious(); + return true; + + case 208: // DOWN + menu.selectNext(); + return true; + + case 28: // ENTER + case 15: // TAB + if (menu.hasItems()) { + menu.confirmSelection(); + return true; + } + return false; + + case 1: // ESCAPE + dismiss(); + return true; + + default: + return false; + } + } + + /** + * Handle mouse click. + * @return true if the click was consumed + */ + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (!active) return false; + return menu.mouseClicked(mouseX, mouseY, button); + } + + /** + * Handle mouse scroll. + * @return true if the scroll was consumed + */ + public boolean mouseScrolled(int mouseX, int mouseY, int delta) { + if (!active) return false; + return menu.mouseScrolled(mouseX, mouseY, delta); + } + + // ==================== ITEM SELECTION ==================== + + /** + * Handle when an item is selected from the menu. + */ + private void handleItemSelected(AutocompleteItem item) { + if (insertCallback == null || item == null) return; + + // Don't insert "No suggestions" placeholder + if (item.getName().equals("No suggestions")) { + return; + } + + String insertText = item.getInsertText(); + insertCallback.insertText(insertText, prefixStartPosition); + + active = false; + } + + // ==================== DISMISS ==================== + + /** + * Dismiss autocomplete. + */ + public void dismiss() { + if (active) { + menu.hide(); + active = false; + currentPrefix = ""; + prefixStartPosition = -1; + explicitTrigger = false; + } + } + + // ==================== DRAWING ==================== + + /** + * Draw the autocomplete menu. + */ + public void draw(int mouseX, int mouseY) { + if (active) { + menu.draw(mouseX, mouseY); + } + } + + // ==================== STATE QUERIES ==================== + + public boolean isActive() { + return active; + } + + public boolean isVisible() { + return active && menu.isVisible(); + } + + public AutocompleteMenu getMenu() { + return menu; + } + + // ==================== HELPER METHODS ==================== + + /** + * Find the word being typed at the cursor position. + */ + private String findCurrentWord(String text, int cursorPos) { + if (text == null || cursorPos <= 0) return ""; + + int start = cursorPos; + while (start > 0 && Character.isJavaIdentifierPart(text.charAt(start - 1))) { + start--; + } + + return text.substring(start, cursorPos); + } + + /** + * Find the receiver expression before a dot. + */ + private String findReceiverExpression(String text, int dotPos) { + if (text == null || dotPos <= 0) return ""; + + int end = dotPos; + int start = end; + int parenDepth = 0; + int bracketDepth = 0; + + // Walk backwards to find the start of the expression + while (start > 0) { + char c = text.charAt(start - 1); + + if (c == ')') { + parenDepth++; + start--; + } else if (c == '(') { + if (parenDepth > 0) { + parenDepth--; + start--; + } else { + break; + } + } else if (c == ']') { + bracketDepth++; + start--; + } else if (c == '[') { + if (bracketDepth > 0) { + bracketDepth--; + start--; + } else { + break; + } + } else if (Character.isJavaIdentifierPart(c) || c == '.') { + start--; + } else if (Character.isWhitespace(c)) { + // Skip whitespace within expression (e.g., after method call) + if (parenDepth > 0 || bracketDepth > 0) { + start--; + } else { + break; + } + } else { + break; + } + } + + String expr = text.substring(start, end).trim(); + + // Clean up the expression + expr = expr.replaceAll("\\s+", ""); + + return expr; + } + + /** + * Check if cursor is after a dot. + */ + private boolean isAfterDot(String text, int cursorPos) { + // Look backwards, skipping any identifier characters + int pos = cursorPos - 1; + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) { + pos--; + } + return pos >= 0 && text.charAt(pos) == '.'; + } + + /** + * Check if a prefix is valid (identifier characters only). + */ + private boolean isValidPrefix(String prefix) { + if (prefix.isEmpty()) return true; + if (!Character.isJavaIdentifierStart(prefix.charAt(0))) return false; + for (int i = 1; i < prefix.length(); i++) { + if (!Character.isJavaIdentifierPart(prefix.charAt(i))) return false; + } + return true; + } + + /** + * Get line number (0-indexed) for a position. + */ + private int getLineNumber(String text, int position) { + int line = 0; + for (int i = 0; i < position && i < text.length(); i++) { + if (text.charAt(i) == '\n') line++; + } + return line; + } + + /** + * Get the current line text. + */ + private String getCurrentLine(String text, int position) { + int lineStart = text.lastIndexOf('\n', position - 1) + 1; + int lineEnd = text.indexOf('\n', position); + if (lineEnd < 0) lineEnd = text.length(); + return text.substring(lineStart, lineEnd); + } + + /** + * Get column position within the current line. + */ + private int getColumnPosition(String text, int position) { + int lineStart = text.lastIndexOf('\n', position - 1) + 1; + return position - lineStart; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java new file mode 100644 index 000000000..ea56e7deb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java @@ -0,0 +1,488 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.ScaledResolution; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * UI component for displaying autocomplete suggestions. + * Styled similar to SearchReplaceBar with IntelliJ-like appearance. + */ +public class AutocompleteMenu extends Gui { + + // ==================== CONSTANTS ==================== + private static final int MAX_VISIBLE_ITEMS = 10; + private static final int ITEM_HEIGHT = 16; + private static final int PADDING = 4; + private static final int ICON_WIDTH = 16; + private static final int MIN_WIDTH = 200; + private static final int MAX_WIDTH = 400; + private static final int HINT_HEIGHT = 18; + + // ==================== COLORS ==================== + private static final int BG_COLOR = 0xFF2d2d30; + private static final int BORDER_COLOR = 0xFF3c3c3c; + private static final int SELECTED_BG = 0xFF094771; + private static final int HOVER_BG = 0xFF37373d; + private static final int TEXT_COLOR = 0xFFe0e0e0; + private static final int DIM_TEXT_COLOR = 0xFF808080; + private static final int HIGHLIGHT_COLOR = 0xFFFFD866; + private static final int HINT_BG_COLOR = 0xFF252526; + private static final int SCROLLBAR_BG = 0xFF3c3c3c; + private static final int SCROLLBAR_FG = 0xFF606060; + + // ==================== STATE ==================== + private boolean visible = false; + private List items = new ArrayList<>(); + private int selectedIndex = 0; + private int scrollOffset = 0; + private int hoveredIndex = -1; + + // ==================== POSITION ==================== + private int x, y; + private int width, height; + private int menuWidth; + + // ==================== FONT ==================== + private final FontRenderer font; + + // ==================== CALLBACK ==================== + private AutocompleteCallback callback; + + /** + * Callback interface for autocomplete events. + */ + public interface AutocompleteCallback { + void onItemSelected(AutocompleteItem item); + void onDismiss(); + } + + public AutocompleteMenu() { + this.font = Minecraft.getMinecraft().fontRenderer; + } + + public void setCallback(AutocompleteCallback callback) { + this.callback = callback; + } + + // ==================== VISIBILITY ==================== + + /** + * Show the menu at the specified position with the given items. + */ + public void show(int x, int y, List items, int viewportWidth, int viewportHeight) { + this.items = items != null ? new ArrayList<>(items) : new ArrayList<>(); + this.selectedIndex = 0; + this.scrollOffset = 0; + this.hoveredIndex = -1; + + // Calculate menu dimensions + calculateDimensions(x, y, viewportWidth, viewportHeight); + + this.visible = !this.items.isEmpty(); + } + + /** + * Update the items while keeping the menu open. + */ + public void updateItems(List newItems) { + this.items = newItems != null ? new ArrayList<>(newItems) : new ArrayList<>(); + + // Clamp selection + if (selectedIndex >= items.size()) { + selectedIndex = Math.max(0, items.size() - 1); + } + + // Clamp scroll + int maxScroll = Math.max(0, items.size() - MAX_VISIBLE_ITEMS); + if (scrollOffset > maxScroll) { + scrollOffset = maxScroll; + } + + visible = !items.isEmpty(); + } + + /** + * Hide the menu. + */ + public void hide() { + visible = false; + if (callback != null) { + callback.onDismiss(); + } + } + + public boolean isVisible() { + return visible; + } + + public boolean hasItems() { + return !items.isEmpty(); + } + + // ==================== DIMENSIONS ==================== + + private void calculateDimensions(int cursorX, int cursorY, int viewportWidth, int viewportHeight) { + // Calculate width based on item content + int maxItemWidth = MIN_WIDTH; + for (AutocompleteItem item : items) { + int itemWidth = ICON_WIDTH + PADDING + font.getStringWidth(item.getName()) + PADDING; + if (item.getTypeLabel() != null) { + itemWidth += font.getStringWidth(item.getTypeLabel()) + PADDING * 2; + } + maxItemWidth = Math.max(maxItemWidth, itemWidth); + } + menuWidth = Math.min(maxItemWidth + 20, MAX_WIDTH); // +20 for scrollbar + + // Calculate height + int visibleItems = Math.min(items.size(), MAX_VISIBLE_ITEMS); + int menuHeight = visibleItems * ITEM_HEIGHT + HINT_HEIGHT + PADDING * 2; + + // Position below cursor, or above if not enough space + this.x = cursorX; + this.y = cursorY; + + // Check if menu would go off-screen to the right + if (x + menuWidth > viewportWidth - 10) { + x = viewportWidth - menuWidth - 10; + } + + // Check if menu would go off-screen to the bottom + if (y + menuHeight > viewportHeight - 10) { + // Show above cursor instead + y = cursorY - menuHeight - 20; // 20 for line height + } + + // Ensure not off-screen to the left or top + x = Math.max(5, x); + y = Math.max(5, y); + + this.width = menuWidth; + this.height = menuHeight; + } + + // ==================== SELECTION ==================== + + /** + * Move selection up. + */ + public void selectPrevious() { + if (items.isEmpty()) return; + + selectedIndex--; + if (selectedIndex < 0) { + selectedIndex = items.size() - 1; + scrollOffset = Math.max(0, items.size() - MAX_VISIBLE_ITEMS); + } else if (selectedIndex < scrollOffset) { + scrollOffset = selectedIndex; + } + } + + /** + * Move selection down. + */ + public void selectNext() { + if (items.isEmpty()) return; + + selectedIndex++; + if (selectedIndex >= items.size()) { + selectedIndex = 0; + scrollOffset = 0; + } else if (selectedIndex >= scrollOffset + MAX_VISIBLE_ITEMS) { + scrollOffset = selectedIndex - MAX_VISIBLE_ITEMS + 1; + } + } + + /** + * Confirm the current selection. + */ + public void confirmSelection() { + if (items.isEmpty() || selectedIndex < 0 || selectedIndex >= items.size()) { + hide(); + return; + } + + AutocompleteItem selected = items.get(selectedIndex); + if (callback != null) { + callback.onItemSelected(selected); + } + hide(); + } + + /** + * Get the currently selected item. + */ + public AutocompleteItem getSelectedItem() { + if (items.isEmpty() || selectedIndex < 0 || selectedIndex >= items.size()) { + return null; + } + return items.get(selectedIndex); + } + + // ==================== DRAWING ==================== + + /** + * Draw the autocomplete menu. + */ + public void draw(int mouseX, int mouseY) { + if (!visible || items.isEmpty()) return; + + // Update hover state + updateHoverState(mouseX, mouseY); + + // Enable scissoring to clip content + GL11.glEnable(GL11.GL_SCISSOR_TEST); + setScissor(x - 2, y - 2, width + 4, height + 4); + + // Draw background + drawRect(x, y, x + width, y + height, BG_COLOR); + + // Draw border + drawRect(x, y, x + width, y + 1, BORDER_COLOR); + drawRect(x, y + height - 1, x + width, y + height, BORDER_COLOR); + drawRect(x, y, x + 1, y + height, BORDER_COLOR); + drawRect(x + width - 1, y, x + width, y + height, BORDER_COLOR); + + // Draw items + int itemY = y + PADDING; + int visibleItems = Math.min(items.size(), MAX_VISIBLE_ITEMS); + + for (int i = 0; i < visibleItems; i++) { + int itemIndex = scrollOffset + i; + if (itemIndex >= items.size()) break; + + AutocompleteItem item = items.get(itemIndex); + boolean isSelected = (itemIndex == selectedIndex); + boolean isHovered = (itemIndex == hoveredIndex); + + drawItem(item, x + PADDING, itemY, width - PADDING * 2 - 8, isSelected, isHovered); + itemY += ITEM_HEIGHT; + } + + // Draw scrollbar if needed + if (items.size() > MAX_VISIBLE_ITEMS) { + drawScrollbar(); + } + + // Draw hint bar at bottom + drawHintBar(); + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } + + /** + * Draw a single autocomplete item. + */ + private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth, + boolean selected, boolean hovered) { + // Background + if (selected) { + drawRect(itemX - 2, itemY, itemX + itemWidth + 2, itemY + ITEM_HEIGHT, SELECTED_BG); + } else if (hovered) { + drawRect(itemX - 2, itemY, itemX + itemWidth + 2, itemY + ITEM_HEIGHT, HOVER_BG); + } + + int textX = itemX; + int textY = itemY + (ITEM_HEIGHT - font.FONT_HEIGHT) / 2; + + // Draw icon + String icon = item.getIconId(); + int iconColor = item.getIconColor(); + font.drawString(icon, textX + (ICON_WIDTH - font.getStringWidth(icon)) / 2, textY, iconColor); + textX += ICON_WIDTH; + + // Draw name with match highlighting + drawHighlightedText(item.getName(), item.getMatchIndices(), textX, textY, + item.isDeprecated() ? DIM_TEXT_COLOR : TEXT_COLOR); + + // Draw type label on the right + if (item.getTypeLabel() != null && !item.getTypeLabel().isEmpty()) { + String typeLabel = item.getTypeLabel(); + int typeLabelWidth = font.getStringWidth(typeLabel); + int typeLabelX = itemX + itemWidth - typeLabelWidth - PADDING; + font.drawString(typeLabel, typeLabelX, textY, DIM_TEXT_COLOR); + } + } + + /** + * Draw text with specific characters highlighted. + */ + private void drawHighlightedText(String text, int[] matchIndices, int x, int y, int baseColor) { + if (matchIndices == null || matchIndices.length == 0) { + font.drawString(text, x, y, baseColor); + return; + } + + // Create a set of highlighted indices for quick lookup + java.util.Set highlightSet = new java.util.HashSet<>(); + for (int idx : matchIndices) { + highlightSet.add(idx); + } + + // Draw character by character + int currentX = x; + for (int i = 0; i < text.length(); i++) { + String ch = String.valueOf(text.charAt(i)); + int color = highlightSet.contains(i) ? HIGHLIGHT_COLOR : baseColor; + font.drawString(ch, currentX, y, color); + currentX += font.getStringWidth(ch); + } + } + + /** + * Draw the scrollbar. + */ + private void drawScrollbar() { + int scrollbarX = x + width - 8; + int scrollbarY = y + PADDING; + int scrollbarHeight = MAX_VISIBLE_ITEMS * ITEM_HEIGHT; + + // Background + drawRect(scrollbarX, scrollbarY, scrollbarX + 6, scrollbarY + scrollbarHeight, SCROLLBAR_BG); + + // Thumb + float thumbRatio = (float) MAX_VISIBLE_ITEMS / items.size(); + int thumbHeight = Math.max(20, (int) (scrollbarHeight * thumbRatio)); + float thumbPosRatio = (float) scrollOffset / Math.max(1, items.size() - MAX_VISIBLE_ITEMS); + int thumbY = scrollbarY + (int) ((scrollbarHeight - thumbHeight) * thumbPosRatio); + + drawRect(scrollbarX + 1, thumbY, scrollbarX + 5, thumbY + thumbHeight, SCROLLBAR_FG); + } + + /** + * Draw the hint bar at the bottom (shows Tab/Enter to confirm). + */ + private void drawHintBar() { + int hintY = y + height - HINT_HEIGHT; + + // Darker background for hint area + drawRect(x + 1, hintY, x + width - 1, y + height - 1, HINT_BG_COLOR); + + // Draw hints + int hintTextY = hintY + (HINT_HEIGHT - font.FONT_HEIGHT) / 2; + int hintX = x + PADDING; + + // Tab hint + drawKeyHint("Tab", hintX, hintTextY); + hintX += font.getStringWidth("Tab") + 8; + font.drawString("or", hintX, hintTextY, DIM_TEXT_COLOR); + hintX += font.getStringWidth("or") + 4; + + // Enter hint + drawKeyHint("Enter", hintX, hintTextY); + hintX += font.getStringWidth("Enter") + 8; + font.drawString("to insert", hintX, hintTextY, DIM_TEXT_COLOR); + } + + /** + * Draw a key hint with a darker background box. + */ + private void drawKeyHint(String key, int x, int y) { + int keyWidth = font.getStringWidth(key); + int boxPadding = 2; + + // Draw box + drawRect(x - boxPadding, y - boxPadding, + x + keyWidth + boxPadding, y + font.FONT_HEIGHT + boxPadding, + 0xFF404040); + + // Draw key text + font.drawString(key, x, y, TEXT_COLOR); + } + + // ==================== MOUSE HANDLING ==================== + + /** + * Update hover state based on mouse position. + */ + private void updateHoverState(int mouseX, int mouseY) { + hoveredIndex = -1; + + if (!isMouseInBounds(mouseX, mouseY)) return; + + int itemY = y + PADDING; + int visibleItems = Math.min(items.size(), MAX_VISIBLE_ITEMS); + + for (int i = 0; i < visibleItems; i++) { + if (mouseY >= itemY && mouseY < itemY + ITEM_HEIGHT) { + hoveredIndex = scrollOffset + i; + break; + } + itemY += ITEM_HEIGHT; + } + } + + /** + * Handle mouse click. + * @return true if click was consumed + */ + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Check if click is outside menu + if (!isMouseInBounds(mouseX, mouseY)) { + hide(); + return false; + } + + // Check if clicking on an item + if (button == 0 && hoveredIndex >= 0 && hoveredIndex < items.size()) { + selectedIndex = hoveredIndex; + confirmSelection(); + return true; + } + + return true; // Consume click if inside menu + } + + /** + * Handle mouse scroll. + * @return true if scroll was consumed + */ + public boolean mouseScrolled(int mouseX, int mouseY, int delta) { + if (!visible || !isMouseInBounds(mouseX, mouseY)) return false; + + if (items.size() > MAX_VISIBLE_ITEMS) { + if (delta > 0) { + scrollOffset = Math.max(0, scrollOffset - 1); + } else { + scrollOffset = Math.min(items.size() - MAX_VISIBLE_ITEMS, scrollOffset + 1); + } + return true; + } + + return false; + } + + private boolean isMouseInBounds(int mouseX, int mouseY) { + return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; + } + + // ==================== UTILITY ==================== + + private void setScissor(int x, int y, int width, int height) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scaleFactor = sr.getScaleFactor(); + + int scissorX = x * scaleFactor; + int scissorY = (sr.getScaledHeight() - y - height) * scaleFactor; + int scissorW = width * scaleFactor; + int scissorH = height * scaleFactor; + + GL11.glScissor(scissorX, scissorY, scissorW, scissorH); + } + + // ==================== GETTERS ==================== + + public int getX() { return x; } + public int getY() { return y; } + public int getWidth() { return width; } + public int getHeight() { return height; } + public List getItems() { return items; } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java new file mode 100644 index 000000000..1ada3614a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java @@ -0,0 +1,63 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import java.util.List; + +/** + * Abstract provider for autocomplete suggestions. + * Implementations handle different contexts (after dot, in identifier, etc.) + */ +public interface AutocompleteProvider { + + /** + * Context information for autocomplete requests. + */ + class Context { + /** Full text of the document */ + public final String text; + /** Current cursor position in the document */ + public final int cursorPosition; + /** The current line number (0-indexed) */ + public final int lineNumber; + /** Position of cursor within the current line */ + public final int columnPosition; + /** The current line text */ + public final String currentLine; + /** The word being typed (prefix before cursor, after last separator) */ + public final String prefix; + /** Start position of the prefix in the document */ + public final int prefixStart; + /** Whether this is after a dot (member access) */ + public final boolean isMemberAccess; + /** Expression before the dot (for member access) */ + public final String receiverExpression; + /** Whether autocomplete was explicitly triggered (CTRL+Space) */ + public final boolean explicitTrigger; + + public Context(String text, int cursorPosition, int lineNumber, int columnPosition, + String currentLine, String prefix, int prefixStart, + boolean isMemberAccess, String receiverExpression, boolean explicitTrigger) { + this.text = text; + this.cursorPosition = cursorPosition; + this.lineNumber = lineNumber; + this.columnPosition = columnPosition; + this.currentLine = currentLine; + this.prefix = prefix; + this.prefixStart = prefixStart; + this.isMemberAccess = isMemberAccess; + this.receiverExpression = receiverExpression; + this.explicitTrigger = explicitTrigger; + } + } + + /** + * Get autocomplete suggestions for the given context. + * @param context The context containing cursor position, text, etc. + * @return List of autocomplete items, sorted by relevance + */ + List getSuggestions(Context context); + + /** + * Check if this provider can handle the given context. + */ + boolean canProvide(Context context); +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java new file mode 100644 index 000000000..fc536102c --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -0,0 +1,288 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.*; + +import java.util.*; + +/** + * Autocomplete provider for JavaScript/ECMAScript scripts. + * Uses JSTypeRegistry for type resolution. + */ +public class JSAutocompleteProvider implements AutocompleteProvider { + + private ScriptDocument document; + private final JSTypeRegistry registry; + + // Variable types inferred from the document + private Map variableTypes = new HashMap<>(); + + public JSAutocompleteProvider() { + this.registry = JSTypeRegistry.getInstance(); + if (!registry.isInitialized()) { + registry.initializeFromResources(); + } + } + + public void setDocument(ScriptDocument document) { + this.document = document; + } + + /** + * Update the variable type map from document analysis. + */ + public void updateVariableTypes(Map types) { + this.variableTypes = types != null ? new HashMap<>(types) : new HashMap<>(); + } + + @Override + public boolean canProvide(Context context) { + return document != null && document.isJavaScript(); + } + + @Override + public List getSuggestions(Context context) { + List items = new ArrayList<>(); + + if (context.isMemberAccess) { + // Member access: resolve type of receiver and get its members + addMemberSuggestions(context, items); + } else { + // Identifier context: show variables, functions, types in scope + addScopeSuggestions(context, items); + } + + // Filter and score by prefix + filterAndScore(items, context.prefix); + + // Sort by score + Collections.sort(items); + + // Limit results + if (items.size() > 50) { + items = items.subList(0, 50); + } + + return items; + } + + /** + * Add suggestions for member access (after dot). + */ + private void addMemberSuggestions(Context context, List items) { + String receiverExpr = context.receiverExpression; + if (receiverExpr == null || receiverExpr.isEmpty()) { + return; + } + + // Infer type of receiver + String receiverType = inferExpressionType(receiverExpr); + if (receiverType == null) { + return; + } + + JSTypeInfo typeInfo = registry.getType(receiverType); + if (typeInfo == null) { + return; + } + + // Add methods + addMethodsFromType(typeInfo, items, new HashSet<>()); + + // Add fields + addFieldsFromType(typeInfo, items, new HashSet<>()); + } + + /** + * Recursively add methods from a type and its parents. + */ + private void addMethodsFromType(JSTypeInfo type, List items, Set added) { + for (JSMethodInfo method : type.getMethods().values()) { + String name = method.getName(); + // Skip overload markers (name$1, name$2, etc.) + if (name.contains("$")) { + name = name.substring(0, name.indexOf('$')); + } + if (!added.contains(name)) { + added.add(name); + items.add(AutocompleteItem.fromJSMethod(method)); + } + } + + // Add from parent type + if (type.getResolvedParent() != null) { + addMethodsFromType(type.getResolvedParent(), items, added); + } + } + + /** + * Recursively add fields from a type and its parents. + */ + private void addFieldsFromType(JSTypeInfo type, List items, Set added) { + for (JSFieldInfo field : type.getFields().values()) { + if (!added.contains(field.getName())) { + added.add(field.getName()); + items.add(AutocompleteItem.fromJSField(field)); + } + } + + // Add from parent type + if (type.getResolvedParent() != null) { + addFieldsFromType(type.getResolvedParent(), items, added); + } + } + + /** + * Add suggestions based on current scope (not after a dot). + */ + private void addScopeSuggestions(Context context, List items) { + // Add local variables + for (Map.Entry entry : variableTypes.entrySet()) { + String varName = entry.getKey(); + String varType = entry.getValue(); + + items.add(new AutocompleteItem.Builder() + .name(varName) + .insertText(varName) + .kind(AutocompleteItem.Kind.VARIABLE) + .typeLabel(varType != null ? varType : "any") + .build()); + } + + // Add hook functions + for (String hookName : registry.getHookNames()) { + List sigs = registry.getHookSignatures(hookName); + if (!sigs.isEmpty()) { + JSTypeRegistry.HookSignature sig = sigs.get(0); + items.add(new AutocompleteItem.Builder() + .name(hookName) + .insertText(hookName) + .kind(AutocompleteItem.Kind.METHOD) + .typeLabel("hook") + .signature("function " + hookName + "(e: " + sig.paramType + ")") + .documentation(sig.doc) + .build()); + } + } + + // Add global types + for (String typeName : registry.getTypeNames()) { + JSTypeInfo type = registry.getType(typeName); + if (type != null) { + items.add(new AutocompleteItem.Builder() + .name(typeName) + .insertText(typeName) + .kind(AutocompleteItem.Kind.CLASS) + .typeLabel("interface") + .build()); + } + } + + // Add JavaScript keywords + addJSKeywords(items); + } + + /** + * Infer the type of an expression. + */ + private String inferExpressionType(String expr) { + if (expr == null || expr.isEmpty()) { + return null; + } + + expr = expr.trim(); + + // Check if it's a known variable + if (variableTypes.containsKey(expr)) { + return variableTypes.get(expr); + } + + // Handle method chain: x.y.z() -> resolve step by step + if (expr.contains(".")) { + String[] parts = expr.split("\\."); + String currentType = null; + + // Start with the first part + if (variableTypes.containsKey(parts[0])) { + currentType = variableTypes.get(parts[0]); + } + + if (currentType == null) { + return null; + } + + // Walk through the chain + for (int i = 1; i < parts.length; i++) { + String member = parts[i]; + // Remove () for method calls + if (member.endsWith("()")) { + member = member.substring(0, member.length() - 2); + } + + JSTypeInfo typeInfo = registry.getType(currentType); + if (typeInfo == null) { + return null; + } + + // Check method + JSMethodInfo method = typeInfo.getMethod(member); + if (method != null) { + currentType = method.getReturnType(); + continue; + } + + // Check field + JSFieldInfo field = typeInfo.getField(member); + if (field != null) { + currentType = field.getType(); + continue; + } + + // Unknown member + return null; + } + + return currentType; + } + + return null; + } + + /** + * Add JavaScript keywords. + */ + private void addJSKeywords(List items) { + String[] keywords = { + "function", "var", "let", "const", "if", "else", "for", "while", "do", + "switch", "case", "break", "continue", "return", "try", "catch", "finally", + "throw", "new", "typeof", "instanceof", "in", "of", "this", "null", + "undefined", "true", "false", "async", "await", "yield", "class", "extends", + "import", "export", "default" + }; + + for (String keyword : keywords) { + items.add(AutocompleteItem.keyword(keyword)); + } + } + + /** + * Filter items by prefix and calculate match scores. + */ + private void filterAndScore(List items, String prefix) { + if (prefix == null || prefix.isEmpty()) { + for (AutocompleteItem item : items) { + item.calculateMatchScore(""); + } + return; + } + + Iterator iter = items.iterator(); + while (iter.hasNext()) { + AutocompleteItem item = iter.next(); + int score = item.calculateMatchScore(prefix); + if (score < 0) { + iter.remove(); + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java new file mode 100644 index 000000000..802482040 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -0,0 +1,287 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +/** + * Autocomplete provider for Java/Groovy scripts. + * Uses ScriptDocument for type resolution and scope analysis. + */ +public class JavaAutocompleteProvider implements AutocompleteProvider { + + private ScriptDocument document; + + public void setDocument(ScriptDocument document) { + this.document = document; + } + + @Override + public boolean canProvide(Context context) { + return document != null && !document.isJavaScript(); + } + + @Override + public List getSuggestions(Context context) { + List items = new ArrayList<>(); + + if (document == null) { + return items; + } + + if (context.isMemberAccess) { + // Member access: resolve type of receiver and get its members + addMemberSuggestions(context, items); + } else { + // Identifier context: show variables, methods, types in scope + addScopeSuggestions(context, items); + } + + // Filter and score by prefix + filterAndScore(items, context.prefix); + + // Sort by score + Collections.sort(items); + + // Limit results + if (items.size() > 50) { + items = items.subList(0, 50); + } + + return items; + } + + /** + * Add suggestions for member access (after dot). + */ + private void addMemberSuggestions(Context context, List items) { + String receiverExpr = context.receiverExpression; + if (receiverExpr == null || receiverExpr.isEmpty()) { + return; + } + + // Resolve the type of the receiver expression + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, context.prefixStart); + + if (receiverType == null || !receiverType.isResolved()) { + return; + } + + Class clazz = receiverType.getJavaClass(); + if (clazz == null) { + // Try ScriptTypeInfo + if (receiverType instanceof ScriptTypeInfo) { + addScriptTypeMembers((ScriptTypeInfo) receiverType, items); + } + return; + } + + // Add methods + Set addedMethods = new HashSet<>(); + for (Method method : clazz.getMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + String sig = method.getName() + "(" + method.getParameterCount() + ")"; + if (!addedMethods.contains(sig)) { + addedMethods.add(sig); + MethodInfo methodInfo = MethodInfo.fromReflection(method, receiverType); + items.add(AutocompleteItem.fromMethod(methodInfo)); + } + } + } + + // Add fields + for (Field field : clazz.getFields()) { + if (Modifier.isPublic(field.getModifiers())) { + FieldInfo fieldInfo = FieldInfo.fromReflection(field, receiverType); + items.add(AutocompleteItem.fromField(fieldInfo)); + } + } + + // Add enum constants if it's an enum + if (clazz.isEnum()) { + for (Object constant : clazz.getEnumConstants()) { + String constantName = constant.toString(); + items.add(new AutocompleteItem.Builder() + .name(constantName) + .insertText(constantName) + .kind(AutocompleteItem.Kind.ENUM_CONSTANT) + .typeLabel(receiverType.getSimpleName()) + .build()); + } + } + } + + /** + * Add members from a script-defined type. + */ + private void addScriptTypeMembers(ScriptTypeInfo scriptType, List items) { + // Add methods (getMethods returns Map>) + for (List overloads : scriptType.getMethods().values()) { + for (MethodInfo method : overloads) { + items.add(AutocompleteItem.fromMethod(method)); + } + } + + // Add fields (getFields returns Map) + for (FieldInfo field : scriptType.getFields().values()) { + items.add(AutocompleteItem.fromField(field)); + } + + // Add parent class members + if (scriptType.hasSuperClass()) { + TypeInfo superType = scriptType.getSuperClass(); + if (superType != null && superType.getJavaClass() != null) { + Class superClazz = superType.getJavaClass(); + Set addedMethods = new HashSet<>(); + for (Method method : superClazz.getMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + String sig = method.getName() + "(" + method.getParameterCount() + ")"; + if (!addedMethods.contains(sig)) { + addedMethods.add(sig); + MethodInfo methodInfo = MethodInfo.fromReflection(method, superType); + items.add(AutocompleteItem.fromMethod(methodInfo)); + } + } + } + } + } + } + + /** + * Add suggestions based on current scope (not after a dot). + */ + private void addScopeSuggestions(Context context, List items) { + int pos = context.cursorPosition; + + // Find containing method + MethodInfo containingMethod = document.findContainingMethod(pos); + + // Add local variables + if (containingMethod != null) { + Map locals = document.getLocalsForMethod(containingMethod); + if (locals != null) { + for (FieldInfo local : locals.values()) { + if (local.isVisibleAt(pos)) { + items.add(AutocompleteItem.fromField(local)); + } + } + } + + // Add method parameters + for (FieldInfo param : containingMethod.getParameters()) { + items.add(AutocompleteItem.fromField(param)); + } + } + + // Add global fields + for (FieldInfo globalField : document.getGlobalFields().values()) { + if (globalField.isVisibleAt(pos)) { + items.add(AutocompleteItem.fromField(globalField)); + } + } + + // Add enclosing type fields (find via script types map) + ScriptTypeInfo enclosingType = findEnclosingType(pos); + if (enclosingType != null) { + for (FieldInfo field : enclosingType.getFields().values()) { + if (field.isVisibleAt(pos)) { + items.add(AutocompleteItem.fromField(field)); + } + } + + // Add methods from enclosing type + for (List overloads : enclosingType.getMethods().values()) { + for (MethodInfo method : overloads) { + items.add(AutocompleteItem.fromMethod(method)); + } + } + } + + // Add script-defined methods + for (MethodInfo method : document.getAllMethods()) { + items.add(AutocompleteItem.fromMethod(method)); + } + + // Add imported types + for (TypeInfo type : document.getImportedTypes()) { + items.add(AutocompleteItem.fromType(type)); + } + + // Add script-defined types + for (ScriptTypeInfo scriptType : document.getScriptTypesMap().values()) { + items.add(AutocompleteItem.fromType(scriptType)); + } + + // Add keywords + addKeywords(items); + } + + /** + * Add Java keywords. + */ + private void addKeywords(List items) { + String[] keywords = { + "if", "else", "for", "while", "do", "switch", "case", "break", "continue", + "return", "try", "catch", "finally", "throw", "throws", "new", "this", "super", + "true", "false", "null", "instanceof", "import", "class", "interface", "enum", + "extends", "implements", "public", "private", "protected", "static", "final", + "abstract", "synchronized", "volatile", "transient", "native", "void", + "boolean", "byte", "short", "int", "long", "float", "double", "char" + }; + + for (String keyword : keywords) { + items.add(AutocompleteItem.keyword(keyword)); + } + } + + /** + * Find the enclosing script type at a position. + * This is a workaround since findEnclosingScriptType is package-private. + */ + private ScriptTypeInfo findEnclosingType(int position) { + for (ScriptTypeInfo type : document.getScriptTypesMap().values()) { + if (type.containsPosition(position)) { + return type; + } + } + return null; + } + + /** + * Filter items by prefix and calculate match scores. + */ + private void filterAndScore(List items, String prefix) { + if (prefix == null || prefix.isEmpty()) { + // No filtering needed, all items get a base score + for (AutocompleteItem item : items) { + item.calculateMatchScore(""); + } + return; + } + + // Filter and score + Iterator iter = items.iterator(); + while (iter.hasNext()) { + AutocompleteItem item = iter.next(); + int score = item.calculateMatchScore(prefix); + if (score < 0) { + iter.remove(); + } + } + } + + /** + * Builder class for AutocompleteItem to handle cases without source data. + */ + private static class AutocompleteItemBuilder { + // This would be needed if AutocompleteItem had a builder pattern + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java index fdf540934..456e08e02 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -161,7 +161,7 @@ public class ScriptDocument { * Check if this is a JavaScript/ECMAScript document. */ public boolean isJavaScript() { - return "ECMAScript".equalsIgnoreCase(language); + return false;//"ECMAScript".equalsIgnoreCase(language); } // ==================== TEXT MANAGEMENT ==================== @@ -2592,7 +2592,7 @@ public class ScriptDocument { * * This is THE method that should be used for all type resolution needs. */ - TypeInfo resolveExpressionType(String expr, int position) { + public TypeInfo resolveExpressionType(String expr, int position) { expr = expr.trim(); if (expr.isEmpty()) { @@ -5183,4 +5183,47 @@ public class ScriptDocument { return null; } + + // ==================== AUTOCOMPLETE SUPPORT ==================== + + /** + * Find the method that contains the given position. + * Public accessor for autocomplete. + */ + public MethodInfo findContainingMethod(int position) { + return findMethodAtPosition(position); + } + + /** + * Get local variables for a specific method. + * Used by autocomplete to show variables in scope. + */ + public Map getLocalsForMethod(MethodInfo method) { + if (method == null) return null; + return methodLocals.get(method.getDeclarationOffset()); + } + + /** + * Get all imported types that have been resolved. + * Used by autocomplete to show available types. + */ + public List getImportedTypes() { + List types = new ArrayList<>(); + for (ImportData imp : imports) { + if (!imp.isWildcard() && imp.isResolved()) { + TypeInfo type = typeResolver.resolveSimpleName(imp.getSimpleName(), importsBySimpleName, wildcardPackages); + if (type != null && type.isResolved()) { + types.add(type); + } + } + } + return types; + } + + /** + * Get script types as a Map (needed by autocomplete). + */ + public Map getScriptTypesMap() { + return Collections.unmodifiableMap(scriptTypes); + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java index 7234197d1..bc6b71688 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java @@ -335,6 +335,20 @@ public class JSTypeRegistry { return types.values(); } + /** + * Get all type names. + */ + public Set getTypeNames() { + return types.keySet(); + } + + /** + * Get all hook function names. + */ + public Set getHookNames() { + return hooks.keySet(); + } + /** * Get all registered hooks. */ @@ -365,10 +379,16 @@ public class JSTypeRegistry { public static class HookSignature { public final String paramName; public final String paramType; + public final String doc; public HookSignature(String paramName, String paramType) { + this(paramName, paramType, null); + } + + public HookSignature(String paramName, String paramType, String doc) { this.paramName = paramName; this.paramType = paramType; + this.doc = doc; } @Override diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index f8f262893..8815cd55a 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -30,6 +30,9 @@ public class ScriptEditorKeys extends KeyPresetManager { // View public final KeyPreset FULLSCREEN = add("Toggle Fullscreen").setDefaultState(Keyboard.KEY_F11, false, false, false); + + // Autocomplete + public final KeyPreset AUTOCOMPLETE = add("Autocomplete").setDefaultState(Keyboard.KEY_SPACE, true, false, false); public ScriptEditorKeys() { super("script_editor");