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..143b4b2 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..02c0a4b
--- /dev/null
+++ b/src/main/java/org/scijava/plugins/scripting/groovy/completion/GroovyDialect.java
@@ -0,0 +1,84 @@
+/*
+ * #%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.Collections;
+import java.util.List;
+import java.util.Map;
+
+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 {
+
+ @Override
+ public boolean supports(final ScriptLanguage language) {
+ return language instanceof GroovyScriptLanguage;
+ }
+
+ @Override
+ public String languageId() {
+ return "groovy";
+ }
+
+ @Override
+ public Header header(final Map> parameters) {
+ // Declare the types of each parameter for type reasoning; Groovy gets
+ // the very Java objects, named in full, so nothing needs importing
+ final List declarations = new ArrayList<>();
+ parameters.forEach((name, type) -> declarations.add(parameterType(
+ 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;
+ }
+}
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..d9aafc6
--- /dev/null
+++ b/src/test/java/org/scijava/plugins/scripting/groovy/completion/GroovyDialectTest.java
@@ -0,0 +1,112 @@
+/*
+ * #%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.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", java.util.List.class);
+ parameters.put("count", int.class);
+ // Anonymous classes have no name to write.
+ parameters.put("thing", 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 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());
+ }
+ }
+}