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()); + } + } +}