Fix autocomplete trigger after dot

This commit is contained in:
bigguy345 committed 2026-02-03 06:54:09 +02:00
1 parent d63a1b8d4c
commit 4e78ffb024
3 files changed
+80 -25

No files matched your search

@@ -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();
}
@@ -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;
}
@@ -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<AutocompleteItem> items) {
int pos = context.cursorPosition;
// Find containing method
MethodInfo containingMethod = document.findContainingMethod(pos);
// Add local variables
if (containingMethod != null) {
Map<String, FieldInfo> 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<MethodInfo> overloads : enclosingType.getMethods().values()) {
for (MethodInfo method : overloads) {