diff --git a/README.md b/README.md
index 4f898ca..b77b071 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,13 @@ in addition to being usable directly as a `javax.script.ScriptEngineFactory`,
it also provides some functionality on top, such as the ability to generate
lines of script code based on SciJava events.
+## Code completion
+
+The Script Editor completes code in Groovy scripts with
+`scijava-code-lsp-groovy`'s language server, driven by Groovy's own
+compiler; this plugin shows it the scripts as plain Groovy (`GroovyDialect`),
+their `#@` parameters declared.
+
For a complete list of scripting languages available as part of the SciJava
platform, see the
[Scripting](https://github.com/scijava/scijava-common/wiki/Scripting) page on
diff --git a/pom.xml b/pom.xml
index 0101349..b4eaefd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -95,6 +95,13 @@
**/script_templates/**
1.2.1
+
+
+ 2.101.0
+ 0.1.0-SNAPSHOT
+ 0.1.0-SNAPSHOT
+
+ 2.14.0
@@ -103,6 +110,18 @@
org.scijava
scijava-common
+
+ org.scijava
+ scijava-code-lsp
+ ${scijava-code-lsp.version}
+
+
+ org.scijava
+ scijava-code-lsp-groovy
+ ${scijava-code-lsp-groovy.version}
+
+ test
+
diff --git a/src/main/java/org/scijava/plugins/scripting/groovy/completion/GroovyDialect.java b/src/main/java/org/scijava/plugins/scripting/groovy/completion/GroovyDialect.java
new file mode 100644
index 0000000..b69006c
--- /dev/null
+++ b/src/main/java/org/scijava/plugins/scripting/groovy/completion/GroovyDialect.java
@@ -0,0 +1,116 @@
+/*
+ * #%L
+ * JSR-223-compliant Groovy scripting language plugin.
+ * %%
+ * Copyright (C) 2014 - 2026 SciJava developers.
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+package org.scijava.plugins.scripting.groovy.completion;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import groovyjarjarantlr4.v4.runtime.Vocabulary;
+
+import org.apache.groovy.parser.antlr4.GroovyLexer;
+import org.scijava.code.lsp.ScriptDialect;
+import org.scijava.plugin.Plugin;
+import org.scijava.plugins.scripting.groovy.GroovyScriptLanguage;
+import org.scijava.script.ScriptLanguage;
+
+/**
+ * Describes how to turn Groovy scripts into plain Groovy code, for
+ * {@code groovy} language servers (e.g. scijava-code-lsp-groovy's).
+ *
+ * @author Gabriel Selzer
+ */
+@Plugin(type = ScriptDialect.class, name = "Groovy")
+public class GroovyDialect implements ScriptDialect {
+
+ /** Names Groovy reserves, which cannot be declared. */
+ private static final Set KEYWORDS = keywords();
+
+ @Override
+ public boolean supports(final ScriptLanguage language) {
+ return language instanceof GroovyScriptLanguage;
+ }
+
+ @Override
+ public String languageId() {
+ return "groovy";
+ }
+
+ @Override
+ public Header header(final Map variables) {
+ // Declare the types of each variable for type reasoning; Groovy gets
+ // the very Java objects, named in full, so nothing needs importing
+ final List declarations = new ArrayList<>();
+ variables.forEach((name, v) -> {
+ // NB: Names Groovy reserves cannot be declared.
+ if (!KEYWORDS.contains(name)) declarations.add(parameterType(v
+ .type()) + " " + name);
+ });
+ // Combine into a Header
+ return new Header(String.join("; ", declarations), Collections
+ .emptySet());
+ }
+
+ /**
+ * @param type the Java type (of a SciJava Script parameter) declared in the
+ * script
+ * @return the Groovy type of the object given to the script: the Java type
+ * itself, by its full name (or {@code Object} for anonymous and
+ * local classes, which have no name to write)
+ */
+ static String parameterType(final Class> type) {
+ final String name = type.getCanonicalName();
+ return name == null ? "Object" : name;
+ }
+
+ /** Groovy's keywords */
+ private static Set keywords() {
+ // NB: The lexer literals don't contain primitives - we add those ourselves
+ final Set out = new TreeSet<>(Arrays.asList("true", "false",
+ "boolean", "byte", "char", "short", "int", "long", "float", "double"));
+ final Vocabulary v = GroovyLexer.VOCABULARY;
+ for (int i = 0; i <= v.getMaxTokenType(); i++) {
+ // NB: The vocabulary has every kind of token: keywords, operators and
+ // punctuation, which have fixed text (their literal name, quoted, e.g.
+ // "'class'"), plus identifiers, strings and numbers, which don't
+ // (null). We want just the literals that are words, i.e. keywords
+ // (with hyphens, as in "non-sealed").
+ final String literal = v.getLiteralName(i);
+ if (literal == null) continue;
+ final String word = literal.substring(1, literal.length() - 1);
+ if (word.matches("[a-z][a-z-]*")) out.add(word);
+ }
+ return out;
+ }
+}
diff --git a/src/test/java/org/scijava/plugins/scripting/groovy/completion/GroovyDialectTest.java b/src/test/java/org/scijava/plugins/scripting/groovy/completion/GroovyDialectTest.java
new file mode 100644
index 0000000..9c5988a
--- /dev/null
+++ b/src/test/java/org/scijava/plugins/scripting/groovy/completion/GroovyDialectTest.java
@@ -0,0 +1,124 @@
+/*
+ * #%L
+ * JSR-223-compliant Groovy scripting language plugin.
+ * %%
+ * Copyright (C) 2014 - 2026 SciJava developers.
+ * %%
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ * #L%
+ */
+
+package org.scijava.plugins.scripting.groovy.completion;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.lsp4j.Diagnostic;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.scijava.Context;
+import org.scijava.code.lsp.LanguageServerService;
+import org.scijava.code.lsp.ScriptDialect.Header;
+import org.scijava.code.lsp.ScriptDialect.Variable;
+import org.scijava.code.lsp.ScriptSession;
+import org.scijava.script.ScriptLanguage;
+import org.scijava.script.ScriptService;
+
+/**
+ * Tests {@link GroovyDialect}
+ *
+ * @author Gabriel Selzer
+ */
+public class GroovyDialectTest {
+
+ private GroovyDialect dialect;
+ private Context context;
+ private ScriptLanguage groovy;
+
+ @Before
+ public void setUp() {
+ dialect = new GroovyDialect();
+ context = new Context(ScriptService.class, LanguageServerService.class);
+ groovy = context.service(ScriptService.class).getLanguageByName("Groovy");
+ }
+
+ @After
+ public void tearDown() {
+ context.dispose();
+ }
+
+ @Test
+ public void testScriptsLanguageAdherence() {
+ // Ensure the dialect supports the Groovy Script Language
+ assertTrue(dialect.supports(groovy));
+ }
+
+ @Test
+ public void testHeader() {
+ // SciJava Script parameters should be declared in Groovy code with
+ // their Java types, e.g. "java.util.List items"
+ final Map parameters = new LinkedHashMap<>();
+ parameters.put("items", Variable.ofType(java.util.List.class));
+ parameters.put("count", Variable.ofType(int.class));
+ // Anonymous classes have no name to write.
+ parameters.put("thing", Variable.ofType(new Object() {}.getClass()));
+ final Header header = dialect.header(parameters);
+ assertEquals("java.util.List items; int count; Object thing", header
+ .code());
+ // Nothing is imported.
+ assertTrue(header.hiddenNames().isEmpty());
+ }
+
+ @Test
+ public void testKeywordsNotDeclared() {
+ // Names Groovy reserves (e.g. a live interpreter's binding named "in")
+ // cannot be declared.
+ final Map variables = new LinkedHashMap<>();
+ variables.put("in", Variable.ofType(String.class));
+ variables.put("int", Variable.ofType(String.class));
+ variables.put("name", Variable.ofType(String.class));
+ assertEquals("java.lang.String name", dialect.header(variables).code());
+ }
+
+ @Test
+ public void testGroovyServerIntegration() throws Exception {
+ // With scijava-code-lsp-groovy's server: the parameters' declarations,
+ // and the blanked parameter lines, parse.
+ final CompletableFuture> heard =
+ new CompletableFuture<>();
+ try (ScriptSession session = new ScriptSession(context.service(
+ LanguageServerService.class), groovy))
+ {
+ session.setDiagnosticsListener(heard::complete);
+ session.update("#@ String name\n#@ int count\nprintln name * count\n");
+ final List problems = heard.get(20, TimeUnit.SECONDS);
+ assertTrue(problems.toString(), problems.isEmpty());
+ }
+ }
+}