[ 
https://issues.apache.org/jira/browse/GROOVY-12411?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18126056#comment-18126056
 ] 

ASF GitHub Bot commented on GROOVY-12411:
-----------------------------------------

Copilot commented on code in PR #2936:
URL: https://github.com/apache/groovy/pull/2936#discussion_r4241503202


##########
subprojects/groovy-lsp/src/main/java/org/apache/groovy/lsp/internal/LanguageServerContext.java:
##########
@@ -0,0 +1,513 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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.
+ */
+package org.apache.groovy.lsp.internal;
+
+import org.apache.groovy.lsp.internal.compile.CompilationSnapshot;
+import org.apache.groovy.lsp.internal.compile.CompiledDocument;
+import org.apache.groovy.lsp.internal.compile.CompilerSettings;
+import org.apache.groovy.lsp.internal.compile.GroovyCompiler;
+import org.apache.groovy.lsp.internal.compile.WorkspaceScanner;
+import org.apache.groovy.lsp.internal.diagnostic.DiagnosticConverter;
+import org.apache.groovy.lsp.internal.position.PositionEncoding;
+import org.apache.groovy.lsp.internal.compile.ImportSupport;
+import org.apache.groovy.lsp.internal.compile.WorkspaceLayout;
+import org.apache.groovy.lsp.internal.protocol.ClientFeatures;
+import org.apache.groovy.lsp.internal.util.Uris;
+import org.apache.groovy.lsp.internal.workspace.DocumentStore;
+import org.apache.groovy.lsp.internal.workspace.TextDocument;
+
+import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
+import org.eclipse.lsp4j.WorkspaceEdit;
+import org.eclipse.lsp4j.ClientCapabilities;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.ProgressParams;
+import org.eclipse.lsp4j.PublishDiagnosticsParams;
+import org.eclipse.lsp4j.WorkDoneProgressBegin;
+import org.eclipse.lsp4j.WorkDoneProgressCreateParams;
+import org.eclipse.lsp4j.WorkDoneProgressEnd;
+import org.eclipse.lsp4j.jsonrpc.messages.Either;
+import org.eclipse.lsp4j.services.LanguageClient;
+
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Mutable session state for one language-server process.
+ */
+public final class LanguageServerContext {
+
+    private final DocumentStore documents = new DocumentStore();
+    private final GroovyCompiler compiler = new GroovyCompiler();
+    private final WorkspaceScanner scanner = new WorkspaceScanner();
+    private final DiagnosticConverter diagnostics = new DiagnosticConverter();
+    private final Object compileLock = new Object();
+
+    private final AtomicReference<LanguageClient> client = new 
AtomicReference<>();
+    private final AtomicReference<ClientCapabilities> clientCapabilities = new 
AtomicReference<>();
+    private final AtomicReference<PositionEncoding> positionEncoding = new 
AtomicReference<>(PositionEncoding.UTF16);
+    private final AtomicReference<CompilerSettings> settings = new 
AtomicReference<>(CompilerSettings.defaults());
+    private final AtomicReference<CompilationSnapshot> snapshot = new 
AtomicReference<>(CompilationSnapshot.EMPTY);
+    private final AtomicReference<ClassLoader> parentLoader = new 
AtomicReference<>();
+    private volatile boolean initialized;
+    private volatile boolean shutdown;
+    private volatile Integer exitCode;
+    private final Set<URI> workspaceFolders = Collections.synchronizedSet(new 
LinkedHashSet<>());
+
+    private final ScheduledExecutorService debounce = 
Executors.newSingleThreadScheduledExecutor(r -> {
+        Thread thread = new Thread(r, "groovy-lsp-compile");
+        thread.setDaemon(true);
+        return thread;
+    });
+    private ScheduledFuture<?> pendingCompile;
+    private long compileGeneration;
+    private String compileFingerprint;
+
+    /**
+     * Creates a session.
+     */
+    public LanguageServerContext() {
+        // Fields are initialized at declaration; the constructor exists so 
tests can build a session directly.
+    }
+
+    /**
+     * Compiles open documents plus workspace sources and publishes 
diagnostics.
+     * Unchanged inputs reuse the last snapshot.
+     */
+    public CompilationSnapshot recompile() {
+        synchronized (compileLock) {
+            final long generation;
+            final CompilerSettings effective;
+            final List<Path> extra;
+            final Collection<TextDocument> open;
+            final ClassLoader loader;
+            final String fingerprint;
+            final boolean reuse;
+            synchronized (this) {
+                if (shutdown) {
+                    return snapshot.get();
+                }
+                if (pendingCompile != null) {
+                    pendingCompile.cancel(false);
+                    pendingCompile = null;
+                }
+                List<URI> folders;
+                synchronized (workspaceFolders) {
+                    folders = new ArrayList<>(workspaceFolders);
+                }
+                effective = WorkspaceLayout.withInferred(settings.get(), 
folders);
+                extra = scanner.scan(folders, effective.getSourcePaths());
+                open = documents.snapshot();
+                loader = compileParentLoader();
+                fingerprint = fingerprint(open, extra, effective, loader);
+                if (fingerprint.equals(compileFingerprint) && snapshot.get() 
!= CompilationSnapshot.EMPTY) {
+                    // Another compile already committed this snapshot but may
+                    // not have published yet. Fall through to 
publishDiagnostics.
+                    reuse = true;
+                    generation = compileGeneration;
+                } else {
+                    reuse = false;
+                    generation = ++compileGeneration;
+                }
+            }
+            if (!reuse) {
+                String token = "groovy-compile-" + generation;
+                beginProgress(token, "Compiling Groovy");
+                CompilationSnapshot compiled;
+                try {
+                    compiled = compiler.compile(open, extra, effective, 
loader);
+                } finally {
+                    endProgress(token);
+                }
+                synchronized (this) {
+                    if (generation == compileGeneration) {
+                        compileFingerprint = fingerprint;
+                        snapshot.set(compiled);

Review Comment:
   Replacing the snapshot drops URIs for deleted files or removed workspace 
folders, but `publishDiagnostics()` only publishes documents in the new 
snapshot. Any diagnostics previously sent for a dropped URI remain visible 
indefinitely; compare old/new snapshot URIs and publish an empty diagnostic 
list for removed entries.



##########
subprojects/groovy-lsp/src/main/java/org/apache/groovy/lsp/internal/compile/JavaSymbolIndex.java:
##########
@@ -0,0 +1,263 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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.
+ */
+package org.apache.groovy.lsp.internal.compile;
+
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.tree.LineMap;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.util.SourcePositions;
+import com.sun.source.util.TreePathScanner;
+import com.sun.source.util.Trees;
+import org.apache.groovy.lsp.internal.util.Uris;
+
+import javax.lang.model.element.Name;
+import javax.tools.JavaFileObject;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Types and members compiled from workspace {@code .java} sources.
+ * Coordinates are Groovy 1-based carets.
+ */
+public final class JavaSymbolIndex {
+
+    public static final JavaSymbolIndex EMPTY = new JavaSymbolIndex(Map.of(), 
Map.of(), Map.of());
+
+    private final Map<String, JavaSymbol> byName;
+    private final Map<String, List<JavaSymbol>> bySimpleName;
+    private final Map<String, List<JavaSymbol>> members;
+
+    private JavaSymbolIndex(final Map<String, JavaSymbol> byName, final 
Map<String, List<JavaSymbol>> bySimpleName,
+                            final Map<String, List<JavaSymbol>> members) {
+        this.byName = byName;
+        this.bySimpleName = bySimpleName;
+        this.members = members;
+    }
+
+    /**
+     * Walks {@code units} using {@code trees} captured from the javac task
+     * before {@code generate()}.
+     *
+     * @param trees javac tree API
+     * @param units parsed compilation units
+     * @return an index, never {@code null}
+     */
+    public static JavaSymbolIndex of(final Trees trees, final Iterable<? 
extends CompilationUnitTree> units) {
+        if (trees == null || units == null) {
+            return EMPTY;
+        }
+        Map<String, JavaSymbol> names = new LinkedHashMap<>();
+        Map<String, List<JavaSymbol>> simple = new LinkedHashMap<>();
+        Map<String, List<JavaSymbol>> members = new LinkedHashMap<>();
+        SourcePositions positions = trees.getSourcePositions();
+        for (CompilationUnitTree unit : units) {
+            if (unit == null || isStub(unit.getSourceFile())) {
+                continue;
+            }
+            URI uri = Uris.normalize(unit.getSourceFile().toUri());
+            LineMap lineMap = unit.getLineMap();
+            new IndexingScanner(uri, unit, positions, lineMap, new 
IndexMaps(names, simple, members)).scan(unit, null);
+        }
+        return new JavaSymbolIndex(Collections.unmodifiableMap(new 
LinkedHashMap<>(names)),
+                copyLists(simple), copyLists(members));
+    }
+
+    /**
+     * @param qualifiedName FQCN
+     * @return the type, or {@code null}
+     */
+    public JavaSymbol type(final String qualifiedName) {
+        return qualifiedName == null ? null : byName.get(qualifiedName);
+    }
+
+    /**
+     * @return every indexed type, in compilation-unit order
+     */
+    public List<JavaSymbol> types() {
+        return List.copyOf(byName.values());
+    }
+
+    /**
+     * @param simpleName unqualified name
+     * @return hits, possibly empty
+     */
+    public List<JavaSymbol> typesNamed(final String simpleName) {
+        if (simpleName == null) {
+            return List.of();
+        }
+        List<JavaSymbol> hits = bySimpleName.get(simpleName);
+        return hits == null ? List.of() : hits;
+    }
+
+    /**
+     * Members of {@code owner} with {@code simpleName}.
+     *
+     * @param owner FQCN
+     * @param simpleName member name
+     * @return hits, possibly empty
+     */
+    public List<JavaSymbol> members(final String owner, final String 
simpleName) {
+        if (owner == null || simpleName == null) {
+            return List.of();
+        }
+        List<JavaSymbol> hits = members.get(owner + "#" + simpleName);
+        return hits == null ? List.of() : hits;
+    }
+
+    private static boolean isStub(final JavaFileObject file) {
+        if (file == null || file.toUri() == null) {
+            return true;
+        }
+        String scheme = file.toUri().getScheme();
+        return scheme != null && "string".equalsIgnoreCase(scheme);
+    }
+
+    private static Map<String, List<JavaSymbol>> copyLists(final Map<String, 
List<JavaSymbol>> source) {
+        Map<String, List<JavaSymbol>> copy = new LinkedHashMap<>();
+        for (Map.Entry<String, List<JavaSymbol>> entry : source.entrySet()) {
+            copy.put(entry.getKey(), List.copyOf(entry.getValue()));
+        }
+        return Map.copyOf(copy);
+    }
+
+    /**
+     * One Java type or member.
+     *
+     * @param qualifiedName FQCN for types; {@code owner#name} for members
+     * @param simpleName identifier
+     * @param owner FQCN of the enclosing type, or {@code null} for a 
top-level type
+     * @param uri source file
+     * @param line 1-based start line
+     * @param column 1-based start column
+     * @param endLine 1-based end line
+     * @param endColumn exclusive 1-based end column
+     */
+    public record JavaSymbol(String qualifiedName, String simpleName, String 
owner, URI uri,
+                             int line, int column, int endLine, int endColumn) 
{
+    }
+
+    private record IndexMaps(Map<String, JavaSymbol> names, Map<String, 
List<JavaSymbol>> simple,
+                             Map<String, List<JavaSymbol>> members) {
+    }
+
+    private static final class IndexingScanner extends TreePathScanner<Void, 
Void> {
+        private final URI uri;
+        private final CompilationUnitTree unit;
+        private final SourcePositions positions;
+        private final LineMap lineMap;
+        private final IndexMaps maps;
+        private final List<String> enclosing = new ArrayList<>();
+
+        private IndexingScanner(final URI uri, final CompilationUnitTree unit, 
final SourcePositions positions,
+                                final LineMap lineMap, final IndexMaps maps) {
+            this.uri = uri;
+            this.unit = unit;
+            this.positions = positions;
+            this.lineMap = lineMap;
+            this.maps = maps;
+        }
+
+        @Override
+        public Void visitClass(final ClassTree node, final Void unused) {
+            String simpleName = text(node.getSimpleName());
+            if (simpleName.isEmpty()) {
+                return super.visitClass(node, unused);
+            }
+            String qualified = qualified(simpleName);
+            JavaSymbol symbol = symbol(qualified, simpleName, 
enclosing.isEmpty() ? null : enclosing.get(enclosing.size() - 1),
+                    node);
+            maps.names().put(qualified, symbol);
+            maps.simple().computeIfAbsent(simpleName, key -> new 
ArrayList<>()).add(symbol);
+            enclosing.add(qualified);
+            try {
+                return super.visitClass(node, unused);
+            } finally {
+                enclosing.remove(enclosing.size() - 1);
+            }
+        }
+
+        @Override
+        public Void visitMethod(final MethodTree node, final Void unused) {
+            addMember(text(node.getName()), node);
+            return super.visitMethod(node, unused);
+        }
+
+        @Override
+        public Void visitVariable(final VariableTree node, final Void unused) {
+            Tree.Kind parent = getCurrentPath() == null || 
getCurrentPath().getParentPath() == null
+                    ? null : 
getCurrentPath().getParentPath().getLeaf().getKind();
+            if (parent == Tree.Kind.CLASS || parent == Tree.Kind.INTERFACE || 
parent == Tree.Kind.ENUM
+                    || parent == Tree.Kind.RECORD) {
+                addMember(text(node.getName()), node);
+            }
+            return super.visitVariable(node, unused);
+        }
+
+        private void addMember(final String simpleName, final Tree node) {
+            if (simpleName.isEmpty() || "<init>".equals(simpleName) || 
enclosing.isEmpty()) {
+                return;
+            }
+            String owner = enclosing.get(enclosing.size() - 1);
+            JavaSymbol symbol = symbol(owner + "#" + simpleName, simpleName, 
owner, node);
+            maps.members().computeIfAbsent(owner + "#" + simpleName, key -> 
new ArrayList<>()).add(symbol);
+        }
+
+        private String qualified(final String simpleName) {
+            if (enclosing.isEmpty()) {
+                String pkg = packageName();
+                return pkg.isEmpty() ? simpleName : pkg + "." + simpleName;
+            }
+            return enclosing.get(enclosing.size() - 1) + "$" + simpleName;
+        }
+
+        private String packageName() {
+            if (unit.getPackageName() == null) {
+                return "";
+            }
+            return unit.getPackageName().toString();
+        }
+
+        private JavaSymbol symbol(final String qualified, final String 
simpleName, final String owner, final Tree node) {
+            long start = positions.getStartPosition(unit, node);
+            long end = positions.getEndPosition(unit, node);
+            if (start < 0) {
+                start = 0;
+            }
+            if (end < start) {
+                end = start;
+            }
+            int line = (int) lineMap.getLineNumber(start);
+            int column = (int) lineMap.getColumnNumber(start);
+            int endLine = (int) lineMap.getLineNumber(end);
+            int endColumn = (int) lineMap.getColumnNumber(end);
+            return new JavaSymbol(qualified, simpleName, owner, uri, line, 
column, endLine, endColumn);

Review Comment:
   Javac `LineMap` columns are UTF-16 columns, while `JavaSymbol` documents 
them as Groovy code-point columns and navigation later passes them through 
`Positions.toRange` as code points. Definitions after supplementary characters 
are therefore shifted; convert using the Java source line or preserve the 
UTF-16 coordinate semantics through link construction.



##########
subprojects/groovy-lsp/src/main/java/org/apache/groovy/lsp/internal/LanguageServerContext.java:
##########
@@ -0,0 +1,513 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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.
+ */
+package org.apache.groovy.lsp.internal;
+
+import org.apache.groovy.lsp.internal.compile.CompilationSnapshot;
+import org.apache.groovy.lsp.internal.compile.CompiledDocument;
+import org.apache.groovy.lsp.internal.compile.CompilerSettings;
+import org.apache.groovy.lsp.internal.compile.GroovyCompiler;
+import org.apache.groovy.lsp.internal.compile.WorkspaceScanner;
+import org.apache.groovy.lsp.internal.diagnostic.DiagnosticConverter;
+import org.apache.groovy.lsp.internal.position.PositionEncoding;
+import org.apache.groovy.lsp.internal.compile.ImportSupport;
+import org.apache.groovy.lsp.internal.compile.WorkspaceLayout;
+import org.apache.groovy.lsp.internal.protocol.ClientFeatures;
+import org.apache.groovy.lsp.internal.util.Uris;
+import org.apache.groovy.lsp.internal.workspace.DocumentStore;
+import org.apache.groovy.lsp.internal.workspace.TextDocument;
+
+import org.eclipse.lsp4j.ApplyWorkspaceEditParams;
+import org.eclipse.lsp4j.WorkspaceEdit;
+import org.eclipse.lsp4j.ClientCapabilities;
+import org.eclipse.lsp4j.Diagnostic;
+import org.eclipse.lsp4j.ProgressParams;
+import org.eclipse.lsp4j.PublishDiagnosticsParams;
+import org.eclipse.lsp4j.WorkDoneProgressBegin;
+import org.eclipse.lsp4j.WorkDoneProgressCreateParams;
+import org.eclipse.lsp4j.WorkDoneProgressEnd;
+import org.eclipse.lsp4j.jsonrpc.messages.Either;
+import org.eclipse.lsp4j.services.LanguageClient;
+
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Mutable session state for one language-server process.
+ */
+public final class LanguageServerContext {
+
+    private final DocumentStore documents = new DocumentStore();
+    private final GroovyCompiler compiler = new GroovyCompiler();
+    private final WorkspaceScanner scanner = new WorkspaceScanner();
+    private final DiagnosticConverter diagnostics = new DiagnosticConverter();
+    private final Object compileLock = new Object();
+
+    private final AtomicReference<LanguageClient> client = new 
AtomicReference<>();
+    private final AtomicReference<ClientCapabilities> clientCapabilities = new 
AtomicReference<>();
+    private final AtomicReference<PositionEncoding> positionEncoding = new 
AtomicReference<>(PositionEncoding.UTF16);
+    private final AtomicReference<CompilerSettings> settings = new 
AtomicReference<>(CompilerSettings.defaults());
+    private final AtomicReference<CompilationSnapshot> snapshot = new 
AtomicReference<>(CompilationSnapshot.EMPTY);
+    private final AtomicReference<ClassLoader> parentLoader = new 
AtomicReference<>();
+    private volatile boolean initialized;
+    private volatile boolean shutdown;
+    private volatile Integer exitCode;
+    private final Set<URI> workspaceFolders = Collections.synchronizedSet(new 
LinkedHashSet<>());
+
+    private final ScheduledExecutorService debounce = 
Executors.newSingleThreadScheduledExecutor(r -> {
+        Thread thread = new Thread(r, "groovy-lsp-compile");
+        thread.setDaemon(true);
+        return thread;
+    });
+    private ScheduledFuture<?> pendingCompile;
+    private long compileGeneration;
+    private String compileFingerprint;
+
+    /**
+     * Creates a session.
+     */
+    public LanguageServerContext() {
+        // Fields are initialized at declaration; the constructor exists so 
tests can build a session directly.
+    }
+
+    /**
+     * Compiles open documents plus workspace sources and publishes 
diagnostics.
+     * Unchanged inputs reuse the last snapshot.
+     */
+    public CompilationSnapshot recompile() {
+        synchronized (compileLock) {
+            final long generation;
+            final CompilerSettings effective;
+            final List<Path> extra;
+            final Collection<TextDocument> open;
+            final ClassLoader loader;
+            final String fingerprint;
+            final boolean reuse;
+            synchronized (this) {
+                if (shutdown) {
+                    return snapshot.get();
+                }
+                if (pendingCompile != null) {
+                    pendingCompile.cancel(false);
+                    pendingCompile = null;
+                }
+                List<URI> folders;
+                synchronized (workspaceFolders) {
+                    folders = new ArrayList<>(workspaceFolders);
+                }
+                effective = WorkspaceLayout.withInferred(settings.get(), 
folders);
+                extra = scanner.scan(folders, effective.getSourcePaths());
+                open = documents.snapshot();
+                loader = compileParentLoader();
+                fingerprint = fingerprint(open, extra, effective, loader);
+                if (fingerprint.equals(compileFingerprint) && snapshot.get() 
!= CompilationSnapshot.EMPTY) {
+                    // Another compile already committed this snapshot but may
+                    // not have published yet. Fall through to 
publishDiagnostics.
+                    reuse = true;
+                    generation = compileGeneration;
+                } else {
+                    reuse = false;
+                    generation = ++compileGeneration;
+                }
+            }
+            if (!reuse) {
+                String token = "groovy-compile-" + generation;
+                beginProgress(token, "Compiling Groovy");
+                CompilationSnapshot compiled;
+                try {
+                    compiled = compiler.compile(open, extra, effective, 
loader);
+                } finally {
+                    endProgress(token);
+                }
+                synchronized (this) {
+                    if (generation == compileGeneration) {
+                        compileFingerprint = fingerprint;
+                        snapshot.set(compiled);
+                    }
+                }
+            }
+        }
+        publishDiagnostics();
+        return snapshot.get();
+    }
+
+    /**
+     * Schedules a recompile after {@code delayMs} of quiet.
+     *
+     * @param delayMs debounce delay
+     */
+    public synchronized void scheduleRecompile(final long delayMs) {
+        if (shutdown || debounce.isShutdown()) {
+            return;
+        }
+        if (pendingCompile != null) {
+            pendingCompile.cancel(false);
+        }
+        pendingCompile = debounce.schedule(this::recompile, delayMs, 
TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Publishes diagnostics for every compiled document.
+     */
+    public void publishDiagnostics() {
+        LanguageClient languageClient = client.get();
+        if (languageClient == null) {
+            return;
+        }
+        for (var document : snapshot.get().documents()) {
+            languageClient.publishDiagnostics(new PublishDiagnosticsParams(
+                    document.getUri().toString(), diagnosticsFor(document)));
+        }

Review Comment:
   These pushed diagnostics omit the document version. Because compilation runs 
asynchronously, a newer `didChange` can arrive while an older snapshot is 
compiling, and the client cannot identify and discard the stale publication. 
Set `PublishDiagnosticsParams.version` for open-document versions.



##########
subprojects/groovy-lsp/src/main/java/org/apache/groovy/lsp/internal/feature/CompletionService.java:
##########
@@ -0,0 +1,519 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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.
+ */
+package org.apache.groovy.lsp.internal.feature;
+
+import groovy.lang.groovydoc.Groovydoc;
+import org.apache.groovy.lsp.internal.compile.AstQuery;
+import org.apache.groovy.lsp.internal.compile.CompilationSnapshot;
+import org.apache.groovy.lsp.internal.compile.CompiledDocument;
+import org.apache.groovy.lsp.internal.compile.ImportSupport;
+import org.apache.groovy.lsp.internal.compile.TypeIndex;
+import org.apache.groovy.lsp.internal.position.PositionEncoding;
+import org.apache.groovy.lsp.internal.position.Positions;
+import org.apache.groovy.lsp.internal.util.GroovyKeywords;
+import org.apache.groovy.lsp.internal.workspace.TextDocument;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotatedNode;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.ModuleNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.PropertyNode;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MapEntryExpression;
+import org.codehaus.groovy.ast.expr.MapExpression;
+import org.codehaus.groovy.ast.expr.PropertyExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
+import org.eclipse.lsp4j.CompletionItem;
+import org.eclipse.lsp4j.CompletionItemKind;
+import org.eclipse.lsp4j.CompletionList;
+import org.eclipse.lsp4j.InsertTextFormat;
+import org.eclipse.lsp4j.MarkupContent;
+import org.eclipse.lsp4j.MarkupKind;
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.jsonrpc.messages.Either;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Keyword, snippet, member, workspace-type and GDK completions.
+ * Unimported workspace types carry {@code additionalTextEdits}.
+ */
+public final class CompletionService {
+
+    static final int MAX_ITEMS = 80;
+    private static final List<String> GDK_NAMES = gdkMethodNames();
+
+    /**
+     * Completes at {@code position} in {@code document}.
+     *
+     * @param document open document
+     * @param snapshot latest compile
+     * @param position LSP position
+     * @param encoding negotiated encoding
+     * @return a completion list (incomplete when truncated)
+     */
+    public CompletionList complete(final TextDocument document, final 
CompilationSnapshot snapshot,
+                                   final Position position, final 
PositionEncoding encoding) {
+        List<CompletionItem> items = new ArrayList<>();
+        String prefix = prefixAt(document, position, encoding);
+        boolean memberAccess = isMemberAccess(document, position, encoding);
+
+        CompiledDocument compiled = snapshot == null ? null : 
snapshot.get(document);
+        ModuleNode module = compiled == null ? null : compiled.getModule();
+
+        if (!memberAccess) {
+            addMatching(items, GroovyKeywords.KEYWORDS, prefix, 
CompletionItemKind.Keyword);
+            addSnippets(items, prefix);
+        }
+
+        if (module != null) {
+            addFromAst(items, document, compiled, snapshot, position, 
encoding, module);
+        } else if (memberAccess && prefix.length() >= 2) {
+            addGdk(items, prefix);
+        }
+
+        boolean incomplete = items.size() > MAX_ITEMS;
+        if (incomplete) {
+            items = new ArrayList<>(items.subList(0, MAX_ITEMS));
+        }
+        return new CompletionList(incomplete, items);
+    }
+
+    /**
+     * Fills documentation from the compiled snapshot using {@code item.data}.
+     *
+     * @param item unresolved item
+     * @param snapshot latest compile
+     * @return the same item, possibly with documentation
+     */
+    public CompletionItem resolve(final CompletionItem item, final 
CompilationSnapshot snapshot) {
+        if (item == null || item.getData() == null || snapshot == null) {
+            return item;
+        }
+        Map<String, String> data = stringMap(item.getData());
+        if (data.isEmpty()) {
+            return item;
+        }
+        String kind = data.get("k");
+        String name = data.get("n");
+        if ("t".equals(kind) && name != null) {
+            TypeIndex.TypeHit hit = snapshot.types().byName(name);
+            if (hit != null && !hit.documentation().isEmpty()) {
+                item.setDocumentation(Either.forRight(new 
MarkupContent(MarkupKind.MARKDOWN, hit.documentation())));
+            }
+        }
+        if ("m".equals(kind) && name != null) {
+            String owner = data.get("o");
+            MethodNode method = MethodBinding.findMethod(snapshot, owner, 
name, data.get("d"));
+            if (method != null) {
+                String docs = documentation(method);
+                if (!docs.isEmpty()) {
+                    item.setDocumentation(Either.forRight(new 
MarkupContent(MarkupKind.MARKDOWN, docs)));
+                }
+            }
+        }
+        return item;
+    }
+
+    static String prefixAt(final TextDocument document, final Position 
position,
+                           final PositionEncoding encoding) {
+        if (document == null || position == null) {
+            return "";
+        }
+        int[] groovy = document.toGroovy(position, encoding);
+        String line = Positions.lineText(document.getText(), groovy[0]);
+        int end = caretOffset(line, groovy[1]);
+        int start = skipIdentifierBack(line, end);
+        return line.substring(start, end);
+    }
+
+    static boolean isMemberAccess(final TextDocument document, final Position 
position,
+                                  final PositionEncoding encoding) {
+        if (document == null || position == null) {
+            return false;
+        }
+        int[] groovy = document.toGroovy(position, encoding);
+        String line = Positions.lineText(document.getText(), groovy[0]);
+        int idx = skipWhitespaceBack(line, caretOffset(line, groovy[1]));
+        idx = skipIdentifierBack(line, idx);
+        return idx > 0 && line.codePointBefore(idx) == '.';
+    }
+
+    private static int caretOffset(final String line, final int groovyColumn) {
+        int col = Math.max(groovyColumn - 1, 0);
+        int cpCount = line.codePointCount(0, line.length());
+        if (col > cpCount) {
+            col = cpCount;
+        }
+        return line.offsetByCodePoints(0, col);
+    }
+
+    private static int skipWhitespaceBack(final String line, int idx) {
+        while (idx > 0 && Character.isWhitespace(line.codePointBefore(idx))) {
+            idx -= Character.charCount(line.codePointBefore(idx));
+        }
+        return idx;
+    }
+
+    private static int skipIdentifierBack(final String line, int idx) {
+        while (idx > 0) {
+            int cp = line.codePointBefore(idx);
+            if (!Character.isJavaIdentifierPart(cp) && cp != '$') {
+                break;
+            }
+            idx -= Character.charCount(cp);
+        }
+        return idx;
+    }
+
+    private static void addOverrideCompletions(final List<CompletionItem> 
items, final ModuleNode module,
+                                               final TextDocument document, 
final Position position,
+                                               final PositionEncoding 
encoding, final String prefix) {
+        ClassNode enclosing = enclosingClass(module, document, position, 
encoding);
+        if (enclosing == null || enclosing.isInterface()) {
+            return;
+        }
+        for (MethodNode method : 
CodeActionService.missingAbstracts(enclosing)) {
+            if (!prefixMatches(method.getName(), prefix)) {
+                continue;
+            }
+            CompletionItem item = new CompletionItem(method.getName());
+            item.setKind(CompletionItemKind.Method);
+            item.setDetail("override " + signature(method));
+            item.setSortText("0" + method.getName());
+            item.setInsertText(CodeActionService.overrideSnippet(method));
+            item.setInsertTextFormat(InsertTextFormat.Snippet);
+            items.add(item);
+        }
+    }
+
+    private static ClassNode enclosingClass(final ModuleNode module, final 
TextDocument document,
+                                            final Position position, final 
PositionEncoding encoding) {
+        ClassNode enclosing = null;
+        for (ASTNode node : AstQuery.containing(module, document, position, 
encoding)) {
+            if (node instanceof ClassNode classNode) {
+                enclosing = classNode;
+            }
+        }
+        return enclosing;

Review Comment:
   `AstQuery.containing` returns nodes innermost-first, but this loop keeps 
overwriting the match and returns the outermost class. Override completion 
inside a nested class therefore inspects the outer class and suggests the wrong 
stubs; return the first `ClassNode` instead.



##########
subprojects/groovy-lsp/src/test/groovy/org/apache/groovy/lsp/internal/compile/CompilerSafetyTest.groovy:
##########
@@ -0,0 +1,524 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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.
+ */
+package org.apache.groovy.lsp.internal.compile
+
+import com.sun.source.tree.CompilationUnitTree
+import com.sun.source.util.JavacTask
+import com.sun.source.util.Trees
+import groovy.lang.GroovyClassLoader
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Proxy
+import java.nio.file.FileSystems
+import java.nio.file.Path
+import java.nio.file.attribute.PosixFilePermissions
+import javax.tools.JavaFileObject
+import javax.tools.ToolProvider
+import org.codehaus.groovy.control.CompilationUnit
+import javax.tools.Diagnostic
+import org.junit.jupiter.api.Assumptions
+import org.junit.jupiter.api.io.TempDir
+
+import org.apache.groovy.lsp.internal.diagnostic.DiagnosticConverter
+import org.apache.groovy.lsp.internal.position.PositionEncoding
+import org.apache.groovy.lsp.internal.workspace.TextDocument
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.ErrorCollector
+import org.codehaus.groovy.control.SourceUnit
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage
+import org.codehaus.groovy.syntax.SyntaxException
+import org.apache.groovy.lsp.internal.LanguageServerContext
+import org.apache.groovy.lsp.internal.util.Uris
+import org.eclipse.lsp4j.TextDocumentItem
+import org.junit.jupiter.api.Test
+
+import java.nio.file.Files
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.atomic.AtomicReference
+import java.util.concurrent.locks.LockSupport
+
+final class CompilerSafetyTest {
+
+    @TempDir
+    Path folder
+
+
+    @Test
+    void astTestClosureDoesNotRunWhenDisabled() {
+        System.clearProperty('groovy.asttest.enable')
+        def compiler = new GroovyCompiler()
+        def settings = CompilerSettings.defaults()
+        def uri = URI.create('file:///tmp/AstTestSafety.groovy')
+        def src = '''
+            import groovy.transform.ASTTest
+            import org.codehaus.groovy.control.CompilePhase
+            @ASTTest(phase = CompilePhase.SEMANTIC_ANALYSIS, value = { throw 
new RuntimeException('asttest-executed') })
+            class Victim {}
+            '''
+        def snapshot = compiler.compile(
+                [new TextDocument(uri, 'groovy', 1, src)],
+                [],
+                settings,
+                CompilerSafetyTest.classLoader)
+        def compiled = snapshot.get(uri)
+        assert compiled != null
+        assert System.getProperty('groovy.asttest.enable') == null
+        assert compiled.errors.every { 
!it.toString().contains('asttest-executed') }
+    }
+
+    @Test
+    void diagnosticsAreScopedToOwningFile() {
+        def config = new CompilerConfiguration()
+        def a = new SourceUnit('file:///tmp/A.groovy', 'class A {', config, 
null, null)
+        def b = new SourceUnit('file:///tmp/B.groovy', 'class B {}', config, 
null, null)
+        def collector = new ErrorCollector(config)
+        collector.addErrorAndContinue(new SyntaxErrorMessage(new 
SyntaxException('bad A', 1, 1), a))
+        def docA = new CompiledDocument(URI.create('file:///tmp/A.groovy'), 1, 
'class A {', null, a, collector)
+        def docB = new CompiledDocument(URI.create('file:///tmp/B.groovy'), 1, 
'class B {}', null, b, collector)
+        def converter = new DiagnosticConverter()
+        assert converter.convert(docA, PositionEncoding.UTF16).size() == 1
+        assert converter.convert(docB, PositionEncoding.UTF16).isEmpty()
+    }
+
+    @Test
+    void compilerReusesThenClosesClassLoader() {
+        def compiler = new GroovyCompiler()
+        def settings = CompilerSettings.defaults()
+        def uri = URI.create('file:///tmp/Loader.groovy')
+        def src = 'class Loader {}\n'
+        compiler.compile([new TextDocument(uri, 'groovy', 1, src)], [], 
settings, CompilerSafetyTest.classLoader)
+        compiler.compile([new TextDocument(uri, 'groovy', 2, src)], [], 
settings, CompilerSafetyTest.classLoader)
+        compiler.close()
+        def again = compiler.compile([new TextDocument(uri, 'groovy', 3, 
src)], [], settings, CompilerSafetyTest.classLoader)
+        assert again.get(uri).module != null
+        compiler.close()
+    }
+
+    @Test
+    void compilerReadsExtraFilesAndRestoresProcessFlags() {
+        def dir = Files.createTempDirectory('groovy-lsp-extra')
+        def extra = dir.resolve('Extra.groovy')
+        Files.writeString(extra, 'class Extra {}\n')
+        def note = dir.resolve('readme.txt')
+        Files.writeString(note, 'not groovy')
+        def nested = dir.resolve('nested')
+        Files.createDirectory(nested)
+        System.setProperty('groovy.asttest.enable', 'true')
+        System.setProperty('groovy.grape.enable', 'true')

Review Comment:
   These global properties are cleared only after compilation and assertions 
complete. If setup or compilation throws, the test leaves both flags enabled 
for subsequent tests; capture their prior values and restore them in the 
existing `finally` block.



##########
subprojects/groovy-lsp/src/main/java/org/apache/groovy/lsp/internal/feature/RenameService.java:
##########
@@ -0,0 +1,297 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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.
+ */
+package org.apache.groovy.lsp.internal.feature;
+
+import org.apache.groovy.lsp.internal.compile.AstQuery;
+import org.apache.groovy.lsp.internal.compile.Identifiers;
+import org.apache.groovy.lsp.internal.compile.CompilationSnapshot;
+import org.apache.groovy.lsp.internal.compile.CompiledDocument;
+import org.apache.groovy.lsp.internal.position.PositionEncoding;
+import org.apache.groovy.lsp.internal.position.Positions;
+import org.apache.groovy.lsp.internal.util.Uris;
+import org.apache.groovy.lsp.internal.workspace.TextDocument;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.InnerClassNode;
+import org.codehaus.groovy.ast.ModuleNode;
+import org.eclipse.lsp4j.FileRename;
+import org.eclipse.lsp4j.Location;
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.PrepareRenameResult;
+import org.eclipse.lsp4j.Range;
+import org.eclipse.lsp4j.RenameFile;
+import org.eclipse.lsp4j.ResourceOperation;
+import org.eclipse.lsp4j.SnippetTextEdit;
+import org.eclipse.lsp4j.TextDocumentEdit;
+import org.eclipse.lsp4j.TextEdit;
+import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
+import org.eclipse.lsp4j.WorkspaceEdit;
+import org.eclipse.lsp4j.jsonrpc.messages.Either;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Prepare-rename and rename using reference locations. Calls whose
+ * {@code methodTarget} is empty are refused: a name-and-arity guess is
+ * good enough for hover, not for rewriting source.
+ */
+public final class RenameService {
+
+    private final NavigationService navigation = new NavigationService();
+
+    /**
+     * @param document open document
+     * @param snapshot latest compile
+     * @param position LSP position
+     * @param encoding negotiated encoding
+     * @return the range of the identifier, or {@code null} when it cannot be 
renamed
+     */
+    public Either<Range, PrepareRenameResult> prepareRename(final TextDocument 
document,
+                                                            final 
CompilationSnapshot snapshot,
+                                                            final Position 
position,
+                                                            final 
PositionEncoding encoding) {
+        CompiledDocument compiled = snapshot == null ? null : 
snapshot.get(document);
+        if (compiled == null || compiled.getModule() == null) {
+            return null;
+        }
+        ASTNode node = AstQuery.nodeAt(compiled.getModule(), document, 
position, encoding);
+        if (!MethodBinding.isRenameSafe(node, snapshot)) {
+            return null;
+        }
+        String name = Identifiers.nameOf(node);
+        if (name == null) {
+            return null;
+        }
+        Range range = Positions.toIdentifierRange(node, document.getText(), 
encoding);
+        if (range == null) {
+            return null;
+        }
+        return Either.forRight(new PrepareRenameResult(range, name));
+    }
+
+    /**
+     * @param document open document
+     * @param snapshot latest compile
+     * @param position LSP position
+     * @param newName replacement identifier
+     * @param encoding negotiated encoding
+     * @return a workspace edit
+     */
+    public WorkspaceEdit rename(final TextDocument document, final 
CompilationSnapshot snapshot,
+                                final Position position, final String newName, 
final PositionEncoding encoding) {
+        ASTNode node = renameTarget(document, snapshot, position, newName, 
encoding);
+        if (node == null) {
+            return emptyEdit();
+        }
+        Map<String, List<TextEdit>> changes = referenceEdits(document, 
snapshot, position, encoding, newName);
+        WorkspaceEdit edit = new WorkspaceEdit(changes);
+        addFileRename(edit, document, node, newName, changes);
+        return edit;
+    }
+
+    /**
+     * Inverse of class→file rename: rewrite the type whose name equals the
+     * old stem. The client already moves the file; this returns text edits
+     * only.
+     *
+     * @param files rename pairs
+     * @param snapshot latest compile
+     * @param encoding negotiated encoding
+     * @return a workspace edit, never {@code null}
+     */
+    public WorkspaceEdit willRenameFiles(final List<FileRename> files, final 
CompilationSnapshot snapshot,
+                                         final PositionEncoding encoding) {
+        WorkspaceEdit combined = new WorkspaceEdit(new HashMap<>());
+        if (files == null || snapshot == null) {
+            return combined;
+        }
+        for (FileRename file : files) {
+            if (file == null) {
+                continue;
+            }
+            WorkspaceEdit one = willRenameFile(file.getOldUri(), 
file.getNewUri(), snapshot, encoding);
+            if (one.getChanges() != null) {
+                one.getChanges().forEach((uri, edits) ->
+                        combined.getChanges().computeIfAbsent(uri, key -> new 
ArrayList<>()).addAll(edits));
+            }
+        }
+        return combined;
+    }
+
+    private WorkspaceEdit willRenameFile(final String oldUriString, final 
String newUriString,
+                                         final CompilationSnapshot snapshot, 
final PositionEncoding encoding) {
+        if (oldUriString == null || newUriString == null) {
+            return emptyEdit();
+        }
+        URI oldUri = Uris.parse(oldUriString);
+        URI newUri = Uris.parse(newUriString);
+        String oldName = Uris.fileName(oldUri);
+        String newName = Uris.fileName(newUri);
+        String oldExt = Uris.groovyExtension(oldName);
+        String newExt = Uris.groovyExtension(newName);
+        if (oldExt == null || newExt == null || !oldExt.equals(newExt)) {
+            return emptyEdit();
+        }
+        String oldStem = oldName.substring(0, oldName.length() - 
oldExt.length());
+        String newStem = newName.substring(0, newName.length() - 
newExt.length());
+        if (!isIdentifier(newStem) || !sameDirectory(oldUri, newUri)) {
+            return emptyEdit();
+        }
+        CompiledDocument compiled = snapshot.get(oldUri);
+        if (compiled == null || compiled.getModule() == null) {
+            return emptyEdit();
+        }
+        ClassNode type = uniqueStemClass(compiled.getModule(), oldStem);
+        if (type == null) {
+            return emptyEdit();
+        }
+        Range nameRange = Positions.toNameRange(type, compiled.getText(), 
encoding);
+        if (nameRange == null) {
+            return emptyEdit();
+        }
+        TextDocument document = compiled.toTextDocument();
+        return renameText(document, snapshot, nameRange.getStart(), newStem, 
encoding);
+    }
+
+    private WorkspaceEdit renameText(final TextDocument document, final 
CompilationSnapshot snapshot,
+                                     final Position position, final String 
newName,
+                                     final PositionEncoding encoding) {
+        if (renameTarget(document, snapshot, position, newName, encoding) == 
null) {
+            return emptyEdit();
+        }
+        return new WorkspaceEdit(referenceEdits(document, snapshot, position, 
encoding, newName));
+    }
+
+    private ASTNode renameTarget(final TextDocument document, final 
CompilationSnapshot snapshot,
+                                 final Position position, final String newName,
+                                 final PositionEncoding encoding) {
+        if (document == null || newName == null || newName.isBlank() || 
!isIdentifier(newName)) {
+            return null;
+        }
+        CompiledDocument compiled = snapshot == null ? null : 
snapshot.get(document);
+        if (compiled == null || compiled.getModule() == null) {
+            return null;
+        }
+        ASTNode node = AstQuery.nodeAt(compiled.getModule(), document, 
position, encoding);
+        return MethodBinding.isRenameSafe(node, snapshot) ? node : null;
+    }
+
+    private Map<String, List<TextEdit>> referenceEdits(final TextDocument 
document,
+                                                       final 
CompilationSnapshot snapshot,
+                                                       final Position position,
+                                                       final PositionEncoding 
encoding,
+                                                       final String newName) {
+        List<Location> locations = navigation.references(document, snapshot, 
position, encoding, true);
+        Map<String, List<TextEdit>> changes = new HashMap<>();
+        for (Location location : locations) {
+            changes.computeIfAbsent(location.getUri(), key -> new 
ArrayList<>())
+                    .add(new TextEdit(location.getRange(), newName));
+        }
+        return changes;
+    }
+
+    private static WorkspaceEdit emptyEdit() {
+        return new WorkspaceEdit(Map.of());
+    }
+
+    private static ClassNode uniqueStemClass(final ModuleNode module, final 
String stem) {
+        ClassNode match = null;
+        for (ClassNode classNode : module.getClasses()) {
+            if (classNode instanceof InnerClassNode || classNode.isScript() || 
classNode.getLineNumber() <= 0) {
+                continue;
+            }
+            if (stem.equals(classNode.getNameWithoutPackage())) {
+                if (match != null) {
+                    return null;
+                }
+                match = classNode;
+            }
+        }
+        return match;
+    }
+
+    private static boolean sameDirectory(final URI oldUri, final URI newUri) {
+        String oldPath = oldUri.getPath();
+        String newPath = newUri.getPath();
+        if (oldPath == null || newPath == null) {
+            return false;
+        }
+        int oldSlash = Uris.lastSeparator(oldPath);
+        int newSlash = Uris.lastSeparator(newPath);
+        if (oldSlash < 0 || newSlash < 0) {
+            return false;
+        }
+        return oldPath.substring(0, oldSlash).equals(newPath.substring(0, 
newSlash));
+    }
+
+    private static void addFileRename(final WorkspaceEdit edit, final 
TextDocument document, final ASTNode node,
+                                      final String newName, final Map<String, 
List<TextEdit>> changes) {
+        if (!(node instanceof ClassNode classNode)) {
+            return;
+        }
+        String fileName = Uris.fileName(document.getUri());
+        String extension = Uris.groovyExtension(fileName);
+        if (extension == null) {
+            return;
+        }
+        String stem = fileName.substring(0, fileName.length() - 
extension.length());
+        if (!stem.equals(classNode.getNameWithoutPackage())) {
+            return;
+        }
+        String oldUri = document.getUri().toString();
+        int slash = Uris.lastSeparator(oldUri);
+        if (slash < 0) {
+            return;
+        }
+        String newUri = oldUri.substring(0, slash + 1) + newName + extension;
+        List<Either<TextDocumentEdit, ResourceOperation>> documentChanges = 
new ArrayList<>();
+        for (Map.Entry<String, List<TextEdit>> entry : changes.entrySet()) {
+            List<Either<TextEdit, SnippetTextEdit>> eitherEdits = 
entry.getValue().stream()
+                    .map(textEdit -> Either.<TextEdit, 
SnippetTextEdit>forLeft(textEdit))
+                    .toList();
+            documentChanges.add(Either.forLeft(new TextDocumentEdit(
+                    new VersionedTextDocumentIdentifier(entry.getKey(), null), 
eitherEdits)));
+        }
+        documentChanges.add(Either.forRight(new RenameFile(oldUri, newUri)));
+        edit.setDocumentChanges(documentChanges);
+    }
+
+    static boolean isIdentifier(final String name) {
+        if (name == null || name.isEmpty()) {
+            return false;
+        }
+        int first = name.codePointAt(0);
+        if (!Character.isJavaIdentifierStart(first)) {
+            return false;
+        }
+        int i = Character.charCount(first);
+        while (i < name.length()) {
+            int cp = name.codePointAt(i);
+            if (!Character.isJavaIdentifierPart(cp)) {
+                return false;
+            }
+            i += Character.charCount(cp);
+        }
+        return true;

Review Comment:
   Reserved words such as `class`, `def`, and `true` satisfy the Java 
identifier checks, so rename currently accepts them and produces invalid Groovy 
source. Reject names in `GroovyKeywords` after validating their code points.



##########
subprojects/groovy-lsp/src/main/java/org/apache/groovy/lsp/internal/compile/WorkspaceScanner.java:
##########
@@ -0,0 +1,107 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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.
+ */
+package org.apache.groovy.lsp.internal.compile;
+
+import org.apache.groovy.lsp.internal.util.Uris;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * Finds Groovy and Java sources under workspace folders and configured source 
paths.
+ */
+public final class WorkspaceScanner {
+
+    private static final int MAX_FILES = 4000;
+
+    /**
+     * Scans {@code roots} plus {@code sourcePaths} (resolved against each 
root)
+     * for Groovy files, skipping {@code build}, {@code .git} and similar.
+     *
+     * @param roots workspace folders
+     * @param sourcePaths extra relative or absolute source roots
+     * @return discovered files, each path at most once
+     */
+    public List<Path> scan(final Collection<URI> roots, final List<String> 
sourcePaths) {
+        List<Path> files = new ArrayList<>();
+        List<Path> searchRoots = new ArrayList<>();
+        if (roots != null) {
+            for (URI root : roots) {
+                if (root == null || 
!"file".equalsIgnoreCase(root.getScheme())) {
+                    continue;
+                }
+                
searchRoots.addAll(WorkspaceLayout.searchRoots(Uris.toPath(root), sourcePaths));
+            }
+        }
+        Set<Path> seen = new HashSet<>();
+        Set<Path> walked = new HashSet<>();
+        for (Path root : searchRoots) {
+            if (files.size() >= MAX_FILES) {
+                break;
+            }
+            if (!walked.add(normalized(root))) {
+                continue;
+            }
+            addGroovyFiles(files, root, seen);
+        }
+        return files;
+    }
+
+    private void addGroovyFiles(final List<Path> files, final Path root, final 
Set<Path> seen) {
+        if (!Files.isDirectory(root)) {
+            return;
+        }
+        try (Stream<Path> stream = Files.walk(root)) {
+            stream.filter(Files::isRegularFile)
+                    .filter(p -> {
+                        String name = p.getFileName().toString();
+                        return Uris.isGroovyFileName(name) || 
Uris.isJavaFileName(name);
+                    })
+                    .filter(p -> !isIgnored(root.relativize(p)))

Review Comment:
   Filtering ignored paths after `Files.walk` does not prune those directory 
subtrees: the walker still descends through all of `.git`, `node_modules`, 
`build`, and similar trees before rejecting their files. On a workspace-root 
fallback this can make scanning very slow or exhaust resources; use 
`walkFileTree` and return `SKIP_SUBTREE` from `preVisitDirectory`.



##########
subprojects/groovy-lsp/src/test/groovy/org/apache/groovy/lsp/internal/compile/CompilerSafetyTest.groovy:
##########
@@ -0,0 +1,524 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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.
+ */
+package org.apache.groovy.lsp.internal.compile
+
+import com.sun.source.tree.CompilationUnitTree
+import com.sun.source.util.JavacTask
+import com.sun.source.util.Trees
+import groovy.lang.GroovyClassLoader
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Proxy
+import java.nio.file.FileSystems
+import java.nio.file.Path
+import java.nio.file.attribute.PosixFilePermissions
+import javax.tools.JavaFileObject
+import javax.tools.ToolProvider
+import org.codehaus.groovy.control.CompilationUnit
+import javax.tools.Diagnostic
+import org.junit.jupiter.api.Assumptions
+import org.junit.jupiter.api.io.TempDir
+
+import org.apache.groovy.lsp.internal.diagnostic.DiagnosticConverter
+import org.apache.groovy.lsp.internal.position.PositionEncoding
+import org.apache.groovy.lsp.internal.workspace.TextDocument
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.ErrorCollector
+import org.codehaus.groovy.control.SourceUnit
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage
+import org.codehaus.groovy.syntax.SyntaxException
+import org.apache.groovy.lsp.internal.LanguageServerContext
+import org.apache.groovy.lsp.internal.util.Uris
+import org.eclipse.lsp4j.TextDocumentItem
+import org.junit.jupiter.api.Test
+
+import java.nio.file.Files
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.atomic.AtomicReference
+import java.util.concurrent.locks.LockSupport
+
+final class CompilerSafetyTest {

Review Comment:
   This test class mutates JVM system properties but does not acquire the 
repository's system-property resource lock. Parallel JUnit execution can race 
with other tests; the established convention is 
`@ResourceLock(Resources.SYSTEM_PROPERTIES)` (for example, 
`InvokerFactoryTest.groovy:286-302` and 
`CachedMethodDirectInvokerTest.java:60-72`).



##########
subprojects/groovy-lsp/src/main/java/org/apache/groovy/lsp/internal/compile/LspJavaCompiler.java:
##########
@@ -0,0 +1,245 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you 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.
+ */
+package org.apache.groovy.lsp.internal.compile;
+
+import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.util.JavacTask;
+import com.sun.source.util.Trees;
+import org.apache.groovy.lsp.internal.util.Uris;
+import groovy.lang.GroovyObject;
+import org.codehaus.groovy.control.CompilationUnit;
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.codehaus.groovy.tools.javac.JavaCompiler;
+import org.codehaus.groovy.tools.javac.JavaCompilerFactory;
+
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Joint-compilation javac backend for groovy-lsp. Uses the public
+ * {@code javax.tools} / {@code com.sun.source} APIs (no {@code 
--add-exports}).
+ * Open buffers overlay the filesystem; stub sources stay in memory
+ * ({@code memStub}).
+ */
+public final class LspJavaCompiler implements JavaCompiler, 
JavaCompilerFactory {
+
+    private final Map<URI, String> overlays;
+    private JavaSymbolIndex index = JavaSymbolIndex.EMPTY;
+    private final Map<URI, List<JavaMessage>> messages = new LinkedHashMap<>();
+
+    /**
+     * @param overlays open Java buffers keyed by normalized URI
+     */
+    public LspJavaCompiler(final Map<URI, String> overlays) {
+        this.overlays = overlays == null ? Map.of() : Map.copyOf(overlays);
+    }
+
+    /**
+     * @return types and members from the last javac run
+     */
+    public JavaSymbolIndex index() {
+        return index;
+    }
+
+    /**
+     * @return javac diagnostics from the last run, keyed by source URI
+     */
+    public Map<URI, List<JavaMessage>> messages() {
+        return Map.copyOf(messages);
+    }
+
+    @Override
+    public JavaCompiler createCompiler(final CompilerConfiguration config) {
+        return this;
+    }
+
+    @Override
+    public void compile(final List<String> files, final CompilationUnit cu) {
+        index = JavaSymbolIndex.EMPTY;
+        messages.clear();
+        var javac = ToolProvider.getSystemJavaCompiler();
+        if (javac == null || cu == null) {
+            return;
+        }
+        CompilerConfiguration config = cu.getConfiguration();
+        DiagnosticCollector<JavaFileObject> diagnostics = new 
DiagnosticCollector<>();
+        Charset charset = Charset.forName(config.getSourceEncoding());
+        try (StandardJavaFileManager fileManager = 
javac.getStandardFileManager(diagnostics, Locale.ENGLISH, charset)) {
+            Set<JavaFileObject> units = new LinkedHashSet<>();
+            if (cu.getJavaCompilationUnitSet() != null) {
+                units.addAll(cu.getJavaCompilationUnitSet());
+            }
+            addSources(units, files, fileManager);
+            JavacTask task = (JavacTask) javac.getTask(null, fileManager, 
diagnostics, parameters(config),
+                    null, units);
+            Trees treeApi = Trees.instance(task);
+            Iterable<? extends CompilationUnitTree> trees = task.parse();
+            task.analyze();
+            task.generate();
+            index = JavaSymbolIndex.of(treeApi, trees);
+            collectDiagnostics(diagnostics);
+        } catch (IOException | RuntimeException ignored) {
+            // keep whatever diagnostics were collected
+        }
+    }
+
+    private void addSources(final Set<JavaFileObject> units, final 
List<String> files,
+                            final StandardJavaFileManager fileManager) {
+        Set<URI> seen = recordedUris(units);
+        addOverlays(units, seen);
+        addRemainingFiles(units, files, fileManager, seen);
+    }
+
+    private static Set<URI> recordedUris(final Set<JavaFileObject> units) {
+        return units.stream()
+                .filter(unit -> unit != null && unit.toUri() != null)
+                .map(unit -> Uris.normalize(unit.toUri()))
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+    }
+
+    private void addOverlays(final Set<JavaFileObject> units, final Set<URI> 
seen) {
+        for (Map.Entry<URI, String> overlay : overlays.entrySet()) {
+            URI uri = Uris.normalize(overlay.getKey());
+            if (seen.add(uri)) {
+                units.add(new BufferJavaFileObject(uri, overlay.getValue()));
+            }
+        }
+    }
+
+    private void addRemainingFiles(final Set<JavaFileObject> units, final 
List<String> files,
+                                   final StandardJavaFileManager fileManager, 
final Set<URI> seen) {
+        if (files == null) {
+            return;
+        }
+        List<File> remaining = new ArrayList<>();
+        for (String file : files) {
+            if (file != null) {
+                addIfNewFile(remaining, file, seen);
+            }
+        }
+        if (!remaining.isEmpty()) {
+            for (JavaFileObject object : 
fileManager.getJavaFileObjectsFromFiles(remaining)) {
+                units.add(object);
+            }
+        }
+    }
+
+    private void addIfNewFile(final List<File> remaining, final String file, 
final Set<URI> seen) {
+        Path path = Path.of(file);
+        URI uri = Uris.normalize(path.toUri());
+        if (!overlays.containsKey(uri) && seen.add(uri) && 
Files.isRegularFile(path)) {
+            remaining.add(path.toFile());
+        }
+    }
+
+    private void collectDiagnostics(final DiagnosticCollector<JavaFileObject> 
diagnostics) {
+        for (Diagnostic<? extends JavaFileObject> diagnostic : 
diagnostics.getDiagnostics()) {
+            URI uri = diagnosticUri(diagnostic);
+            if (uri != null) {
+                messages.computeIfAbsent(uri, key -> new 
ArrayList<>()).add(toMessage(diagnostic));
+            }
+        }
+    }
+
+    private static URI diagnosticUri(final Diagnostic<? extends 
JavaFileObject> diagnostic) {
+        if (diagnostic == null || diagnostic.getKind() == 
Diagnostic.Kind.NOTE) {
+            return null;
+        }
+        JavaFileObject source = diagnostic.getSource();
+        if (source == null || source.toUri() == null) {
+            return null;
+        }
+        URI uri = Uris.normalize(source.toUri());
+        return "string".equalsIgnoreCase(uri.getScheme()) ? null : uri;
+    }
+
+    private static JavaMessage toMessage(final Diagnostic<? extends 
JavaFileObject> diagnostic) {
+        int line = bounded(diagnostic.getLineNumber());
+        int column = bounded(diagnostic.getColumnNumber());
+        String text = diagnostic.getMessage(Locale.ENGLISH);
+        boolean error = diagnostic.getKind() == Diagnostic.Kind.ERROR;
+        return new JavaMessage(line, column, line, column + 1, text == null ? 
"" : text, error);

Review Comment:
   `Diagnostic.getColumnNumber()` is a 1-based UTF-16 column, but `JavaMessage` 
declares these values as Groovy code-point coordinates and 
`DiagnosticConverter` converts them as code points. Any javac diagnostic after 
a supplementary character is shifted; convert from UTF-16 using the source line 
here, or carry the coordinate encoding explicitly.





> Add a first-party Language Server Protocol implementation (groovy-lsp)
> ----------------------------------------------------------------------
>
>                 Key: GROOVY-12411
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12411
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Priority: Major
>
> Apache Groovy needs a language server that uses the compiler in this 
> repository. Community servers compile against a published Groovy jar, so 
> parser and type-system changes reach an editor only after that jar is 
> released. This change adds an optional {{groovy-lsp}} module and a 
> {{groovylsp}} command. Editors, and later AI tools, share that language core. 
> It is not a full IDE, and it does not replace Groovy-Eclipse or IntelliJ.
> h2. Why this shape
> The server compiles the open buffers with the in-tree compiler and answers 
> Language Server Protocol (LSP) requests from that snapshot. A language change 
> in this tree shows up in the server without forking a community project. The 
> server refuses an edit that would guess. It does not rename an unbound 
> dynamic call, and it does not rewrite every token that happens to spell the 
> same name.
> There is no public extension SPI in this first cut. No ServiceLoader hook, no 
> OSGi bundle, and no second hover or completion API. Static type checking of 
> annotated members is core behavior, not a plugin. A later {{groovy-mcp}} 
> wrapper may call the in-process engine. That wrapper is not part of this 
> change.
> h2. What lands
>  * Optional subproject {{{}groovy-lsp{}}}. It is not part of 
> {{{}groovy-all{}}}. The binary distribution adds {{groovylsp}} and 
> {{{}groovylsp.bat{}}}.
>  * The default transport is stdio. {{--socket}} accepts one TCP client. 
> {{--help}} and {{--version}} print and exit. On stdio, standard output is 
> redirected to standard error, so a print from Groovy code cannot corrupt the 
> protocol stream.
>  * The protocol is LSP 3.18, carried by Eclipse LSP4J 1.0. Fields from a 
> newer draft that the server does not understand are ignored. A capability is 
> advertised only when it is implemented. LSP4J is an EPL 2.0 Category B 
> binary. Its sources are not copied into Groovy.
>  * Open Groovy buffers compile through semantic analysis. The server does not 
> generate Groovy class files. Errors are collected, and a partial AST is still 
> queried.
>  * {{.java}} files in the workspace are joint-compiled with 
> {{JavaAwareCompilationUnit}} ({{{}memStub{}}}) and {{javax.tools}} javac. 
> Annotation processing is off.
>  * After semantic analysis, {{StaticTypeCheckingPass}} type-checks members 
> annotated {{@TypeChecked}} or {{{}@CompileStatic{}}}. It skips 
> {{@CompileDynamic}} and {{{}TypeCheckingMode.SKIP{}}}. It does not force 
> static checking on ordinary dynamic Groovy, and it does not raise the compile 
> phase.
>  * {{LocatedMessage.toDiagnostic()}} and {{write()}} report the message with 
> no line or column when the parse context is null. They no longer throw 
> {{NullPointerException}} in that case.
>  * {{ASTNode}} column javadoc now matches the compiler. Columns are 1-based 
> Unicode code points, the end column is exclusive, and a missing position is 
> -1. Getters, setters, and stored values are unchanged. See GROOVY-12085.
>  * User guide {{{}groovy-lsp.adoc{}}}. Contributor map 
> {{{}subprojects/groovy-lsp/ARCHITECTURE.md{}}}.
> h2. In-process engine
> {{GroovyLanguageEngine}} answers the same questions as the LSP handlers and 
> returns JDK records. Constructing it does not scan a workspace, so it does 
> not compile a checkout root.
>  * {{open(Path)}} reads one file from disk and returns its URI. Queries take 
> that URI and use 0-based UTF-16 positions.
>  * {{{}openBuffer{}}}, {{{}updateBuffer{}}}, and {{closeBuffer}} take any URI 
> the caller chooses. A URI whose scheme is not {{file}} is not treated as a 
> path. The engine does not reserve buffer names.
>  * {{open}} and {{openBuffer}} compile immediately. {{updateBuffer}} only 
> replaces the text. The next query compiles when the buffer has changed, and 
> {{compileNow}} compiles at once. {{closeBuffer}} drops the buffer and 
> compiles what remains.
>  * {{setParentLoader}} sets the parent loader. {{setClasspath}} replaces the 
> compile classpath. {{putExtra}} stores an opaque fingerprint so an in-place 
> change to the same loader still forces a new compile.
>  * Rename, format, and organize-imports return edits. They do not write files.
> Groovy Console and groovysh are not connected to this engine in this change.
> h2. What an editor gets
> Recognized sources are {{{}.groovy{}}}, {{{}.gvy{}}}, {{{}.gy{}}}, 
> {{{}.gsh{}}}, or language id {{{}groovy{}}}.
>  * *Editing.* Incremental sync. Completion of keywords, snippets, members, 
> workspace types with an import edit, and GDK members when a prefix is typed. 
> Groovydoc is filled when the client resolves a completion item. Format and 
> range format indent by braces and leave the text of a multiline string or a 
> block comment unchanged. A typed newline indents the next line. A typed 
> closing brace outdents, except inside a string, a comment, or a slashy regex.
>  * *Imports and types.* Organize imports keeps star, static, and alias 
> imports, and does not re-insert Groovy's default imports. {{source.fixAll}} 
> only deletes unused imports. Other actions add one import, add every 
> unambiguous missing import in one edit, add a package taken from the file 
> path, and replace the {{def}} immediately before a field or local name with 
> an inferred type.
>  * *Generate.* Implement abstract methods by inserting an {{@Override}} stub 
> that throws {{{}UnsupportedOperationException{}}}. Add {{@Override}} on a 
> method that already overrides. Drop the {{private}} modifier on a field that 
> has no accessor, so Groovy treats the field as a property. Insert 
> {{{}@groovy.transform.ToString{}}}, 
> {{{}@groovy.transform.EqualsAndHashCode{}}}, or 
> {{{}@groovy.transform.TupleConstructor{}}}. Those three actions insert the 
> annotation only. They do not paste a method body. {{@ToString}} is skipped 
> when that annotation, {{{}@Canonical{}}}, or {{toString}} is already present. 
> {{@EqualsAndHashCode}} is skipped when that annotation, {{{}@Canonical{}}}, 
> {{{}equals{}}}, or {{hashCode}} is already present. {{@TupleConstructor}} is 
> skipped when that annotation, {{{}@Canonical{}}}, {{{}@MapConstructor{}}}, or 
> a declared constructor is already present, and when the class has no instance 
> field.
>  * *Create class.* For an unresolved type, offer a sibling {{Name.groovy}} 
> next to the referencing file, using that file's package. A qualified name is 
> accepted only when its package is the same. The action is skipped when that 
> type is already defined, when the sibling is already open or on disk, or when 
> the client did not advertise file creation ({{{}documentChanges{}}} plus the 
> create resource operation). The stub is written only in that new file. The 
> create neither overwrites nor ignores an existing file.
>  * *Calls.* At a call site, complete a parameter name as a named-argument 
> prefix when the call is bound or has one arity match. {{refactor.rewrite}} 
> converts such a positional call to named arguments.
>  * *Navigation.* Definition, type definition, implementation, declaration, 
> and references. References match the declaration by owner, name, and 
> descriptor, not by the bare spelling. Call hierarchy and type hierarchy. 
> Document symbols and workspace symbols. A workspace-symbol query may be a 
> case-insensitive substring or CamelHumps, so {{ASF}} finds 
> {{{}AbstractServerFactory{}}}. Folding, selection ranges, document highlight, 
> and document links on imports.
>  * *Presentation.* Push and pull diagnostics: compiler messages, unused 
> imports, static-type errors on annotated members, and javac messages from a 
> joint compile. Semantic tokens for the whole file or a range. Inlay hints for 
> declaration types and for parameter names. A parameter-name inlay is labelled 
> inferred unless the compiler bound the call, and an inferred inlay does not 
> insert text. Code lenses show a reference count.
>  * *Workspace.* Workspace folders, watched files, configuration, and progress 
> when the client asks for it. {{workspace/willRenameFiles}} updates one 
> top-level Groovy class whose name matches the file name without its 
> extension. It refuses a script, a file that declares several types, a class 
> name that does not match, and {{{}.java{}}}.
> Commands:
>  * {{groovy.lsp.organizeImports}}
>  * {{groovy.lsp.showReferences}}
>  * {{{}groovy.lsp.gotoSuperMethod{}}}, the nearest overridden method that has 
> source. It skips {{Object}} and {{{}GroovyObject{}}}.
> h2. Rename
> {{methodTarget}} is often empty on a dynamic call. A match by name and arity 
> is enough to display, and not enough to edit.
> ||Feature||When the compiler bound the call||When it did not||
> |Hover|The bound signature|One arity match on a resolved receiver, marked 
> {{{}_inferred_{}}}. Otherwise the name only|
> |Definition|That method|Every arity match that has source. Never an arbitrary 
> first hit|
> |Rename and prepare-rename|Allowed|Refused|
> A class, method, field, parameter, or bound local is renamed by identity. 
> Renaming a class also renames the file when the class name matches the file 
> name without its extension.
> h2. Groovy and Java in one workspace
> Joint compilation lets Groovy and Java sources in the workspace see each 
> other's types. Go to definition can cross that boundary, and javac 
> diagnostics are published with the Groovy ones. javac writes class files to a 
> private temporary directory whose name starts with {{{}groovy-lsp-javac-{}}}, 
> not under the user home directory. On POSIX the directory is owner-only. It 
> is removed on the next compile and when the compiler closes. If that 
> directory cannot be created, the compile continues with Groovy only.
> This is not a Java language server. Java completion, Java formatting, and 
> Java-only rename stay with a Java language server. Eclipse JDT and 
> groovy-eclipse remain the stack for a full Java IDE. No source is copied from 
> groovy-language-server-edt, and its identifier-scan rename is not used.
> ||Question||groovy-lsp||Other Groovy language servers||
> |Which compiler|The compiler in this repository|A published Groovy jar, or 
> JDT plus groovy-eclipse|
> |Java IDE features|Joint type resolution, and definition from Groovy into 
> Java|groovy-language-server-edt also completes, formats, and renames Java|
> |GDSL and dependency download|Not included|groovyls adds Jenkins GDSL and 
> dependency download|
> |Unbound-call rename|Refused|groovy-language-server-edt can rename by 
> scanning the identifier text|
> h2. Configuration and safety
> On {{{}initialized{}}}, if the client advertised 
> {{{}workspace/configuration{}}}, the server asks for the {{groovy}} section 
> without blocking the JSON-RPC reader. The client can also push 
> {{{}workspace/didChangeConfiguration{}}}.
> ||Key||Meaning||
> |classpath|Extra jars and directories. The server does not run Gradle or 
> Maven|
> |sourcePaths|Extra source roots. A relative path is resolved from a workspace 
> folder|
> |grapeEnabled|Allow {{@Grab}} while compiling. Default is off|
> |astTestEnabled|Allow {{@ASTTest}} while compiling. Default is off|
> Any other key under {{groovy}} is stored as text. The server does not 
> interpret it and does not download anything for it. Changing such a key 
> builds a new class loader and recompiles.
> An empty source-path list and an empty classpath are filled separately, and 
> only when the client left that list empty. Source roots are every existing 
> directory among {{{}src/main/groovy{}}}, {{{}src/test/groovy{}}}, 
> {{{}src/main/java{}}}, and {{{}src/test/java{}}}. If none of those four 
> exists, the server uses {{src}} when that directory exists, and otherwise the 
> workspace folder itself. The classpath gains jars that sit directly under 
> {{lib}} or {{{}libs{}}}.
> Diagnostics for one file stay on that file. Unchanged buffers are not 
> compiled again. {{@Grab}} stays off unless the client turns it on, both by 
> omitting its global transform and by the process property 
> {{{}groovy.grape.enable{}}}. {{@ASTTest}} is a local transform, so it is 
> turned off only by {{{}groovy.asttest.enable{}}}. Both properties are 
> process-wide for the duration of a compile, then restored. One compile holds 
> them so another compile cannot restore them early.
> h2. Out of scope
>  * A full Java language server.
>  * GDSL and Jenkins pipeline descriptors.
>  * Invoking Gradle or Maven, including the Tooling API.
>  * A debugger or a test runner.
>  * Renaming by identifier text.
>  * Name-span fields on {{{}AnnotatedNode{}}}. Identifier ranges stay inside 
> groovy-lsp.
>  * A public plugin SPI.
>  * Wiring Groovy Console or groovysh.
> h2. Positions
> This is not an LSP break. Compiler columns are already 1-based Unicode code 
> points (GROOVY-12085). LSP positions stay 0-based in the encoding chosen at 
> initialize: {{utf-16}} unless the client offers {{utf-8}} or {{{}utf-32{}}}. 
> The server converts both ways, including characters outside the Basic 
> Multilingual Plane. Clients keep sending positions as they do today.
> h2. Running
> {code:java}
> groovylsp
> groovylsp --stdio
> groovylsp --socket PORT
> groovylsp --help
> groovylsp --version
> {code}
> An editor starts that process and speaks JSON-RPC on its standard input and 
> output. After shutdown, {{exit}} ends the process with code 0. {{exit}} 
> without a preceding shutdown ends it with code 1.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to