From 16236c6ba0851b54ac133c0ca02a9b44fd86c627 Mon Sep 17 00:00:00 2001 From: trustytrojan <87675609+trustytrojan@users.noreply.github.com> Date: Mon, 18 May 2026 15:21:52 -0600 Subject: [PATCH 1/5] Add semantic token support --- .../groovyls/GroovyLanguageServer.java | 12 + .../net/prominic/groovyls/GroovyServices.java | 19 ++ .../providers/SemanticTokensProvider.java | 303 ++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java diff --git a/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java b/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java index 46e4042e..f1a29269 100644 --- a/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java +++ b/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java @@ -26,11 +26,14 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collections; import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.CompletionOptions; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.SemanticTokensLegend; +import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.SignatureHelpOptions; import org.eclipse.lsp4j.TextDocumentSyncKind; @@ -43,6 +46,7 @@ import net.prominic.groovyls.config.CompilationUnitFactory; import net.prominic.groovyls.config.ICompilationUnitFactory; +import net.prominic.groovyls.providers.SemanticTokensProvider; public class GroovyLanguageServer implements LanguageServer, LanguageClientAware { @@ -93,6 +97,14 @@ public CompletableFuture initialize(InitializeParams params) { signatureHelpOptions.setTriggerCharacters(Arrays.asList("(", ",")); serverCapabilities.setSignatureHelpProvider(signatureHelpOptions); + // Register semantic tokens provider for full document tokenization + SemanticTokensWithRegistrationOptions semanticTokensOptions = new SemanticTokensWithRegistrationOptions(); + semanticTokensOptions.setLegend(new SemanticTokensLegend( + SemanticTokensProvider.TOKEN_TYPES, + Collections.emptyList())); + semanticTokensOptions.setFull(true); + serverCapabilities.setSemanticTokensProvider(semanticTokensOptions); + InitializeResult initializeResult = new InitializeResult(serverCapabilities); return CompletableFuture.completedFuture(initializeResult); } diff --git a/src/main/java/net/prominic/groovyls/GroovyServices.java b/src/main/java/net/prominic/groovyls/GroovyServices.java index b234642a..a47c5c1b 100644 --- a/src/main/java/net/prominic/groovyls/GroovyServices.java +++ b/src/main/java/net/prominic/groovyls/GroovyServices.java @@ -71,6 +71,8 @@ import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.ReferenceParams; import org.eclipse.lsp4j.RenameParams; +import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensParams; import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SignatureHelpParams; import org.eclipse.lsp4j.SymbolInformation; @@ -102,6 +104,7 @@ import net.prominic.groovyls.providers.SignatureHelpProvider; import net.prominic.groovyls.providers.TypeDefinitionProvider; import net.prominic.groovyls.providers.WorkspaceSymbolProvider; +import net.prominic.groovyls.providers.SemanticTokensProvider; import net.prominic.groovyls.util.FileContentsTracker; import net.prominic.groovyls.util.GroovyLanguageServerUtils; import net.prominic.lsp.utils.Positions; @@ -120,6 +123,7 @@ public class GroovyServices implements TextDocumentService, WorkspaceService, La private ScanResult classGraphScanResult = null; private GroovyClassLoader classLoader = null; private URI previousContext = null; + private SemanticTokensProvider semanticTokensProvider = null; public GroovyServices(ICompilationUnitFactory factory) { compilationUnitFactory = factory; @@ -358,6 +362,21 @@ public CompletableFuture>> docume return provider.provideDocumentSymbols(params.getTextDocument()); } + @Override + public CompletableFuture semanticTokensFull(SemanticTokensParams params) { + TextDocumentIdentifier textDocument = params.getTextDocument(); + URI uri = URI.create(textDocument.getUri()); + recompileIfContextChanged(uri); + + // Ensure semantic tokens provider is initialized + if (semanticTokensProvider == null) { + semanticTokensProvider = new SemanticTokensProvider(fileContentsTracker, astVisitor); + } + + // Provide semantic tokens - GDSL symbols are injected before LSP transmission + return CompletableFuture.completedFuture(semanticTokensProvider.provideFull(textDocument)); + } + @Override public CompletableFuture> symbol(WorkspaceSymbolParams params) { WorkspaceSymbolProvider provider = new WorkspaceSymbolProvider(astVisitor); diff --git a/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java b/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java new file mode 100644 index 00000000..17652ae0 --- /dev/null +++ b/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java @@ -0,0 +1,303 @@ +package net.prominic.groovyls.providers; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensLegend; +import org.eclipse.lsp4j.TextDocumentIdentifier; + +import net.prominic.groovyls.util.FileContentsTracker; +import net.prominic.groovyls.compiler.ast.ASTNodeVisitor; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Variable; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.PropertyNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.ClassExpression; +import org.codehaus.groovy.ast.expr.PropertyExpression; +import org.codehaus.groovy.ast.ClassNode; +import net.prominic.groovyls.compiler.util.GroovyASTUtils; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import net.prominic.groovyls.util.GroovyLanguageServerUtils; + +/** + * Semantic tokens provider that emits tokens for functions/methods and local/global variables by analyzing the AST. + * + * This implementation uses the Groovy AST to precisely locate declarations and method calls, + * ensuring that tokens are only emitted for actual code elements and never for string literals + * or comments. Tokens are encoded using the LSP delta format. + */ +public class SemanticTokensProvider { + private final FileContentsTracker fileContentsTracker; + + // token types used in the legend (must match order below) + public static final List TOKEN_TYPES = Collections.unmodifiableList(Arrays.asList( + "namespace","class","enum","interface","struct","typeParameter","type", + "parameter","variable","property","enumMember","event","function","method", + "macro","keyword","modifier","comment","string","number","regexp","operator" + )); + + private final ASTNodeVisitor astVisitor; + + public SemanticTokensProvider(FileContentsTracker fileContentsTracker, ASTNodeVisitor astVisitor) { + this.fileContentsTracker = fileContentsTracker; + this.astVisitor = astVisitor; + } + + public SemanticTokensLegend getLegend() { + return new SemanticTokensLegend(TOKEN_TYPES, Collections.emptyList()); + } + + public SemanticTokens provideFull(TextDocumentIdentifier textDocument) { + URI uri = URI.create(textDocument.getUri()); + String text = fileContentsTracker.getContents(uri); + if (text == null || astVisitor == null || uri == null) { + return new SemanticTokens(new ArrayList<>()); + } + + List tokens = new ArrayList<>(); + Set emitted = new HashSet<>(); + List nodes = astVisitor.getNodes(uri); + + for (ASTNode node : nodes) { + // 0) Method calls: color method name when we can resolve a method on the receiver + if (node instanceof MethodCallExpression) { + processMethodCall((MethodCallExpression) node, tokens, emitted); + } + + // 1) Handle property/field access expressions (e.g., this.x, Closure.DELEGATE_FIRST) + if (node instanceof PropertyExpression) { + processPropertyExpression((PropertyExpression) node, tokens, emitted); + } + + // 2) Declarations: methods, variables, fields, properties, parameters + processDeclaration(node, text, tokens, emitted); + } + + if (tokens.isEmpty()) { + return new SemanticTokens(new ArrayList<>()); + } + + tokens.sort(Comparator.comparingInt((Token t) -> t.line).thenComparingInt(t -> t.startChar)); + + return encodeDeltaTokens(tokens); + } + + private void processMethodCall(MethodCallExpression call, List tokens, Set emitted) { + String methodName = call.getMethodAsString(); + if (methodName == null || methodName.isEmpty()) return; + + List overloads = GroovyASTUtils.getMethodOverloadsFromCallExpression(call, astVisitor); + if (overloads == null || overloads.isEmpty()) return; + + Range methodRange = GroovyLanguageServerUtils.astNodeToRange(call.getMethod()); + if (methodRange == null) return; + + Position pos = new Position(methodRange.getStart().getLine(), methodRange.getStart().getCharacter()); + String key = pos.line + ":" + pos.col + ":" + methodName + ":" + "method"; + if (emitted.add(key)) { + tokens.add(new Token(pos.line, pos.col, methodName.length(), tokenTypeIndex("method"))); + } + } + + private void processPropertyExpression(PropertyExpression pe, List tokens, Set emitted) { + String propName = pe.getPropertyAsString(); + if (propName == null || propName.isEmpty()) return; + + ASTNode propNode = (ASTNode) pe.getProperty(); + Range propRange = GroovyLanguageServerUtils.astNodeToRange(propNode); + if (propRange == null) return; + + boolean fieldExists = false; + + // Check 1: Check the enclosing class scope + ClassNode enclosing = (ClassNode) GroovyASTUtils.getEnclosingNodeOfType(pe, ClassNode.class, astVisitor); + if (enclosing != null) { + if (enclosing.getField(propName) != null || enclosing.getProperty(propName) != null) { + fieldExists = true; + } + } + + // Check 2: Check the target object expression's resolved type (Fixes Closure.DELEGATE_FIRST) + if (!fieldExists && pe.getObjectExpression() != null) { + ClassNode targetClass = (pe.getObjectExpression() instanceof ClassExpression) + ? pe.getObjectExpression().getType() + : GroovyASTUtils.getTypeOfNode(pe.getObjectExpression(), astVisitor); + + if (targetClass != null && (targetClass.getField(propName) != null || targetClass.getProperty(propName) != null)) { + fieldExists = true; + } + } + + if (!fieldExists) return; + + Position pos = new Position(propRange.getStart().getLine(), propRange.getStart().getCharacter()); + String key = pos.line + ":" + pos.col + ":" + propName + ":" + "property"; + if (emitted.add(key)) { + tokens.add(new Token(pos.line, pos.col, propName.length(), tokenTypeIndex("property"))); + } + } + + private void processDeclaration(ASTNode node, String text, List tokens, Set emitted) { + Range range = GroovyLanguageServerUtils.astNodeToRange(node); + if (range == null) return; + + if (node instanceof MethodNode && ((MethodNode) node).isConstructor()) { + processConstructorDeclaration((MethodNode) node, text, range, tokens, emitted); + return; + } + + String name = getDeclarationName(node); + if (name == null || "this".equals(name) || "super".equals(name)) return; + + int startOffset = lineColToOffset(text, range.getStart().getLine(), range.getStart().getCharacter()); + int endOffset = lineColToOffset(text, range.getEnd().getLine(), range.getEnd().getCharacter()); + if (startOffset < 0 || endOffset <= startOffset) return; + + int found = findExactTokenOffset(text, name, startOffset, endOffset); + if (found == -1) return; + + Position pos = toLineCol(text, found); + int tokenType = (node instanceof MethodNode) ? tokenTypeIndex("function") : tokenTypeIndex("variable"); + String key = pos.line + ":" + pos.col + ":" + name + ":" + tokenType; + if (emitted.add(key)) { + tokens.add(new Token(pos.line, pos.col, name.length(), tokenType)); + } + } + + private void processConstructorDeclaration(MethodNode mn, String text, Range range, List tokens, Set emitted) { + ClassNode declaringClass = mn.getDeclaringClass(); + String className = declaringClass != null ? declaringClass.getNameWithoutPackage() : null; + if (className == null || className.isEmpty()) return; + + int startOffsetCtor = lineColToOffset(text, range.getStart().getLine(), range.getStart().getCharacter()); + int endOffsetCtor = lineColToOffset(text, range.getEnd().getLine(), range.getEnd().getCharacter()); + + int foundCtor = findExactTokenOffset(text, className, startOffsetCtor, endOffsetCtor); + if (foundCtor == -1) return; + + Position posCtor = toLineCol(text, foundCtor); + int tokenTypeCtor = tokenTypeIndex("class"); + String keyCtor = posCtor.line + ":" + posCtor.col + ":" + className + ":" + tokenTypeCtor; + if (emitted.add(keyCtor)) { + tokens.add(new Token(posCtor.line, posCtor.col, className.length(), tokenTypeCtor)); + } + } + + private String getDeclarationName(ASTNode node) { + if (node instanceof MethodNode) return ((MethodNode) node).getName(); + if (node instanceof Variable) return ((Variable) node).getName(); + if (node instanceof FieldNode) return ((FieldNode) node).getName(); + if (node instanceof PropertyNode) return ((PropertyNode) node).getName(); + if (node instanceof Parameter) return ((Parameter) node).getName(); + return null; + } + + private int findExactTokenOffset(String text, String name, int startOffset, int endOffset) { + int found = startOffset; + while (found >= 0) { + found = text.indexOf(name, found); + if (found == -1 || found >= endOffset) { + return -1; + } + boolean beforeValid = (found == 0) || !Character.isJavaIdentifierPart(text.charAt(found - 1)); + boolean afterValid = (found + name.length() >= text.length()) || !Character.isJavaIdentifierPart(text.charAt(found + name.length())); + if (beforeValid && afterValid) { + return found; + } + found++; + } + return -1; + } + + private SemanticTokens encodeDeltaTokens(List tokens) { + List data = new ArrayList<>(); + int prevLine = 0; + int prevChar = 0; + boolean first = true; + for (Token t : tokens) { + int deltaLine = first ? t.line : t.line - prevLine; + int deltaStart = first ? t.startChar : (deltaLine == 0 ? t.startChar - prevChar : t.startChar); + data.add(deltaLine); + data.add(deltaStart); + data.add(t.length); + data.add(t.tokenType); + data.add(0); // modifiers bitset + + prevLine = t.line; + prevChar = t.startChar; + first = false; + } + return new SemanticTokens(data); + } + + private int lineColToOffset(String text, int line, int col) { + if (line < 0) return -1; + int curLine = 0; + int offset = 0; + int len = text.length(); + while (offset < len && curLine < line) { + if (text.charAt(offset) == '\n') { + curLine++; + } + offset++; + } + if (curLine != line) return -1; + return Math.min(offset + col, len); + } + + private int tokenTypeIndex(String type) { + int idx = TOKEN_TYPES.indexOf(type); + return idx >= 0 ? idx : 0; + } + + private static class Token { + final int line; + final int startChar; + final int length; + final int tokenType; + + Token(int line, int startChar, int length, int tokenType) { + this.line = line; + this.startChar = startChar; + this.length = length; + this.tokenType = tokenType; + } + } + + private static class Position { + final int line; + final int col; + + Position(int line, int col) { + this.line = line; + this.col = col; + } + } + + private Position toLineCol(String text, int offset) { + int line = 0; + int col = 0; + int i = 0; + while (i < offset) { + char c = text.charAt(i); + if (c == '\n') { + line++; + col = 0; + } else { + col++; + } + i++; + } + return new Position(line, col); + } +} \ No newline at end of file From 6ab5c93ebf66c67e6f73b6cdfbbe81d55d845541 Mon Sep 17 00:00:00 2001 From: trustytrojan <87675609+trustytrojan@users.noreply.github.com> Date: Tue, 26 May 2026 16:28:35 -0600 Subject: [PATCH 2/5] Send semantic tokens for class symbols/references, including within import statements --- .../groovyls/providers/SemanticTokensProvider.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java b/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java index 17652ae0..dd0a6d82 100644 --- a/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java @@ -20,6 +20,7 @@ import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.Variable; import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.ImportNode; import org.codehaus.groovy.ast.PropertyNode; import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.expr.ClassExpression; @@ -147,6 +148,7 @@ private void processPropertyExpression(PropertyExpression pe, List tokens } } + // probably should be named `processSymbol` and/or should be split up by type a bit more private void processDeclaration(ASTNode node, String text, List tokens, Set emitted) { Range range = GroovyLanguageServerUtils.astNodeToRange(node); if (range == null) return; @@ -167,13 +169,21 @@ private void processDeclaration(ASTNode node, String text, List tokens, S if (found == -1) return; Position pos = toLineCol(text, found); - int tokenType = (node instanceof MethodNode) ? tokenTypeIndex("function") : tokenTypeIndex("variable"); + int tokenType = tokenTypeIndexFromNode(node); String key = pos.line + ":" + pos.col + ":" + name + ":" + tokenType; if (emitted.add(key)) { tokens.add(new Token(pos.line, pos.col, name.length(), tokenType)); } } + private int tokenTypeIndexFromNode(ASTNode node) { + if (node instanceof MethodNode) + return tokenTypeIndex("function"); + if (node instanceof ClassNode || node instanceof ImportNode) + return tokenTypeIndex("class"); + return tokenTypeIndex("variable"); + } + private void processConstructorDeclaration(MethodNode mn, String text, Range range, List tokens, Set emitted) { ClassNode declaringClass = mn.getDeclaringClass(); String className = declaringClass != null ? declaringClass.getNameWithoutPackage() : null; @@ -199,6 +209,8 @@ private String getDeclarationName(ASTNode node) { if (node instanceof FieldNode) return ((FieldNode) node).getName(); if (node instanceof PropertyNode) return ((PropertyNode) node).getName(); if (node instanceof Parameter) return ((Parameter) node).getName(); + if (node instanceof ClassNode) return ((ClassNode) node).getName(); + if (node instanceof ImportNode) return ((ImportNode) node).getClassName(); return null; } From c4afe6709f20b7b9aee5668310d9c32504deb6ac Mon Sep 17 00:00:00 2001 From: trustytrojan <87675609+trustytrojan@users.noreply.github.com> Date: Wed, 27 May 2026 07:55:36 -0600 Subject: [PATCH 3/5] Add/modify copyright notices --- .../groovyls/GroovyLanguageServer.java | 2 ++ .../net/prominic/groovyls/GroovyServices.java | 2 ++ .../providers/SemanticTokensProvider.java | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java b/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java index f1a29269..14446700 100644 --- a/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java +++ b/src/main/java/net/prominic/groovyls/GroovyLanguageServer.java @@ -1,5 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // Copyright 2022 Prominic.NET, Inc. +// Copyright 2026 trustytrojan // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,6 +15,7 @@ // limitations under the License // // Author: Prominic.NET, Inc. +// Author: trustytrojan // No warranty of merchantability or fitness of any kind. // Use this software at your own risk. //////////////////////////////////////////////////////////////////////////////// diff --git a/src/main/java/net/prominic/groovyls/GroovyServices.java b/src/main/java/net/prominic/groovyls/GroovyServices.java index a47c5c1b..82b4014b 100644 --- a/src/main/java/net/prominic/groovyls/GroovyServices.java +++ b/src/main/java/net/prominic/groovyls/GroovyServices.java @@ -1,5 +1,6 @@ //////////////////////////////////////////////////////////////////////////////// // Copyright 2022 Prominic.NET, Inc. +// Copyright 2026 trustytrojan // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,6 +15,7 @@ // limitations under the License // // Author: Prominic.NET, Inc. +// Author: trustytrojan // No warranty of merchantability or fitness of any kind. // Use this software at your own risk. //////////////////////////////////////////////////////////////////////////////// diff --git a/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java b/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java index dd0a6d82..fd831310 100644 --- a/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java @@ -1,3 +1,22 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright 2026 trustytrojan +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License +// +// Author: trustytrojan +// No warranty of merchantability or fitness of any kind. +// Use this software at your own risk. +//////////////////////////////////////////////////////////////////////////////// package net.prominic.groovyls.providers; import java.net.URI; From 11e7bcd9b86155ca60aa8b8c80e2a833937e6c72 Mon Sep 17 00:00:00 2001 From: trustytrojan <87675609+trustytrojan@users.noreply.github.com> Date: Wed, 27 May 2026 11:01:19 -0600 Subject: [PATCH 4/5] Don't color in undefined nodes, color all Closure variables as functions --- .../providers/SemanticTokensProvider.java | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java b/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java index fd831310..baea577b 100644 --- a/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java @@ -36,13 +36,13 @@ import net.prominic.groovyls.util.FileContentsTracker; import net.prominic.groovyls.compiler.ast.ASTNodeVisitor; import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.MethodNode; import org.codehaus.groovy.ast.Variable; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.ImportNode; import org.codehaus.groovy.ast.PropertyNode; import org.codehaus.groovy.ast.Parameter; -import org.codehaus.groovy.ast.expr.ClassExpression; import org.codehaus.groovy.ast.expr.PropertyExpression; import org.codehaus.groovy.ast.ClassNode; import net.prominic.groovyls.compiler.util.GroovyASTUtils; @@ -139,20 +139,9 @@ private void processPropertyExpression(PropertyExpression pe, List tokens boolean fieldExists = false; - // Check 1: Check the enclosing class scope - ClassNode enclosing = (ClassNode) GroovyASTUtils.getEnclosingNodeOfType(pe, ClassNode.class, astVisitor); - if (enclosing != null) { - if (enclosing.getField(propName) != null || enclosing.getProperty(propName) != null) { - fieldExists = true; - } - } - - // Check 2: Check the target object expression's resolved type (Fixes Closure.DELEGATE_FIRST) + // Check the target object expression's ClassNode for a field/property of the same name if (!fieldExists && pe.getObjectExpression() != null) { - ClassNode targetClass = (pe.getObjectExpression() instanceof ClassExpression) - ? pe.getObjectExpression().getType() - : GroovyASTUtils.getTypeOfNode(pe.getObjectExpression(), astVisitor); - + ClassNode targetClass = GroovyASTUtils.getTypeOfNode(pe.getObjectExpression(), astVisitor); if (targetClass != null && (targetClass.getField(propName) != null || targetClass.getProperty(propName) != null)) { fieldExists = true; } @@ -169,6 +158,9 @@ private void processPropertyExpression(PropertyExpression pe, List tokens // probably should be named `processSymbol` and/or should be split up by type a bit more private void processDeclaration(ASTNode node, String text, List tokens, Set emitted) { + // undefined variables should be uncolored + if (GroovyASTUtils.getDefinition(node, true, astVisitor) == null) return; + Range range = GroovyLanguageServerUtils.astNodeToRange(node); if (range == null) return; @@ -196,7 +188,8 @@ private void processDeclaration(ASTNode node, String text, List tokens, S } private int tokenTypeIndexFromNode(ASTNode node) { - if (node instanceof MethodNode) + if (node instanceof MethodNode + || GroovyASTUtils.getTypeOfNode(node, astVisitor).equals(ClassHelper.CLOSURE_TYPE)) return tokenTypeIndex("function"); if (node instanceof ClassNode || node instanceof ImportNode) return tokenTypeIndex("class"); From 9f75c4c3262ac63e1dd4cfb90e1934c4d2acc48e Mon Sep 17 00:00:00 2001 From: trustytrojan <87675609+trustytrojan@users.noreply.github.com> Date: Wed, 27 May 2026 12:59:50 -0600 Subject: [PATCH 5/5] let's not cause the same problem in `HoverProvider` --- .../prominic/groovyls/providers/SemanticTokensProvider.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java b/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java index baea577b..60ba704d 100644 --- a/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java +++ b/src/main/java/net/prominic/groovyls/providers/SemanticTokensProvider.java @@ -158,9 +158,6 @@ private void processPropertyExpression(PropertyExpression pe, List tokens // probably should be named `processSymbol` and/or should be split up by type a bit more private void processDeclaration(ASTNode node, String text, List tokens, Set emitted) { - // undefined variables should be uncolored - if (GroovyASTUtils.getDefinition(node, true, astVisitor) == null) return; - Range range = GroovyLanguageServerUtils.astNodeToRange(node); if (range == null) return;