← 输入框|github.com|刷新
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a308f24
Provide code completion
gselzer Oct 6, 2026
526729e
Build Jedi's source from a ScriptDocument
gselzer Oct 6, 2026
e2b486c
Complete via jedi-language-server
gselzer Oct 6, 2026
3ef818c
Offer hover and diagnostics from jedi-language-server
gselzer Oct 6, 2026
5fd45f7
Offer jedi-language-server as the Python language server
gselzer Oct 6, 2026
1d07b04
Fix image parameters, and orphaned servers
gselzer Oct 6, 2026
d43e685
Serve appose-python as language servers
gselzer Oct 7, 2026
5ecc6a0
Move off scijava-code-api, now removed
gselzer Oct 7, 2026
a6fa199
Tell language servers when script environments change
gselzer Oct 7, 2026
4142f8f
Take over the Python completion front from code-lsp
gselzer Oct 8, 2026
4b4e8bc
Follow code-lsp's packages by purpose
gselzer Oct 8, 2026
42c0286
Set script files as suppliers in tests
gselzer Oct 8, 2026
c1be7b6
Take completions as one answer in tests
gselzer Oct 8, 2026
0fc15fc
Combine jedi and the syntax-only server in one server
gselzer Oct 8, 2026
dabdd81
Unify Environment's search paths and classpath
gselzer Oct 9, 2026
105b768
Serve plain Python: sessions declare the parameters
gselzer Oct 9, 2026
d752803
Name what the declarations import, so it is not completed
gselzer Oct 9, 2026
35543a2
Follow ScriptDialect and ScriptDocument into code-lsp's core package
gselzer Oct 9, 2026
54899bc
Offer jedi-language-server as a ServerProcessPlugin
gselzer Oct 12, 2026
2b09eb2
Gather the completion code into the completion package
gselzer Oct 12, 2026
342671d
Fold PythonSyntax into PythonSyntaxLanguageServer
gselzer Oct 12, 2026
fa75940
Fold DelegatingLanguageServer into PythonCompletionServer
gselzer Oct 12, 2026
9552b91
Complete Appose Python with jedi alone
gselzer Oct 12, 2026
4cbc7e3
Choose each script's environment as it is analyzed
gselzer Oct 13, 2026
cd8b723
Get jedi from code-lsp-jedi; route scripts between its servers
gselzer Oct 15, 2026
3a784e5
Serve the script language by its name, as code-lsp now asks
gselzer Oct 16, 2026
856b76f
Offer only an Appose-Python dialect: completion moves to code-lsp-jedi
gselzer Oct 16, 2026
27e57f7
Test only Appose specifics: plain Python's tests move to code-lsp-jedi
gselzer Oct 16, 2026
e631dcf
Follow scijava-code-lsp's rename of runtimeType to parameterType
gselzer Oct 16, 2026
048fae7
Follow scijava-code-lsp's ScriptDialect.header
gselzer Oct 16, 2026
9664a7c
Need scijava-code-lsp-jedi only to test
gselzer Oct 16, 2026
f47ea10
Follow scijava-code-lsp: an Optional file, and a non-null live state
gselzer Oct 19, 2026
b9d633e
Analyze scripts with the packages they declare, as the dialect says
gselzer Oct 20, 2026
cd073e6
Prebuild script environments for completion, not overlays
gselzer Oct 20, 2026
5cd99d0
Follow scijava-code-lsp's sessions and environments
gselzer Oct 21, 2026
3d971b4
Follow scijava-code-lsp's ScriptDialect.header Variables
gselzer Oct 23, 2026
7723894
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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,44 @@ memory) it holds, is otherwise released when the application exits.
Text printed to stdout and stderr appears in the Script Editor's output
panes. Errors are reported with a Python traceback that refers to the
script's own line numbers.

## Code completion

The Script Editor completes code in these scripts with a Python language
server, `scijava-code-lsp-jedi`'s:
[jedi-language-server](https://github.com/pappasam/jedi-language-server)
(built on [Jedi](https://jedi.readthedocs.io/)). This plugin's
`ApposePythonDialect` shows it each script as plain Python, and says where
the script runs (so any other client of `scijava-code-lsp`'s
`LanguageServerService` gets the same). It completes names in scope,
the script's `#@` parameters, `task`, builtins and keywords, and members of
objects and modules; and it gives function signatures (with parameter names
and types), documentation, hover and syntax errors.

The server runs in a small environment of its own (`code-lsp-jedi`: Python,
Jedi, jedi-language-server, NumPy and uv), which is built in the background
the first time you complete code, and takes a while only then; until it is, there are no completions. The server
talks to the editor via the
[Language Server Protocol](https://microsoft.github.io/language-server-protocol/)
(see `scijava-code-lsp`); it sees the script as plain Python, with its `#@`
parameters declared. One server runs per script environment; servers
unused for 10 minutes stop. Their error output goes to
`code-lsp-jedi-server.log`, next to Appose's environments.

Once a script's environment is built, by running the script (in this session
or an earlier one), the server analyzes the script against it, so that the packages
installed there are known too. Packages the script declares (in its
`# /// script` block) but its environment lacks, e.g. a dependency you just
added, are installed for completion only, into a separate directory, a moment
after you stop editing the dependencies; so they are known before you run the
script. (`scijava-code-lsp-jedi` does this, for any Python script.) This
covers PyPI packages; conda packages (`[tool.pixi.dependencies]`)
and environments declared in files are known once the script has run.
Completion never builds a script's environment itself, and never disturbs
its worker. Script parameters are known by the Python types they have at run
time, e.g. images as NumPy arrays.

Typing never waits for the server: if its answer for a word is not ready
within a moment (the first answer for a package can take a few seconds), it
is shown once ready. Questions that are no longer of interest (e.g. as you
type on) are cancelled.
18 changes: 17 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,12 @@
<appose.version>0.12.0</appose.version>
<imglib2-appose.version>0.9.0</imglib2-appose.version>

<!-- TEMP: for Task#log(); drop once propagated to pom-scijava. -->
<!-- TEMP: Remove once pom-scijava manages scijava-common 2.101.0.
NB: Task#log() needs it. -->
<scijava-common.version>2.101.0</scijava-common.version>

<scijava-code-lsp.version>0.1.0-SNAPSHOT</scijava-code-lsp.version>
<scijava-code-lsp-jedi.version>0.1.0-SNAPSHOT</scijava-code-lsp-jedi.version>
</properties>

<dependencies>
Expand All @@ -106,6 +110,18 @@
<groupId>org.scijava</groupId>
<artifactId>script-editor</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-jedi</artifactId>
<version>${scijava-code-lsp-jedi.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
Expand Up @@ -37,7 +37,6 @@
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
Expand All @@ -58,7 +57,7 @@

import org.apposed.appose.Appose;
import org.apposed.appose.BuildException;
import org.apposed.appose.Builder;
import org.apposed.appose.Environment;
import org.apposed.appose.NDArray;
import org.apposed.appose.Service;
import org.apposed.appose.Service.TaskStatus;
Expand All @@ -70,7 +69,6 @@
import org.scijava.log.Logger;
import org.scijava.module.ModuleItem;
import org.scijava.plugin.Parameter;
import org.scijava.plugins.scripting.appose.python._internal.InlineMetadata;
import org.scijava.plugins.scripting.appose.python._internal.ResidentWorker;
import org.scijava.plugins.scripting.appose.python._internal.ResidentWorkerService;
import org.scijava.plugins.scripting.appose.python._internal.SciJavaTasks;
Expand Down Expand Up @@ -440,112 +438,32 @@ static boolean ownsNDArray(final Object value, final NDArray nd) {
}

/**
* Gets the resident worker for the script's Appose environment, which the
* script declares either inline, via a PEP 723 {@code # /// script}
* metadata block, or in a file named by the {@code env} (and optionally
* {@code scheme}) attributes of its {@code #@script} directive. The
* environment itself is built lazily, by the worker's first task.
* Gets the resident worker for the script's Appose environment (see
* {@link ScriptEnvironment}). The environment itself is built lazily, by
* the worker's first task.
*/
private ResidentWorker worker(final ScriptInfo info, final String script)
throws ScriptException
{
if (info == null) throw noEnvironment();
final String envRef = info.get("env");
final String metadata;
final ScriptEnvironment env;
try {
metadata = InlineMetadata.extract(script);
env = ScriptEnvironment.of(script, info.getPath(), info.get("env"), info
.get("scheme"));
}
catch (final IllegalArgumentException e) {
catch (final IllegalArgumentException | IOException e) {
throw scriptException(e.getMessage(), e);
}
if (metadata != null) {
if (envRef != null) {
throw new ScriptException("The script declares its environment " +
"twice: inline, and via #@script(env=\"" + envRef + "\"). " +
"Remove one of them.");
}
return inlineWorker(metadata, info.getPath());
}
if (envRef == null) throw noEnvironment();

final File envFile = resolveEnvFile(envRef, info.getPath());
if (!envFile.isFile()) {
throw new ScriptException("Appose environment file not found: " +
envFile.getAbsolutePath());
}
final String content;
try {
content = new String(Files.readAllBytes(envFile.toPath()),
StandardCharsets.UTF_8);
}
catch (final IOException e) {
throw scriptException("Cannot read Appose environment file: " +
envFile.getAbsolutePath(), e);
}
return worker(envName(envFile), content, info.get("scheme"));
if (env == null) throw noEnvironment();
return env.worker(workerService);
}

/** Gets the resident worker for an environment declared inline. */
private ResidentWorker inlineWorker(final String metadata,
final String scriptPath) throws ScriptException
{
// Note: Name the environment after the script, as for environment
// files, so that editing the metadata updates the environment and
// replaces its worker, rather than leaving the old one running.
// Unsaved scripts, e.g. new from a template, have no path to go by.
final String envName = scriptPath == null ? "script-" + String.format(
"%08x", metadata.hashCode()) : envName(new File(scriptPath));
final String content;
try {
content = InlineMetadata.toPyProject(metadata, envName);
}
catch (final IllegalArgumentException e) {
throw scriptException(e.getMessage(), e);
}
return worker(envName, content, InlineMetadata.SCHEME);
}

/**
* Gets the resident worker for the named environment, built from the given
* configuration.
*/
private ResidentWorker worker(final String envName, final String content,
final String scheme)
{
return workerService.worker(envName, scheme + "\n" + content, () -> {
final Builder<?> builder = Appose.content(content);
return scheme == null ? builder : builder.scheme(scheme);
},
// Note: On Windows, importing numpy from a task hangs unless numpy
// was imported during worker initialization.
env -> env.python().init(
"try:\n import numpy\nexcept ImportError:\n pass\n"));
}

/** Resolves an (optionally relative) env file path against the script path. */
static File resolveEnvFile(final String envRef, final String scriptPath) {
final File envFile = new File(envRef);
if (envFile.isAbsolute()) return envFile;
if (scriptPath != null) {
final File parent = new File(scriptPath).getAbsoluteFile().getParentFile();
if (parent != null) return new File(parent, envRef);
}
return envFile;
}

/**
* Derives a valid Appose environment name from an environment file: its
* base name, plus a hash of its absolute path to avoid collisions between
* like-named files in different directories. E.g.:
* {@code /path/to/cellcast.toml} becomes {@code cellcast-1a2b3c4d}.
*/
static String envName(final File envFile) {
final String fileName = envFile.getName();
final int dot = fileName.lastIndexOf('.');
final String baseName = dot > 0 ? fileName.substring(0, dot) : fileName;
final String path = envFile.getAbsoluteFile().toPath().normalize().toString();
return baseName.replaceAll("[^a-zA-Z0-9_-]", "_") + "-" +
String.format("%08x", path.hashCode());
/** Creates the service of a worker in the given (built) environment. */
static Service launch(final Environment env) {
// Note: On Windows, importing numpy from a task hangs unless numpy
// was imported during worker initialization.
return env.python().init(
"try:\n import numpy\nexcept ImportError:\n pass\n");
}

private static ScriptException noEnvironment() {
Expand Down
Loading