Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d52fab4
Provide toolkit-agnostic Jython code completion
ctrueden Jun 22, 2026
6da840c
WIP
ctrueden Oct 3, 2026
d6cbdf1
Remove snapshot dependency
gselzer Oct 3, 2026
5813d17
Grab inherited methods/fields
gselzer Oct 4, 2026
ed91469
WIP: AI-generated overloaded params
gselzer Oct 4, 2026
d8fbc6f
Provide signature help
gselzer Oct 6, 2026
da5bc98
Type script parameters via a JythonDialect
gselzer Oct 6, 2026
77b2d7f
Add a Python language server's completions, via Java stubs
gselzer Oct 6, 2026
a8dd8c0
Serve Jython as in-process language servers
gselzer Oct 7, 2026
0dd6198
Move off scijava-code-api, now removed
gselzer Oct 7, 2026
2134858
Ask the language servers through script sessions in tests
gselzer Oct 7, 2026
f724d7f
Follow code-lsp's packages by purpose
gselzer Oct 8, 2026
9413b08
Test signature order, not fit
gselzer Oct 8, 2026
dcfcc54
Combine Jython's analysis and jedi in one server
gselzer Oct 8, 2026
be381ca
Unify Environment's search paths and classpath
gselzer Oct 9, 2026
657bb21
Serve plain Python: sessions declare the parameters
gselzer Oct 9, 2026
8a9ac66
Declare parameters with the types Jython converts them to
gselzer Oct 9, 2026
4dd6ae9
Name what the declarations import, so it is not completed
gselzer Oct 9, 2026
e58e4a6
Follow ScriptDialect and ScriptDocument into code-lsp's core package
gselzer Oct 9, 2026
fca039d
Gather completion into its package; get jedi from code-lsp-jedi
gselzer Oct 15, 2026
ae27804
Serve the script language by its name, as code-lsp now asks
gselzer Oct 16, 2026
bb21017
Offer only a Jython dialect: the server moves to code-lsp-jython
gselzer Oct 16, 2026
e28d1c2
Test only the dialect: the server's tests move to code-lsp-jython
gselzer Oct 16, 2026
6a9c499
Follow scijava-code-lsp's rename of runtimeType to parameterType
gselzer Oct 16, 2026
2711b87
Follow scijava-code-lsp's ScriptDialect.header
gselzer Oct 16, 2026
a5f3553
Need scijava-code-lsp-jython only to test
gselzer Oct 16, 2026
7371b04
Follow scijava-code-lsp: sessions are made with the service
gselzer Oct 20, 2026
f692d17
Test completion with scijava-code-lsp-jython's jedi
gselzer Oct 21, 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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,25 @@ 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 Jython scripts: Java classes, their
members and constructors (with their overloads), imports, the script's `#@`
parameters and, in a live interpreter, its variables. This is Jython's own
completion, which knows Java exactly, by `scijava-code-lsp-jython`'s language
server; this plugin shows it the scripts as plain Jython (`JythonDialect`),
their parameters declared.

A Python language server (jedi-language-server, offered by
`scijava-code-lsp-jedi`, which runs it in a small environment of its own,
built the first time it is needed) adds its completions too: for Python
modules, and for Java classes, from Python type stubs (`.pyi`) of the Java
packages the script imports, written in the background
to `~/.cache/scijava/jython-stubs` (or `-Dscijava.jython.stubs=<dir>`). The
server analyzes the script as CPython (Jython's standard library is Python
2's, but mostly the same); so it is asked for completions only, not for
problems.

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
21 changes: 21 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@
<properties>
<package-name>org.scijava.plugins.scripting.jython</package-name>

<scijava-common.version>2.101.0</scijava-common.version>
<scijava-code-lsp.version>0.1.0-SNAPSHOT</scijava-code-lsp.version>
<scijava-code-lsp-jython.version>0.1.0-SNAPSHOT</scijava-code-lsp-jython.version>
<!-- NB: As Fiji ships, for scijava-code-lsp (lsp4j); newer than pom-scijava 34's. -->
<gson.version>2.14.0</gson.version>
<!-- NB: As scijava-code-lsp-jedi is built with; newer than pom-scijava 34's. -->
<jackson.version>2.19.2</jackson.version>

<license.licenseName>bsd_2</license.licenseName>
<license.copyrightOwners>SciJava developers.</license.copyrightOwners>
<license.excludes>**/script_templates/**</license.excludes>
Expand All @@ -98,6 +106,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-jython</artifactId>
<version>${scijava-code-lsp-jython.version}</version>
<!-- NB: For integration tests; applications choose their servers. -->
<scope>test</scope>
</dependency>

<!-- Third-party dependencies -->
<dependency>
Expand All @@ -112,4 +132,5 @@
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*-
* #%L
* JSR-223-compliant Jython scripting language plugin.
* %%
* Copyright (C) 2020 - 2025 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.jython.completion;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.scijava.code.lsp.ScriptDialect;
import org.scijava.plugin.Plugin;
import org.scijava.plugins.scripting.jython.JythonScriptLanguage;
import org.scijava.script.ScriptLanguage;

/**
* Describes how to turn Jython scripts into plain Jython code. Scripts see
* their parameters as the Python values Jython converts them to (e.g.
* {@code str} for strings, {@code int} for integers, {@code array.array} for
* arrays), or else as the Java objects themselves (e.g.
* {@code net.imglib2.img.Img}).
* <p>
* Each is declared as an instance of its type, in code both Jython's own
* analysis (Python 2) and a Python language server knowing the Java packages
* from stubs (see scijava-code-lsp-jython) read: e.g.
* {@code import net.imglib2.img; image = net.imglib2.img.Img(); name = str()}.
* </p>
*
* @author Gabriel Selzer
*/
@Plugin(type = ScriptDialect.class, name = "Jython")
public class JythonDialect implements ScriptDialect {

@Override
public boolean supports(final ScriptLanguage language) {
return language instanceof JythonScriptLanguage;
}

@Override
public String languageId() {
return "jython";
}

@Override
public Header header(final Map<String, Class<?>> parameters) {
// Declare each parameter as an instance of its type, for type reasoning
final List<String> types = new ArrayList<>();
final List<String> declarations = new ArrayList<>();
parameters.forEach((name, type) -> {
final String t = parameterType(type);
types.add(t);
declarations.add(name + " = " + t.replace('$', '.') + "()");
});
// And import the packages of those types, before the declarations
final List<String> code = new ArrayList<>();
final Set<String> hidden = new TreeSet<>();
for (final String p : packages(types)) {
code.add("import " + p);
// NB: "import a.b" binds a.
hidden.add(p.split("\\.")[0]);
}
code.addAll(declarations);
// Combine into a Header
return new Header(String.join("; ", code), hidden);
}

/**
* @param type the Java type (of a SciJava Script parameter) declared in the
* script
* @return the type of the object given to the script by Jython: a Python
* type for the values Jython converts (e.g. {@code str}), or else
* the Java type itself
*/
static String parameterType(final Class<?> type) {
final Class<?> t = boxed(type);
// NB: As Jython converts them (Py.java2py).
if (t == String.class || t == Character.class) return "str";
if (t == Integer.class || t == Long.class || t == Short.class ||
t == Byte.class || t == BigInteger.class) return "int";
if (t == Double.class || t == Float.class) return "float";
if (t == Boolean.class) return "bool";
if (t.isArray()) return "array.array";
return t.getName();
}

/**
* The packages or modules of the given types that a declaration refers to
* (none for builtins, e.g. {@code str}).
*/
static Set<String> packages(final Collection<String> types) {
final Set<String> packages = new TreeSet<>();
for (final String t : types) {
if (t.lastIndexOf('.') > 0) packages.add(t.substring(0, t.lastIndexOf(
'.')));
}
return packages;
}

/** Maps primitive types to their wrappers. */
private static Class<?> boxed(final Class<?> type) {
if (!type.isPrimitive()) return type;
if (type == int.class) return Integer.class;
if (type == long.class) return Long.class;
if (type == double.class) return Double.class;
if (type == float.class) return Float.class;
if (type == boolean.class) return Boolean.class;
if (type == char.class) return Character.class;
if (type == short.class) return Short.class;
if (type == byte.class) return Byte.class;
return Object.class; // void
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*-
* #%L
* JSR-223-compliant Jython scripting language plugin.
* %%
* Copyright (C) 2020 - 2025 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.jython.completion;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.eclipse.lsp4j.CompletionItem;
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 JythonDialect}: Jython scripts, as plain Jython for
* scijava-code-lsp-jython's server (which its own tests test).
*
* @author Gabriel Selzer
*/
public class JythonDialectTest {

private Context context;
private ScriptLanguage jython;

@Before
public void setUp() {
context = new Context(ScriptService.class, LanguageServerService.class);
jython = context.service(ScriptService.class).getLanguageByName("Jython");
}

@After
public void tearDown() {
context.dispose();
}

@Test
public void testScriptsAreServed() {
// Jython scripts become plain Jython, which scijava-code-lsp-jython
// serves.
final JythonDialect dialect = new JythonDialect();
assertTrue(dialect.supports(jython));
assertEquals("jython", dialect.languageId());
assertTrue(context.service(LanguageServerService.class).supports(jython));
}

@Test
public void testParameterType() {
// As Jython converts them: e.g. strings and numbers to Python's.
assertEquals("str", JythonDialect.parameterType(String.class));
assertEquals("int", JythonDialect.parameterType(int.class));
assertEquals("int", JythonDialect.parameterType(Long.class));
assertEquals("float", JythonDialect.parameterType(double.class));
assertEquals("bool", JythonDialect.parameterType(boolean.class));
assertEquals("array.array", JythonDialect.parameterType(
double[].class));
// Or else as the Java objects themselves.
assertEquals("java.math.BigDecimal", JythonDialect.parameterType(
java.math.BigDecimal.class));
}

@Test
public void testHeader() {
// Each parameter is declared as an instance of its type, whose package
// is imported.
final Map<String, Class<?>> parameters = new LinkedHashMap<>();
parameters.put("d", java.math.BigDecimal.class);
parameters.put("n", int.class);
parameters.put("e", Map.Entry.class);
parameters.put("a", double[].class);
final Header header = new JythonDialect().header(parameters);
assertEquals("import array; import java.math; import java.util; " +
"d = java.math.BigDecimal(); n = int(); " +
"e = java.util.Map.Entry(); a = array.array()", header.code());
// The names the imports bind ("import a.b" binds a) are the header's
// alone.
assertEquals(Arrays.asList("array", "java"), Arrays.asList(header
.hiddenNames().toArray()));
}

@Test
public void testScriptParameters() {
final String header = "" + //
"#@ String (label=\"Please enter your name\") name\n" + //
"#@output String greeting\n" + //
"#@ int count\n" + //
"#@ NoSuchType mystery\n";
assertTrue(complete(header + "x = na").contains("name"));
assertTrue(complete(header + "myst").contains("mystery"));
// As the Python values Jython converts them to: e.g. strings are str,
// whose members are Python's, not java.lang.String's.
final List<String> nameMembers = complete(header + "name.upp");
assertTrue("expected upper among " + nameMembers, nameMembers.contains(
"upper"));
assertTrue(complete(header + "name.toUpp").isEmpty());
// Outputs are known too, even before the script assigns them.
assertTrue(complete(header + "greeting.st").contains("strip"));
// Numbers are int, not java.lang.Integer.
assertFalse(complete(header + "count.intV").contains("intValue"));
// Packages imported only to declare those are not completed.
assertFalse(complete(header + "j").contains("java"));
}


@Test
public void testAssignmentShadowsScriptParameter() {
final List<String> texts = complete(
"#@ String name\nfrom java.util import ArrayList\nname = ArrayList()\nname.ensureCap");
assertTrue("expected ArrayList member among " + texts, texts.contains(
"ensureCapacity"));
}


// -- Helper methods --

private List<String> complete(final String code) {
try (ScriptSession session = new ScriptSession(context.service(
LanguageServerService.class), jython))
{
return session.completion(code, code.length()).get(30, TimeUnit.SECONDS)
.getItems().stream().map(CompletionItem::getLabel).collect(Collectors
.toList());
}
catch (final Exception exc) {
throw new RuntimeException(exc);
}
}
}
Loading