Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d0e14f2
Provide Groovy code completion
gselzer Oct 6, 2026
4e1b586
Build the analyzed source from a ScriptDocument
gselzer Oct 6, 2026
12446b7
Serve Groovy as an in-process language server
gselzer Oct 7, 2026
8b757ba
Move off scijava-code-api, now removed
gselzer Oct 7, 2026
2d98f0b
Type script bindings as last assigned
gselzer Oct 7, 2026
62b2a6a
Report problems in Groovy scripts
gselzer Oct 7, 2026
910870e
Ask the language servers through script sessions in tests
gselzer Oct 7, 2026
b6710f9
Follow code-lsp's packages by purpose
gselzer Oct 8, 2026
de1354b
Test signature order, not fit
gselzer Oct 8, 2026
ff2938a
Map diagnostics with ScriptDocument directly
gselzer Oct 9, 2026
b75a1d2
Serve plain Groovy: sessions declare the parameters
gselzer Oct 9, 2026
9df4493
Follow ScriptDialect and ScriptDocument into code-lsp's core package
gselzer Oct 9, 2026
fa20695
Gather completion into its package, and follow code-lsp
gselzer Oct 15, 2026
e63763c
Serve the script language by its name, as code-lsp now asks
gselzer Oct 16, 2026
495b9b9
Offer only a Groovy dialect: the server moves to code-lsp-groovy
gselzer Oct 16, 2026
818f326
Test only the dialect: the server's tests move to code-lsp-groovy
gselzer Oct 16, 2026
a06d9b6
Say what GroovyDialectTest's diagnostics test checks
gselzer Oct 16, 2026
d20b6f3
Test the dialect itself, and need the server only to test
gselzer Oct 16, 2026
d3b34d6
Follow scijava-code-lsp's ScriptDialect.header
gselzer Oct 16, 2026
2c4e64b
Follow scijava-code-lsp: sessions are made with the service
gselzer Oct 20, 2026
19bee5b
Follow scijava-code-lsp: problems arrive through the listener
gselzer Oct 21, 2026
2f835ef
Follow scijava-code-lsp's ScriptDialect.header Variables
gselzer Oct 23, 2026
428c43c
POM: say when scijava-common 2.101.0 needn't be pinned
gselzer Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@
<license.excludes>**/script_templates/**</license.excludes>

<gpars.version>1.2.1</gpars.version>

<!-- TEMP: Remove once pom-scijava manages scijava-common 2.101.0. -->
<scijava-common.version>2.101.0</scijava-common.version>
<scijava-code-lsp.version>0.1.0-SNAPSHOT</scijava-code-lsp.version>
<scijava-code-lsp-groovy.version>0.1.0-SNAPSHOT</scijava-code-lsp-groovy.version>
<!-- NB: As Fiji ships, for scijava-code-lsp (lsp4j). -->
<gson.version>2.14.0</gson.version>
</properties>

<dependencies>
Expand All @@ -103,6 +110,18 @@
<groupId>org.scijava</groupId>
<artifactId>scijava-common</artifactId>
</dependency>
<dependency>
<groupId>org.scijava</groupId>
<artifactId>scijava-code-lsp</artifactId>
<version>${scijava-code-lsp.version}</version>
</dependency>
<dependency>
<groupId>org.scijava</groupId>
<artifactId>scijava-code-lsp-groovy</artifactId>
<version>${scijava-code-lsp-groovy.version}</version>
<!-- NB: For integration tests; applications choose their servers. -->
<scope>test</scope>
</dependency>

<!-- Third-party dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String, Variable> 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<String> 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<String> keywords() {
// NB: The lexer literals don't contain primitives - we add those ourselves
final Set<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Variable> 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<String, Variable> 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<List<Diagnostic>> 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<Diagnostic> problems = heard.get(20, TimeUnit.SECONDS);
assertTrue(problems.toString(), problems.isEmpty());
}
}
}
Loading