From 4e78ffb024082dfe5b40ad688a2d033dfcd306cf Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 3 Feb 2026 06:54:09 +0200 Subject: [PATCH] Fix autocomplete trigger after dot --- .../autocomplete/AutocompleteManager.java | 52 ++++++++++++++----- .../autocomplete/JSAutocompleteProvider.java | 2 +- .../JavaAutocompleteProvider.java | 51 ++++++++++++++---- 3 files changed, 80 insertions(+), 25 deletions(-) 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 index e5d13f286..d6196edc4 100644 --- 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 @@ -179,18 +179,27 @@ public class AutocompleteManager { // Check if we're typing an identifier if (Character.isJavaIdentifierPart(c)) { + // GuiScriptTextArea passes a cursor position captured BEFORE insertion while `text` + // is already the post-insert text. When this happens, cursorPosition points at the + // newly inserted character, not the caret-after-insert. Normalize to a caret position + // for prefix extraction/update. + int effectiveCursor = cursorPosition; + if (effectiveCursor >= 0 && effectiveCursor < text.length() && text.charAt(effectiveCursor) == c) { + effectiveCursor = effectiveCursor + 1; + } + if (active) { // Update existing autocomplete - updatePrefix(text, cursorPosition); + updatePrefix(text, effectiveCursor); } else if (Character.isJavaIdentifierStart(c)) { // Check if we're after a dot with whitespace (e.g., "obj. |" where | is cursor) - int dotPos = findDotBeforeWhitespace(text, cursorPosition - 1); + int dotPos = findDotBeforeWhitespace(text, effectiveCursor - 1); if (dotPos >= 0) { // We're typing after a dot (with possible whitespace), trigger member access - triggerAfterDot(text, cursorPosition); + triggerAfterDot(text, effectiveCursor); } else { // Potentially start new autocomplete - maybeStartAutocomplete(text, cursorPosition, false); + maybeStartAutocomplete(text, effectiveCursor, false); } } return; @@ -283,29 +292,39 @@ public class AutocompleteManager { * Trigger autocomplete after a dot is typed. */ private void triggerAfterDot(String text, int cursorPosition) { - // Find the receiver expression before the dot - // First check if immediately before cursor - int dotPos = cursorPosition - 1; - if (dotPos < 0 || text.charAt(dotPos) != '.') { - // Look backwards skipping whitespace to find dot - dotPos = findDotBeforeWhitespace(text, cursorPosition - 1); + // Normalize cursor semantics. + // Some callers pass a pre-insert cursor index (points at '.') even though the caret + // is visually after the inserted '.' in the current text. + int caretPos = Math.max(0, Math.min(cursorPosition, text.length())); + + int dotPos = -1; + if (caretPos > 0 && caretPos <= text.length() && text.charAt(caretPos - 1) == '.') { + // Caret-after-dot case + dotPos = caretPos - 1; + } else if (caretPos >= 0 && caretPos < text.length() && text.charAt(caretPos) == '.') { + // Pre-insert cursor case: cursorPosition points at the dot itself + dotPos = caretPos; + caretPos = Math.min(caretPos + 1, text.length()); + } else { + // Look backwards skipping whitespace/identifier to find dot + dotPos = findDotBeforeWhitespace(text, caretPos - 1); } if (dotPos < 0) return; String receiverExpr = findReceiverExpression(text, dotPos); - String prefix = findCurrentWord(text, cursorPosition); + String prefix = findCurrentWord(text, caretPos); // Find where the prefix actually starts (after dot + any whitespace) int prefixStart = dotPos + 1; - while (prefixStart < cursorPosition && Character.isWhitespace(text.charAt(prefixStart))) { + while (prefixStart < caretPos && Character.isWhitespace(text.charAt(prefixStart))) { prefixStart++; } prefixStartPosition = prefixStart; currentPrefix = prefix; - showSuggestions(text, cursorPosition, prefix, prefixStartPosition, true, receiverExpr); + showSuggestions(text, caretPos, prefix, prefixStartPosition, true, receiverExpr); } /** @@ -379,7 +398,12 @@ public class AutocompleteManager { // Resolve receiver type if member access if (isMemberAccess && receiverExpr != null) { - TypeInfo receiverType = document.resolveExpressionType(receiverExpr, prefixStart); + // Use a position inside the receiver/scope for resolution. + // `prefixStart` can land on end-exclusive boundaries (e.g., right at bodyEnd), + // causing findInnermostScopeAt(prefixStart) to return null. + int dotPos = findDotBeforeWhitespace(text, prefixStart - 1); + int resolvePos = dotPos >= 0 ? dotPos : prefixStart; + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, resolvePos); if (receiverType != null && receiverType.isResolved()) { currentReceiverFullName = receiverType.getFullName(); } 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 index 2fd28c4b6..1ba3d9769 100644 --- 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 @@ -40,7 +40,7 @@ public class JSAutocompleteProvider extends JavaAutocompleteProvider { } // Use ScriptDocument's resolveExpressionType - handles both Java and JS - TypeInfo receiverType = document.resolveExpressionType(receiverExpr, context.prefixStart); + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, getMemberAccessResolvePosition(context)); if (receiverType == null || !receiverType.isResolved()) { return; } 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 index 81e105b4b..7756376aa 100644 --- 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 @@ -43,13 +43,14 @@ public class JavaAutocompleteProvider implements AutocompleteProvider { String ownerFullName = null; boolean isStaticContext = false; if (context.isMemberAccess && context.receiverExpression != null) { + int resolvePos = getMemberAccessResolvePosition(context); TypeInfo receiverType = document.resolveExpressionType( - context.receiverExpression, context.prefixStart); + context.receiverExpression, resolvePos); if (receiverType != null && receiverType.isResolved()) { ownerFullName = receiverType.getFullName(); } // Check if accessing through a class (static context) vs instance - isStaticContext = isStaticAccess(context.receiverExpression, context.prefixStart); + isStaticContext = isStaticAccess(context.receiverExpression, resolvePos); } if (context.isMemberAccess) { @@ -79,14 +80,14 @@ public class JavaAutocompleteProvider implements AutocompleteProvider { } // Resolve the type of the receiver expression - TypeInfo receiverType = document.resolveExpressionType(receiverExpr, context.prefixStart); + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, getMemberAccessResolvePosition(context)); if (receiverType == null || !receiverType.isResolved()) { return; } // Determine if this is a static context (accessing a class type) - boolean isStaticContext = isStaticAccess(receiverExpr, context.prefixStart); + boolean isStaticContext = isStaticAccess(receiverExpr, getMemberAccessResolvePosition(context)); Class clazz = receiverType.getJavaClass(); if (clazz == null) { @@ -129,6 +130,36 @@ public class JavaAutocompleteProvider implements AutocompleteProvider { } } + + /** + * Pick a stable position for resolving member-access receiver types. + * + * Using `prefixStart` can land on end-exclusive scope boundaries (position == bodyEnd) + * depending on caller cursor semantics and surrounding syntax, causing scope lookups + * to fail. For member access we prefer the dot position (or nearest non-whitespace) + * immediately before prefixStart. + */ + protected int getMemberAccessResolvePosition(Context context) { + if (context == null || context.text == null || context.text.isEmpty()) { + return 0; + } + + int pos = Math.max(0, Math.min(context.prefixStart, context.text.length())); + int i = Math.min(pos - 1, context.text.length() - 1); + + // Skip whitespace backwards + while (i >= 0 && Character.isWhitespace(context.text.charAt(i))) { + i--; + } + + // Prefer the dot index if present + if (i >= 0 && context.text.charAt(i) == '.') { + return i; + } + + // Fallback: use the provided prefixStart (caret context) + return Math.max(0, Math.min(context.prefixStart, context.text.length())); + } /** * Add members from a script-defined type. @@ -184,10 +215,10 @@ public class JavaAutocompleteProvider implements AutocompleteProvider { */ protected 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); @@ -198,20 +229,20 @@ public class JavaAutocompleteProvider implements AutocompleteProvider { } } } - + // 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) { @@ -220,7 +251,7 @@ public class JavaAutocompleteProvider implements AutocompleteProvider { items.add(AutocompleteItem.fromField(field)); } } - + // Add methods from enclosing type for (List overloads : enclosingType.getMethods().values()) { for (MethodInfo method : overloads) {