Skip to content

GROOVY-12411: Add a first-party Language Server Protocol implementation (groovy-lsp) - #2936

Open
daniellansun wants to merge 1 commit into
masterfrom
GROOVY-12411
Open

daniellansun wants to merge 1 commit into
masterfrom
GROOVY-12411

Conversation

@daniellansun

Copy link
Copy Markdown
Contributor

@codecov-commenter

codecov-commenter commented Sep 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.31829% with 1301 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.6828%. Comparing base (c3c8739) to head (292fc1b).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
...e/groovy/lsp/internal/feature/SupportServices.java 69.2202% 84 Missing and 141 partials ⚠️
...groovy/lsp/internal/feature/NavigationService.java 61.8557% 81 Missing and 104 partials ⚠️
...groovy/lsp/internal/feature/CodeActionService.java 60.7143% 49 Missing and 61 partials ⚠️
...groovy/lsp/internal/feature/CompletionService.java 73.9865% 36 Missing and 41 partials ⚠️
.../groovy/lsp/internal/feature/HierarchyService.java 66.1111% 23 Missing and 38 partials ⚠️
...g/apache/groovy/lsp/internal/compile/AstQuery.java 58.7413% 30 Missing and 29 partials ⚠️
.../groovy/lsp/internal/feature/SourceGeneration.java 62.8205% 26 Missing and 32 partials ⚠️
...che/groovy/lsp/internal/compile/ImportSupport.java 66.9231% 23 Missing and 20 partials ⚠️
...che/groovy/lsp/internal/feature/RenameService.java 68.1482% 19 Missing and 24 partials ⚠️
...apache/groovy/lsp/internal/position/Positions.java 75.7576% 11 Missing and 29 partials ⚠️
... and 30 more
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##               master      #2936        +/-   ##
==================================================
+ Coverage     71.6082%   71.6828%   +0.0746%     
- Complexity      38057      39758      +1701     
==================================================
  Files            1588       1632        +44     
  Lines          137068     141942      +4874     
  Branches        25505      26657      +1152     
==================================================
+ Hits            98152     101748      +3596     
- Misses          30011      30506       +495     
- Partials         8905       9688       +783     
Files with missing lines Coverage Δ
src/main/java/org/codehaus/groovy/ast/ASTNode.java 93.5484% <ø> (ø)
...dehaus/groovy/control/messages/LocatedMessage.java 69.5652% <100.0000%> (+7.6605%) ⬆️
...pache/groovy/lsp/internal/compile/JavaMessage.java 100.0000% <100.0000%> (ø)
...groovy/lsp/internal/position/PositionEncoding.java 100.0000% <100.0000%> (ø)
.../org/apache/groovy/lsp/spi/GroovyLspExtension.java 100.0000% <100.0000%> (ø)
...va/org/apache/groovy/lsp/spi/GroovyLspSession.java 100.0000% <100.0000%> (ø)
...va/org/apache/groovy/lsp/GroovyLanguageServer.java 94.5946% <94.5946%> (ø)
...lsp/internal/protocol/ServerCapabilityFactory.java 96.7213% <96.7213%> (ø)
...e/groovy/lsp/internal/workspace/DocumentStore.java 90.9091% <90.9091%> (ø)
.../groovy/lsp/internal/compile/CompiledDocument.java 88.2353% <88.2353%> (ø)
... and 36 more

... and 14 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

JMH summary — classic (commit 433b87c)

Speedup vs trailing 90-day baseline on gh-pages. Higher = faster.
1.00 = in line with history. Per-benchmark ratio, geomean within group.
Time-per-op units inverted so direction is consistent. The calibrated
column divides out this runner's speed vs the baseline hardware, as
measured by Groovy-independent pure-Java ruler benchmarks.

Group Speedup Calibrated n
bench 0.964 × 1.005 × 124
core 1.029 × 0.992 × 108
grails 1.018 × 1.054 × 80

No benchmark is ≥1.5× slower than its 90-day baseline.

Runner calibration (this run vs baseline hardware): bench 0.96× (27 rulers) · core-ag 0.99× (3 rulers) · core-hz 1.12× (3 rulers) · grails-ad 0.95× (3 rulers) · grails-ez 0.98× (3 rulers)

Baseline: dev/bench/jmh/<part>/classic/data.js on gh-pages, trailing 90 days. Daily dashboard · Per-suite raw data

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

JMH summary — indy (commit 433b87c)

Speedup vs trailing 90-day baseline on gh-pages. Higher = faster.
1.00 = in line with history. Per-benchmark ratio, geomean within group.
Time-per-op units inverted so direction is consistent. The calibrated
column divides out this runner's speed vs the baseline hardware, as
measured by Groovy-independent pure-Java ruler benchmarks.

Group Speedup Calibrated n
bench 1.527 × 1.039 × 124
core 3.614 × 3.623 × 108
grails 2.405 × 2.545 × 80

⚠️ 4 benchmarks at least 1.5× slower than the 90-day baseline:

  • org.apache.groovy.bench.AryBench.groovyCS ( {"n":"1000000"} ) — 2.93× slower (calibrated)
  • org.apache.groovy.bench.StaticMethodCallIndyBench.staticChain_groovyCS — 1.73× slower (calibrated)
  • org.apache.groovy.perf.MethodInvocationBench.benchmarkPolymorphicCallSite — 1.56× slower (calibrated)
  • org.apache.groovy.bench.dispatch.CallsiteBench.dispatch_3_polymorphic_groovy — 1.54× slower (calibrated)

⚠️ Runner speed differs ≥15% from the historical baseline hardware for: bench. Raw speedups are not meaningful for those parts — use the calibrated column.

Runner calibration (this run vs baseline hardware): bench 1.47× (27 rulers) · core-ag 0.95× (3 rulers) · core-hz 1.08× (3 rulers) · grails-ad 0.96× (3 rulers) · grails-ez 0.93× (3 rulers)

Baseline: dev/bench/jmh/<part>/indy/data.js on gh-pages, trailing 90 days. Daily dashboard · Per-suite raw data

@blackdrag

Copy link
Copy Markdown
Contributor

I am wondering about a few things in this:

  • is the change to 1-base in columns not a breaking change?
  • I need a more clear answer to what this is not supposed to solve and what it is supposed to solve. So far I understand only that it does not want to be a complete workspace/IDE solution and it does not support anything but Groovy itself. What it is trying to solve seems to be that the other implementations are lagging behind. But those implementations also implement much more of the IDE concept... especially for specific modes, GDSL support, joint compilation, cross language renames, ... So I am not yet clear what the intended scope and differentiator of this implementation are.
  • Let me extend the renames point ... In dynamically typed Groovy, where MethodCallExpression.methodTarget is normally not available, what semantic guarantees are intended for definition, references and rename? The current fallback in NavigationService.resolveMethod() appears to select a method based on receiver type, name and argument count. Is this intentionally a best-effort heuristic, and if so, should operations such as rename avoid making potentially incorrect edits when the target cannot be uniquely determined? A heuristic is good enough for Hover I think, but in case of rename this can be a problem.
  • You mention groovy-language-server, what about tomaszrup/groovy-language-server-edt and trustytrojan/groovyls? How do you see this implementation in relation to those?
  • What do you actually want to use this for? And how should others use it? And more from the IDE-side how are IDEs supposed to use this? Is it even intended for direct use in a IDE?
  • What is the vision for this? Are other LSP implementations supposed to use this as base, or is this intended to be a standalone Groovy language server?

Yeah, I know, a wild mix of specific and very broad questions. I am not against the idea at all; I just want to understand more clearly why this PR came into existence in the first place and what you see its role being.

@daniellansun

Copy link
Copy Markdown
Contributor Author

I am wondering about a few things in this:

  • is the change to 1-base in columns not a breaking change?
  • I need a more clear answer to what this is not supposed to solve and what it is supposed to solve. So far I understand only that it does not want to be a complete workspace/IDE solution and it does not support anything but Groovy itself. What it is trying to solve seems to be that the other implementations are lagging behind. But those implementations also implement much more of the IDE concept... especially for specific modes, GDSL support, joint compilation, cross language renames, ... So I am not yet clear what the intended scope and differentiator of this implementation are.
  • Let me extend the renames point ... In dynamically typed Groovy, where MethodCallExpression.methodTarget is normally not available, what semantic guarantees are intended for definition, references and rename? The current fallback in NavigationService.resolveMethod() appears to select a method based on receiver type, name and argument count. Is this intentionally a best-effort heuristic, and if so, should operations such as rename avoid making potentially incorrect edits when the target cannot be uniquely determined? A heuristic is good enough for Hover I think, but in case of rename this can be a problem.
  • You mention groovy-language-server, what about tomaszrup/groovy-language-server-edt and trustytrojan/groovyls? How do you see this implementation in relation to those?
  • What do you actually want to use this for? And how should others use it? And more from the IDE-side how are IDEs supposed to use this? Is it even intended for direct use in a IDE?
  • What is the vision for this? Are other LSP implementations supposed to use this as base, or is this intended to be a standalone Groovy language server?

Yeah, I know, a wild mix of specific and very broad questions. I am not against the idea at all; I just want to understand more clearly why this PR came into existence in the first place and what you see its role being.

Thanks for taking the time to unpack this — the mix of questions is fair, and I should have been clearer about scope from the start.

1-based columns. That is GROOVY-12085 in the compiler AST (1-based Unicode code points). It is not an LSP coordinate change. The protocol stays 0-based in the encoding negotiated at initialize; the server converts, and clients never see AST columns. Existing LSP clients do not need to change how they send positions. This module also does not add name-span fields to AnnotatedNode; identifier selection for rename/highlight is done in groovy-lsp from the node’s existing source span.

What this is for, and what it is not. It is a Groovy language core for LSP, not a workspace/IDE. It is meant to stay current with the in-tree compiler so editors (and later AI tools) get diagnostics, completion, hover, navigation, and conservative rename on .groovy sources. It does not try to match the IDE-shaped servers on GDSL, joint Groovy+Java compilation, cross-language rename, Gradle/Maven invocation, a debugger, or a test runner. Those are real strengths of the other implementations; they are out of scope here on purpose. The differentiator is “same compiler as the language, small surface, safe edits,” not “more IDE features.”

Rename vs hover when methodTarget is missing. You are right that a name-and-arity fallback is a heuristic, and that it is a problem for rename. That is now the policy: hover may show a unique arity match and mark it _inferred_; definition lists every such candidate rather than picking the first; prepare-rename and rename refuse unbound dynamic calls. They only run on declarations (by owner + name + descriptor) and on calls/properties the compiler actually bound. A wrong edit is worse than a refused rename.

The other servers. Prominic groovy-language-server is the earlier community server; this is a new implementation, not a fork. trustytrojan/groovyls (Apache 2.0) is the natural place for Jenkins GDSL and a forced STC pass — those can sit on this compile/protocol loop later via the incubating GroovyLspExtension SPI, without living in this module. tomaszrup/groovy-language-server-edt (EPL, JDT + groovy-eclipse) is complementary: joint compilation and Java interop need that stack. We do not copy it, and we do not use its identifier-scan rename fallback.

How to use it. Yes, it is intended for direct use in an IDE or editor: start groovylsp and speak stdio JSON-RPC. The client supplies groovy.classpath (we never invoke Gradle or Maven). The same process is the semantic backend for Groovy-aware AI; a later groovy-mcp is expected to wrap it so a model can ask what an identifier is without embedding the compiler.

Vision. Both: a standalone first-party Groovy language server that an editor can run today, and a small compile + protocol loop that other Apache-2.0 Groovy LSPs (and groovy-mcp) can extend instead of forking. It is not a replacement for the JDT/groovy-eclipse stack, and it will not grow into a full IDE.

Happy to go further on any of these if that still feels thin.

@asf-gitbox-commits
asf-gitbox-commits force-pushed the GROOVY-12411 branch 7 times, most recently from 3fb2138 to 56fc0c7 Compare September 17, 2026 13:49
@testlens-app

testlens-app Bot commented Sep 17, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 292fc1b
▶️ Tests: 121101 executed
⚪️ Checks: 32/32 completed


Learn more about TestLens at testlens.app/docs.

@sonarqubecloud

Copy link
Copy Markdown

@blackdrag

Copy link
Copy Markdown
Contributor

Sorry for taking a while, but this topic here broad and it took me a while to do some research. And you made further changes anyway.

It does not try to match the IDE-shaped servers on GDSL, joint Groovy+Java compilation, cross-language rename, Gradle/Maven invocation, a debugger, or a test runner. Those are real strengths of the other implementations; they are out of scope here on purpose. The differentiator is “same compiler as the language, small surface, safe edits,” not “more IDE features.”

I think it is a healthy approach to not to try to solve these. But again I am wondering about "its role being".

You say no joint Groovy+Java compilation, but you do have joint compilation in there. It is just no joint compilation dictated by the IDE or tooling outside the LS. You seem to want to enable things as they are possible on the compiler level. Which is I think a good approach, but then you also have this extension mechanism. First of all I am questioning why the LS uses that itself. I get that you want to enrich the code with more semantic information, which is normally not in there, because it is supposed to be resolved at runtime. Which means the direct method call targets you actually set there are potentially not the actual targets. Which is... well, something we have to live with I guess. Of course a completion with a selection of a method could actually select the real target, but then this information is lost later. My "pain point" here is that you use the extension mechanism for this and this is probably also the only reason the extension mechanism exists. Also to do this no extension mechanism is actually needed I would say.

If I am wrong here, then I would like to hear more. Your vision was not giving much information in that respect - only a groovy-mcp, for which I currently fail to see the need of an extension mechanism.

Well to make things shorter:

  • IDE usage: groovy-lsp will most probably have performance problems with anything but a small project. Tooling environment and groovy-lsp can differ a lot. For example for custom compiler configurations, special class paths and so on. At the same time it does not replace a more serious plugin that supports cross-language renames for example. So using it in an IDE is possible, but limited. For people that want a more lean IDE probably nice, but for those this lsp itself may be too heavy.
  • usage in other lsp projects is for similar reasons limited.
  • What is the extension mechanism actually for? Using multiple extensions we may run into problems of ordering and other problems. It is something to maintain. And it is currently only useful to the semantic enriching you added, which does not have to be an extension at all imho.
  • Maybe it could replace Prominic groovy-language-server
  • trustytrojan/groovyls is I think originally a fork of it, but I am not really seeing that or other LSPs for groovy using this groovy-lsp as base at the moment. I have trouble seeing the resuse case in other LSPs

Again, do not get me wrong. I am not pushing back on the PR idea in general. I am trying to work the picture out. The picture of what this PR wants to be and what it does not want to be. Only after I have a sharp image I can actually see if the PR goes in that direction or not.

If for example the idea is "just" a groovy-mcp, then I would drop the extension mechanism. Which is a suggestion I would only be able to do once I have a more complete and sharper picture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants