From 39891ea03b3d53137b1dc48e6c4375d023aa1a85 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer Date: Sun, 4 Oct 2026 14:29:11 -0500 Subject: [PATCH 01/21] Rework editor completion onto the neutral CodeCompleter SPI Make the script editor a thin adapter over scijava-common's toolkit-agnostic completion SPI, so languages contribute completion with no Swing/RSTA code: - SciJavaCompletionProvider: the single bridge translating neutral Completions to RSTA completions (BasicCompletion, or FunctionCompletion for callables so parameter assistance engages), and neutral ParameterChoices to RSTA's ParameterChoicesProvider. - CodeCompleterLanguageSupport: lightweight completion tier installed for any language providing a CodeCompleterPlugin. - SciJavaAutoCompletion + SciJavaCompletion/SciJavaFunctionCompletion + AdditionalEdits: apply language-neutral auto-imports (Completion.TextEdit) on acceptance, across both basic and function completions. - EditorPane: resolve completion via explicit tiers (heavy RSTA plugin -> neutral CodeCompleter -> RSTA legacy -> Java fallback), tracking the installed support; existing LanguageSupportPlugins keep working. - PromptPane: REPL Tab-completion via CodeCompletionService, so every language gets baseline interpreter completion. - ClassUtil: reduced to a thin facade over scijava-common's ClassIndex. pom.xml carries a temporary scijava-common.version override until a release pins the new SPI. Co-Authored-By: Claude Opus 4.8 --- pom.xml | 5 + .../scijava/ui/swing/script/EditorPane.java | 30 +- .../scijava/ui/swing/script/PromptPane.java | 99 +++- .../autocompletion/AdditionalEdits.java | 50 ++ .../script/autocompletion/ClassUtil.java | 475 +++--------------- .../CodeCompleterLanguageSupport.java | 83 +++ .../autocompletion/SciJavaAutoCompletion.java | 108 ++++ .../autocompletion/SciJavaCompletion.java | 64 +++ .../SciJavaCompletionProvider.java | 281 +++++++++++ .../SciJavaFunctionCompletion.java | 68 +++ .../SciJavaCompletionProviderTest.java | 143 ++++++ 11 files changed, 985 insertions(+), 421 deletions(-) create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java create mode 100644 src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java diff --git a/pom.xml b/pom.xml index fc087bed..7485a995 100644 --- a/pom.xml +++ b/pom.xml @@ -140,6 +140,11 @@ + 11 + + 2.100.2-SNAPSHOT org.scijava.ui.swing.script.Main org.scijava.ui.swing.script bsd_2 diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 22c3067a..3aec38e0 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -94,6 +94,8 @@ import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptService; import org.scijava.ui.swing.script.vim.VimHandler; +import org.scijava.script.complete.CodeCompletionService; +import org.scijava.ui.swing.script.autocompletion.CodeCompleterLanguageSupport; import org.scijava.util.FileUtils; /** @@ -120,6 +122,8 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { private boolean autoCompletionJavaFallback; private boolean autoCompletionWithoutKey; private String supportStatus; + /** The completion support currently installed on this pane, if any. */ + private LanguageSupport installedSupport; private final ErrorParser errorHighlighter; private final JMenu noneLangSyntaxMenu; private final EditorPaneActions actions; @@ -131,6 +135,8 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { @Parameter private LanguageSupportService languageSupportService; @Parameter + private CodeCompletionService codeCompletionService; + @Parameter private ScriptService scriptService; @Parameter private ScriptHeaderService scriptHeaderService; @@ -731,11 +737,11 @@ protected void setLanguage(final ScriptLanguage language, final boolean addHeader) { // uninstall existing language support. - LanguageSupport support = - languageSupportService.getLanguageSupport(currentLanguage); - if (support != null) { - support.uninstall(this); + if (installedSupport != null) { + installedSupport.uninstall(this); + installedSupport = null; } + LanguageSupport support; String languageName; String defaultExtension; @@ -791,15 +797,24 @@ protected void setLanguage(final ScriptLanguage language, return; // no need to update console any further } String supportLevel = "SciJava supported"; - // try to get language support for current language, may be null. + // Tier 1: a heavy RSTA LanguageSupportPlugin (e.g. the Java parser). support = languageSupportService.getLanguageSupport(currentLanguage); - // that did not work. See if there is internal support for it. + // Tier 2: a toolkit-agnostic CodeCompleterPlugin for this language. + if (support == null && + codeCompletionService.getCompleterPlugin(currentLanguage) != null) + { + support = new CodeCompleterLanguageSupport( + codeCompletionService.getCompleterPlugin(currentLanguage), + currentLanguage); + } + + // Tier 3: RSTA's own built-in support for the syntax style. if (support == null) { support = LanguageSupportFactory.get().getSupportFor(styleName); supportLevel = "Legacy supported"; } - // that did not work, Fallback to Java + // Tier 4: fall back to Java completion, if requested. if (support == null && autoCompletionJavaFallback) { support = languageSupportService.getLanguageSupport(scriptService.getLanguageByName("Java")); supportLevel = "N/A. Using Java as fallback"; @@ -808,6 +823,7 @@ protected void setLanguage(final ScriptLanguage language, support.setAutoCompleteEnabled(autoCompletionEnabled); support.setAutoActivationEnabled(autoCompletionWithoutKey); support.install(this); + installedSupport = support; if (!autoCompletionEnabled) supportLevel += " but currently disabled\n"; else { diff --git a/src/main/java/org/scijava/ui/swing/script/PromptPane.java b/src/main/java/org/scijava/ui/swing/script/PromptPane.java index 42d1a2c6..700d4aef 100644 --- a/src/main/java/org/scijava/ui/swing/script/PromptPane.java +++ b/src/main/java/org/scijava/ui/swing/script/PromptPane.java @@ -31,18 +31,27 @@ import static java.awt.event.KeyEvent.VK_DOWN; import static java.awt.event.KeyEvent.VK_ENTER; +import static java.awt.event.KeyEvent.VK_TAB; import static java.awt.event.KeyEvent.VK_UP; import java.awt.Rectangle; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.PrintStream; +import java.util.List; +import javax.script.ScriptEngine; import javax.swing.JTextArea; import javax.swing.text.BadLocationException; import org.scijava.Context; +import org.scijava.script.ScriptInterpreter; +import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptREPL; +import org.scijava.script.complete.CodeCompletionService; +import org.scijava.script.complete.Completion; +import org.scijava.script.complete.CompletionRequest; +import org.scijava.script.complete.CompletionResult; import org.scijava.thread.ThreadService; import org.scijava.util.ClassUtils; import org.scijava.util.Types; @@ -104,6 +113,12 @@ public void keyPressed(final KeyEvent event) { event.consume(); } break; + case VK_TAB: + if (!executing) { + complete(); + event.consume(); + } + break; } } @@ -178,11 +193,91 @@ private void execute() { } private ThreadService threadService() { + return context().service(ThreadService.class); + } + + private Context context() { // HACK: Get the SciJava context from the REPL. // This can be fixed if/when the REPL offers a getter for it. - final Context ctx = (Context) ClassUtils.getValue(// + return (Context) ClassUtils.getValue(// Types.field(repl.getClass(), "context"), repl); - return ctx.service(ThreadService.class); + } + + /** + * Tab-completion for the REPL prompt. Delegates to the language-agnostic + * {@link CodeCompletionService}, which inspects the live engine bindings, so + * every scripting language gets at least baseline completion here. + */ + private void complete() { + final ScriptInterpreter interpreter = repl.getInterpreter(); + if (interpreter == null) return; + final ScriptEngine engine = interpreter.getEngine(); + final ScriptLanguage language = interpreter.getLanguage(); + final int caret = textArea.getCaretPosition(); + final String text = textArea.getText(); + + final CompletionResult result; + try { + result = context().service(CodeCompletionService.class).complete(// + new CompletionRequest(text, caret, language, engine, // + engine == null ? null : engine.getContext())); + } + catch (final Exception exc) { + return; + } + + final List completions = result.completions(); + if (completions.isEmpty()) { + textArea.getToolkit().beep(); + return; + } + final int start = Math.min(Math.max(result.replaceStart(), 0), caret); + + if (completions.size() == 1) { + replaceRange(start, caret, completions.get(0).insertionText()); + return; + } + + // Multiple candidates: insert their longest common prefix (if it extends + // what is already typed), and list the options in the output pane. + final String lcp = longestCommonPrefix(completions); + final String current = text.substring(start, caret); + if (lcp.length() > current.length()) { + replaceRange(start, caret, lcp); + } + else { + final StringBuilder sb = new StringBuilder(); + for (final Completion c : completions) { + if (sb.length() > 0) sb.append(" "); + sb.append(c.displayText()); + } + output.append(sb.append("\n").toString()); + } + } + + private void replaceRange(final int start, final int end, + final String replacement) + { + try { + textArea.getDocument().remove(start, end - start); + textArea.getDocument().insertString(start, replacement, null); + } + catch (final BadLocationException exc) { + // ignore: the prompt changed underneath us + } + } + + private static String longestCommonPrefix(final List completions) { + String prefix = completions.get(0).insertionText(); + for (final Completion c : completions) { + final String s = c.insertionText(); + int i = 0; + final int max = Math.min(prefix.length(), s.length()); + while (i < max && prefix.charAt(i) == s.charAt(i)) i++; + prefix = prefix.substring(0, i); + if (prefix.isEmpty()) break; + } + return prefix; } // -- Helper classes -- diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java new file mode 100644 index 00000000..f19af216 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java @@ -0,0 +1,50 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.util.List; + +import org.scijava.script.complete.Completion.TextEdit; + +/** + * Implemented by RSTA completions that carry + * {@link org.scijava.script.complete.Completion#additionalEdits() additional + * edits} (e.g. auto-imports) to apply when accepted. Both the + * {@code BasicCompletion}-based and {@code FunctionCompletion}-based SciJava + * completions implement this, so {@link SciJavaAutoCompletion} can apply the + * edits regardless of the underlying RSTA completion type. + * + * @author Curtis Rueden + */ +public interface AdditionalEdits { + + /** Extra edits to apply when the completion is accepted; never null. */ + List getAdditionalEdits(); +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java index a0a45ef4..e391de7a 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java @@ -6,13 +6,13 @@ * %% * 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 @@ -28,450 +28,101 @@ */ package org.scijava.ui.swing.script.autocompletion; -import java.io.File; -import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.net.URL; -import java.net.URLConnection; import java.util.ArrayList; -import java.util.Enumeration; import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; import java.util.List; -import java.util.Map; -import java.util.Scanner; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Stream; import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.CompletionProvider; +import org.scijava.script.complete.ClassIndex; +/** + * Thin facade over the toolkit-agnostic {@link ClassIndex} in scijava-common, + * retained for the script editor's Swing-specific completion code. New code + * should use {@link ClassIndex} directly; only the RSTA-specific + * {@link #classUnavailableCompletions} helper is genuinely editor-side. + * + * @author Albert Cardona + * @author Tiago Ferreira + * @deprecated Use {@link ClassIndex} for class discovery and documentation; this + * facade exists to ease migration. + */ +@Deprecated public class ClassUtil { - - static final String scijava_javadoc_URL = "https://javadoc.scijava.org/"; // with ending slash - - /** Cache of class names vs list of URLs found in the pom.xml files of their contaning jar files, if any. */ - static private final Map class_urls = new HashMap<>(); - - static private final Map package_urls = new HashMap<>(); - - static private boolean ready = false; - - /** Cache of subURL javadoc at https://javadoc.scijava.org */ - static private final HashMap scijava_javadoc_URLs = new HashMap<>(); - - static public final void ensureCache() { - synchronized (class_urls) { - if (class_urls.isEmpty()) { - final ArrayList dirs = new ArrayList<>(); - dirs.add(System.getProperty("java.home")); - dirs.add(System.getProperty("ij.dir")); - class_urls.putAll(findAllClasses(dirs)); - // Soft attempt at getting all packages (will get them wrong if multiple jars have the same packages) - for (final Map.Entry entry: class_urls.entrySet()) { - final int idot = entry.getKey().lastIndexOf('.'); - if (-1 == idot) continue; // no package - final String package_name = entry.getKey().substring(0, idot); - if (package_urls.containsKey(package_name)) continue; - package_urls.put(package_name, entry.getValue()); - } - ready = true; - } - } - } - - static public final boolean isCacheReady() { - return ready; - } - - static public final void ensureSciJavaSubURLCache() { - synchronized (scijava_javadoc_URLs) { - if (!scijava_javadoc_URLs.isEmpty()) return; - Scanner scanner = null; - try { - final Pattern pattern = Pattern.compile("
"); - final URLConnection connection = new URL(scijava_javadoc_URL).openConnection(); - scanner = new Scanner(connection.getInputStream()); - while (scanner.hasNext()) { - final Matcher matcher = pattern.matcher(scanner.nextLine()); - if (matcher.find()) { - String name = matcher.group(1).toLowerCase(); - if (name.endsWith("/")) name = name.substring(0, name.length() -1); - scijava_javadoc_URLs.put(name, scijava_javadoc_URL + matcher.group(1)); - } - } - scanner.close(); - } catch ( Exception e ) { - e.printStackTrace(); - } finally { - if (null != scanner) scanner.close(); - } - } - } - - static public HashMap findClassDocumentationURLs(final String s) { - ensureCache(); - final HashMap matches = new HashMap<>(); - for (final Map.Entry entry: class_urls.entrySet()) { - if (entry.getKey().contains(s)) { - final JarProperties props = entry.getValue(); - matches.put(entry.getKey(), new JarProperties(props.name, new ArrayList(props.urls))); - } - } - return matches; - } - - static public HashMap> findDocumentationForClass(final String s) { - final HashMap matches = findClassDocumentationURLs(s); - ensureSciJavaSubURLCache(); - - final Pattern javaPackages = Pattern.compile("^(java|javax|org\\.omg|org\\.w3c|org\\.xml|org\\.ietf\\.jgss)\\..*$"); - final String version = System.getProperty("java.version"); - final String majorVersion = version.startsWith("1.") ? - version.substring(2, version.indexOf('.', 2)) - : version.substring(0, version.indexOf('.')); - final String javaDoc = "java" + majorVersion; - - final HashMap> class_urls = new HashMap<>(); - - for (final Map.Entry entry: matches.entrySet()) { - final String classname = entry.getKey(); - final ArrayList urls = new ArrayList<>(); - class_urls.put(classname, urls); - if (javaPackages.matcher(classname).matches()) { - urls.add(scijava_javadoc_URLs.get(javaDoc) + classname.replace('.', '/') + ".html"); - } else { - final JarProperties props = entry.getValue(); - // Find the first URL with git in it - for (final String url : props.urls) { - final boolean github = url.contains("/github.com"), - gitlab = url.contains("/gitlab.com"); - if (github || gitlab) { - // Find the 5th slash, e.g. https://github.com/imglib/imglib2/ - int count = 0; - int last = 0; - while (count < 5) { - last = url.indexOf('/', last + 1); - if (-1 == last) break; // less than 5 found - ++count; - } - String urlbase = url; - if (5 == count) urlbase = url.substring(0, last); // without the ending slash - // Assume maven, since these URLs were found in a pom.xml: src/main/java/ - urls.add(urlbase + (gitlab ? "/-" : "") + "/blob/master/src/main/java/" + classname.replace('.', '/') + ".java"); - break; - } - } - // Try to find a javadoc in the scijava website - if (null != props.name) { - String scijava_javadoc_url = scijava_javadoc_URLs.get(props.name.toLowerCase()); - if (null == scijava_javadoc_url) { - // Try cropping name at the first whitespace if any (e.g. "ImgLib2 Core Library" to "ImgLib2") - for (final String word: props.name.split(" ")) { - scijava_javadoc_url = scijava_javadoc_URLs.get(word.toLowerCase()); - if (null != scijava_javadoc_url) break; // found a valid one - } - } - if (null != scijava_javadoc_url) { - urls.add(scijava_javadoc_url + classname.replace('.', '/') + ".html"); - } else { - // Try Fiji: could be a plugin - Scanner scanner = null; - try { - final String url = scijava_javadoc_URL + "Fiji/" + classname.replace('.', '/') + ".html"; - final URLConnection c = new URL(url).openConnection(); - scanner = new Scanner(c.getInputStream()); - while (scanner.hasNext()) { - final String line = scanner.nextLine(); - if (line.contains("")) { - if (!line.contains("<title>404")) { - urls.add(url); - } - break; - } - } - } catch (Exception e) { - // Ignore: 404 that wasn't redirected to an error page - } finally { - if (null != scanner) scanner.close(); - } - } - } - } - } - return class_urls; - } - - static public final class JarProperties { - public final ArrayList<String> urls; - public String name = null; - public JarProperties(final String name, final ArrayList<String> urls) { - this.name = name; - this.urls = urls; - } + public static void ensureCache() { + ClassIndex.ensureCache(); } - - static public final HashMap<String, JarProperties> findAllClasses(final List<String> jar_folders) { - // Find all jar files - final ArrayList<String> jarFilePaths = new ArrayList<String>(); - final LinkedList<String> dirs = new LinkedList<>(jar_folders); - final HashSet<String> seenDirs = new HashSet<>(); - while (!dirs.isEmpty()) { - final String filepath = dirs.removeFirst(); - if (null == filepath) continue; - final File file = new File(filepath); - seenDirs.add(file.getAbsolutePath()); - if (file.exists() && file.isDirectory()) { - for (final File child : file.listFiles()) { - final String childfilepath = child.getAbsolutePath(); - if (seenDirs.contains(childfilepath)) continue; - if (child.isDirectory()) dirs.add(childfilepath); - else if (childfilepath.endsWith(".jar")) jarFilePaths.add(childfilepath); - } - } - } - // Find all classes from all jar files - final HashMap<String, JarProperties> class_urls = new HashMap<>(); - final Pattern urlpattern = Pattern.compile(">(http.*?)<"); - final Pattern namepattern = Pattern.compile("<name>(.*?)<"); - for (final String jarpath : jarFilePaths) { - JarFile jar = null; - try { - jar = new JarFile(jarpath); - final Enumeration<JarEntry> entries = jar.entries(); - final ArrayList<String> urls = new ArrayList<>(); - final JarProperties props = new JarProperties(null, urls); - // For every filepath in the jar zip archive - while (entries.hasMoreElements()) { - final JarEntry entry = entries.nextElement(); - if (entry.isDirectory()) continue; - if (entry.getName().endsWith(".class")) { - String classname = entry.getName().replace('/', '.'); - final int idollar = classname.indexOf('$'); - if (-1 != idollar) { - classname = classname.substring(0, idollar); // truncate at the first dollar sign - } else { - classname = classname.substring(0, classname.length() - 6); // without .class - } - class_urls.put(classname, props); - } else if (entry.getName().endsWith("/pom.xml")) { - final Scanner scanner = new Scanner(jar.getInputStream(entry)); - while (scanner.hasNext()) { - final String line = scanner.nextLine(); - final Matcher matcher1 = urlpattern.matcher(line); - if (matcher1.find()) { - urls.add(matcher1.group(1)); - } - if (null == props.name) { - final Matcher matcher2 = namepattern.matcher(line); - if (matcher2.find()) { - props.name = matcher2.group(1); - } - } - } - scanner.close(); - } - } - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (null != jar) try { - jar.close(); - } catch (IOException e) { e.printStackTrace(); } - } - } - return class_urls; + + public static boolean isCacheReady() { + return ClassIndex.isCacheReady(); } - - static public final Stream<String> findPackageNamesStartingWith(final String text) { - ensureCache(); - return package_urls.keySet().stream().filter(s -> s.startsWith(text)); + + public static Stream<String> findPackageNamesStartingWith(final String t) { + return ClassIndex.findPackageNamesStartingWith(t); } - - static public final Stream<String> findClassNamesForPackage(final String packageName) { - ensureCache(); - if (null == packageName || packageName.length() == 0) - return class_urls.keySet().stream(); - return class_urls.keySet().stream().filter(s -> s.startsWith(packageName) && -1 == s.indexOf('.', packageName.length() + 2)); + + public static Stream<String> findClassNamesForPackage(final String pkg) { + return ClassIndex.findClassNamesForPackage(pkg); } - - /** - * - * @param text A left-justified substring of a fully qualified class name, with the package. - * @return - */ - static public final Stream<String> findClassNamesStartingWith(final String text) { - ensureCache(); - if (text.length() == 0) - return class_urls.keySet().stream(); - return class_urls.keySet().stream().filter(s -> s.startsWith(text)); + + public static Stream<String> findClassNamesStartingWith(final String t) { + return ClassIndex.findClassNamesStartingWith(t); } - - /** - * - * @param text A substring of a class fully qualified name. - * @return - */ - static public final Stream<String> findClassNamesContaining(final String text) { - ensureCache(); - return class_urls.keySet().stream().filter(s -> s.contains(text)); + + public static Stream<String> findClassNamesContaining(final String t) { + return ClassIndex.findClassNamesContaining(t); } - - /** - * Find simple class names starting with "text", returning the fully qualified class names. - * @param text - * @return - */ - static public final ArrayList<String> findSimpleClassNamesStartingWith(final String text) { - ensureCache(); - final ArrayList<String> matches = new ArrayList<>(); - if (0 == text.length()) - return matches; - for (final String classname: class_urls.keySet()) { - final int idot = classname.lastIndexOf('.'); - final String simplename = -1 == idot ? classname : classname.substring(idot + 1); - if (simplename.startsWith(text)) matches.add(classname); - } - return matches; + + public static ArrayList<String> findSimpleClassNamesStartingWith( + final String t) + { + return ClassIndex.findSimpleClassNamesStartingWith(t); } - private static String getJavaDocLink(final Class<?> c) { - final String name = c.getCanonicalName(); - final String pkg = getDocPackage(name); - if (pkg == null) return name; - final String url = String.format("%s%s/index.html?%s.html", scijava_javadoc_URL, pkg, name.replace(".", "/")); - return String.format("<a href='%s';>%s</a>", url, name); + public static HashMap<String, ArrayList<String>> findDocumentationForClass( + final String s) + { + return ClassIndex.findDocumentationForClass(s); } - private static String getDocPackage(final String classCanonicalName) { - //TODO: Do this programmatically - if (classCanonicalName.startsWith("ij.")) - return "ImageJ1"; - else if (classCanonicalName.startsWith("sc.fiji")) - return "Fiji"; - else if (classCanonicalName.startsWith("net.imagej")) - return "ImageJ"; - else if (classCanonicalName.startsWith("net.imglib2")) - return "ImgLib2"; - else if (classCanonicalName.startsWith("org.scijava")) - return "SciJava"; - else if (classCanonicalName.startsWith("loci.formats")) - return "Bio-Formats"; - if (classCanonicalName.startsWith("java.")) - return "Java8"; - else if (classCanonicalName.startsWith("sc.iview")) - return "SciView"; - else if (classCanonicalName.startsWith("weka.")) - return "Weka"; - else if (classCanonicalName.startsWith("inra.ijpb")) - return "MorphoLibJ"; - return null; + protected static String getSummaryCompletion(final Field field, + final Class<?> c) + { + return ClassIndex.getSummaryCompletion(field, c); } - /** - * Assembles an HTML-formatted auto-completion summary with functional - * hyperlinks - * - * @param field the field being documented - * @param c the class being documented. Expected to be documented at the - * Scijava API documentation portal. - * @return the completion summary - */ - protected static String getSummaryCompletion(final Field field, final Class<?> c) { - final StringBuffer summary = new StringBuffer(); - summary.append("<b>").append(field.getName()).append("</b>"); - summary.append(" (").append(field.getType().getName()).append(")"); - summary.append("<DL>"); - summary.append("<DT><b>Defined in:</b>"); - summary.append("<DD>").append(getJavaDocLink(c)); - summary.append("</DL>"); - return summary.toString(); + protected static String getSummaryCompletion(final Method method, + final Class<?> c) + { + return ClassIndex.getSummaryCompletion(method, c); } - /** - * Assembles an HTML-formatted auto-completion summary with functional - * hyperlinks - * - * @param method the method being documented - * @param c the class being documented. Expected to be documented at the - * Scijava API documentation portal. - * @return the completion summary - */ - protected static String getSummaryCompletion(final Method method, final Class<?> c) { - final StringBuffer summary = new StringBuffer(); - final StringBuffer replacementHeader = new StringBuffer(method.getName()); - final int bIndex = replacementHeader.length(); // remember '(' position - replacementHeader.append("("); - final Parameter[] params = method.getParameters(); - if (params.length > 0) { - for (final Parameter parameter : params) { - replacementHeader.append(parameter.getType().getSimpleName()).append(" ").append(parameter.getName()).append(", "); - } - replacementHeader.setLength(replacementHeader.length() - 2); // remove trailing ', '; - } - replacementHeader.append(")"); - replacementHeader.replace(bIndex, bIndex + 1, "</b>("); // In header, highlight only method name for extra contrast - summary.append("<b>").append(replacementHeader); - summary.append("<DL>"); - summary.append("<DT><b>Returns:</b>"); - summary.append("<DD>").append(method.getReturnType().getSimpleName()); - summary.append("<DT><b>Defined in:</b>"); - summary.append("<DD>").append(getJavaDocLink(c)); - summary.append("</DL>"); - return summary.toString(); + protected static String getSummaryCompletion(final Constructor<?> constructor, + final Class<?> c) + { + return ClassIndex.getSummaryCompletion(constructor, c); } - + /** - * Assembles an HTML-formatted auto-completion summary with functional - * hyperlinks - * - * @param constructor the constructor being documented - * @param c the class being documented. Expected to be documented at the - * Scijava API documentation portal. - * @return the completion summary + * Placeholder RSTA completions warning that a class was unavailable. This is + * the one genuinely Swing/RSTA-specific helper; it stays editor-side. */ - protected static String getSummaryCompletion(final Constructor<?> constructor, final Class<?> c) { - final StringBuffer summary = new StringBuffer(); - final StringBuffer replacementHeader = new StringBuffer(c.getSimpleName()); - final int bIndex = replacementHeader.length(); // remember '(' position - replacementHeader.append("("); - final Parameter[] params = constructor.getParameters(); - if (params.length > 0) { - for (final Parameter parameter : params) { - replacementHeader.append(parameter.getType().getSimpleName()).append(" ").append(parameter.getName()).append(", "); - } - replacementHeader.setLength(replacementHeader.length() - 2); // remove trailing ', '; - } - replacementHeader.append(")"); - replacementHeader.replace(bIndex, bIndex + 1, "</b>("); // In header, highlight only method name for extra contrast - summary.append("<b>").append(replacementHeader); - summary.append("<DL>"); - summary.append("<DT><b>Intantiates:</b>"); - summary.append("<DD>").append(c.getSimpleName()); - summary.append("<DT><b>Defined in:</b>"); - summary.append("<DD>").append(getJavaDocLink(c)); - summary.append("</DL>"); - return summary.toString(); - } - - static List<Completion> classUnavailableCompletions(final CompletionProvider provider, final String pre) { - // placeholder completions to warn users class was not available (repeated to force pop-up display) + static List<Completion> classUnavailableCompletions( + final CompletionProvider provider, final String pre) + { final List<Completion> list = new ArrayList<>(); - final String summary = "Class not found or invalid import. See " - + String.format("<a href='%s';>SciJavaDocs</a>", scijava_javadoc_URL) - + " or <a href='https://search.imagej.net/';>search</a> for help"; + final String summary = "Class not found or invalid import. See " + String + .format("<a href='%s';>SciJavaDocs</a>", ClassIndex.SCIJAVA_JAVADOC_URL) + + " or <a href='https://search.imagej.net/';>search</a> for help"; + // Repeated to force pop-up display. list.add(new BasicCompletion(provider, pre + "?", null, summary)); list.add(new BasicCompletion(provider, pre + "?", null, summary)); return list; } - } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java new file mode 100644 index 00000000..bd783d2c --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java @@ -0,0 +1,83 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import org.fife.rsta.ac.AbstractLanguageSupport; +import org.fife.ui.autocomplete.AutoCompletion; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.scijava.script.ScriptLanguage; +import org.scijava.script.complete.CodeCompleter; + +/** + * An RSTA {@link org.fife.rsta.ac.LanguageSupport} that drives code completion + * from a toolkit-agnostic {@link CodeCompleter}, via + * {@link SciJavaCompletionProvider}. + * <p> + * This is the lightweight completion tier of the script editor: any language + * that contributes a {@link org.scijava.script.complete.CodeCompleterPlugin} + * gets editor completion through this class, with no language-specific Swing or + * RSTA code required. + * </p> + * + * @author Curtis Rueden + */ +public class CodeCompleterLanguageSupport extends AbstractLanguageSupport { + + private final CodeCompleter completer; + private final ScriptLanguage language; + + public CodeCompleterLanguageSupport(final CodeCompleter completer, + final ScriptLanguage language) + { + this.completer = completer; + this.language = language; + setAutoCompleteEnabled(true); + setParameterAssistanceEnabled(true); + setShowDescWindow(true); + } + + @Override + public void install(final RSyntaxTextArea textArea) { + final SciJavaCompletionProvider provider = + new SciJavaCompletionProvider(completer, language); + final AutoCompletion ac = new SciJavaAutoCompletion(provider); + ac.setAutoCompleteEnabled(isAutoCompleteEnabled()); + ac.setAutoActivationEnabled(isAutoActivationEnabled()); + ac.setParameterAssistanceEnabled(isParameterAssistanceEnabled()); + ac.setShowDescWindow(getShowDescWindow()); + ac.install(textArea); + installImpl(textArea, ac); + } + + @Override + public void uninstall(final RSyntaxTextArea textArea) { + uninstallImpl(textArea); + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java new file mode 100644 index 00000000..376c9096 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java @@ -0,0 +1,108 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; + +import org.fife.ui.autocomplete.Completion; +import org.fife.ui.autocomplete.CompletionProvider; +import org.fife.ui.rtextarea.RTextArea; +import org.scijava.script.complete.Completion.TextEdit; + +/** + * An {@link org.fife.ui.autocomplete.AutoCompletion} that, in addition to + * inserting the chosen completion, applies any + * {@link org.scijava.script.complete.Completion#additionalEdits() additional + * edits} the completion carries (see {@link SciJavaCompletion}). This is how + * language-neutral auto-imports are realized in the Swing editor. + * + * @author Curtis Rueden + */ +public class SciJavaAutoCompletion extends + org.fife.ui.autocomplete.AutoCompletion +{ + + public SciJavaAutoCompletion(final CompletionProvider provider) { + super(provider); + } + + @Override + protected void insertCompletion(final Completion c, + final boolean typedParamListStartChar) + { + if (!(c instanceof AdditionalEdits)) { + super.insertCompletion(c, typedParamListStartChar); + return; + } + final List<TextEdit> edits = ((AdditionalEdits) c).getAdditionalEdits(); + if (edits.isEmpty()) { + super.insertCompletion(c, typedParamListStartChar); + return; + } + final JTextComponent comp = getTextComponent(); + final RTextArea area = comp instanceof RTextArea ? (RTextArea) comp : null; + + if (area != null) area.beginAtomicEdit(); + try { + // Insert the primary completion first (at the completion point). + super.insertCompletion(c, typedParamListStartChar); + + // Then apply additional edits. They lie before the completion point, + // so applying them from highest to lowest offset keeps each offset + // valid, and the caret (tracked by the document) shifts accordingly. + final List<TextEdit> sorted = new ArrayList<>(edits); + sorted.sort(Comparator.comparingInt(TextEdit::start).reversed()); + for (final TextEdit edit : sorted) { + applyEdit(comp, edit); + } + } + finally { + if (area != null) area.endAtomicEdit(); + } + } + + private void applyEdit(final JTextComponent comp, final TextEdit edit) { + try { + final int len = edit.end() - edit.start(); + if (len > 0) comp.getDocument().remove(edit.start(), len); + if (edit.newText() != null && !edit.newText().isEmpty()) { + comp.getDocument().insertString(edit.start(), edit.newText(), null); + } + } + catch (final BadLocationException exc) { + // Document changed underneath us; skip this edit rather than fail. + } + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java new file mode 100644 index 00000000..0e23f083 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java @@ -0,0 +1,64 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.util.List; + +import org.fife.ui.autocomplete.BasicCompletion; +import org.fife.ui.autocomplete.CompletionProvider; +import org.scijava.script.complete.Completion.TextEdit; + +/** + * An RSTA {@link BasicCompletion} that also carries + * {@link org.scijava.script.complete.Completion#additionalEdits() additional + * edits} to apply on acceptance (e.g. auto-imports). The edits are applied by + * {@link SciJavaAutoCompletion}. + * + * @author Curtis Rueden + */ +public class SciJavaCompletion extends BasicCompletion implements + AdditionalEdits +{ + + private final List<TextEdit> additionalEdits; + + public SciJavaCompletion(final CompletionProvider provider, + final String replacementText, final String shortDesc, final String summary, + final List<TextEdit> additionalEdits) + { + super(provider, replacementText, shortDesc, summary); + this.additionalEdits = additionalEdits; + } + + @Override + public List<TextEdit> getAdditionalEdits() { + return additionalEdits; + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java new file mode 100644 index 00000000..61f504fd --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java @@ -0,0 +1,281 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.script.ScriptEngine; +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; + +import org.fife.ui.autocomplete.BasicCompletion; +import org.fife.ui.autocomplete.Completion; +import org.fife.ui.autocomplete.DefaultCompletionProvider; +import org.fife.ui.autocomplete.ParameterChoicesProvider; +import org.fife.ui.autocomplete.ParameterizedCompletion; +import org.scijava.script.ScriptLanguage; +import org.scijava.script.complete.CodeCompleter; +import org.scijava.script.complete.CompletionRequest; +import org.scijava.script.complete.CompletionResult; +import org.scijava.script.complete.ParameterChoices; + +/** + * The single bridge between SciJava's toolkit-agnostic code completion SPI + * ({@link CodeCompleter}, {@link org.scijava.script.complete.Completion}) and + * RSyntaxTextArea's Swing-based completion machinery. + * <p> + * This provider delegates all language intelligence to a {@link CodeCompleter}, + * then translates the resulting neutral {@link CompletionResult} into RSTA + * {@link Completion}s. Language adapters therefore need no dependency on Swing or + * RSTA: they implement {@link CodeCompleter} (via a + * {@link org.scijava.script.complete.CodeCompleterPlugin}) and the script editor + * renders the suggestions here. + * </p> + * + * @author Curtis Rueden + */ +public class SciJavaCompletionProvider extends DefaultCompletionProvider { + + private final CodeCompleter completer; + private final ScriptLanguage language; + + /** Optional live engine, set when completing in an interpreter. */ + private ScriptEngine engine; + + // Cache so getAlreadyEnteredText() and getCompletionsImpl() — which RSTA + // calls in quick succession for the same caret — stay consistent and avoid + // recomputation. + private int cachedCaret = -1; + private String cachedText; + private CompletionResult cachedResult; + + /** The parameter-choices provider installed for the cached result, if any. */ + private ParameterChoicesProvider choicesProvider; + + public SciJavaCompletionProvider(final CodeCompleter completer, + final ScriptLanguage language) + { + this.completer = completer; + this.language = language; + setParameterizedCompletionParams('(', ", ", ')'); + // Auto-activate after a letter, digit, '.' or '_'. + setAutoActivationRules(true, "."); + } + + /** Sets a live script engine to enable variable/binding-based completion. */ + public void setEngine(final ScriptEngine engine) { + this.engine = engine; + } + + @Override + public boolean isValidChar(final char c) { + return Character.isLetterOrDigit(c) || c == '.' || c == '_'; + } + + @Override + public String getAlreadyEnteredText(final JTextComponent comp) { + final CompletionResult result = compute(comp); + final int caret = comp.getCaretPosition(); + final int start = Math.min(Math.max(result.replaceStart(), 0), caret); + try { + return comp.getText(start, caret - start); + } + catch (final BadLocationException exc) { + return ""; + } + } + + @Override + public List<Completion> getCompletionsImpl(final JTextComponent comp) { + final CompletionResult result = compute(comp); + final List<org.scijava.script.complete.Completion> source = + result.completions(); + final int n = source.size(); + final List<Completion> out = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + out.add(toRSTA(source.get(i), n - i)); + } + return out; + } + + // -- Helper methods -- + + private CompletionResult compute(final JTextComponent comp) { + final int caret = comp.getCaretPosition(); + final String text; + try { + text = comp.getText(0, comp.getDocument().getLength()); + } + catch (final BadLocationException exc) { + return CompletionResult.EMPTY; + } + if (caret == cachedCaret && text.equals(cachedText) && + cachedResult != null) + { + return cachedResult; + } + CompletionResult result; + try { + final CompletionRequest request = new CompletionRequest(text, caret, + language, engine, engine == null ? null : engine.getContext()); + result = completer.complete(request); + if (result == null) result = CompletionResult.EMPTY; + } + catch (final Exception exc) { + result = CompletionResult.EMPTY; + } + cachedCaret = caret; + cachedText = text; + cachedResult = result; + // Wire up (or clear) parameter assistance for this result's callables. + final ParameterChoices choices = result.parameterChoices(); + choicesProvider = + choices == null ? null : new NeutralChoicesProvider(choices); + setParameterChoicesProvider(choicesProvider); + return result; + } + + /** + * Returns the candidate values for the given parameter, per the completer's + * {@link ParameterChoices} for the completion state at {@code comp}'s caret. + * Mirrors what RSTA's parameter assistance shows; also handy for non-popup + * front ends and tests. + */ + List<Completion> parameterChoices(final JTextComponent comp, + final ParameterizedCompletion.Parameter param) + { + compute(comp); + return choicesProvider == null ? Collections.emptyList() + : choicesProvider.getParameterChoices(comp, param); + } + + /** + * Translates a neutral completion into an RSTA completion. Callables become + * {@link org.fife.ui.autocomplete.FunctionCompletion}s (so parameter + * assistance and parameter choices engage); everything else becomes a + * {@link BasicCompletion}. Either may carry additional edits (e.g. + * auto-imports). The {@code orderRelevance} argument preserves the completer's + * ordering when the completion does not specify its own relevance. + */ + private Completion toRSTA(final org.scijava.script.complete.Completion c, + final int orderRelevance) + { + final Completion rsta = c.isCallable() ? functionCompletion(c) + : basicCompletion(c); + final double rel = c.relevance(); + final int relevance = rel != 0 ? (int) Math.round(rel) : orderRelevance; + if (rsta instanceof BasicCompletion) { + ((BasicCompletion) rsta).setRelevance(relevance); + } + return rsta; + } + + private Completion basicCompletion( + final org.scijava.script.complete.Completion c) + { + return c.additionalEdits().isEmpty() // + ? new BasicCompletion(this, c.insertionText(), c.summary(), + c.description()) // + : new SciJavaCompletion(this, c.insertionText(), c.summary(), + c.description(), c.additionalEdits()); + } + + private Completion functionCompletion( + final org.scijava.script.complete.Completion c) + { + // FunctionCompletion appends the parameter template itself, so strip any + // trailing "()" the completer may have included in the insertion text. + String name = c.insertionText(); + if (name.endsWith("()")) name = name.substring(0, name.length() - 2); + final String returnType = c.returnType() == null ? "" : c.returnType(); + + final SciJavaFunctionCompletion fc = new SciJavaFunctionCompletion(this, + name, returnType, c.additionalEdits()); + fc.setReturnValueDescription(returnType); + fc.setShortDescription(c.summary()); + if (c.description() != null) fc.setSummary(c.description()); + fc.setParams(toRSTAParams(c.parameters())); + return fc; + } + + private static List<ParameterizedCompletion.Parameter> toRSTAParams( + final List<org.scijava.script.complete.Completion.Parameter> params) + { + final List<ParameterizedCompletion.Parameter> out = + new ArrayList<>(params.size()); + for (final org.scijava.script.complete.Completion.Parameter p : params) { + // Pass the type as the parameter's "type object" so the choices + // provider can dispatch on it. + out.add(new ParameterizedCompletion.Parameter(p.type(), p.name())); + } + return out; + } + + /** + * Bridges a neutral {@link ParameterChoices} to RSTA's + * {@link ParameterChoicesProvider}, translating between the two parameter and + * completion representations. + */ + private final class NeutralChoicesProvider implements + ParameterChoicesProvider + { + + private final ParameterChoices choices; + + NeutralChoicesProvider(final ParameterChoices choices) { + this.choices = choices; + } + + @Override + public List<Completion> getParameterChoices(final JTextComponent tc, + final ParameterizedCompletion.Parameter param) + { + final org.scijava.script.complete.Completion.Parameter neutral = + new org.scijava.script.complete.Completion.Parameter(param.getName(), + param.getType()); + final List<org.scijava.script.complete.Completion> result; + try { + result = choices.choicesFor(neutral); + } + catch (final Exception exc) { + return Collections.emptyList(); + } + if (result == null || result.isEmpty()) return Collections.emptyList(); + final List<Completion> out = new ArrayList<>(result.size()); + for (final org.scijava.script.complete.Completion c : result) { + out.add(new BasicCompletion(SciJavaCompletionProvider.this, + c.insertionText(), c.summary(), c.description())); + } + return out; + } + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java new file mode 100644 index 00000000..af110e1a --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java @@ -0,0 +1,68 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.util.Collections; +import java.util.List; + +import org.fife.ui.autocomplete.CompletionProvider; +import org.fife.ui.autocomplete.FunctionCompletion; +import org.scijava.script.complete.Completion.TextEdit; + +/** + * An RSTA {@link FunctionCompletion} for callable completions (methods, + * functions, constructors), so parameter assistance and parameter choices + * engage. It also carries optional + * {@link org.scijava.script.complete.Completion#additionalEdits() additional + * edits} (e.g. the auto-import needed by a constructor completion), applied by + * {@link SciJavaAutoCompletion}. + * + * @author Curtis Rueden + */ +public class SciJavaFunctionCompletion extends FunctionCompletion implements + AdditionalEdits +{ + + private final List<TextEdit> additionalEdits; + + public SciJavaFunctionCompletion(final CompletionProvider provider, + final String name, final String returnType, + final List<TextEdit> additionalEdits) + { + super(provider, name, returnType); + this.additionalEdits = additionalEdits == null ? Collections.emptyList() + : additionalEdits; + } + + @Override + public List<TextEdit> getAdditionalEdits() { + return additionalEdits; + } +} diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java new file mode 100644 index 00000000..ed4dc5d4 --- /dev/null +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -0,0 +1,143 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.List; + +import org.fife.ui.autocomplete.Completion; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.junit.Test; +import org.scijava.script.complete.CompletionResult; + +/** + * Tests that {@link SciJavaCompletionProvider} faithfully bridges neutral + * {@link org.scijava.script.complete.Completion}s to RSTA completions, including + * honoring the completer's reported replacement span. + * + * @author Curtis Rueden + */ +public class SciJavaCompletionProviderTest { + + @Test + public void testBridgesNeutralCompletions() { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("foo.ba"); + area.setCaretPosition(area.getDocument().getLength()); // offset 6 + + // A completer that, regardless of input, suggests two members of "foo", + // replacing the partial token "ba" (which starts at offset 4). + final int replaceStart = 4; + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + request -> new CompletionResult(Arrays.asList( + org.scijava.script.complete.Completion.builder("bar") + .summary("the bar member").build(), + org.scijava.script.complete.Completion.builder("baz").build()), + replaceStart), + null); + + // The provider must report the already-entered text as the replaced span. + assertEquals("ba", provider.getAlreadyEnteredText(area)); + + // And it must surface both suggestions as RSTA completions, in order. + final List<Completion> completions = provider.getCompletionsImpl(area); + assertEquals(2, completions.size()); + assertEquals("bar", completions.get(0).getReplacementText()); + assertEquals("baz", completions.get(1).getReplacementText()); + } + + @Test + public void testAdditionalEditsSurfaceAsSciJavaCompletion() { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("Roi"); + area.setCaretPosition(area.getDocument().getLength()); + + // A class-name completion that also auto-imports at the top of the file. + final org.scijava.script.complete.Completion.TextEdit importEdit = + org.scijava.script.complete.Completion.TextEdit.insert(0, + "from ij.gui import Roi\n"); + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + request -> new CompletionResult(java.util.Collections.singletonList( + org.scijava.script.complete.Completion.builder("Roi") + .additionalEdits(java.util.Collections.singletonList(importEdit)) + .build()), + 0), + null); + + final List<Completion> completions = provider.getCompletionsImpl(area); + assertEquals(1, completions.size()); + // Completions carrying extra edits must be SciJavaCompletions so the + // SciJavaAutoCompletion applies their auto-import on acceptance. + final Completion c = completions.get(0); + org.junit.Assert.assertTrue(c instanceof SciJavaCompletion); + assertEquals(1, ((SciJavaCompletion) c).getAdditionalEdits().size()); + } + + @Test + public void testCallablesBecomeFunctionCompletionsWithChoices() { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("addRoi("); + area.setCaretPosition(area.getDocument().getLength()); + + // A callable completion taking one ij.gui.Roi parameter, plus a + // parameter-choices resolver that offers an in-scope variable for it. + final org.scijava.script.complete.Completion.Parameter roiParam = + new org.scijava.script.complete.Completion.Parameter("roi", "ij.gui.Roi"); + final org.scijava.script.complete.Completion method = + org.scijava.script.complete.Completion.builder("addRoi") + .kind(org.scijava.script.complete.Completion.Kind.METHOD) + .parameters(java.util.Collections.singletonList(roiParam)) + .returnType("void").build(); + final org.scijava.script.complete.ParameterChoices choices = p -> // + "ij.gui.Roi".equals(p.type()) + ? java.util.Collections.singletonList( + org.scijava.script.complete.Completion.of("myRoi")) + : java.util.Collections.emptyList(); + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + request -> new CompletionResult(java.util.Collections.singletonList( + method), area.getDocument().getLength(), choices), + null); + + // Callables surface as FunctionCompletions so parameter assistance works. + final List<Completion> completions = provider.getCompletionsImpl(area); + assertEquals(1, completions.size()); + org.junit.Assert.assertTrue( + completions.get(0) instanceof SciJavaFunctionCompletion); + + // And the neutral ParameterChoices drives RSTA's parameter choices. + final List<Completion> roiChoices = provider.parameterChoices(area, + new org.fife.ui.autocomplete.ParameterizedCompletion.Parameter( + "ij.gui.Roi", "roi")); + assertEquals(1, roiChoices.size()); + assertEquals("myRoi", roiChoices.get(0).getReplacementText()); + } +} From fefd8a5803be570e315d85a246f4db3c3c1d9615 Mon Sep 17 00:00:00 2001 From: Curtis Rueden <ctrueden@wisc.edu> Date: Sun, 21 Jun 2026 21:27:22 -0500 Subject: [PATCH 02/21] Add built-in Python (Jython) syntax highlighter Syntax highlighting requires an RSyntaxTextArea TokenMaker, which is inherently editor-side. Provide it here so the toolkit-agnostic Jython scripting plugin (scripting-jython) need not depend on the editor or RSTA; this replaces the highlighter formerly in script-editor-jython. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- .../script/highliters/PythonHighlighter.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java diff --git a/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java b/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java new file mode 100644 index 00000000..d6d3ae40 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java @@ -0,0 +1,51 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.highliters; + +import org.fife.ui.rsyntaxtextarea.modes.PythonTokenMaker; +import org.scijava.plugin.Plugin; +import org.scijava.ui.swing.script.SyntaxHighlighter; + +/** + * SyntaxHighlighter for "python-(jython)". + * <p> + * Syntax highlighting requires an RSyntaxTextArea {@code TokenMaker}, which is + * inherently editor-side, so the script editor provides it here on behalf of the + * toolkit-agnostic Jython scripting plugin. + * </p> + * + * @author Karl Duderstadt + */ +@Plugin(type = SyntaxHighlighter.class, name = "python-(jython)") +public class PythonHighlighter extends PythonTokenMaker implements + SyntaxHighlighter +{ + // Everything implemented in PythonTokenMaker +} From 7856a235aa8fcce3cb9d3f9fc90c5cb1f56a2b13 Mon Sep 17 00:00:00 2001 From: Curtis Rueden <ctrueden@wisc.edu> Date: Sun, 13 Sep 2026 11:04:16 -0500 Subject: [PATCH 03/21] Update copyright headers for 2026 --- .../ui/swing/script/autocompletion/AdditionalEdits.java | 6 +++--- .../scijava/ui/swing/script/autocompletion/ClassUtil.java | 4 ++-- .../script/autocompletion/CodeCompleterLanguageSupport.java | 6 +++--- .../swing/script/autocompletion/SciJavaAutoCompletion.java | 6 +++--- .../ui/swing/script/autocompletion/SciJavaCompletion.java | 6 +++--- .../script/autocompletion/SciJavaCompletionProvider.java | 6 +++--- .../script/autocompletion/SciJavaFunctionCompletion.java | 6 +++--- .../ui/swing/script/highliters/PythonHighlighter.java | 6 +++--- .../autocompletion/SciJavaCompletionProviderTest.java | 6 +++--- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java index f19af216..0ff10b01 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java @@ -2,17 +2,17 @@ * #%L * Script Editor and Interpreter for SciJava script languages. * %% - * Copyright (C) 2009 - 2025 SciJava developers. + * Copyright (C) 2009 - 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 diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java index e391de7a..0f2f45a2 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java @@ -6,13 +6,13 @@ * %% * 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 diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java index bd783d2c..01af71da 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java @@ -2,17 +2,17 @@ * #%L * Script Editor and Interpreter for SciJava script languages. * %% - * Copyright (C) 2009 - 2025 SciJava developers. + * Copyright (C) 2009 - 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 diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java index 376c9096..909d3900 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java @@ -2,17 +2,17 @@ * #%L * Script Editor and Interpreter for SciJava script languages. * %% - * Copyright (C) 2009 - 2025 SciJava developers. + * Copyright (C) 2009 - 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 diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java index 0e23f083..b2a12685 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java @@ -2,17 +2,17 @@ * #%L * Script Editor and Interpreter for SciJava script languages. * %% - * Copyright (C) 2009 - 2025 SciJava developers. + * Copyright (C) 2009 - 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 diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java index 61f504fd..f7016708 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java @@ -2,17 +2,17 @@ * #%L * Script Editor and Interpreter for SciJava script languages. * %% - * Copyright (C) 2009 - 2025 SciJava developers. + * Copyright (C) 2009 - 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 diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java index af110e1a..e8132ead 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java @@ -2,17 +2,17 @@ * #%L * Script Editor and Interpreter for SciJava script languages. * %% - * Copyright (C) 2009 - 2025 SciJava developers. + * Copyright (C) 2009 - 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 diff --git a/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java b/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java index d6d3ae40..0e06b257 100644 --- a/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java +++ b/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java @@ -2,17 +2,17 @@ * #%L * Script Editor and Interpreter for SciJava script languages. * %% - * Copyright (C) 2009 - 2025 SciJava developers. + * Copyright (C) 2009 - 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 diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index ed4dc5d4..5c436ec8 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -2,17 +2,17 @@ * #%L * Script Editor and Interpreter for SciJava script languages. * %% - * Copyright (C) 2009 - 2025 SciJava developers. + * Copyright (C) 2009 - 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 From 092b7f261acf2754bcd72103be3351df1670f828 Mon Sep 17 00:00:00 2001 From: Curtis Rueden <ctrueden@wisc.edu> Date: Tue, 29 Sep 2026 11:22:32 -0500 Subject: [PATCH 04/21] WIP --- pom.xml | 12 +++-- .../scijava/ui/swing/script/EditorPane.java | 4 +- .../scijava/ui/swing/script/PromptPane.java | 8 ++-- .../autocompletion/AdditionalEdits.java | 4 +- .../script/autocompletion/ClassUtil.java | 4 +- .../CodeCompleterLanguageSupport.java | 14 +++++- .../autocompletion/SciJavaAutoCompletion.java | 4 +- .../autocompletion/SciJavaCompletion.java | 4 +- .../SciJavaCompletionProvider.java | 45 ++++++++++++------- .../SciJavaFunctionCompletion.java | 4 +- .../script/highliters/PythonHighlighter.java | 4 +- .../SciJavaCompletionProviderTest.java | 28 ++++++------ 12 files changed, 81 insertions(+), 54 deletions(-) diff --git a/pom.xml b/pom.xml index 7485a995..e38bac88 100644 --- a/pom.xml +++ b/pom.xml @@ -141,10 +141,11 @@ <properties> <scijava.jvm.version>11</scijava.jvm.version> - <!-- TEMP: depend on the in-development scijava-common providing the new - org.scijava.script.complete code-completion SPI. Remove once a - released scijava-common containing it is pinned by pom-scijava. --> + <!-- TEMP: in-development versions providing the code intelligence SPI. + scijava-code-api needs ParameterScriptProcessor#setLogger from + scijava-common. Remove once releases are pinned by pom-scijava. --> <scijava-common.version>2.100.2-SNAPSHOT</scijava-common.version> + <scijava-code-api.version>0.1.0-SNAPSHOT</scijava-code-api.version> <main-class>org.scijava.ui.swing.script.Main</main-class> <package-name>org.scijava.ui.swing.script</package-name> <license.licenseName>bsd_2</license.licenseName> @@ -160,6 +161,11 @@ <groupId>org.scijava</groupId> <artifactId>scijava-common</artifactId> </dependency> + <dependency> + <groupId>org.scijava</groupId> + <artifactId>scijava-code-api</artifactId> + <version>${scijava-code-api.version}</version> + </dependency> <dependency> <groupId>org.scijava</groupId> <artifactId>scijava-search</artifactId> diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 3aec38e0..db840972 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -94,7 +94,7 @@ import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptService; import org.scijava.ui.swing.script.vim.VimHandler; -import org.scijava.script.complete.CodeCompletionService; +import org.scijava.code.api.CodeCompletionService; import org.scijava.ui.swing.script.autocompletion.CodeCompleterLanguageSupport; import org.scijava.util.FileUtils; @@ -806,7 +806,7 @@ protected void setLanguage(final ScriptLanguage language, { support = new CodeCompleterLanguageSupport( codeCompletionService.getCompleterPlugin(currentLanguage), - currentLanguage); + currentLanguage, log); } // Tier 3: RSTA's own built-in support for the syntax style. diff --git a/src/main/java/org/scijava/ui/swing/script/PromptPane.java b/src/main/java/org/scijava/ui/swing/script/PromptPane.java index 700d4aef..7c97d122 100644 --- a/src/main/java/org/scijava/ui/swing/script/PromptPane.java +++ b/src/main/java/org/scijava/ui/swing/script/PromptPane.java @@ -48,10 +48,10 @@ import org.scijava.script.ScriptInterpreter; import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptREPL; -import org.scijava.script.complete.CodeCompletionService; -import org.scijava.script.complete.Completion; -import org.scijava.script.complete.CompletionRequest; -import org.scijava.script.complete.CompletionResult; +import org.scijava.code.api.CodeCompletionService; +import org.scijava.code.api.Completion; +import org.scijava.code.api.CompletionRequest; +import org.scijava.code.api.CompletionResult; import org.scijava.thread.ThreadService; import org.scijava.util.ClassUtils; import org.scijava.util.Types; diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java index 0ff10b01..728c3fb5 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java @@ -31,11 +31,11 @@ import java.util.List; -import org.scijava.script.complete.Completion.TextEdit; +import org.scijava.code.api.Completion.TextEdit; /** * Implemented by RSTA completions that carry - * {@link org.scijava.script.complete.Completion#additionalEdits() additional + * {@link org.scijava.code.api.Completion#additionalEdits() additional * edits} (e.g. auto-imports) to apply when accepted. Both the * {@code BasicCompletion}-based and {@code FunctionCompletion}-based SciJava * completions implement this, so {@link SciJavaAutoCompletion} can apply the diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java index 0f2f45a2..ad23172e 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java @@ -39,10 +39,10 @@ import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.CompletionProvider; -import org.scijava.script.complete.ClassIndex; +import org.scijava.code.api.ClassIndex; /** - * Thin facade over the toolkit-agnostic {@link ClassIndex} in scijava-common, + * Thin facade over the toolkit-agnostic {@link ClassIndex} in scijava-code-api, * retained for the script editor's Swing-specific completion code. New code * should use {@link ClassIndex} directly; only the RSTA-specific * {@link #classUnavailableCompletions} helper is genuinely editor-side. diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java index 01af71da..2ad53c2c 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java @@ -32,8 +32,9 @@ import org.fife.rsta.ac.AbstractLanguageSupport; import org.fife.ui.autocomplete.AutoCompletion; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.scijava.log.Logger; import org.scijava.script.ScriptLanguage; -import org.scijava.script.complete.CodeCompleter; +import org.scijava.code.api.CodeCompleter; /** * An RSTA {@link org.fife.rsta.ac.LanguageSupport} that drives code completion @@ -41,7 +42,7 @@ * {@link SciJavaCompletionProvider}. * <p> * This is the lightweight completion tier of the script editor: any language - * that contributes a {@link org.scijava.script.complete.CodeCompleterPlugin} + * that contributes a {@link org.scijava.code.api.CodeCompleterPlugin} * gets editor completion through this class, with no language-specific Swing or * RSTA code required. * </p> @@ -52,12 +53,20 @@ public class CodeCompleterLanguageSupport extends AbstractLanguageSupport { private final CodeCompleter completer; private final ScriptLanguage language; + private final Logger log; public CodeCompleterLanguageSupport(final CodeCompleter completer, final ScriptLanguage language) + { + this(completer, language, null); + } + + public CodeCompleterLanguageSupport(final CodeCompleter completer, + final ScriptLanguage language, final Logger log) { this.completer = completer; this.language = language; + this.log = log; setAutoCompleteEnabled(true); setParameterAssistanceEnabled(true); setShowDescWindow(true); @@ -67,6 +76,7 @@ public CodeCompleterLanguageSupport(final CodeCompleter completer, public void install(final RSyntaxTextArea textArea) { final SciJavaCompletionProvider provider = new SciJavaCompletionProvider(completer, language); + provider.setLogger(log); final AutoCompletion ac = new SciJavaAutoCompletion(provider); ac.setAutoCompleteEnabled(isAutoCompleteEnabled()); ac.setAutoActivationEnabled(isAutoActivationEnabled()); diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java index 909d3900..b73d1658 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java @@ -39,12 +39,12 @@ import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.CompletionProvider; import org.fife.ui.rtextarea.RTextArea; -import org.scijava.script.complete.Completion.TextEdit; +import org.scijava.code.api.Completion.TextEdit; /** * An {@link org.fife.ui.autocomplete.AutoCompletion} that, in addition to * inserting the chosen completion, applies any - * {@link org.scijava.script.complete.Completion#additionalEdits() additional + * {@link org.scijava.code.api.Completion#additionalEdits() additional * edits} the completion carries (see {@link SciJavaCompletion}). This is how * language-neutral auto-imports are realized in the Swing editor. * diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java index b2a12685..c0d53f94 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java @@ -33,11 +33,11 @@ import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.CompletionProvider; -import org.scijava.script.complete.Completion.TextEdit; +import org.scijava.code.api.Completion.TextEdit; /** * An RSTA {@link BasicCompletion} that also carries - * {@link org.scijava.script.complete.Completion#additionalEdits() additional + * {@link org.scijava.code.api.Completion#additionalEdits() additional * edits} to apply on acceptance (e.g. auto-imports). The edits are applied by * {@link SciJavaAutoCompletion}. * diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java index f7016708..e110df4b 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java @@ -42,22 +42,23 @@ import org.fife.ui.autocomplete.DefaultCompletionProvider; import org.fife.ui.autocomplete.ParameterChoicesProvider; import org.fife.ui.autocomplete.ParameterizedCompletion; +import org.scijava.log.Logger; import org.scijava.script.ScriptLanguage; -import org.scijava.script.complete.CodeCompleter; -import org.scijava.script.complete.CompletionRequest; -import org.scijava.script.complete.CompletionResult; -import org.scijava.script.complete.ParameterChoices; +import org.scijava.code.api.CodeCompleter; +import org.scijava.code.api.CompletionRequest; +import org.scijava.code.api.CompletionResult; +import org.scijava.code.api.ParameterChoices; /** * The single bridge between SciJava's toolkit-agnostic code completion SPI - * ({@link CodeCompleter}, {@link org.scijava.script.complete.Completion}) and + * ({@link CodeCompleter}, {@link org.scijava.code.api.Completion}) and * RSyntaxTextArea's Swing-based completion machinery. * <p> * This provider delegates all language intelligence to a {@link CodeCompleter}, * then translates the resulting neutral {@link CompletionResult} into RSTA * {@link Completion}s. Language adapters therefore need no dependency on Swing or * RSTA: they implement {@link CodeCompleter} (via a - * {@link org.scijava.script.complete.CodeCompleterPlugin}) and the script editor + * {@link org.scijava.code.api.CodeCompleterPlugin}) and the script editor * renders the suggestions here. * </p> * @@ -68,6 +69,9 @@ public class SciJavaCompletionProvider extends DefaultCompletionProvider { private final CodeCompleter completer; private final ScriptLanguage language; + /** Optional logger, for reporting completer failures. */ + private Logger log; + /** Optional live engine, set when completing in an interpreter. */ private ScriptEngine engine; @@ -91,6 +95,11 @@ public SciJavaCompletionProvider(final CodeCompleter completer, setAutoActivationRules(true, "."); } + /** Sets a logger with which to report completer failures. */ + public void setLogger(final Logger log) { + this.log = log; + } + /** Sets a live script engine to enable variable/binding-based completion. */ public void setEngine(final ScriptEngine engine) { this.engine = engine; @@ -117,7 +126,7 @@ public String getAlreadyEnteredText(final JTextComponent comp) { @Override public List<Completion> getCompletionsImpl(final JTextComponent comp) { final CompletionResult result = compute(comp); - final List<org.scijava.script.complete.Completion> source = + final List<org.scijava.code.api.Completion> source = result.completions(); final int n = source.size(); final List<Completion> out = new ArrayList<>(n); @@ -150,7 +159,9 @@ private CompletionResult compute(final JTextComponent comp) { result = completer.complete(request); if (result == null) result = CompletionResult.EMPTY; } - catch (final Exception exc) { + catch (final Exception | LinkageError exc) { + // NB: Never let a misbehaving completer break the editor. + if (log != null) log.debug("Code completion failed", exc); result = CompletionResult.EMPTY; } cachedCaret = caret; @@ -186,7 +197,7 @@ List<Completion> parameterChoices(final JTextComponent comp, * auto-imports). The {@code orderRelevance} argument preserves the completer's * ordering when the completion does not specify its own relevance. */ - private Completion toRSTA(final org.scijava.script.complete.Completion c, + private Completion toRSTA(final org.scijava.code.api.Completion c, final int orderRelevance) { final Completion rsta = c.isCallable() ? functionCompletion(c) @@ -200,7 +211,7 @@ private Completion toRSTA(final org.scijava.script.complete.Completion c, } private Completion basicCompletion( - final org.scijava.script.complete.Completion c) + final org.scijava.code.api.Completion c) { return c.additionalEdits().isEmpty() // ? new BasicCompletion(this, c.insertionText(), c.summary(), @@ -210,7 +221,7 @@ private Completion basicCompletion( } private Completion functionCompletion( - final org.scijava.script.complete.Completion c) + final org.scijava.code.api.Completion c) { // FunctionCompletion appends the parameter template itself, so strip any // trailing "()" the completer may have included in the insertion text. @@ -228,11 +239,11 @@ private Completion functionCompletion( } private static List<ParameterizedCompletion.Parameter> toRSTAParams( - final List<org.scijava.script.complete.Completion.Parameter> params) + final List<org.scijava.code.api.Completion.Parameter> params) { final List<ParameterizedCompletion.Parameter> out = new ArrayList<>(params.size()); - for (final org.scijava.script.complete.Completion.Parameter p : params) { + for (final org.scijava.code.api.Completion.Parameter p : params) { // Pass the type as the parameter's "type object" so the choices // provider can dispatch on it. out.add(new ParameterizedCompletion.Parameter(p.type(), p.name())); @@ -259,10 +270,10 @@ private final class NeutralChoicesProvider implements public List<Completion> getParameterChoices(final JTextComponent tc, final ParameterizedCompletion.Parameter param) { - final org.scijava.script.complete.Completion.Parameter neutral = - new org.scijava.script.complete.Completion.Parameter(param.getName(), + final org.scijava.code.api.Completion.Parameter neutral = + new org.scijava.code.api.Completion.Parameter(param.getName(), param.getType()); - final List<org.scijava.script.complete.Completion> result; + final List<org.scijava.code.api.Completion> result; try { result = choices.choicesFor(neutral); } @@ -271,7 +282,7 @@ public List<Completion> getParameterChoices(final JTextComponent tc, } if (result == null || result.isEmpty()) return Collections.emptyList(); final List<Completion> out = new ArrayList<>(result.size()); - for (final org.scijava.script.complete.Completion c : result) { + for (final org.scijava.code.api.Completion c : result) { out.add(new BasicCompletion(SciJavaCompletionProvider.this, c.insertionText(), c.summary(), c.description())); } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java index e8132ead..be4b96cf 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java @@ -34,13 +34,13 @@ import org.fife.ui.autocomplete.CompletionProvider; import org.fife.ui.autocomplete.FunctionCompletion; -import org.scijava.script.complete.Completion.TextEdit; +import org.scijava.code.api.Completion.TextEdit; /** * An RSTA {@link FunctionCompletion} for callable completions (methods, * functions, constructors), so parameter assistance and parameter choices * engage. It also carries optional - * {@link org.scijava.script.complete.Completion#additionalEdits() additional + * {@link org.scijava.code.api.Completion#additionalEdits() additional * edits} (e.g. the auto-import needed by a constructor completion), applied by * {@link SciJavaAutoCompletion}. * diff --git a/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java b/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java index 0e06b257..eb83df42 100644 --- a/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java +++ b/src/main/java/org/scijava/ui/swing/script/highliters/PythonHighlighter.java @@ -34,7 +34,7 @@ import org.scijava.ui.swing.script.SyntaxHighlighter; /** - * SyntaxHighlighter for "python-(jython)". + * SyntaxHighlighter for "Jython". * <p> * Syntax highlighting requires an RSyntaxTextArea {@code TokenMaker}, which is * inherently editor-side, so the script editor provides it here on behalf of the @@ -43,7 +43,7 @@ * * @author Karl Duderstadt */ -@Plugin(type = SyntaxHighlighter.class, name = "python-(jython)") +@Plugin(type = SyntaxHighlighter.class, name = "Jython") public class PythonHighlighter extends PythonTokenMaker implements SyntaxHighlighter { diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index 5c436ec8..d79e32a8 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -37,11 +37,11 @@ import org.fife.ui.autocomplete.Completion; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.junit.Test; -import org.scijava.script.complete.CompletionResult; +import org.scijava.code.api.CompletionResult; /** * Tests that {@link SciJavaCompletionProvider} faithfully bridges neutral - * {@link org.scijava.script.complete.Completion}s to RSTA completions, including + * {@link org.scijava.code.api.Completion}s to RSTA completions, including * honoring the completer's reported replacement span. * * @author Curtis Rueden @@ -59,9 +59,9 @@ public void testBridgesNeutralCompletions() { final int replaceStart = 4; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( request -> new CompletionResult(Arrays.asList( - org.scijava.script.complete.Completion.builder("bar") + org.scijava.code.api.Completion.builder("bar") .summary("the bar member").build(), - org.scijava.script.complete.Completion.builder("baz").build()), + org.scijava.code.api.Completion.builder("baz").build()), replaceStart), null); @@ -82,12 +82,12 @@ public void testAdditionalEditsSurfaceAsSciJavaCompletion() { area.setCaretPosition(area.getDocument().getLength()); // A class-name completion that also auto-imports at the top of the file. - final org.scijava.script.complete.Completion.TextEdit importEdit = - org.scijava.script.complete.Completion.TextEdit.insert(0, + final org.scijava.code.api.Completion.TextEdit importEdit = + org.scijava.code.api.Completion.TextEdit.insert(0, "from ij.gui import Roi\n"); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( request -> new CompletionResult(java.util.Collections.singletonList( - org.scijava.script.complete.Completion.builder("Roi") + org.scijava.code.api.Completion.builder("Roi") .additionalEdits(java.util.Collections.singletonList(importEdit)) .build()), 0), @@ -110,17 +110,17 @@ public void testCallablesBecomeFunctionCompletionsWithChoices() { // A callable completion taking one ij.gui.Roi parameter, plus a // parameter-choices resolver that offers an in-scope variable for it. - final org.scijava.script.complete.Completion.Parameter roiParam = - new org.scijava.script.complete.Completion.Parameter("roi", "ij.gui.Roi"); - final org.scijava.script.complete.Completion method = - org.scijava.script.complete.Completion.builder("addRoi") - .kind(org.scijava.script.complete.Completion.Kind.METHOD) + final org.scijava.code.api.Completion.Parameter roiParam = + new org.scijava.code.api.Completion.Parameter("roi", "ij.gui.Roi"); + final org.scijava.code.api.Completion method = + org.scijava.code.api.Completion.builder("addRoi") + .kind(org.scijava.code.api.Completion.Kind.METHOD) .parameters(java.util.Collections.singletonList(roiParam)) .returnType("void").build(); - final org.scijava.script.complete.ParameterChoices choices = p -> // + final org.scijava.code.api.ParameterChoices choices = p -> // "ij.gui.Roi".equals(p.type()) ? java.util.Collections.singletonList( - org.scijava.script.complete.Completion.of("myRoi")) + org.scijava.code.api.Completion.of("myRoi")) : java.util.Collections.emptyList(); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( request -> new CompletionResult(java.util.Collections.singletonList( From 14bf70baabbbe9221793c913e67bb51ca97b584b Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Sun, 4 Oct 2026 14:19:02 -0500 Subject: [PATCH 05/21] Show callable parameters in autocomplete --- .../SciJavaCompletionProvider.java | 3 ++ .../SciJavaCompletionProviderTest.java | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java index e110df4b..3cee8cf7 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java @@ -39,6 +39,7 @@ import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.Completion; +import org.fife.ui.autocomplete.CompletionCellRenderer; import org.fife.ui.autocomplete.DefaultCompletionProvider; import org.fife.ui.autocomplete.ParameterChoicesProvider; import org.fife.ui.autocomplete.ParameterizedCompletion; @@ -91,6 +92,8 @@ public SciJavaCompletionProvider(final CodeCompleter completer, this.completer = completer; this.language = language; setParameterizedCompletionParams('(', ", ", ')'); + // Show each callable's parameters and return type in the popup list. + setListCellRenderer(new CompletionCellRenderer()); // Auto-activate after a letter, digit, '.' or '_'. setAutoActivationRules(true, "."); } diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index d79e32a8..f5f0fb66 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -140,4 +140,32 @@ public void testCallablesBecomeFunctionCompletionsWithChoices() { assertEquals(1, roiChoices.size()); assertEquals("myRoi", roiChoices.get(0).getReplacementText()); } + + @Test + public void testCallablesRenderWithSignatures() { + final String completionText = "img.getAt"; + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText(completionText); + area.setCaretPosition(area.getDocument().getLength()); + + final org.scijava.code.api.Completion method = + org.scijava.code.api.Completion.builder(completionText) + .kind(org.scijava.code.api.Completion.Kind.METHOD) + .parameters(java.util.Collections.singletonList( + new org.scijava.code.api.Completion.Parameter("pos", "long[]"))) + .returnType("java.lang.Object").build(); + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + request -> new CompletionResult(java.util.Collections.singletonList( + method), 0), + null); + + // The popup list must show a callable's parameters, not just its name. + final Completion c = provider.getCompletionsImpl(area).get(0); + final javax.swing.JLabel label = (javax.swing.JLabel) provider + .getListCellRenderer().getListCellRendererComponent( + new javax.swing.JList<>(), c, 0, false, false); + final String text = label.getText(); + org.junit.Assert.assertTrue(text, text.contains("long[]")); + org.junit.Assert.assertTrue(text, text.contains("pos")); + } } From 68ad53e5e6c19458efbc9ac97c724699b14dd40d Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Sun, 4 Oct 2026 17:25:44 -0500 Subject: [PATCH 06/21] WIP: AI-generated overloaded params --- .../script/autocompletion/Overloads.java | 260 ++++++++++++++++++ .../autocompletion/SciJavaAutoCompletion.java | 3 + .../SciJavaCompletionProvider.java | 138 +++++++++- .../SciJavaFunctionCompletion.java | 30 ++ .../script/autocompletion/OverloadsTest.java | 113 ++++++++ .../SciJavaCompletionProviderTest.java | 107 +++++++ 6 files changed, 640 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/Overloads.java create mode 100644 src/test/java/org/scijava/ui/swing/script/autocompletion/OverloadsTest.java diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/Overloads.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/Overloads.java new file mode 100644 index 00000000..6d78a58a --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/Overloads.java @@ -0,0 +1,260 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.lang.reflect.Array; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; + +import org.scijava.code.api.Completion; +import org.scijava.code.api.TypeResolver; + +/** + * Helpers for ranking a callable's overloads against the arguments typed so + * far. + * + * @author Gabriel Selzer + */ +final class Overloads { + + /** Fit of an argument to a parameter: incompatible. */ + static final int NONE = 0; + + /** Fit of an argument to a parameter: compatible after widening. */ + static final int CONVERSION = 1; + + /** Fit of an argument to a parameter: compatible as is. */ + static final int MATCH = 2; + + private static final Map<String, Class<?>> PRIMITIVES = new HashMap<>(); + static { + for (final Class<?> c : new Class<?>[] { boolean.class, byte.class, + char.class, short.class, int.class, long.class, float.class, + double.class, void.class }) + { + PRIMITIVES.put(c.getName(), c); + } + } + + private Overloads() { + // NB: Prevent instantiation of utility class. + } + + /** + * Gets the types of the arguments of the call enclosing the caret, or null + * if the caret is not inside an argument list. An argument's type is null + * if it is unknown, empty, or still RSTA's placeholder (the parameter name + * of the accepted overload). + */ + static List<String> argumentTypes(final JTextComponent comp, + final TypeResolver resolver, final Completion accepted) + { + final List<String> args = argumentsAtCaret(comp); + if (args == null) return null; + final List<Completion.Parameter> params = accepted.parameters(); + final List<String> types = new ArrayList<>(args.size()); + for (int i = 0; i < args.size(); i++) { + final String arg = args.get(i).trim(); + final boolean placeholder = i < params.size() && // + arg.equals(params.get(i).name()); + types.add(arg.isEmpty() || placeholder || resolver == null ? null + : typeOf(resolver, arg)); + } + return types; + } + + /** + * Rates how well {@code overload} fits arguments of the given types: the + * worst fit among its arguments, or {@link #NONE} if more arguments were + * given than it has parameters. + * + * @return {@link #MATCH}, {@link #CONVERSION} or {@link #NONE} + */ + static int fit(final Completion overload, final List<String> argTypes) { + final List<Completion.Parameter> params = overload.parameters(); + int fit = MATCH; + for (int i = 0; i < argTypes.size(); i++) { + if (argTypes.get(i) == null) continue; + if (i >= params.size()) return NONE; + fit = Math.min(fit, fit(argTypes.get(i), params.get(i).type())); + } + return fit; + } + + /** + * Rates passing a value of type {@code argType} to a parameter of type + * {@code paramType}, following Jython's conversions: + * <ul> + * <li>{@link #MATCH}: the same type, an assignable one, or the same kind of + * number (a Python integer to an integral parameter, or a Python float to a + * floating-point one). Unknown types match too, so they never rule out or + * demote an overload.</li> + * <li>{@link #CONVERSION}: a Python integer to a floating-point + * parameter.</li> + * <li>{@link #NONE}: anything else.</li> + * </ul> + */ + static int fit(final String argType, final String paramType) { + if (argType == null || paramType == null || argType.equals(paramType)) { + return MATCH; + } + final Class<?> a = loadType(argType), p = loadType(paramType); + if (a == null || p == null) return MATCH; + final Class<?> ba = box(a), bp = box(p); + if (bp.isAssignableFrom(ba)) return MATCH; + if (isIntegral(ba)) { + if (isIntegral(bp)) return MATCH; + if (isFloating(bp)) return CONVERSION; + } + if (isFloating(ba) && isFloating(bp)) return MATCH; + return NONE; + } + + /** + * Splits the argument list enclosing the caret into its arguments' source + * text, or returns null if the caret is not inside an argument list. + */ + static List<String> argumentsAtCaret(final JTextComponent comp) { + final String text; + try { + text = comp.getText(0, comp.getDocument().getLength()); + } + catch (final BadLocationException exc) { + return null; + } + // Find the unmatched opening parenthesis before the caret. + int open = -1, depth = 0; + for (int i = Math.min(comp.getCaretPosition(), text.length()) - 1; // + i >= 0; i--) + { + final char c = text.charAt(i); + if (c == '\n') return null; + if (c == ')' || c == ']' || c == '}') depth++; + else if (c == '(' || c == '[' || c == '{') { + if (depth > 0) depth--; + else if (c == '(') { + open = i; + break; + } + else return null; // Inside a list or dict, not a call. + } + } + if (open < 0) return null; + // Split on top-level commas, up to the matching closing parenthesis. + final List<String> args = new ArrayList<>(); + int start = open + 1; + char quote = 0; + depth = 0; + for (int i = start; i < text.length(); i++) { + final char c = text.charAt(i); + if (quote != 0) { + if (c == '\\') i++; + else if (c == quote) quote = 0; + continue; + } + if (c == '"' || c == '\'') quote = c; + else if (c == '(' || c == '[' || c == '{') depth++; + else if (c == ')' || c == ']' || c == '}') { + if (depth == 0) { + args.add(text.substring(start, i)); + return args; + } + depth--; + } + else if (c == ',' && depth == 0) { + args.add(text.substring(start, i)); + start = i + 1; + } + else if (c == '\n') break; + } + args.add(text.substring(start)); + return args; + } + + // -- Helper methods -- + + private static String typeOf(final TypeResolver resolver, + final String expression) + { + try { + return resolver.typeOf(expression); + } + catch (final RuntimeException exc) { + return null; + } + } + + /** Loads a type such as {@code int}, {@code long[]} or {@code a.b.C}. */ + private static Class<?> loadType(final String name) { + if (name.endsWith("[]")) { + final Class<?> component = loadType(name.substring(0, name.length() - + 2)); + return component == null ? null : Array.newInstance(component, 0) + .getClass(); + } + final Class<?> primitive = PRIMITIVES.get(name); + if (primitive != null) return primitive; + try { + return Class.forName(name, false, Thread.currentThread() + .getContextClassLoader()); + } + catch (final ClassNotFoundException | LinkageError exc) { + return null; + } + } + + private static Class<?> box(final Class<?> c) { + if (!c.isPrimitive()) return c; + if (c == int.class) return Integer.class; + if (c == long.class) return Long.class; + if (c == double.class) return Double.class; + if (c == float.class) return Float.class; + if (c == boolean.class) return Boolean.class; + if (c == char.class) return Character.class; + if (c == short.class) return Short.class; + if (c == byte.class) return Byte.class; + return Void.class; + } + + private static boolean isIntegral(final Class<?> c) { + return c == Integer.class || c == Long.class || c == Short.class || + c == Byte.class || c == BigInteger.class; + } + + private static boolean isFloating(final Class<?> c) { + return c == Double.class || c == Float.class; + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java index b73d1658..47054ff3 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java @@ -56,6 +56,9 @@ public class SciJavaAutoCompletion extends public SciJavaAutoCompletion(final CompletionProvider provider) { super(provider); + // Expand threshold to describe other overloads + // (2000 is just a magic number) + setParameterDescriptionTruncateThreshold(2000); } @Override diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java index 3cee8cf7..7619c877 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java @@ -31,7 +31,10 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import javax.script.ScriptEngine; import javax.swing.text.BadLocationException; @@ -49,6 +52,7 @@ import org.scijava.code.api.CompletionRequest; import org.scijava.code.api.CompletionResult; import org.scijava.code.api.ParameterChoices; +import org.scijava.code.api.TypeResolver; /** * The single bridge between SciJava's toolkit-agnostic code completion SPI @@ -131,10 +135,19 @@ public List<Completion> getCompletionsImpl(final JTextComponent comp) { final CompletionResult result = compute(comp); final List<org.scijava.code.api.Completion> source = result.completions(); + // Group callables by name, so each can list its sibling overloads. + final Map<String, List<org.scijava.code.api.Completion>> overloads = + new HashMap<>(); + for (final org.scijava.code.api.Completion c : source) { + if (!c.isCallable()) continue; + overloads.computeIfAbsent(stripParens(c.insertionText()), + k -> new ArrayList<>()).add(c); + } final int n = source.size(); final List<Completion> out = new ArrayList<>(n); for (int i = 0; i < n; i++) { - out.add(toRSTA(source.get(i), n - i)); + out.add(toRSTA(comp, source.get(i), n - i, overloads, result + .typeResolver())); } return out; } @@ -200,10 +213,14 @@ List<Completion> parameterChoices(final JTextComponent comp, * auto-imports). The {@code orderRelevance} argument preserves the completer's * ordering when the completion does not specify its own relevance. */ - private Completion toRSTA(final org.scijava.code.api.Completion c, - final int orderRelevance) + private Completion toRSTA(final JTextComponent comp, + final org.scijava.code.api.Completion c, final int orderRelevance, + final Map<String, List<org.scijava.code.api.Completion>> overloads, + final TypeResolver types) { - final Completion rsta = c.isCallable() ? functionCompletion(c) + final Completion rsta = c.isCallable() // + ? functionCompletion(comp, c, overloads.get(stripParens(c + .insertionText())), types) // : basicCompletion(c); final double rel = c.relevance(); final int relevance = rel != 0 ? (int) Math.round(rel) : orderRelevance; @@ -223,13 +240,14 @@ private Completion basicCompletion( c.description(), c.additionalEdits()); } - private Completion functionCompletion( - final org.scijava.code.api.Completion c) + private Completion functionCompletion(final JTextComponent comp, + final org.scijava.code.api.Completion c, + final List<org.scijava.code.api.Completion> overloads, + final TypeResolver types) { // FunctionCompletion appends the parameter template itself, so strip any // trailing "()" the completer may have included in the insertion text. - String name = c.insertionText(); - if (name.endsWith("()")) name = name.substring(0, name.length() - 2); + final String name = stripParens(c.insertionText()); final String returnType = c.returnType() == null ? "" : c.returnType(); final SciJavaFunctionCompletion fc = new SciJavaFunctionCompletion(this, @@ -237,19 +255,117 @@ private Completion functionCompletion( fc.setReturnValueDescription(returnType); fc.setShortDescription(c.summary()); if (c.description() != null) fc.setSummary(c.description()); - fc.setParams(toRSTAParams(c.parameters())); + fc.setParams(toRSTAParams(comp, c, overloads, types)); return fc; } + /** + * Lists the parameters of {@code c}'s other overloads as HTML, one overload + * per line, for RSTA's parameter tooltip; or returns null if there are none. + * If {@code argTypes} is given, the overloads are sorted by how well they fit + * those arguments (see {@link Overloads#fit}): matches first, then those + * needing a conversion (greyed), then incompatible ones (struck out). + */ + static String otherOverloads(final org.scijava.code.api.Completion c, + final List<org.scijava.code.api.Completion> overloads, + final List<String> argTypes) + { + if (overloads == null) return null; + final String own = parameterList(c); + // NB: A map, since reflection can report the same signature twice. + final Map<String, Integer> fits = new LinkedHashMap<>(); + for (final org.scijava.code.api.Completion o : overloads) { + final String sig = parameterList(o); + if (sig.equals(own)) continue; + final int fit = argTypes == null ? Overloads.MATCH : Overloads.fit(o, + argTypes); + fits.merge(sig, fit, Math::max); + } + if (fits.isEmpty()) return null; + // NB: The sort is stable, so equally good fits keep their order. + final List<Map.Entry<String, Integer>> sorted = new ArrayList<>(fits + .entrySet()); + sorted.sort(Map.Entry.<String, Integer> comparingByValue().reversed()); + final StringBuilder sb = new StringBuilder(); + for (final Map.Entry<String, Integer> entry : sorted) { + // NB: A rule between overloads (and after RSTA's own line). + sb.append("<hr>"); + final String sig = escapeHTML(entry.getKey()); + switch (entry.getValue()) { + case Overloads.MATCH: + sb.append(sig); + break; + case Overloads.CONVERSION: + sb.append("<font color=\"gray\">").append(sig).append("</font>"); + break; + default: + sb.append("<font color=\"gray\"><s>").append(sig).append( + "</s></font>"); + } + } + return sb.toString(); + } + + /** + * A short parameter list such as {@code long[] pos, int d}, or {@code ()} if + * there are no parameters. + */ + private static String parameterList( + final org.scijava.code.api.Completion c) + { + final StringBuilder sb = new StringBuilder(); + final List<org.scijava.code.api.Completion.Parameter> params = c + .parameters(); + for (int i = 0; i < params.size(); i++) { + if (i > 0) sb.append(", "); + final String type = params.get(i).type(); + if (type != null) sb.append(type.substring(type.lastIndexOf('.') + 1)); + if (params.get(i).name() != null) { + if (type != null) sb.append(' '); + sb.append(params.get(i).name()); + } + } + return sb.length() == 0 ? "()" : sb.toString(); + } + + private static String escapeHTML(final String s) { + return s.replace("&", "&").replace("<", "<").replace(">", ">"); + } + + private static String stripParens(final String name) { + return name.endsWith("()") ? name.substring(0, name.length() - 2) : name; + } + + /** + * Converts {@code c}'s neutral parameters to RSTA ones. RSTA's parameter + * tooltip shows the current parameter's description, as HTML, below the + * signature: there, we list {@code c}'s other overloads. + */ private static List<ParameterizedCompletion.Parameter> toRSTAParams( - final List<org.scijava.code.api.Completion.Parameter> params) + final JTextComponent comp, final org.scijava.code.api.Completion c, + final List<org.scijava.code.api.Completion> overloads, + final TypeResolver types) { + final List<org.scijava.code.api.Completion.Parameter> params = c + .parameters(); final List<ParameterizedCompletion.Parameter> out = new ArrayList<>(params.size()); for (final org.scijava.code.api.Completion.Parameter p : params) { // Pass the type as the parameter's "type object" so the choices // provider can dispatch on it. - out.add(new ParameterizedCompletion.Parameter(p.type(), p.name())); + out.add(new ParameterizedCompletion.Parameter(p.type(), p.name()) { + + /** + * Computed when RSTA asks, so the list reflects the arguments typed + * so far. RSTA asks when the tooltip opens (before the argument list + * is inserted) and whenever the caret moves to another parameter. + */ + @Override + public String getDescription() { + return otherOverloads(c, overloads, Overloads.argumentTypes(comp, + types, c)); + } + }); } return out; } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java index be4b96cf..4a00a43d 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java @@ -65,4 +65,34 @@ public SciJavaFunctionCompletion(final CompletionProvider provider, public List<TextEdit> getAdditionalEdits() { return additionalEdits; } + + /** + * As {@link FunctionCompletion#addParameters}, which fills in the side + * description window, but without the parameter descriptions: those list + * the other overloads, for the parameter tooltip only (see + * {@link SciJavaCompletionProvider#otherOverloads}). + */ + @Override + protected void addParameters(final StringBuilder sb) { + final int count = getParamCount(); + if (count > 0) { + sb.append("<b>Parameters:</b><br>"); + sb.append("<center><table width='90%'><tr><td>"); + for (int i = 0; i < count; i++) { + final Parameter param = getParam(i); + sb.append("<b>"); + sb.append(param.getName() != null ? param.getName() : param + .getType()); + sb.append("</b><br>"); + } + sb.append("</td></tr></table></center><br><br>"); + } + final String returnDesc = getReturnValueDescription(); + if (returnDesc != null) { + sb.append("<b>Returns:</b><br>"); + sb.append("<center><table width='90%'><tr><td>"); + sb.append(returnDesc); + sb.append("</td></tr></table></center><br><br>"); + } + } } diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/OverloadsTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/OverloadsTest.java new file mode 100644 index 00000000..975b3b3c --- /dev/null +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/OverloadsTest.java @@ -0,0 +1,113 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.junit.Test; +import org.scijava.code.api.Completion; + +/** + * Tests {@link Overloads}. + * + * @author Gabriel Selzer + */ +public class OverloadsTest { + + @Test + public void testArgumentsAtCaret() { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("x = f(g(1, 2), 'a,b', [3, 4], y)"); + area.setCaretPosition(area.getText().indexOf('y')); + assertEquals(Arrays.asList("g(1, 2)", " 'a,b'", " [3, 4]", " y"), + Overloads.argumentsAtCaret(area)); + + // Inside a nested call, the innermost call's arguments. + area.setCaretPosition(area.getText().indexOf('2')); + assertEquals(Arrays.asList("1", " 2"), Overloads.argumentsAtCaret(area)); + + // Outside any call, or inside a list, there is no argument list. + area.setCaretPosition(2); + assertNull(Overloads.argumentsAtCaret(area)); + area.setCaretPosition(area.getText().indexOf('4')); + assertNull(Overloads.argumentsAtCaret(area)); + } + + @Test + public void testArgumentFit() { + // Python ints match integral parameters, and convert to floating ones. + assertEquals(Overloads.MATCH, Overloads.fit("long", "int")); + assertEquals(Overloads.CONVERSION, Overloads.fit("long", "double")); + // Python floats match floating parameters only. + assertEquals(Overloads.MATCH, Overloads.fit("double", "float")); + assertEquals(Overloads.NONE, Overloads.fit("double", "int")); + // Reference types follow assignability, including boxing and arrays. + assertEquals(Overloads.MATCH, Overloads.fit("java.util.ArrayList", + "java.util.List")); + assertEquals(Overloads.MATCH, Overloads.fit("java.lang.Integer", "int")); + assertEquals(Overloads.MATCH, Overloads.fit("java.lang.String", + "java.lang.Object")); + assertEquals(Overloads.NONE, Overloads.fit("java.lang.String", "int[]")); + assertEquals(Overloads.NONE, Overloads.fit("long", "java.lang.String")); + // Unknown types never rule out or demote anything. + assertEquals(Overloads.MATCH, Overloads.fit(null, "int")); + assertEquals(Overloads.MATCH, Overloads.fit("no.such.Type", "int")); + } + + @Test + public void testOverloadFit() { + final Completion floats = method("float", "float"); + // An overload is as good as its worst argument. + assertEquals(Overloads.CONVERSION, Overloads.fit(floats, Arrays.asList( + "double", "long"))); + assertEquals(Overloads.NONE, Overloads.fit(floats, Arrays.asList( + "java.lang.String", "double"))); + // Unknown (null) arguments don't count, but extra known ones do. + assertEquals(Overloads.MATCH, Overloads.fit(floats, Arrays.asList(null, + "double"))); + assertEquals(Overloads.NONE, Overloads.fit(floats, Arrays.asList(null, + null, "double"))); + } + + private static Completion method(final String... paramTypes) { + final List<Completion.Parameter> params = new ArrayList<>(); + for (final String type : paramTypes) { + params.add(new Completion.Parameter(null, type)); + } + return Completion.builder("f").kind(Completion.Kind.METHOD).parameters( + params).build(); + } +} diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index f5f0fb66..ef30cff5 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -168,4 +168,111 @@ public void testCallablesRenderWithSignatures() { org.junit.Assert.assertTrue(text, text.contains("long[]")); org.junit.Assert.assertTrue(text, text.contains("pos")); } + + @Test + public void testParameterTooltipListsOtherOverloads() { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("img.getAt"); + area.setCaretPosition(area.getDocument().getLength()); + + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + request -> new CompletionResult(Arrays.asList( // + method("img.getAt", "int[]"), // + method("img.getAt", "net.imglib2.Localizable"), // + method("img.getAt", "int[]"), // e.g. a bridge method + method("img.getAtX", "long[]")), 0), + null); + final List<Completion> completions = provider.getCompletionsImpl(area); + + // Each overload's tooltip lists the others, once each, even before RSTA + // inserts the argument list... + assertEquals("<hr>Localizable p0", + ((org.fife.ui.autocomplete.ParameterizedCompletion) completions.get(0)) + .getParam(0).getDescription()); + // ...but the side description window does not. + final String summary = completions.get(0).getSummary(); + org.junit.Assert.assertFalse(summary, summary.contains("Localizable")); + + // Inside the argument list, likewise. + area.setText("img.getAt(p0)"); + area.setCaretPosition(10); + final String desc = ((org.fife.ui.autocomplete.ParameterizedCompletion) // + completions.get(0)).getParam(0).getDescription(); + assertEquals("<hr>Localizable p0", desc); + // ...and a callable with no other overloads gets no description. + org.junit.Assert.assertNull(((org.fife.ui.autocomplete.ParameterizedCompletion) // + completions.get(3)).getParam(0).getDescription()); + } + + @Test + public void testParameterTooltipRanksOverloadsByArgumentTypes() { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("a"); + area.setCaretPosition(1); + + final org.scijava.code.api.TypeResolver types = expr -> { + switch (expr) { + case "1.5": return "double"; + case "1": return "long"; + default: return null; + } + }; + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + request -> new CompletionResult(Arrays.asList( // + method("a", "int", "int"), // + method("a", "java.lang.String"), // + method("a", "float", "float"), // + method("a", "long", "long")), 0, null, types), + null); + final List<Completion> completions = provider.getCompletionsImpl(area); + final org.fife.ui.autocomplete.ParameterizedCompletion intOverload = + (org.fife.ui.autocomplete.ParameterizedCompletion) completions.get(0); + final org.fife.ui.autocomplete.ParameterizedCompletion floatOverload = + (org.fife.ui.autocomplete.ParameterizedCompletion) completions.get(2); + final String gray = "<font color=\"gray\">"; + + // Accepted a(int, int), typed a float, and tabbed onward: the float + // overload matches, and the others are struck out. + area.setText("a(1.5, p1)"); + area.setCaretPosition(7); + assertEquals("<hr>float p0, float p1" + // + "<hr>" + gray + "<s>String p0</s></font>" + // + "<hr>" + gray + "<s>long p0, long p1</s></font>", // + intOverload.getParam(1).getDescription()); + + // Accepted a(float, float) and typed an int: the integral overloads + // match, so they come first. + area.setText("a(1, p1)"); + area.setCaretPosition(5); + assertEquals("<hr>int p0, int p1<hr>long p0, long p1" + // + "<hr>" + gray + "<s>String p0</s></font>", // + floatOverload.getParam(1).getDescription()); + + // Accepted a(int, int) and typed an int: the float overload would need a + // conversion, so it comes after the long one, greyed. + assertEquals("<hr>long p0, long p1" + // + "<hr>" + gray + "float p0, float p1</font>" + // + "<hr>" + gray + "<s>String p0</s></font>", // + intOverload.getParam(1).getDescription()); + + // Nothing typed yet: the overloads keep their order, unstyled. + area.setText("a(p0, p1)"); + area.setCaretPosition(2); + assertEquals("<hr>String p0<hr>float p0, float p1<hr>long p0, long p1", // + intOverload.getParam(0).getDescription()); + } + + private static org.scijava.code.api.Completion method(final String name, + final String... paramTypes) + { + final List<org.scijava.code.api.Completion.Parameter> params = + new java.util.ArrayList<>(); + for (int i = 0; i < paramTypes.length; i++) { + params.add(new org.scijava.code.api.Completion.Parameter("p" + i, + paramTypes[i])); + } + return org.scijava.code.api.Completion.builder(name).kind( + org.scijava.code.api.Completion.Kind.METHOD).parameters(params) + .returnType("java.lang.Object").build(); + } } From fb96908fe70a92e9b5925109f2692234c9281dba Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Tue, 6 Oct 2026 10:49:16 -0500 Subject: [PATCH 07/21] Show signature help, lazy docs and live updates * Render the parameter tooltip's overload list from the completer's signature help: its order, styled by fit (conversions greyed, mismatches struck out), ignoring help about nested calls. Remove Overloads, whose language-specific parsing and rules moved to the completers. * Ask for completion descriptions only when RSTA shows them, and show them for function completions too (RSTA's FunctionCompletion dropped them). * Update the completion popup in place when a result announces a better one, if the text and caret are unchanged. * Let completers prepare when a script's completion support installs. * Pass the edited script's file path in completion requests. * Color dimmed overloads from the Look & Feel's disabled text color. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../scijava/ui/swing/script/EditorPane.java | 2 +- .../CodeCompleterLanguageSupport.java | 30 ++ .../script/autocompletion/Overloads.java | 260 ------------------ .../autocompletion/SciJavaAutoCompletion.java | 23 ++ .../autocompletion/SciJavaCompletion.java | 22 +- .../SciJavaCompletionProvider.java | 250 +++++++++++++---- .../SciJavaFunctionCompletion.java | 24 ++ .../script/autocompletion/OverloadsTest.java | 113 -------- .../SciJavaCompletionProviderTest.java | 254 ++++++++++++++--- 9 files changed, 518 insertions(+), 460 deletions(-) delete mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/Overloads.java delete mode 100644 src/test/java/org/scijava/ui/swing/script/autocompletion/OverloadsTest.java diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index db840972..96a8d606 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -806,7 +806,7 @@ protected void setLanguage(final ScriptLanguage language, { support = new CodeCompleterLanguageSupport( codeCompletionService.getCompleterPlugin(currentLanguage), - currentLanguage, log); + currentLanguage, log, this::getFile); } // Tier 3: RSTA's own built-in support for the syntax style. diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java index 2ad53c2c..513a3595 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java @@ -29,12 +29,16 @@ package org.scijava.ui.swing.script.autocompletion; +import java.io.File; +import java.util.function.Supplier; + import org.fife.rsta.ac.AbstractLanguageSupport; import org.fife.ui.autocomplete.AutoCompletion; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.scijava.log.Logger; import org.scijava.script.ScriptLanguage; import org.scijava.code.api.CodeCompleter; +import org.scijava.code.api.CompletionRequest; /** * An RSTA {@link org.fife.rsta.ac.LanguageSupport} that drives code completion @@ -54,6 +58,7 @@ public class CodeCompleterLanguageSupport extends AbstractLanguageSupport { private final CodeCompleter completer; private final ScriptLanguage language; private final Logger log; + private final Supplier<File> file; public CodeCompleterLanguageSupport(final CodeCompleter completer, final ScriptLanguage language) @@ -63,10 +68,22 @@ public CodeCompleterLanguageSupport(final CodeCompleter completer, public CodeCompleterLanguageSupport(final CodeCompleter completer, final ScriptLanguage language, final Logger log) + { + this(completer, language, log, null); + } + + /** + * @param file Supplies the file of the script being edited (or null if + * unsaved), so that completers can resolve files relative to it. + */ + public CodeCompleterLanguageSupport(final CodeCompleter completer, + final ScriptLanguage language, final Logger log, + final Supplier<File> file) { this.completer = completer; this.language = language; this.log = log; + this.file = file; setAutoCompleteEnabled(true); setParameterAssistanceEnabled(true); setShowDescWindow(true); @@ -77,6 +94,7 @@ public void install(final RSyntaxTextArea textArea) { final SciJavaCompletionProvider provider = new SciJavaCompletionProvider(completer, language); provider.setLogger(log); + provider.setFile(file); final AutoCompletion ac = new SciJavaAutoCompletion(provider); ac.setAutoCompleteEnabled(isAutoCompleteEnabled()); ac.setAutoActivationEnabled(isAutoActivationEnabled()); @@ -84,6 +102,18 @@ public void install(final RSyntaxTextArea textArea) { ac.setShowDescWindow(getShowDescWindow()); ac.install(textArea); installImpl(textArea, ac); + + // Let the completer get ready for this script, e.g. warm up caches. + try { + final File f = file == null ? null : file.get(); + final String text = textArea.getText(); + completer.prepare(new CompletionRequest(text, text.length(), language, + null, null, f == null ? null : f.getPath())); + } + catch (final Exception | LinkageError exc) { + // NB: Preparation is optional; never let it break the editor. + if (log != null) log.debug("Completer preparation failed", exc); + } } @Override diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/Overloads.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/Overloads.java deleted file mode 100644 index 6d78a58a..00000000 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/Overloads.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * #%L - * Script Editor and Interpreter for SciJava script languages. - * %% - * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; - -import java.lang.reflect.Array; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.swing.text.BadLocationException; -import javax.swing.text.JTextComponent; - -import org.scijava.code.api.Completion; -import org.scijava.code.api.TypeResolver; - -/** - * Helpers for ranking a callable's overloads against the arguments typed so - * far. - * - * @author Gabriel Selzer - */ -final class Overloads { - - /** Fit of an argument to a parameter: incompatible. */ - static final int NONE = 0; - - /** Fit of an argument to a parameter: compatible after widening. */ - static final int CONVERSION = 1; - - /** Fit of an argument to a parameter: compatible as is. */ - static final int MATCH = 2; - - private static final Map<String, Class<?>> PRIMITIVES = new HashMap<>(); - static { - for (final Class<?> c : new Class<?>[] { boolean.class, byte.class, - char.class, short.class, int.class, long.class, float.class, - double.class, void.class }) - { - PRIMITIVES.put(c.getName(), c); - } - } - - private Overloads() { - // NB: Prevent instantiation of utility class. - } - - /** - * Gets the types of the arguments of the call enclosing the caret, or null - * if the caret is not inside an argument list. An argument's type is null - * if it is unknown, empty, or still RSTA's placeholder (the parameter name - * of the accepted overload). - */ - static List<String> argumentTypes(final JTextComponent comp, - final TypeResolver resolver, final Completion accepted) - { - final List<String> args = argumentsAtCaret(comp); - if (args == null) return null; - final List<Completion.Parameter> params = accepted.parameters(); - final List<String> types = new ArrayList<>(args.size()); - for (int i = 0; i < args.size(); i++) { - final String arg = args.get(i).trim(); - final boolean placeholder = i < params.size() && // - arg.equals(params.get(i).name()); - types.add(arg.isEmpty() || placeholder || resolver == null ? null - : typeOf(resolver, arg)); - } - return types; - } - - /** - * Rates how well {@code overload} fits arguments of the given types: the - * worst fit among its arguments, or {@link #NONE} if more arguments were - * given than it has parameters. - * - * @return {@link #MATCH}, {@link #CONVERSION} or {@link #NONE} - */ - static int fit(final Completion overload, final List<String> argTypes) { - final List<Completion.Parameter> params = overload.parameters(); - int fit = MATCH; - for (int i = 0; i < argTypes.size(); i++) { - if (argTypes.get(i) == null) continue; - if (i >= params.size()) return NONE; - fit = Math.min(fit, fit(argTypes.get(i), params.get(i).type())); - } - return fit; - } - - /** - * Rates passing a value of type {@code argType} to a parameter of type - * {@code paramType}, following Jython's conversions: - * <ul> - * <li>{@link #MATCH}: the same type, an assignable one, or the same kind of - * number (a Python integer to an integral parameter, or a Python float to a - * floating-point one). Unknown types match too, so they never rule out or - * demote an overload.</li> - * <li>{@link #CONVERSION}: a Python integer to a floating-point - * parameter.</li> - * <li>{@link #NONE}: anything else.</li> - * </ul> - */ - static int fit(final String argType, final String paramType) { - if (argType == null || paramType == null || argType.equals(paramType)) { - return MATCH; - } - final Class<?> a = loadType(argType), p = loadType(paramType); - if (a == null || p == null) return MATCH; - final Class<?> ba = box(a), bp = box(p); - if (bp.isAssignableFrom(ba)) return MATCH; - if (isIntegral(ba)) { - if (isIntegral(bp)) return MATCH; - if (isFloating(bp)) return CONVERSION; - } - if (isFloating(ba) && isFloating(bp)) return MATCH; - return NONE; - } - - /** - * Splits the argument list enclosing the caret into its arguments' source - * text, or returns null if the caret is not inside an argument list. - */ - static List<String> argumentsAtCaret(final JTextComponent comp) { - final String text; - try { - text = comp.getText(0, comp.getDocument().getLength()); - } - catch (final BadLocationException exc) { - return null; - } - // Find the unmatched opening parenthesis before the caret. - int open = -1, depth = 0; - for (int i = Math.min(comp.getCaretPosition(), text.length()) - 1; // - i >= 0; i--) - { - final char c = text.charAt(i); - if (c == '\n') return null; - if (c == ')' || c == ']' || c == '}') depth++; - else if (c == '(' || c == '[' || c == '{') { - if (depth > 0) depth--; - else if (c == '(') { - open = i; - break; - } - else return null; // Inside a list or dict, not a call. - } - } - if (open < 0) return null; - // Split on top-level commas, up to the matching closing parenthesis. - final List<String> args = new ArrayList<>(); - int start = open + 1; - char quote = 0; - depth = 0; - for (int i = start; i < text.length(); i++) { - final char c = text.charAt(i); - if (quote != 0) { - if (c == '\\') i++; - else if (c == quote) quote = 0; - continue; - } - if (c == '"' || c == '\'') quote = c; - else if (c == '(' || c == '[' || c == '{') depth++; - else if (c == ')' || c == ']' || c == '}') { - if (depth == 0) { - args.add(text.substring(start, i)); - return args; - } - depth--; - } - else if (c == ',' && depth == 0) { - args.add(text.substring(start, i)); - start = i + 1; - } - else if (c == '\n') break; - } - args.add(text.substring(start)); - return args; - } - - // -- Helper methods -- - - private static String typeOf(final TypeResolver resolver, - final String expression) - { - try { - return resolver.typeOf(expression); - } - catch (final RuntimeException exc) { - return null; - } - } - - /** Loads a type such as {@code int}, {@code long[]} or {@code a.b.C}. */ - private static Class<?> loadType(final String name) { - if (name.endsWith("[]")) { - final Class<?> component = loadType(name.substring(0, name.length() - - 2)); - return component == null ? null : Array.newInstance(component, 0) - .getClass(); - } - final Class<?> primitive = PRIMITIVES.get(name); - if (primitive != null) return primitive; - try { - return Class.forName(name, false, Thread.currentThread() - .getContextClassLoader()); - } - catch (final ClassNotFoundException | LinkageError exc) { - return null; - } - } - - private static Class<?> box(final Class<?> c) { - if (!c.isPrimitive()) return c; - if (c == int.class) return Integer.class; - if (c == long.class) return Long.class; - if (c == double.class) return Double.class; - if (c == float.class) return Float.class; - if (c == boolean.class) return Boolean.class; - if (c == char.class) return Character.class; - if (c == short.class) return Short.class; - if (c == byte.class) return Byte.class; - return Void.class; - } - - private static boolean isIntegral(final Class<?> c) { - return c == Integer.class || c == Long.class || c == Short.class || - c == Byte.class || c == BigInteger.class; - } - - private static boolean isFloating(final Class<?> c) { - return c == Double.class || c == Float.class; - } -} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java index 47054ff3..ff6403a5 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java @@ -59,6 +59,29 @@ public SciJavaAutoCompletion(final CompletionProvider provider) { // Expand threshold to describe other overloads // (2000 is just a magic number) setParameterDescriptionTruncateThreshold(2000); + if (provider instanceof SciJavaCompletionProvider) { + ((SciJavaCompletionProvider) provider).setUpdateListener( + this::refreshPopup); + } + } + + /** + * Shows the current (just improved) completions: in the popup, if it is + * showing, or in a new one, if the previous result had nothing to show. + */ + private void refreshPopup(final boolean previousWasEmpty) { + final JTextComponent comp = getTextComponent(); + if (comp == null || !comp.isFocusOwner()) return; + if (!isPopupVisible() && !previousWasEmpty) return; // NB: User closed it. + // NB: Never insert a lone completion the user has not chosen. + final boolean single = getAutoCompleteSingleChoices(); + setAutoCompleteSingleChoices(false); + try { + doCompletion(); + } + finally { + setAutoCompleteSingleChoices(single); + } } @Override diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java index c0d53f94..d9a39f6f 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java @@ -30,6 +30,7 @@ package org.scijava.ui.swing.script.autocompletion; import java.util.List; +import java.util.function.Supplier; import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.CompletionProvider; @@ -48,15 +49,34 @@ public class SciJavaCompletion extends BasicCompletion implements { private final List<TextEdit> additionalEdits; + private final Supplier<String> description; public SciJavaCompletion(final CompletionProvider provider, final String replacementText, final String shortDesc, final String summary, final List<TextEdit> additionalEdits) { - super(provider, replacementText, shortDesc, summary); + this(provider, replacementText, shortDesc, () -> summary, + additionalEdits); + } + + /** + * @param description Supplies the description shown beside the completion + * list, asked for only when the completion is selected. + */ + public SciJavaCompletion(final CompletionProvider provider, + final String replacementText, final String shortDesc, + final Supplier<String> description, final List<TextEdit> additionalEdits) + { + super(provider, replacementText, shortDesc, null); + this.description = description; this.additionalEdits = additionalEdits; } + @Override + public String getSummary() { + return description == null ? null : description.get(); + } + @Override public List<TextEdit> getAdditionalEdits() { return additionalEdits; diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java index 7619c877..1e2d45c4 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java @@ -29,14 +29,24 @@ package org.scijava.ui.swing.script.autocompletion; +import java.awt.Color; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.function.Supplier; import javax.script.ScriptEngine; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; @@ -52,7 +62,8 @@ import org.scijava.code.api.CompletionRequest; import org.scijava.code.api.CompletionResult; import org.scijava.code.api.ParameterChoices; -import org.scijava.code.api.TypeResolver; +import org.scijava.code.api.SignatureHelp; +import org.scijava.code.api.SignatureHelp.Fit; /** * The single bridge between SciJava's toolkit-agnostic code completion SPI @@ -80,6 +91,9 @@ public class SciJavaCompletionProvider extends DefaultCompletionProvider { /** Optional live engine, set when completing in an interpreter. */ private ScriptEngine engine; + /** Optional supplier of the edited script's file. */ + private Supplier<File> file; + // Cache so getAlreadyEnteredText() and getCompletionsImpl() — which RSTA // calls in quick succession for the same caret — stay consistent and avoid // recomputation. @@ -87,6 +101,17 @@ public class SciJavaCompletionProvider extends DefaultCompletionProvider { private String cachedText; private CompletionResult cachedResult; + /** How long the parameter tooltip waits for better signature help, in ms. */ + private static final long HELP_BUDGET = 150; + + // The signature help for the current text and caret. + private String helpText; + private int helpCaret = -1; + private volatile SignatureHelp help; + + /** Notified when a better result arrives for the current request. */ + private volatile Consumer<Boolean> updateListener; + /** The parameter-choices provider installed for the cached result, if any. */ private ParameterChoicesProvider choicesProvider; @@ -107,6 +132,14 @@ public void setLogger(final Logger log) { this.log = log; } + /** + * Sets a supplier of the file of the script being edited (which may supply + * null, for an unsaved script). + */ + public void setFile(final Supplier<File> file) { + this.file = file; + } + /** Sets a live script engine to enable variable/binding-based completion. */ public void setEngine(final ScriptEngine engine) { this.engine = engine; @@ -146,8 +179,7 @@ public List<Completion> getCompletionsImpl(final JTextComponent comp) { final int n = source.size(); final List<Completion> out = new ArrayList<>(n); for (int i = 0; i < n; i++) { - out.add(toRSTA(comp, source.get(i), n - i, overloads, result - .typeResolver())); + out.add(toRSTA(comp, source.get(i), n - i, overloads)); } return out; } @@ -170,8 +202,10 @@ private CompletionResult compute(final JTextComponent comp) { } CompletionResult result; try { + final File f = file == null ? null : file.get(); final CompletionRequest request = new CompletionRequest(text, caret, - language, engine, engine == null ? null : engine.getContext()); + language, engine, engine == null ? null : engine.getContext(), f == + null ? null : f.getPath()); result = completer.complete(request); if (result == null) result = CompletionResult.EMPTY; } @@ -182,13 +216,47 @@ private CompletionResult compute(final JTextComponent comp) { } cachedCaret = caret; cachedText = text; + adopt(result, comp); + return result; + } + + /** + * Sets a listener to notify when a better result has arrived for the + * current request (see {@link CompletionResult#update()}), e.g. to refresh + * the completion popup. Called on the event dispatch thread. + * + * @param listener Accepts whether the previous result had no completions + * (so that no popup may be showing for it). + */ + public void setUpdateListener(final Consumer<Boolean> listener) { + this.updateListener = listener; + } + + /** Makes the given result current, and awaits its update, if any. */ + private void adopt(final CompletionResult result, final JTextComponent comp) { cachedResult = result; // Wire up (or clear) parameter assistance for this result's callables. final ParameterChoices choices = result.parameterChoices(); choicesProvider = choices == null ? null : new NeutralChoicesProvider(choices); setParameterChoicesProvider(choicesProvider); - return result; + + final CompletableFuture<CompletionResult> update = result.update(); + if (update == null) return; + final String text = cachedText; + final int caret = cachedCaret; + update.whenComplete((better, error) -> { + if (better == null) return; + SwingUtilities.invokeLater(() -> { + // NB: Only if nothing has changed since the request. + if (cachedResult != result || caret != comp.getCaretPosition() || + !text.equals(comp.getText())) return; + final boolean wasEmpty = result.completions().isEmpty(); + adopt(better, comp); + final Consumer<Boolean> listener = updateListener; + if (listener != null) listener.accept(wasEmpty); + }); + }); } /** @@ -215,12 +283,11 @@ List<Completion> parameterChoices(final JTextComponent comp, */ private Completion toRSTA(final JTextComponent comp, final org.scijava.code.api.Completion c, final int orderRelevance, - final Map<String, List<org.scijava.code.api.Completion>> overloads, - final TypeResolver types) + final Map<String, List<org.scijava.code.api.Completion>> overloads) { final Completion rsta = c.isCallable() // ? functionCompletion(comp, c, overloads.get(stripParens(c - .insertionText())), types) // + .insertionText()))) // : basicCompletion(c); final double rel = c.relevance(); final int relevance = rel != 0 ? (int) Math.round(rel) : orderRelevance; @@ -233,17 +300,15 @@ private Completion toRSTA(final JTextComponent comp, private Completion basicCompletion( final org.scijava.code.api.Completion c) { - return c.additionalEdits().isEmpty() // - ? new BasicCompletion(this, c.insertionText(), c.summary(), - c.description()) // - : new SciJavaCompletion(this, c.insertionText(), c.summary(), - c.description(), c.additionalEdits()); + // NB: The description may be expensive (computed on demand): ask for it + // only when RSTA shows it, i.e. when the completion is selected. + return new SciJavaCompletion(this, c.insertionText(), c.summary(), + c::description, c.additionalEdits()); } private Completion functionCompletion(final JTextComponent comp, final org.scijava.code.api.Completion c, - final List<org.scijava.code.api.Completion> overloads, - final TypeResolver types) + final List<org.scijava.code.api.Completion> overloads) { // FunctionCompletion appends the parameter template itself, so strip any // trailing "()" the completer may have included in the insertion text. @@ -254,58 +319,149 @@ private Completion functionCompletion(final JTextComponent comp, name, returnType, c.additionalEdits()); fc.setReturnValueDescription(returnType); fc.setShortDescription(c.summary()); - if (c.description() != null) fc.setSummary(c.description()); - fc.setParams(toRSTAParams(comp, c, overloads, types)); + fc.setDescription(c::description); + fc.setParams(toRSTAParams(comp, c, overloads)); return fc; } /** * Lists the parameters of {@code c}'s other overloads as HTML, one overload * per line, for RSTA's parameter tooltip; or returns null if there are none. - * If {@code argTypes} is given, the overloads are sorted by how well they fit - * those arguments (see {@link Overloads#fit}): matches first, then those - * needing a conversion (greyed), then incompatible ones (struck out). + * <p> + * If the completer's signature help is for this call, its signatures are + * listed in its order, styled by how well they fit the arguments typed so + * far: matches plain, those needing a conversion greyed, mismatches struck + * out. Otherwise, {@code overloads} are listed, unstyled. + * </p> */ static String otherOverloads(final org.scijava.code.api.Completion c, final List<org.scijava.code.api.Completion> overloads, - final List<String> argTypes) + final SignatureHelp help) { - if (overloads == null) return null; final String own = parameterList(c); // NB: A map, since reflection can report the same signature twice. - final Map<String, Integer> fits = new LinkedHashMap<>(); - for (final org.scijava.code.api.Completion o : overloads) { - final String sig = parameterList(o); - if (sig.equals(own)) continue; - final int fit = argTypes == null ? Overloads.MATCH : Overloads.fit(o, - argTypes); - fits.merge(sig, fit, Math::max); + final Map<String, Fit> lines = new LinkedHashMap<>(); + if (help != null && isFor(help, c)) { + for (final SignatureHelp.Signature s : help.signatures()) { + final String sig = parameterList(s.callable()); + if (!sig.equals(own)) lines.putIfAbsent(sig, s.fit()); + } + } + else if (overloads != null) { + for (final org.scijava.code.api.Completion o : overloads) { + final String sig = parameterList(o); + if (!sig.equals(own)) lines.putIfAbsent(sig, Fit.UNKNOWN); + } } - if (fits.isEmpty()) return null; - // NB: The sort is stable, so equally good fits keep their order. - final List<Map.Entry<String, Integer>> sorted = new ArrayList<>(fits - .entrySet()); - sorted.sort(Map.Entry.<String, Integer> comparingByValue().reversed()); + if (lines.isEmpty()) return null; + // NB: Read the color now, so the tooltip follows Look & Feel changes. + final String dim = "<font color=\"" + dimColor() + "\">"; final StringBuilder sb = new StringBuilder(); - for (final Map.Entry<String, Integer> entry : sorted) { + for (final Map.Entry<String, Fit> line : lines.entrySet()) { // NB: A rule between overloads (and after RSTA's own line). sb.append("<hr>"); - final String sig = escapeHTML(entry.getKey()); - switch (entry.getValue()) { - case Overloads.MATCH: - sb.append(sig); + final String sig = escapeHTML(line.getKey()); + switch (line.getValue()) { + case CONVERSION: + sb.append(dim).append(sig).append("</font>"); break; - case Overloads.CONVERSION: - sb.append("<font color=\"gray\">").append(sig).append("</font>"); + case MISMATCH: + sb.append(dim).append("<s>").append(sig).append("</s></font>"); break; default: - sb.append("<font color=\"gray\"><s>").append(sig).append( - "</s></font>"); + sb.append(sig); } } return sb.toString(); } + /** + * True iff the help is about calls to {@code c}, rather than, e.g., a call + * nested in its arguments. + */ + private static boolean isFor(final SignatureHelp help, + final org.scijava.code.api.Completion c) + { + if (help.isEmpty()) return false; + final String name = simpleName(c); + for (final SignatureHelp.Signature s : help.signatures()) { + if (!simpleName(s.callable()).equals(name)) return false; + } + return true; + } + + private static String simpleName(final org.scijava.code.api.Completion c) { + final String name = stripParens(c.insertionText()); + return name.substring(name.lastIndexOf('.') + 1); + } + + /** + * Gets the completer's signature help at the caret (see + * {@link CodeCompleter#signatureHelp}), waiting briefly for a better + * answer, if one is announced. Remembered per text and caret. + */ + SignatureHelp signatureHelp(final JTextComponent comp) { + final String text; + try { + text = comp.getText(0, comp.getDocument().getLength()); + } + catch (final BadLocationException exc) { + return SignatureHelp.NONE; + } + final int caret = comp.getCaretPosition(); + if (caret == helpCaret && text.equals(helpText) && help != null) { + return help; + } + SignatureHelp h; + try { + final File f = file == null ? null : file.get(); + h = completer.signatureHelp(new CompletionRequest(text, caret, language, + engine, engine == null ? null : engine.getContext(), f == null ? null + : f.getPath())); + if (h == null) h = SignatureHelp.NONE; + if (h.update() != null) { + try { + final SignatureHelp better = h.update().get(HELP_BUDGET, + TimeUnit.MILLISECONDS); + if (better != null) h = better; + } + catch (final TimeoutException | ExecutionException exc) { + // NB: Not in time: use what there is, and keep the better help + // for when RSTA asks again (e.g. at the next parameter). + final SignatureHelp first = h; + h.update().thenAccept(better -> { + if (better != null && help == first) help = better; + }); + } + catch (final InterruptedException exc) { + Thread.currentThread().interrupt(); + } + } + } + catch (final Exception | LinkageError exc) { + // NB: Never let a misbehaving completer break the editor. + if (log != null) log.debug("Signature help failed", exc); + h = SignatureHelp.NONE; + } + helpText = text; + helpCaret = caret; + help = h; + return h; + } + + /** + * The Look & Feel's color for disabled text, as an HTML color such as + * {@code #8c8c8c}. RSTA's parameter tooltip takes its other colors from the + * Look & Feel too. + */ + static String dimColor() { + Color c = UIManager.getColor("Label.disabledForeground"); + if (c == null) c = UIManager.getColor("textInactiveText"); + if (c == null) c = Color.GRAY; + return String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c + .getBlue()); + } + /** * A short parameter list such as {@code long[] pos, int d}, or {@code ()} if * there are no parameters. @@ -341,10 +497,9 @@ private static String stripParens(final String name) { * tooltip shows the current parameter's description, as HTML, below the * signature: there, we list {@code c}'s other overloads. */ - private static List<ParameterizedCompletion.Parameter> toRSTAParams( + private List<ParameterizedCompletion.Parameter> toRSTAParams( final JTextComponent comp, final org.scijava.code.api.Completion c, - final List<org.scijava.code.api.Completion> overloads, - final TypeResolver types) + final List<org.scijava.code.api.Completion> overloads) { final List<org.scijava.code.api.Completion.Parameter> params = c .parameters(); @@ -362,8 +517,7 @@ private static List<ParameterizedCompletion.Parameter> toRSTAParams( */ @Override public String getDescription() { - return otherOverloads(c, overloads, Overloads.argumentTypes(comp, - types, c)); + return otherOverloads(c, overloads, signatureHelp(comp)); } }); } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java index 4a00a43d..82f63e78 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java @@ -31,6 +31,7 @@ import java.util.Collections; import java.util.List; +import java.util.function.Supplier; import org.fife.ui.autocomplete.CompletionProvider; import org.fife.ui.autocomplete.FunctionCompletion; @@ -51,6 +52,7 @@ public class SciJavaFunctionCompletion extends FunctionCompletion implements { private final List<TextEdit> additionalEdits; + private Supplier<String> description; public SciJavaFunctionCompletion(final CompletionProvider provider, final String name, final String returnType, @@ -66,6 +68,28 @@ public List<TextEdit> getAdditionalEdits() { return additionalEdits; } + /** + * Sets a supplier of the description (e.g. documentation) shown beside the + * completion list, asked for only when the completion is selected. + */ + public void setDescription(final Supplier<String> description) { + this.description = description; + } + + /** + * Adds the description, if any, below RSTA's short description. (RSTA's + * {@link FunctionCompletion#getSummary} would not show it otherwise.) + */ + @Override + protected boolean possiblyAddDescription(final StringBuilder sb) { + final boolean added = super.possiblyAddDescription(sb); + final String desc = description == null ? null : description.get(); + if (desc == null || desc.isEmpty()) return added; + if (!added) sb.append("<hr><br>"); + sb.append(desc).append("<br><br><br>"); + return true; + } + /** * As {@link FunctionCompletion#addParameters}, which fills in the side * description window, but without the parameter descriptions: those list diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/OverloadsTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/OverloadsTest.java deleted file mode 100644 index 975b3b3c..00000000 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/OverloadsTest.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * #%L - * Script Editor and Interpreter for SciJava script languages. - * %% - * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; -import org.junit.Test; -import org.scijava.code.api.Completion; - -/** - * Tests {@link Overloads}. - * - * @author Gabriel Selzer - */ -public class OverloadsTest { - - @Test - public void testArgumentsAtCaret() { - final RSyntaxTextArea area = new RSyntaxTextArea(); - area.setText("x = f(g(1, 2), 'a,b', [3, 4], y)"); - area.setCaretPosition(area.getText().indexOf('y')); - assertEquals(Arrays.asList("g(1, 2)", " 'a,b'", " [3, 4]", " y"), - Overloads.argumentsAtCaret(area)); - - // Inside a nested call, the innermost call's arguments. - area.setCaretPosition(area.getText().indexOf('2')); - assertEquals(Arrays.asList("1", " 2"), Overloads.argumentsAtCaret(area)); - - // Outside any call, or inside a list, there is no argument list. - area.setCaretPosition(2); - assertNull(Overloads.argumentsAtCaret(area)); - area.setCaretPosition(area.getText().indexOf('4')); - assertNull(Overloads.argumentsAtCaret(area)); - } - - @Test - public void testArgumentFit() { - // Python ints match integral parameters, and convert to floating ones. - assertEquals(Overloads.MATCH, Overloads.fit("long", "int")); - assertEquals(Overloads.CONVERSION, Overloads.fit("long", "double")); - // Python floats match floating parameters only. - assertEquals(Overloads.MATCH, Overloads.fit("double", "float")); - assertEquals(Overloads.NONE, Overloads.fit("double", "int")); - // Reference types follow assignability, including boxing and arrays. - assertEquals(Overloads.MATCH, Overloads.fit("java.util.ArrayList", - "java.util.List")); - assertEquals(Overloads.MATCH, Overloads.fit("java.lang.Integer", "int")); - assertEquals(Overloads.MATCH, Overloads.fit("java.lang.String", - "java.lang.Object")); - assertEquals(Overloads.NONE, Overloads.fit("java.lang.String", "int[]")); - assertEquals(Overloads.NONE, Overloads.fit("long", "java.lang.String")); - // Unknown types never rule out or demote anything. - assertEquals(Overloads.MATCH, Overloads.fit(null, "int")); - assertEquals(Overloads.MATCH, Overloads.fit("no.such.Type", "int")); - } - - @Test - public void testOverloadFit() { - final Completion floats = method("float", "float"); - // An overload is as good as its worst argument. - assertEquals(Overloads.CONVERSION, Overloads.fit(floats, Arrays.asList( - "double", "long"))); - assertEquals(Overloads.NONE, Overloads.fit(floats, Arrays.asList( - "java.lang.String", "double"))); - // Unknown (null) arguments don't count, but extra known ones do. - assertEquals(Overloads.MATCH, Overloads.fit(floats, Arrays.asList(null, - "double"))); - assertEquals(Overloads.NONE, Overloads.fit(floats, Arrays.asList(null, - null, "double"))); - } - - private static Completion method(final String... paramTypes) { - final List<Completion.Parameter> params = new ArrayList<>(); - for (final String type : paramTypes) { - params.add(new Completion.Parameter(null, type)); - } - return Completion.builder("f").kind(Completion.Kind.METHOD).parameters( - params).build(); - } -} diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index ef30cff5..3dd30f26 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -38,6 +38,7 @@ import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.junit.Test; import org.scijava.code.api.CompletionResult; +import org.scijava.code.api.SignatureHelp.Fit; /** * Tests that {@link SciJavaCompletionProvider} faithfully bridges neutral @@ -205,61 +206,240 @@ public void testParameterTooltipListsOtherOverloads() { } @Test - public void testParameterTooltipRanksOverloadsByArgumentTypes() { + public void testParameterTooltipShowsSignatureHelp() { final RSyntaxTextArea area = new RSyntaxTextArea(); area.setText("a"); area.setCaretPosition(1); - final org.scijava.code.api.TypeResolver types = expr -> { - switch (expr) { - case "1.5": return "double"; - case "1": return "long"; - default: return null; - } - }; + final org.scijava.code.api.Completion ints = method("a", "int", "int"); + final org.scijava.code.api.Completion strings = method("a", + "java.lang.String"); + final org.scijava.code.api.Completion floats = method("a", "float", + "float"); + final org.scijava.code.api.Completion longs = method("a", "long", "long"); + final org.scijava.code.api.CompletionRequest[] asked = { null }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - request -> new CompletionResult(Arrays.asList( // - method("a", "int", "int"), // - method("a", "java.lang.String"), // - method("a", "float", "float"), // - method("a", "long", "long")), 0, null, types), - null); + new org.scijava.code.api.CodeCompleter() { + + @Override + public CompletionResult complete( + final org.scijava.code.api.CompletionRequest request) + { + return new CompletionResult(Arrays.asList(ints, strings, floats, + longs), 0); + } + + @Override + public org.scijava.code.api.SignatureHelp signatureHelp( + final org.scijava.code.api.CompletionRequest request) + { + asked[0] = request; + // The completer rates and orders the signatures: here, as for a + // float typed as the first argument. + return new org.scijava.code.api.SignatureHelp(Arrays.asList( + signature(floats, Fit.MATCH), // + signature(ints, Fit.MISMATCH), // + signature(longs, Fit.CONVERSION), // + signature(strings, Fit.MISMATCH)), 1, 1); + } + }, null); final List<Completion> completions = provider.getCompletionsImpl(area); final org.fife.ui.autocomplete.ParameterizedCompletion intOverload = (org.fife.ui.autocomplete.ParameterizedCompletion) completions.get(0); - final org.fife.ui.autocomplete.ParameterizedCompletion floatOverload = - (org.fife.ui.autocomplete.ParameterizedCompletion) completions.get(2); - final String gray = "<font color=\"gray\">"; + final String gray = "<font color=\"" + SciJavaCompletionProvider + .dimColor() + "\">"; - // Accepted a(int, int), typed a float, and tabbed onward: the float - // overload matches, and the others are struck out. + // Accepted a(int, int), typed a float, and tabbed onward: the completer's + // order, styled by fit, without the accepted overload. area.setText("a(1.5, p1)"); area.setCaretPosition(7); assertEquals("<hr>float p0, float p1" + // - "<hr>" + gray + "<s>String p0</s></font>" + // - "<hr>" + gray + "<s>long p0, long p1</s></font>", // + "<hr>" + gray + "long p0, long p1</font>" + // + "<hr>" + gray + "<s>String p0</s></font>", // intOverload.getParam(1).getDescription()); + // The completer was asked about the caret. + assertEquals(7, asked[0].offset()); + assertEquals("a(1.5, p1)", asked[0].text()); + } + + @Test + public void testParameterTooltipIgnoresOtherCalls() { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("a"); + area.setCaretPosition(1); + final org.scijava.code.api.Completion ints = method("a", "int", "int"); + final org.scijava.code.api.Completion floats = method("a", "float", + "float"); + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + new org.scijava.code.api.CodeCompleter() { + + @Override + public CompletionResult complete( + final org.scijava.code.api.CompletionRequest request) + { + return new CompletionResult(Arrays.asList(ints, floats), 0); + } - // Accepted a(float, float) and typed an int: the integral overloads - // match, so they come first. - area.setText("a(1, p1)"); + @Override + public org.scijava.code.api.SignatureHelp signatureHelp( + final org.scijava.code.api.CompletionRequest request) + { + // The caret is in a call nested in a's arguments: b's signatures. + return new org.scijava.code.api.SignatureHelp(Arrays.asList( + signature(method("b", "double"), Fit.MATCH)), 0, 2); + } + }, null); + final org.fife.ui.autocomplete.ParameterizedCompletion intOverload = + (org.fife.ui.autocomplete.ParameterizedCompletion) provider + .getCompletionsImpl(area).get(0); + area.setText("a(b(|), p1)"); + area.setCaretPosition(4); + // Not b's: a's other overloads, unranked. + assertEquals("<hr>float p0, float p1", intOverload.getParam(0) + .getDescription()); + } + + @Test + public void testLazyDescriptions() { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("np.ze"); area.setCaretPosition(5); - assertEquals("<hr>int p0, int p1<hr>long p0, long p1" + // - "<hr>" + gray + "<s>String p0</s></font>", // - floatOverload.getParam(1).getDescription()); + final java.util.concurrent.atomic.AtomicInteger asked = + new java.util.concurrent.atomic.AtomicInteger(); + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + request -> new CompletionResult(Arrays.asList( // + org.scijava.code.api.Completion.builder("zeros").kind( + org.scijava.code.api.Completion.Kind.METHOD).parameters(Arrays + .asList(new org.scijava.code.api.Completion.Parameter("shape", + "int"))).lazyDescription(() -> { + asked.incrementAndGet(); + return "Return a new array of zeros."; + }).build(), // + org.scijava.code.api.Completion.builder("zeta").lazyDescription( + () -> { + asked.incrementAndGet(); + return "The zeta function."; + }).build()), 3), null); + final List<Completion> completions = provider.getCompletionsImpl(area); + // Nothing is described until RSTA shows a completion's description... + assertEquals(0, asked.get()); + // ...and then it is shown, for functions as for everything else. + org.junit.Assert.assertTrue(completions.get(0).getSummary().contains( + "Return a new array of zeros.")); + assertEquals("The zeta function.", completions.get(1).getSummary()); + assertEquals(2, asked.get()); + } - // Accepted a(int, int) and typed an int: the float overload would need a - // conversion, so it comes after the long one, greyed. - assertEquals("<hr>long p0, long p1" + // - "<hr>" + gray + "float p0, float p1</font>" + // - "<hr>" + gray + "<s>String p0</s></font>", // - intOverload.getParam(1).getDescription()); + @Test + public void testResultUpdates() throws Exception { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("np.z"); + area.setCaretPosition(4); + final java.util.concurrent.CompletableFuture<CompletionResult> later = + new java.util.concurrent.CompletableFuture<>(); + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + request -> CompletionResult.EMPTY.withUpdate(later), null); + final Boolean[] notified = { null }; + provider.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); + + // At first, nothing; then the better result arrives. + assertEquals(0, provider.getCompletionsImpl(area).size()); + later.complete(new CompletionResult(Arrays.asList( + org.scijava.code.api.Completion.of("zeros")), 3)); + javax.swing.SwingUtilities.invokeAndWait(() -> {}); + assertEquals(Boolean.TRUE, notified[0]); + final List<Completion> completions = provider.getCompletionsImpl(area); + assertEquals(1, completions.size()); + assertEquals("zeros", completions.get(0).getReplacementText()); + + // An update for a request the user has typed past is ignored. + final java.util.concurrent.CompletableFuture<CompletionResult> stale = + new java.util.concurrent.CompletableFuture<>(); + final SciJavaCompletionProvider provider2 = new SciJavaCompletionProvider( + request -> CompletionResult.EMPTY.withUpdate(stale), null); + notified[0] = null; + provider2.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); + provider2.getCompletionsImpl(area); + area.setText("np.ze"); + area.setCaretPosition(5); + stale.complete(new CompletionResult(Arrays.asList( + org.scijava.code.api.Completion.of("zeros")), 3)); + javax.swing.SwingUtilities.invokeAndWait(() -> {}); + org.junit.Assert.assertNull(notified[0]); + } - // Nothing typed yet: the overloads keep their order, unstyled. - area.setText("a(p0, p1)"); + @Test + public void testPrepareOnInstall() { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("import numpy\n"); + final org.scijava.code.api.CompletionRequest[] prepared = { null }; + final java.io.File file = new java.io.File("scripts", "blur.py"); + final CodeCompleterLanguageSupport support = + new CodeCompleterLanguageSupport(new org.scijava.code.api.CodeCompleter() + { + + @Override + public CompletionResult complete( + final org.scijava.code.api.CompletionRequest request) + { + return CompletionResult.EMPTY; + } + + @Override + public void prepare( + final org.scijava.code.api.CompletionRequest request) + { + prepared[0] = request; + } + }, null, null, () -> file); + support.install(area); + assertEquals("import numpy\n", prepared[0].text()); + assertEquals(file.getPath(), prepared[0].path()); + support.uninstall(area); + } + + @Test + public void testDimColorFollowsLookAndFeel() { + final Object old = javax.swing.UIManager.get("Label.disabledForeground"); + try { + javax.swing.UIManager.put("Label.disabledForeground", + new java.awt.Color(0x12, 0x34, 0xab)); + assertEquals("#1234ab", SciJavaCompletionProvider.dimColor()); + } + finally { + javax.swing.UIManager.put("Label.disabledForeground", old); + } + } + + @Test + public void testRequestCarriesScriptPath() { + final RSyntaxTextArea area = new RSyntaxTextArea(); + area.setText("x"); + area.setCaretPosition(1); + final String[] path = { "unset" }; + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + request -> { + path[0] = request.path(); + return CompletionResult.EMPTY; + }, null); + + // Without a file supplier (e.g. in an interpreter): no path. + provider.getCompletionsImpl(area); + org.junit.Assert.assertNull(path[0]); + + // With one: the script's path, even if it changes (e.g. Save As). + final java.io.File[] file = { new java.io.File("scripts", "a.py") }; + provider.setFile(() -> file[0]); + area.setText("xy"); area.setCaretPosition(2); - assertEquals("<hr>String p0<hr>float p0, float p1<hr>long p0, long p1", // - intOverload.getParam(0).getDescription()); + provider.getCompletionsImpl(area); + assertEquals(file[0].getPath(), path[0]); + } + + private static org.scijava.code.api.SignatureHelp.Signature signature( + final org.scijava.code.api.Completion callable, final Fit fit) + { + return new org.scijava.code.api.SignatureHelp.Signature(callable, fit); } private static org.scijava.code.api.Completion method(final String name, From 19b5db13ded4796b18ab3f13df63fb9b3e540f86 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Tue, 6 Oct 2026 13:09:22 -0500 Subject: [PATCH 08/21] Tell completers when a script's editor closes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../java/org/scijava/ui/swing/script/EditorPane.java | 11 +++++++++++ .../java/org/scijava/ui/swing/script/TextEditor.java | 2 ++ .../autocompletion/CodeCompleterLanguageSupport.java | 10 ++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 96a8d606..8c63d3f9 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -733,6 +733,17 @@ protected void setLanguage(final ScriptLanguage language) { * @see #setLanguageByFileName(String) * @see #setLanguage(ScriptLanguage) */ + /** + * Uninstalls the language support (e.g. code completion), as the editor + * is closed. + */ + void releaseLanguageSupport() { + if (installedSupport != null) { + installedSupport.uninstall(this); + installedSupport = null; + } + } + protected void setLanguage(final ScriptLanguage language, final boolean addHeader) { diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 9a347f1e..f6ab0e64 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -3152,6 +3152,8 @@ private void switchTabRelative(final int delta) { private void removeTab(final int index) { final int menuItemIndex = index + tabsMenuTabsStart; try { + // NB: Let the completer release what it keeps for the script. + getEditorPane(index).releaseLanguageSupport(); tabbed.remove(index); tabsMenuItems.remove(tabsMenu.getItem(menuItemIndex)); tabsMenu.remove(menuItemIndex); diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java index 513a3595..32ac84a0 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java @@ -119,5 +119,15 @@ public void install(final RSyntaxTextArea textArea) { @Override public void uninstall(final RSyntaxTextArea textArea) { uninstallImpl(textArea); + // Let the completer release what it keeps for this script. + try { + final File f = file == null ? null : file.get(); + final String text = textArea.getText(); + completer.closed(new CompletionRequest(text, text.length(), language, + null, null, f == null ? null : f.getPath())); + } + catch (final Exception | LinkageError exc) { + if (log != null) log.debug("Completer release failed", exc); + } } } From fe3ed2a6806b7c807de09ce79ce1309ad0482cec Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Tue, 6 Oct 2026 13:19:09 -0500 Subject: [PATCH 09/21] Show hover docs, problems and signatures as typed For languages whose completer offers them: what is known about the code under the mouse as tooltip (HoverToolTipSupplier); problems as squiggles (DiagnosticsParser, an RSTA parser asking in the background); and, once a call's parenthesis is typed, its signatures in a popup following the caret through the call (SignaturePopup). Hover and problems can be turned off in the Options menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../scijava/ui/swing/script/EditorPane.java | 44 ++- .../scijava/ui/swing/script/TextEditor.java | 19 ++ .../CodeCompleterLanguageSupport.java | 55 ++++ .../autocompletion/DiagnosticsParser.java | 152 ++++++++++ .../autocompletion/HoverToolTipSupplier.java | 143 ++++++++++ .../script/autocompletion/SignaturePopup.java | 259 ++++++++++++++++++ .../CodeIntelligenceUITest.java | 169 ++++++++++++ 7 files changed, 838 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java create mode 100644 src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 8c63d3f9..c02fa547 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -119,6 +119,8 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { private boolean undoInProgress; private boolean redoInProgress; private boolean autoCompletionEnabled; + private boolean hoverEnabled = true; + private boolean diagnosticsEnabled = true; private boolean autoCompletionJavaFallback; private boolean autoCompletionWithoutKey; private String supportStatus; @@ -815,9 +817,13 @@ protected void setLanguage(final ScriptLanguage language, if (support == null && codeCompletionService.getCompleterPlugin(currentLanguage) != null) { - support = new CodeCompleterLanguageSupport( - codeCompletionService.getCompleterPlugin(currentLanguage), - currentLanguage, log, this::getFile); + final CodeCompleterLanguageSupport completerSupport = + new CodeCompleterLanguageSupport(codeCompletionService + .getCompleterPlugin(currentLanguage), currentLanguage, log, + this::getFile); + completerSupport.setHoverEnabled(hoverEnabled); + completerSupport.setDiagnosticsEnabled(diagnosticsEnabled); + support = completerSupport; } // Tier 3: RSTA's own built-in support for the syntax style. @@ -889,6 +895,32 @@ public boolean isAutoCompletionEnabled() { return autoCompletionEnabled; } + /** + * Toggles whether what is known about the code under the mouse (e.g. a + * function's documentation) is shown, for languages whose completer knows. + */ + public void setHoverEnabled(final boolean enabled) { + hoverEnabled = enabled; + if (currentLanguage != null) setLanguage(currentLanguage); + } + + public boolean isHoverEnabled() { + return hoverEnabled; + } + + /** + * Toggles whether problems in the code (e.g. syntax errors) are shown as + * the code is edited, for languages whose completer finds them. + */ + public void setDiagnosticsEnabled(final boolean enabled) { + diagnosticsEnabled = enabled; + if (currentLanguage != null) setLanguage(currentLanguage); + } + + public boolean isDiagnosticsEnabled() { + return diagnosticsEnabled; + } + public boolean isAutoCompletionKeyless() { return autoCompletionWithoutKey; } @@ -1096,6 +1128,8 @@ public void convertSpacesToTabs() { public static final String AUTOCOMPLETE_PREFS = "script.editor.AC"; public static final String AUTOCOMPLETE_KEYLESS_PREFS = "script.editor.ACNoKey"; public static final String AUTOCOMPLETE_FALLBACK_PREFS = "script.editor.ACFallback"; + public static final String HOVER_PREFS = "script.editor.Hover"; + public static final String DIAGNOSTICS_PREFS = "script.editor.Diagnostics"; public static final String MARK_OCCURRENCES_PREFS = "script.editor.Occurrences"; public static final String VIM_PREFS = "script.editor.Vim"; public static final String FOLDERS_PREFS = "script.editor.folders"; @@ -1127,6 +1161,8 @@ public void loadPreferences() { setKeylessAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_KEYLESS_PREFS, true)); // true for backwards compatibility with ImageJ macro setFallbackAutoCompletion(prefService.getBoolean(getClass(), AUTOCOMPLETE_FALLBACK_PREFS, false)); setMarkOccurrences(prefService.getBoolean(getClass(), MARK_OCCURRENCES_PREFS, false)); + setHoverEnabled(prefService.getBoolean(getClass(), HOVER_PREFS, true)); + setDiagnosticsEnabled(prefService.getBoolean(getClass(), DIAGNOSTICS_PREFS, true)); setMarginLineEnabled(prefService.getBoolean(getClass(), MARGIN_VISIBLE_PREFS, false)); setVimEnabled(prefService.getBoolean(getClass(), VIM_PREFS, false)); applyTheme(themeName()); @@ -1183,6 +1219,8 @@ public void savePreferences(final String top_folders, final String theme) { prefService.put(getClass(), AUTOCOMPLETE_PREFS, isAutoCompletionEnabled()); prefService.put(getClass(), AUTOCOMPLETE_KEYLESS_PREFS, isAutoCompletionKeyless()); prefService.put(getClass(), AUTOCOMPLETE_FALLBACK_PREFS, isAutoCompletionFallbackEnabled()); + prefService.put(getClass(), HOVER_PREFS, isHoverEnabled()); + prefService.put(getClass(), DIAGNOSTICS_PREFS, isDiagnosticsEnabled()); prefService.put(getClass(), MARGIN_VISIBLE_PREFS, isMarginLineEnabled()); prefService.put(getClass(), MARK_OCCURRENCES_PREFS, getMarkOccurrences()); prefService.put(getClass(), VIM_PREFS, isVimEnabled()); diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index f6ab0e64..8486a80a 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -230,6 +230,7 @@ public class TextEditor extends JFrame implements ActionListener, private FindAndReplaceDialog findDialog; private JCheckBoxMenuItem autoSave, wrapLines, tabsEmulated, autoImport, autocompletion, fallbackAutocompletion, keylessAutocompletion, + hoverDocs, diagnostics, markOccurences, paintTabs, whiteSpace, marginLine, lockPane, vimMode; private ButtonGroup themeRadioGroup; private JTextArea errorScreen = new ConsoleArea(ConsoleArea.Content.ERRORS); @@ -647,6 +648,8 @@ public TextEditor(final Context context) { options.add(autocompletion); options.add(keylessAutocompletion); options.add(fallbackAutocompletion); + options.add(hoverDocs); + options.add(diagnostics); GuiUtils.addMenubarSeparator(options, "Key Bindings:"); options.add(vimMode); @@ -1133,6 +1136,20 @@ private void initializeDynamicMenuComponents() { fallbackAutocompletion.setToolTipText("<HTML>If selected, Java completions will be used when scripting<br>" + "a language for which auto-completions are not available"); fallbackAutocompletion.addItemListener(e -> setFallbackAutoCompletion(fallbackAutocompletion.getState())); + hoverDocs = new JCheckBoxMenuItem("Show Documentation on Hover", true); + hoverDocs.setToolTipText("<HTML>If selected, resting the mouse on code shows what is known<br>" + + "about it, e.g. a function's documentation. NB: Not all languages support this feature"); + hoverDocs.addItemListener(e -> { + for (int i = 0; i < tabbed.getTabCount(); i++) + getEditorPane(i).setHoverEnabled(hoverDocs.getState()); + }); + diagnostics = new JCheckBoxMenuItem("Show Problems While Editing", true); + diagnostics.setToolTipText("<HTML>If selected, problems in the code (e.g. syntax errors) are<br>" + + "underlined as you edit. NB: Not all languages support this feature"); + diagnostics.addItemListener(e -> { + for (int i = 0; i < tabbed.getTabCount(); i++) + getEditorPane(i).setDiagnosticsEnabled(diagnostics.getState()); + }); vimMode = new JCheckBoxMenuItem("Vim Mode", false); vimMode.setToolTipText("<HTML>Edit with vim-style modal key bindings.<br>" + "Press Esc for normal mode; :w saves, :q closes the tab"); @@ -2544,6 +2561,8 @@ else if (tabSize == Integer.parseInt(item.getText())) { autocompletion.setState(pane.isAutoCompletionEnabled()); fallbackAutocompletion.setState(pane.isAutoCompletionFallbackEnabled()); keylessAutocompletion.setState(pane.isAutoCompletionKeyless()); + hoverDocs.setState(pane.isHoverEnabled()); + diagnostics.setState(pane.isDiagnosticsEnabled()); sourceTreePanel.rebuildSourceTree(pane); } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java index 32ac84a0..c33bf202 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java @@ -30,6 +30,8 @@ package org.scijava.ui.swing.script.autocompletion; import java.io.File; +import java.util.Map; +import java.util.WeakHashMap; import java.util.function.Supplier; import org.fife.rsta.ac.AbstractLanguageSupport; @@ -59,6 +61,12 @@ public class CodeCompleterLanguageSupport extends AbstractLanguageSupport { private final ScriptLanguage language; private final Logger log; private final Supplier<File> file; + private boolean hoverEnabled = true; + private boolean diagnosticsEnabled = true; + + /** What was installed in each text area, to uninstall. */ + private final Map<RSyntaxTextArea, Installed> installed = + new WeakHashMap<>(); public CodeCompleterLanguageSupport(final CodeCompleter completer, final ScriptLanguage language) @@ -89,6 +97,22 @@ public CodeCompleterLanguageSupport(final CodeCompleter completer, setShowDescWindow(true); } + /** + * Sets whether to show what the completer knows about the code under the + * mouse (see {@link CodeCompleter#hover}). Applies to later installs. + */ + public void setHoverEnabled(final boolean enabled) { + hoverEnabled = enabled; + } + + /** + * Sets whether to show the problems the completer finds (see + * {@link CodeCompleter#diagnose}). Applies to later installs. + */ + public void setDiagnosticsEnabled(final boolean enabled) { + diagnosticsEnabled = enabled; + } + @Override public void install(final RSyntaxTextArea textArea) { final SciJavaCompletionProvider provider = @@ -103,6 +127,23 @@ public void install(final RSyntaxTextArea textArea) { ac.install(textArea); installImpl(textArea, ac); + // Documentation on hover, problems as squiggles, signatures as typed. + final Installed extras = new Installed(); + if (hoverEnabled) { + textArea.setToolTipSupplier(new HoverToolTipSupplier(completer, + language, file, log)); + } + if (diagnosticsEnabled) { + extras.parser = new DiagnosticsParser(textArea, completer, language, + file, log); + textArea.addParser(extras.parser); + } + if (isParameterAssistanceEnabled()) { + extras.popup = new SignaturePopup(textArea, provider::signatureHelp); + extras.popup.install(); + } + installed.put(textArea, extras); + // Let the completer get ready for this script, e.g. warm up caches. try { final File f = file == null ? null : file.get(); @@ -119,6 +160,14 @@ public void install(final RSyntaxTextArea textArea) { @Override public void uninstall(final RSyntaxTextArea textArea) { uninstallImpl(textArea); + final Installed extras = installed.remove(textArea); + if (extras != null) { + if (textArea.getToolTipSupplier() instanceof HoverToolTipSupplier) { + textArea.setToolTipSupplier(null); + } + if (extras.parser != null) textArea.removeParser(extras.parser); + if (extras.popup != null) extras.popup.uninstall(); + } // Let the completer release what it keeps for this script. try { final File f = file == null ? null : file.get(); @@ -130,4 +179,10 @@ public void uninstall(final RSyntaxTextArea textArea) { if (log != null) log.debug("Completer release failed", exc); } } + + private static final class Installed { + + private DiagnosticsParser parser; + private SignaturePopup popup; + } } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java new file mode 100644 index 00000000..ea8ca6ee --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java @@ -0,0 +1,152 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.io.File; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import javax.swing.SwingUtilities; +import javax.swing.text.BadLocationException; +import javax.swing.text.Element; + +import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.parser.AbstractParser; +import org.fife.ui.rsyntaxtextarea.parser.DefaultParseResult; +import org.fife.ui.rsyntaxtextarea.parser.DefaultParserNotice; +import org.fife.ui.rsyntaxtextarea.parser.ParseResult; +import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; +import org.scijava.code.api.CodeCompleter; +import org.scijava.code.api.CompletionRequest; +import org.scijava.code.api.Diagnostic; +import org.scijava.log.Logger; +import org.scijava.script.ScriptLanguage; + +/** + * Shows the problems the completer finds in the script (see + * {@link CodeCompleter#diagnose}), e.g. syntax errors, as squiggles, with the + * problem as tooltip. + * <p> + * RSyntaxTextArea parses a while after each edit, on the event dispatch + * thread; this parser asks the completer then, and shows its answer once it + * arrives (parsing anew). Until then, the problems found before stay. + * </p> + * + * @author Gabriel Selzer + */ +public class DiagnosticsParser extends AbstractParser { + + private final RSyntaxTextArea textArea; + private final CodeCompleter completer; + private final ScriptLanguage language; + private final Supplier<File> file; + private final Logger log; + + /** The text asked about last, if not answered yet. */ + private String pending; + + /** The text answered last, and the problems found in it. */ + private String answered; + private List<Diagnostic> problems = Collections.emptyList(); + + public DiagnosticsParser(final RSyntaxTextArea textArea, + final CodeCompleter completer, final ScriptLanguage language, + final Supplier<File> file, final Logger log) + { + this.textArea = textArea; + this.completer = completer; + this.language = language; + this.file = file; + this.log = log; + } + + @Override + public ParseResult parse(final RSyntaxDocument doc, final String style) { + final DefaultParseResult result = new DefaultParseResult(this); + final String text; + try { + text = doc.getText(0, doc.getLength()); + } + catch (final BadLocationException exc) { + return result; + } + if (!text.equals(answered) && !text.equals(pending)) ask(text); + // NB: Until the answer arrives, show the problems found before (where + // they still fit). + final Element root = doc.getDefaultRootElement(); + for (final Diagnostic d : problems) { + if (d.start() >= text.length() && text.length() > 0) continue; + final int start = Math.max(0, Math.min(d.start(), text.length() - 1)); + final int length = Math.max(1, Math.min(d.end(), text.length()) - + start); + final DefaultParserNotice notice = new DefaultParserNotice(this, d + .message(), root.getElementIndex(start), start, length); + notice.setLevel(level(d.severity())); + result.addNotice(notice); + } + return result; + } + + private void ask(final String text) { + pending = text; + CompletableFuture<List<Diagnostic>> answer; + try { + final File f = file == null ? null : file.get(); + answer = completer.diagnose(new CompletionRequest(text, text.length(), + language, null, null, f == null ? null : f.getPath())); + } + catch (final Exception | LinkageError exc) { + // NB: Never let a misbehaving completer break the editor. + if (log != null) log.debug("Diagnosis failed", exc); + answer = CompletableFuture.completedFuture(Collections.emptyList()); + } + answer.whenComplete((found, error) -> SwingUtilities.invokeLater(() -> { + if (!text.equals(pending)) return; // NB: Superseded. + pending = null; + answered = text; + problems = found == null ? Collections.emptyList() : found; + textArea.forceReparsing(this); + })); + } + + private static ParserNotice.Level level(final Diagnostic.Severity s) { + switch (s) { + case ERROR: + return ParserNotice.Level.ERROR; + case WARNING: + return ParserNotice.Level.WARNING; + default: + return ParserNotice.Level.INFO; + } + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java new file mode 100644 index 00000000..ece4c884 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java @@ -0,0 +1,143 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.awt.event.MouseEvent; +import java.io.File; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.fife.ui.rtextarea.RTextArea; +import org.fife.ui.rtextarea.ToolTipSupplier; +import org.scijava.code.api.CodeCompleter; +import org.scijava.code.api.CompletionRequest; +import org.scijava.code.api.Hover; +import org.scijava.log.Logger; +import org.scijava.script.ScriptLanguage; + +/** + * Shows what the completer knows about the code under the mouse (see + * {@link CodeCompleter#hover}), e.g. a function's documentation, as a tooltip. + * <p> + * RSyntaxTextArea asks for tooltips as the mouse moves, on the event dispatch + * thread: so the completer's answer for a word is asked once, and awaited only + * briefly; until it arrives, there is no tooltip. + * </p> + * + * @author Gabriel Selzer + */ +public class HoverToolTipSupplier implements ToolTipSupplier { + + /** How long to wait for an answer, in milliseconds. */ + private static final long BUDGET = 100; + + /** How much of an answer to show, at most, in lines. */ + private static final int MAX_LINES = 25; + + private final CodeCompleter completer; + private final ScriptLanguage language; + private final Supplier<File> file; + private final Logger log; + + /** The last question (text and word), and its answer. */ + private String askedText; + private int askedWord = -1; + private CompletableFuture<Hover> answer; + + public HoverToolTipSupplier(final CodeCompleter completer, + final ScriptLanguage language, final Supplier<File> file, + final Logger log) + { + this.completer = completer; + this.language = language; + this.file = file; + this.log = log; + } + + @Override + public String getToolTipText(final RTextArea textArea, final MouseEvent e) { + return toolTip(textArea.getText(), textArea.viewToModel2D(e.getPoint())); + } + + /** Gets the tooltip for an offset of a text, or null if none. */ + String toolTip(final String text, final int offset) { + if (offset < 0 || offset >= text.length() || !Character + .isJavaIdentifierPart(text.charAt(offset))) return null; + int word = offset; + while (word > 0 && Character.isJavaIdentifierPart(text.charAt(word - 1))) { + word--; + } + if (word != askedWord || !text.equals(askedText)) { + askedText = text; + askedWord = word; + answer = ask(text, offset); + } + try { + final Hover h = answer.get(BUDGET, TimeUnit.MILLISECONDS); + return h == null || h.isEmpty() ? null : "<html>" + truncate(h + .text()) + "</html>"; + } + catch (final Exception exc) { + return null; // NB: Not yet; or failed. + } + } + + private CompletableFuture<Hover> ask(final String text, final int offset) { + try { + final File f = file == null ? null : file.get(); + final Hover h = completer.hover(new CompletionRequest(text, offset, + language, null, null, f == null ? null : f.getPath())); + if (h == null) return CompletableFuture.completedFuture(Hover.NONE); + if (!h.isEmpty() || h.update() == null) { + return CompletableFuture.completedFuture(h); + } + return h.update(); + } + catch (final Exception | LinkageError exc) { + // NB: Never let a misbehaving completer break the editor. + if (log != null) log.debug("Hover failed", exc); + return CompletableFuture.completedFuture(Hover.NONE); + } + } + + /** Keeps the first lines of long answers (e.g. docstrings). */ + static String truncate(final String html) { + int end = -1; + for (int i = 0; i < MAX_LINES; i++) { + end = html.indexOf('\n', end + 1); + if (end < 0) return html; + } + // NB: Close an open <pre>, lest the rest of the tooltip be preformatted. + final String head = html.substring(0, end); + final boolean pre = head.lastIndexOf("<pre>") > head.lastIndexOf("</pre>"); + return head + "\n…" + (pre ? "</pre>" : ""); + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java new file mode 100644 index 00000000..58c1a9c5 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java @@ -0,0 +1,259 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.awt.Color; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.geom.Rectangle2D; +import java.util.List; +import java.util.function.Function; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JWindow; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.UIManager; +import javax.swing.event.CaretListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.JTextComponent; + +import org.scijava.code.api.Completion; +import org.scijava.code.api.SignatureHelp; + +/** + * Shows the signatures of the call being typed (see + * {@link org.scijava.code.api.CodeCompleter#signatureHelp}), e.g. all + * overloads of {@code max} once {@code Math.max(} is typed: best fits first, + * the parameter being typed in bold. It follows the caret through the call, + * and closes once the caret leaves it, or on Escape. + * <p> + * (Accepting a completion shows RSyntaxTextArea's own parameter assistance + * instead; this popup is for calls typed by hand.) + * </p> + * + * @author Gabriel Selzer + */ +public class SignaturePopup { + + /** How long to wait before asking again for slow help, in milliseconds. */ + private static final int RETRY_DELAY = 400; + + private final JTextComponent textArea; + private final Function<JTextComponent, SignatureHelp> help; + private final KeyListener keys; + private final CaretListener caret; + private final FocusListener focus; + private final Timer retry; + private JWindow window; + private JLabel label; + + /** + * @param textArea Where calls are typed. + * @param help Gets the signature help at the caret. + */ + public SignaturePopup(final JTextComponent textArea, + final Function<JTextComponent, SignatureHelp> help) + { + this.textArea = textArea; + this.help = help; + retry = new Timer(RETRY_DELAY, e -> refresh(false)); + retry.setRepeats(false); + keys = new KeyAdapter() { + + @Override + public void keyTyped(final KeyEvent e) { + final char c = e.getKeyChar(); + // NB: After the character is inserted. + if (c == '(') SwingUtilities.invokeLater(() -> refresh(true)); + } + + @Override + public void keyPressed(final KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ESCAPE && isShowing()) hide(); + } + }; + caret = e -> { + if (isShowing()) SwingUtilities.invokeLater(() -> refresh(false)); + }; + focus = new FocusAdapter() { + + @Override + public void focusLost(final FocusEvent e) { + hide(); + } + }; + } + + /** Starts following the typing. */ + public void install() { + textArea.addKeyListener(keys); + textArea.addCaretListener(caret); + textArea.addFocusListener(focus); + } + + /** Stops following the typing. */ + public void uninstall() { + textArea.removeKeyListener(keys); + textArea.removeCaretListener(caret); + textArea.removeFocusListener(focus); + retry.stop(); + hide(); + if (window != null) window.dispose(); + window = null; + } + + public boolean isShowing() { + return window != null && window.isVisible(); + } + + public void hide() { + if (window != null) window.setVisible(false); + } + + /** + * Shows the help at the caret, or hides the popup if there is none. + * + * @param opening Whether a call was just opened (so that slow help is + * asked for again, shortly). + */ + void refresh(final boolean opening) { + if (!textArea.isShowing()) return; + final SignatureHelp h = help.apply(textArea); + if (h == null || h.isEmpty()) { + hide(); + if (opening) retry.restart(); + return; + } + show(html(h), h.callStart()); + } + + /** Renders signatures as HTML, one per line, best fits first. */ + static String html(final SignatureHelp help) { + final String dim = "<font color=\"" + SciJavaCompletionProvider + .dimColor() + "\">"; + final StringBuilder sb = new StringBuilder("<html>"); + boolean first = true; + for (final SignatureHelp.Signature s : help.signatures()) { + if (!first) sb.append("<br>"); + first = false; + final String sig = signature(s.callable(), help.activeParameter()); + switch (s.fit()) { + case CONVERSION: + sb.append(dim).append(sig).append("</font>"); + break; + case MISMATCH: + sb.append(dim).append("<s>").append(sig).append("</s></font>"); + break; + default: + sb.append(sig); + } + } + return sb.append("</html>").toString(); + } + + /** Renders one signature, e.g. {@code max(double a, <b>double b</b>)}. */ + private static String signature(final Completion c, final int active) { + String name = c.insertionText(); + if (name.endsWith("()")) name = name.substring(0, name.length() - 2); + final StringBuilder sb = new StringBuilder(escape(name)).append('('); + final List<Completion.Parameter> params = c.parameters(); + for (int i = 0; i < params.size(); i++) { + if (i > 0) sb.append(", "); + final Completion.Parameter p = params.get(i); + final String type = p.type(); + final StringBuilder param = new StringBuilder(); + if (type != null) param.append(type.substring(type.lastIndexOf('.') + + 1)); + if (p.name() != null) { + if (type != null) param.append(' '); + param.append(p.name()); + } + final String text = escape(param.toString()); + sb.append(i == active ? "<b>" + text + "</b>" : text); + } + sb.append(')'); + if (c.returnType() != null) sb.append(" → ").append(escape(c + .returnType())); + return sb.toString(); + } + + private void show(final String html, final int callStart) { + final Window owner = SwingUtilities.getWindowAncestor(textArea); + if (owner == null) return; + if (window == null || window.getOwner() != owner) { + if (window != null) window.dispose(); + window = new JWindow(owner); + window.setFocusableWindowState(false); + label = new JLabel(); + label.setOpaque(true); + final Color bg = UIManager.getColor("ToolTip.background"); + final Color fg = UIManager.getColor("ToolTip.foreground"); + if (bg != null) label.setBackground(bg); + if (fg != null) label.setForeground(fg); + label.setFont(textArea.getFont()); + label.setBorder(BorderFactory.createCompoundBorder(UIManager.getBorder( + "ToolTip.border") != null ? UIManager.getBorder("ToolTip.border") + : BorderFactory.createLineBorder(Color.GRAY), BorderFactory + .createEmptyBorder(2, 4, 2, 4))); + window.getContentPane().add(label); + } + label.setText(html); + window.pack(); + try { + // Above the call's line, starting at its opening parenthesis. + final Rectangle2D r = textArea.modelToView2D(Math.max(0, Math.min( + callStart, textArea.getDocument().getLength()))); + final Point p = new Point((int) r.getX(), (int) r.getY()); + SwingUtilities.convertPointToScreen(p, textArea); + int y = p.y - window.getHeight() - 2; + if (y < 0) y = p.y + (int) r.getHeight() + 2; + window.setLocation(p.x, y); + } + catch (final BadLocationException exc) { + return; + } + final Rectangle visible = textArea.getVisibleRect(); + if (visible.isEmpty()) return; + window.setVisible(true); + } + + private static String escape(final String s) { + return s.replace("&", "&").replace("<", "<").replace(">", ">"); + } +} diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java new file mode 100644 index 00000000..938d958e --- /dev/null +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -0,0 +1,169 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.swing.SwingUtilities; + +import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.parser.ParseResult; +import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; +import org.junit.Test; +import org.scijava.code.api.CodeCompleter; +import org.scijava.code.api.Completion; +import org.scijava.code.api.CompletionRequest; +import org.scijava.code.api.CompletionResult; +import org.scijava.code.api.Diagnostic; +import org.scijava.code.api.Hover; +import org.scijava.code.api.SignatureHelp; +import org.scijava.code.api.SignatureHelp.Fit; + +/** + * Tests {@link HoverToolTipSupplier}, {@link DiagnosticsParser} and + * {@link SignaturePopup}, with a fake completer. + * + * @author Gabriel Selzer + */ +public class CodeIntelligenceUITest { + + @Test + public void testHover() { + final AtomicInteger asked = new AtomicInteger(); + final CodeCompleter completer = new FakeCompleter() { + + @Override + public Hover hover(final CompletionRequest request) { + asked.incrementAndGet(); + final String text = request.text(); + final int at = request.offset(); + return text.startsWith("dumps", at - 1) || text.startsWith("dumps", + at - 2) ? new Hover("<pre>Serialize.</pre>", 0, 5) : Hover.NONE; + } + }; + final HoverToolTipSupplier supplier = new HoverToolTipSupplier(completer, + null, null, null); + final String text = "x.dumps(1)"; + assertEquals("<html><pre>Serialize.</pre></html>", supplier.toolTip(text, + 3)); + // Elsewhere in the same word: the same answer, not asked again. + assertEquals("<html><pre>Serialize.</pre></html>", supplier.toolTip(text, + 4)); + assertEquals(1, asked.get()); + // Not on a word: nothing, without asking. + assertNull(supplier.toolTip(text, 7)); + assertNull(supplier.toolTip(text, -1)); + assertEquals(1, asked.get()); + } + + @Test + public void testTruncate() { + final StringBuilder long_ = new StringBuilder("<pre>"); + for (int i = 0; i < 40; i++) long_.append("line ").append(i).append('\n'); + final String cut = HoverToolTipSupplier.truncate(long_.append("</pre>") + .toString()); + assertTrue(cut.contains("line 24")); + assertTrue(!cut.contains("line 25")); + assertTrue(cut.endsWith("…</pre>")); + assertEquals("<pre>short</pre>", HoverToolTipSupplier.truncate( + "<pre>short</pre>")); + } + + @Test + public void testDiagnostics() throws Exception { + final CompletableFuture<List<Diagnostic>> found = new CompletableFuture<>(); + final CodeCompleter completer = new FakeCompleter() { + + @Override + public CompletableFuture<List<Diagnostic>> diagnose( + final CompletionRequest request) + { + return found; + } + }; + final RSyntaxTextArea[] area = { null }; + SwingUtilities.invokeAndWait(() -> area[0] = new RSyntaxTextArea( + "x = 1\ny = (1 +\n")); + final DiagnosticsParser parser = new DiagnosticsParser(area[0], completer, + null, null, null); + final RSyntaxDocument doc = (RSyntaxDocument) area[0].getDocument(); + // Asked; no answer yet: no problems. + assertTrue(parser.parse(doc, null).getNotices().isEmpty()); + // The answer: a problem on the second line. + found.complete(Collections.singletonList(new Diagnostic(10, 12, + Diagnostic.Severity.ERROR, "Unclosed (", "fake"))); + SwingUtilities.invokeAndWait(() -> {}); + final ParseResult result = parser.parse(doc, null); + assertEquals(1, result.getNotices().size()); + final ParserNotice notice = result.getNotices().get(0); + assertEquals("Unclosed (", notice.getMessage()); + assertEquals(1, notice.getLine()); + assertEquals(10, notice.getOffset()); + assertEquals(2, notice.getLength()); + assertEquals(ParserNotice.Level.ERROR, notice.getLevel()); + } + + @Test + public void testSignaturesHTML() { + final Completion max = Completion.builder("max").kind( + Completion.Kind.METHOD).parameters(Arrays.asList(new Completion.Parameter( + "a", "double"), new Completion.Parameter("b", "double"))).returnType( + "double").build(); + final Completion maxInt = Completion.builder("max").kind( + Completion.Kind.METHOD).parameters(Arrays.asList(new Completion.Parameter( + "a", "int"), new Completion.Parameter("b", "int"))).build(); + final String html = SignaturePopup.html(new SignatureHelp(Arrays.asList( + new SignatureHelp.Signature(max, Fit.MATCH), new SignatureHelp.Signature( + maxInt, Fit.MISMATCH)), 1, 8)); + // The parameter being typed in bold; mismatches struck out. + assertTrue(html, html.startsWith( + "<html>max(double a, <b>double b</b>) → double<br>")); + assertTrue(html, html.contains("<s>max(int a, <b>int b</b>)</s>")); + } + + /** A completer completing nothing. */ + private static class FakeCompleter implements CodeCompleter { + + @Override + public CompletionResult complete(final CompletionRequest request) { + return CompletionResult.EMPTY; + } + } +} From b842236fdeef62793cfcf0604ae316c24ffbd482 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Wed, 7 Oct 2026 10:07:16 -0500 Subject: [PATCH 10/21] Talk to language servers The editor is now an LSP client of a LanguageServerService's merged server per language: DocumentSync keeps the server's copy of the script up to date (opened on install, changed with each edit, closed on uninstall, reopened when saved under another name). SciJavaCompletionProvider turns LSP completions into RSTA's: snippets with label details become function completions (parameter assistance with types), overloads listed with their signature-help fits, documentation resolved when shown, slower completions (UpdatingCompletionList) replacing the first. Parameter choices are what the server completes at the parameter. Hover, problems (as published) and the signature popup use the server too. Until code-api goes, CodeCompleters can still be used directly, via code-lsp's adapter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- pom.xml | 5 + .../scijava/ui/swing/script/EditorPane.java | 24 +- .../CodeCompleterLanguageSupport.java | 188 ----- .../autocompletion/DiagnosticsParser.java | 106 +-- .../script/autocompletion/DocumentSync.java | 158 ++++ .../autocompletion/HoverToolTipSupplier.java | 70 +- .../LanguageServerLanguageSupport.java | 270 ++++++ .../SciJavaCompletionProvider.java | 768 ++++++++++-------- .../script/autocompletion/SignatureLabel.java | 144 ++++ .../script/autocompletion/SignaturePopup.java | 49 +- .../CodeIntelligenceUITest.java | 52 +- .../SciJavaCompletionProviderTest.java | 31 +- 12 files changed, 1168 insertions(+), 697 deletions(-) delete mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/SignatureLabel.java diff --git a/pom.xml b/pom.xml index e38bac88..4627aa59 100644 --- a/pom.xml +++ b/pom.xml @@ -166,6 +166,11 @@ <artifactId>scijava-code-api</artifactId> <version>${scijava-code-api.version}</version> </dependency> + <dependency> + <groupId>org.scijava</groupId> + <artifactId>scijava-code-lsp</artifactId> + <version>${scijava-code-api.version}</version> + </dependency> <dependency> <groupId>org.scijava</groupId> <artifactId>scijava-search</artifactId> diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index c02fa547..a900fc43 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -95,7 +95,8 @@ import org.scijava.script.ScriptService; import org.scijava.ui.swing.script.vim.VimHandler; import org.scijava.code.api.CodeCompletionService; -import org.scijava.ui.swing.script.autocompletion.CodeCompleterLanguageSupport; +import org.scijava.code.lsp.LanguageServerService; +import org.scijava.ui.swing.script.autocompletion.LanguageServerLanguageSupport; import org.scijava.util.FileUtils; /** @@ -139,6 +140,8 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { @Parameter private CodeCompletionService codeCompletionService; @Parameter + private LanguageServerService languageServerService; + @Parameter private ScriptService scriptService; @Parameter private ScriptHeaderService scriptHeaderService; @@ -813,17 +816,14 @@ protected void setLanguage(final ScriptLanguage language, // Tier 1: a heavy RSTA LanguageSupportPlugin (e.g. the Java parser). support = languageSupportService.getLanguageSupport(currentLanguage); - // Tier 2: a toolkit-agnostic CodeCompleterPlugin for this language. - if (support == null && - codeCompletionService.getCompleterPlugin(currentLanguage) != null) - { - final CodeCompleterLanguageSupport completerSupport = - new CodeCompleterLanguageSupport(codeCompletionService - .getCompleterPlugin(currentLanguage), currentLanguage, log, - this::getFile); - completerSupport.setHoverEnabled(hoverEnabled); - completerSupport.setDiagnosticsEnabled(diagnosticsEnabled); - support = completerSupport; + // Tier 2: language servers for this language. + if (support == null && languageServerService.supports(currentLanguage)) { + final LanguageServerLanguageSupport serverSupport = + new LanguageServerLanguageSupport(languageServerService.server( + currentLanguage), currentLanguage, log, this::getFile); + serverSupport.setHoverEnabled(hoverEnabled); + serverSupport.setDiagnosticsEnabled(diagnosticsEnabled); + support = serverSupport; } // Tier 3: RSTA's own built-in support for the syntax style. diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java deleted file mode 100644 index c33bf202..00000000 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/CodeCompleterLanguageSupport.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * #%L - * Script Editor and Interpreter for SciJava script languages. - * %% - * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; - -import java.io.File; -import java.util.Map; -import java.util.WeakHashMap; -import java.util.function.Supplier; - -import org.fife.rsta.ac.AbstractLanguageSupport; -import org.fife.ui.autocomplete.AutoCompletion; -import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; -import org.scijava.log.Logger; -import org.scijava.script.ScriptLanguage; -import org.scijava.code.api.CodeCompleter; -import org.scijava.code.api.CompletionRequest; - -/** - * An RSTA {@link org.fife.rsta.ac.LanguageSupport} that drives code completion - * from a toolkit-agnostic {@link CodeCompleter}, via - * {@link SciJavaCompletionProvider}. - * <p> - * This is the lightweight completion tier of the script editor: any language - * that contributes a {@link org.scijava.code.api.CodeCompleterPlugin} - * gets editor completion through this class, with no language-specific Swing or - * RSTA code required. - * </p> - * - * @author Curtis Rueden - */ -public class CodeCompleterLanguageSupport extends AbstractLanguageSupport { - - private final CodeCompleter completer; - private final ScriptLanguage language; - private final Logger log; - private final Supplier<File> file; - private boolean hoverEnabled = true; - private boolean diagnosticsEnabled = true; - - /** What was installed in each text area, to uninstall. */ - private final Map<RSyntaxTextArea, Installed> installed = - new WeakHashMap<>(); - - public CodeCompleterLanguageSupport(final CodeCompleter completer, - final ScriptLanguage language) - { - this(completer, language, null); - } - - public CodeCompleterLanguageSupport(final CodeCompleter completer, - final ScriptLanguage language, final Logger log) - { - this(completer, language, log, null); - } - - /** - * @param file Supplies the file of the script being edited (or null if - * unsaved), so that completers can resolve files relative to it. - */ - public CodeCompleterLanguageSupport(final CodeCompleter completer, - final ScriptLanguage language, final Logger log, - final Supplier<File> file) - { - this.completer = completer; - this.language = language; - this.log = log; - this.file = file; - setAutoCompleteEnabled(true); - setParameterAssistanceEnabled(true); - setShowDescWindow(true); - } - - /** - * Sets whether to show what the completer knows about the code under the - * mouse (see {@link CodeCompleter#hover}). Applies to later installs. - */ - public void setHoverEnabled(final boolean enabled) { - hoverEnabled = enabled; - } - - /** - * Sets whether to show the problems the completer finds (see - * {@link CodeCompleter#diagnose}). Applies to later installs. - */ - public void setDiagnosticsEnabled(final boolean enabled) { - diagnosticsEnabled = enabled; - } - - @Override - public void install(final RSyntaxTextArea textArea) { - final SciJavaCompletionProvider provider = - new SciJavaCompletionProvider(completer, language); - provider.setLogger(log); - provider.setFile(file); - final AutoCompletion ac = new SciJavaAutoCompletion(provider); - ac.setAutoCompleteEnabled(isAutoCompleteEnabled()); - ac.setAutoActivationEnabled(isAutoActivationEnabled()); - ac.setParameterAssistanceEnabled(isParameterAssistanceEnabled()); - ac.setShowDescWindow(getShowDescWindow()); - ac.install(textArea); - installImpl(textArea, ac); - - // Documentation on hover, problems as squiggles, signatures as typed. - final Installed extras = new Installed(); - if (hoverEnabled) { - textArea.setToolTipSupplier(new HoverToolTipSupplier(completer, - language, file, log)); - } - if (diagnosticsEnabled) { - extras.parser = new DiagnosticsParser(textArea, completer, language, - file, log); - textArea.addParser(extras.parser); - } - if (isParameterAssistanceEnabled()) { - extras.popup = new SignaturePopup(textArea, provider::signatureHelp); - extras.popup.install(); - } - installed.put(textArea, extras); - - // Let the completer get ready for this script, e.g. warm up caches. - try { - final File f = file == null ? null : file.get(); - final String text = textArea.getText(); - completer.prepare(new CompletionRequest(text, text.length(), language, - null, null, f == null ? null : f.getPath())); - } - catch (final Exception | LinkageError exc) { - // NB: Preparation is optional; never let it break the editor. - if (log != null) log.debug("Completer preparation failed", exc); - } - } - - @Override - public void uninstall(final RSyntaxTextArea textArea) { - uninstallImpl(textArea); - final Installed extras = installed.remove(textArea); - if (extras != null) { - if (textArea.getToolTipSupplier() instanceof HoverToolTipSupplier) { - textArea.setToolTipSupplier(null); - } - if (extras.parser != null) textArea.removeParser(extras.parser); - if (extras.popup != null) extras.popup.uninstall(); - } - // Let the completer release what it keeps for this script. - try { - final File f = file == null ? null : file.get(); - final String text = textArea.getText(); - completer.closed(new CompletionRequest(text, text.length(), language, - null, null, f == null ? null : f.getPath())); - } - catch (final Exception | LinkageError exc) { - if (log != null) log.debug("Completer release failed", exc); - } - } - - private static final class Installed { - - private DiagnosticsParser parser; - private SignaturePopup popup; - } -} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java index ea8ca6ee..1445be58 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java @@ -29,16 +29,15 @@ package org.scijava.ui.swing.script.autocompletion; -import java.io.File; import java.util.Collections; import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; import javax.swing.SwingUtilities; import javax.swing.text.BadLocationException; import javax.swing.text.Element; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.DiagnosticSeverity; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.parser.AbstractParser; @@ -46,20 +45,13 @@ import org.fife.ui.rsyntaxtextarea.parser.DefaultParserNotice; import org.fife.ui.rsyntaxtextarea.parser.ParseResult; import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; -import org.scijava.code.api.CodeCompleter; -import org.scijava.code.api.CompletionRequest; -import org.scijava.code.api.Diagnostic; -import org.scijava.log.Logger; -import org.scijava.script.ScriptLanguage; /** - * Shows the problems the completer finds in the script (see - * {@link CodeCompleter#diagnose}), e.g. syntax errors, as squiggles, with the - * problem as tooltip. + * Shows the problems language servers publish for the script (e.g. syntax + * errors), as squiggles, with the problem as tooltip. * <p> - * RSyntaxTextArea parses a while after each edit, on the event dispatch - * thread; this parser asks the completer then, and shows its answer once it - * arrives (parsing anew). Until then, the problems found before stay. + * Servers publish problems when they like (e.g. a moment after each change): + * each time, the text area parses anew to show them. * </p> * * @author Gabriel Selzer @@ -67,27 +59,20 @@ public class DiagnosticsParser extends AbstractParser { private final RSyntaxTextArea textArea; - private final CodeCompleter completer; - private final ScriptLanguage language; - private final Supplier<File> file; - private final Logger log; - /** The text asked about last, if not answered yet. */ - private String pending; + /** The problems last published. */ + private volatile List<Diagnostic> problems = Collections.emptyList(); - /** The text answered last, and the problems found in it. */ - private String answered; - private List<Diagnostic> problems = Collections.emptyList(); - - public DiagnosticsParser(final RSyntaxTextArea textArea, - final CodeCompleter completer, final ScriptLanguage language, - final Supplier<File> file, final Logger log) - { + public DiagnosticsParser(final RSyntaxTextArea textArea) { this.textArea = textArea; - this.completer = completer; - this.language = language; - this.file = file; - this.log = log; + } + + /** Shows the given problems (from any thread). */ + public void accept(final List<Diagnostic> found) { + SwingUtilities.invokeLater(() -> { + problems = found == null ? Collections.emptyList() : found; + textArea.forceReparsing(this); + }); } @Override @@ -100,53 +85,34 @@ public ParseResult parse(final RSyntaxDocument doc, final String style) { catch (final BadLocationException exc) { return result; } - if (!text.equals(answered) && !text.equals(pending)) ask(text); - // NB: Until the answer arrives, show the problems found before (where - // they still fit). final Element root = doc.getDefaultRootElement(); for (final Diagnostic d : problems) { - if (d.start() >= text.length() && text.length() > 0) continue; - final int start = Math.max(0, Math.min(d.start(), text.length() - 1)); - final int length = Math.max(1, Math.min(d.end(), text.length()) - - start); - final DefaultParserNotice notice = new DefaultParserNotice(this, d - .message(), root.getElementIndex(start), start, length); - notice.setLevel(level(d.severity())); + int start = DocumentSync.offset(text, d.getRange().getStart()); + final int end = DocumentSync.offset(text, d.getRange().getEnd()); + if (start >= text.length() && text.length() > 0) { + start = text.length() - 1; // NB: E.g. "unexpected end of file". + } + start = Math.max(0, start); + final int length = Math.max(1, Math.min(end, text.length()) - start); + final DefaultParserNotice notice = new DefaultParserNotice(this, + message(d), root.getElementIndex(start), start, length); + notice.setLevel(level(d.getSeverity())); result.addNotice(notice); } return result; } - private void ask(final String text) { - pending = text; - CompletableFuture<List<Diagnostic>> answer; - try { - final File f = file == null ? null : file.get(); - answer = completer.diagnose(new CompletionRequest(text, text.length(), - language, null, null, f == null ? null : f.getPath())); - } - catch (final Exception | LinkageError exc) { - // NB: Never let a misbehaving completer break the editor. - if (log != null) log.debug("Diagnosis failed", exc); - answer = CompletableFuture.completedFuture(Collections.emptyList()); - } - answer.whenComplete((found, error) -> SwingUtilities.invokeLater(() -> { - if (!text.equals(pending)) return; // NB: Superseded. - pending = null; - answered = text; - problems = found == null ? Collections.emptyList() : found; - textArea.forceReparsing(this); - })); + private static String message(final Diagnostic d) { + if (d.getMessage() == null) return ""; + return d.getMessage().isLeft() ? d.getMessage().getLeft() : d.getMessage() + .getRight().getValue(); } - private static ParserNotice.Level level(final Diagnostic.Severity s) { - switch (s) { - case ERROR: - return ParserNotice.Level.ERROR; - case WARNING: - return ParserNotice.Level.WARNING; - default: - return ParserNotice.Level.INFO; + private static ParserNotice.Level level(final DiagnosticSeverity s) { + if (s == null || s == DiagnosticSeverity.Error) { + return ParserNotice.Level.ERROR; } + return s == DiagnosticSeverity.Warning ? ParserNotice.Level.WARNING + : ParserNotice.Level.INFO; } } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java new file mode 100644 index 00000000..9604d940 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java @@ -0,0 +1,158 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.io.File; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.lsp4j.services.LanguageServer; +import org.scijava.code.lsp.LspClient; +import org.scijava.script.ScriptLanguage; + +/** + * Keeps a language server's copy of an edited script up to date: opens it, + * sends its changes (the whole text), and closes it. Its URI is its file's, + * if saved; otherwise an {@code untitled:} one. Saving it under another name + * closes the old document and opens the new one. + * + * @author Gabriel Selzer + */ +public class DocumentSync { + + private static final AtomicInteger UNTITLED = new AtomicInteger(); + + private final LanguageServer server; + private final ScriptLanguage language; + private final String untitled; + private volatile Supplier<File> file; + + private String uri; + private String sent; + private int version; + + public DocumentSync(final LanguageServer server, + final ScriptLanguage language) + { + this.server = server; + this.language = language; + final List<String> extensions = language == null ? Collections + .emptyList() : language.getExtensions(); + untitled = "untitled:/script-" + UNTITLED.incrementAndGet() + (extensions + .isEmpty() ? "" : "." + extensions.get(0)); + } + + /** The server documents are synchronized with. */ + public LanguageServer server() { + return server; + } + + /** Sets a supplier of the script's file (which may supply null). */ + public void setFile(final Supplier<File> file) { + this.file = file; + } + + /** + * Brings the server's copy up to date with the given text (opening it, if + * needed). + * + * @return The document's URI. + */ + public synchronized String sync(final String text) { + final String current = currentUri(); + if (uri != null && !uri.equals(current)) close(); + if (uri == null) { + uri = current; + version = 1; + sent = text; + server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams( + new TextDocumentItem(uri, languageId(), version, text))); + } + else if (!text.equals(sent)) { + sent = text; + version++; + server.getTextDocumentService().didChange(new DidChangeTextDocumentParams( + new VersionedTextDocumentIdentifier(uri, version), Collections + .singletonList(new TextDocumentContentChangeEvent(text)))); + } + return uri; + } + + /** Closes the server's copy, if open. */ + public synchronized void close() { + if (uri == null) return; + server.getTextDocumentService().didClose(new DidCloseTextDocumentParams( + new TextDocumentIdentifier(uri))); + uri = null; + sent = null; + } + + /** The open document's URI, or null if none. */ + public synchronized String uri() { + return uri; + } + + /** The text last sent, or null if none. */ + public synchronized String text() { + return sent; + } + + /** The position of an offset of the given text, for requests. */ + public static Position position(final String text, final int offset) { + return LspClient.position(text, Math.max(0, Math.min(offset, text + .length()))); + } + + /** The offset of a position in the given text. */ + public static int offset(final String text, final Position position) { + return LspClient.offset(text, position); + } + + private String currentUri() { + final Supplier<File> f = file; + final File path = f == null ? null : f.get(); + return path == null ? untitled : path.getAbsoluteFile().toURI() + .toString(); + } + + private String languageId() { + return language == null ? "plaintext" : language.getLanguageName(); + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java index ece4c884..39c0ac54 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java @@ -30,26 +30,26 @@ package org.scijava.ui.swing.script.autocompletion; import java.awt.event.MouseEvent; -import java.io.File; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.HoverParams; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.MarkedString; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.fife.ui.rtextarea.RTextArea; import org.fife.ui.rtextarea.ToolTipSupplier; -import org.scijava.code.api.CodeCompleter; -import org.scijava.code.api.CompletionRequest; -import org.scijava.code.api.Hover; import org.scijava.log.Logger; -import org.scijava.script.ScriptLanguage; /** - * Shows what the completer knows about the code under the mouse (see - * {@link CodeCompleter#hover}), e.g. a function's documentation, as a tooltip. + * Shows what the language server knows about the code under the mouse + * ({@code textDocument/hover}), e.g. a function's documentation, as a tooltip. * <p> * RSyntaxTextArea asks for tooltips as the mouse moves, on the event dispatch - * thread: so the completer's answer for a word is asked once, and awaited only - * briefly; until it arrives, there is no tooltip. + * thread: so the server is asked once per word, and awaited only briefly; + * until it answers, there is no tooltip. * </p> * * @author Gabriel Selzer @@ -62,9 +62,7 @@ public class HoverToolTipSupplier implements ToolTipSupplier { /** How much of an answer to show, at most, in lines. */ private static final int MAX_LINES = 25; - private final CodeCompleter completer; - private final ScriptLanguage language; - private final Supplier<File> file; + private final DocumentSync sync; private final Logger log; /** The last question (text and word), and its answer. */ @@ -72,13 +70,8 @@ public class HoverToolTipSupplier implements ToolTipSupplier { private int askedWord = -1; private CompletableFuture<Hover> answer; - public HoverToolTipSupplier(final CodeCompleter completer, - final ScriptLanguage language, final Supplier<File> file, - final Logger log) - { - this.completer = completer; - this.language = language; - this.file = file; + public HoverToolTipSupplier(final DocumentSync sync, final Logger log) { + this.sync = sync; this.log = log; } @@ -101,9 +94,8 @@ String toolTip(final String text, final int offset) { answer = ask(text, offset); } try { - final Hover h = answer.get(BUDGET, TimeUnit.MILLISECONDS); - return h == null || h.isEmpty() ? null : "<html>" + truncate(h - .text()) + "</html>"; + final String html = html(answer.get(BUDGET, TimeUnit.MILLISECONDS)); + return html == null ? null : "<html>" + truncate(html) + "</html>"; } catch (final Exception exc) { return null; // NB: Not yet; or failed. @@ -112,22 +104,34 @@ String toolTip(final String text, final int offset) { private CompletableFuture<Hover> ask(final String text, final int offset) { try { - final File f = file == null ? null : file.get(); - final Hover h = completer.hover(new CompletionRequest(text, offset, - language, null, null, f == null ? null : f.getPath())); - if (h == null) return CompletableFuture.completedFuture(Hover.NONE); - if (!h.isEmpty() || h.update() == null) { - return CompletableFuture.completedFuture(h); - } - return h.update(); + final String uri = sync.sync(text); + return sync.server().getTextDocumentService().hover(new HoverParams( + new TextDocumentIdentifier(uri), DocumentSync.position(text, offset))); } catch (final Exception | LinkageError exc) { - // NB: Never let a misbehaving completer break the editor. + // NB: Never let a misbehaving server break the editor. if (log != null) log.debug("Hover failed", exc); - return CompletableFuture.completedFuture(Hover.NONE); + return CompletableFuture.completedFuture(null); } } + /** A hover's contents, as HTML; or null if none. */ + static String html(final Hover hover) { + if (hover == null || hover.getContents() == null) return null; + final Either<java.util.List<Either<String, MarkedString>>, MarkupContent> contents = + hover.getContents(); + if (contents.isRight()) { + return SciJavaCompletionProvider.html(Either.forRight(contents + .getRight())); + } + final StringBuilder sb = new StringBuilder(); + for (final Either<String, MarkedString> part : contents.getLeft()) { + if (sb.length() > 0) sb.append("\n\n"); + sb.append(part.isLeft() ? part.getLeft() : part.getRight().getValue()); + } + return SciJavaCompletionProvider.html(Either.forLeft(sb.toString())); + } + /** Keeps the first lines of long answers (e.g. docstrings). */ static String truncate(final String html) { int end = -1; diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java new file mode 100644 index 00000000..8422642d --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java @@ -0,0 +1,270 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.io.File; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import org.eclipse.lsp4j.MessageActionItem; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.LanguageClientAware; +import org.eclipse.lsp4j.services.LanguageServer; +import org.fife.rsta.ac.AbstractLanguageSupport; +import org.fife.ui.autocomplete.AutoCompletion; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.scijava.code.api.CodeCompleter; +import org.scijava.code.lsp.compat.CodeCompleterLanguageServer; +import org.scijava.log.Logger; +import org.scijava.script.ScriptLanguage; + +/** + * RSyntaxTextArea language support from a language server (usually one + * standing for all of a script language's, see + * {@code org.scijava.code.lsp.LanguageServerService}): completion with + * parameter assistance, documentation on hover, problems as squiggles, and + * signatures as a call is typed. The edited script is kept in sync with the + * server (see {@link DocumentSync}): opened on install, changed with each + * edit, closed on uninstall. + * + * @author Curtis Rueden + * @author Gabriel Selzer + */ +public class LanguageServerLanguageSupport extends AbstractLanguageSupport { + + private final LanguageServer server; + private final ScriptLanguage language; + private final Logger log; + private final Supplier<File> file; + private boolean hoverEnabled = true; + private boolean diagnosticsEnabled = true; + + /** What was installed in each text area, to uninstall. */ + private final Map<RSyntaxTextArea, Installed> installed = + new WeakHashMap<>(); + + /** + * @param server The language server (one per support: it is told about the + * script, and publishes its problems to this support). + * @param language The script's language. + * @param log Where to report problems, or null. + * @param file Supplies the file of the script being edited (or null if + * unsaved). + */ + public LanguageServerLanguageSupport(final LanguageServer server, + final ScriptLanguage language, final Logger log, + final Supplier<File> file) + { + this.server = server; + this.language = language; + this.log = log; + this.file = file; + setAutoCompleteEnabled(true); + setParameterAssistanceEnabled(true); + setShowDescWindow(true); + } + + /** + * Uses a single code-api completer. TEMP: Until code-api is removed. + */ + public LanguageServerLanguageSupport(final CodeCompleter completer, + final ScriptLanguage language, final Logger log, + final Supplier<File> file) + { + this(new CodeCompleterLanguageServer(completer, language), language, log, + file); + } + + /** + * Sets whether to show what the server knows about the code under the + * mouse. Applies to later installs. + */ + public void setHoverEnabled(final boolean enabled) { + hoverEnabled = enabled; + } + + /** + * Sets whether to show the problems the server finds. Applies to later + * installs. + */ + public void setDiagnosticsEnabled(final boolean enabled) { + diagnosticsEnabled = enabled; + } + + @Override + public void install(final RSyntaxTextArea textArea) { + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + server, language); + provider.setLogger(log); + provider.setFile(file); + final DocumentSync sync = provider.sync(); + final AutoCompletion ac = new SciJavaAutoCompletion(provider); + ac.setAutoCompleteEnabled(isAutoCompleteEnabled()); + ac.setAutoActivationEnabled(isAutoActivationEnabled()); + ac.setParameterAssistanceEnabled(isParameterAssistanceEnabled()); + ac.setShowDescWindow(getShowDescWindow()); + ac.install(textArea); + installImpl(textArea, ac); + + // Documentation on hover, problems as squiggles, signatures as typed. + final Installed extras = new Installed(sync); + if (hoverEnabled) { + textArea.setToolTipSupplier(new HoverToolTipSupplier(sync, log)); + } + if (diagnosticsEnabled) { + extras.parser = new DiagnosticsParser(textArea); + textArea.addParser(extras.parser); + } + if (server instanceof LanguageClientAware) { + ((LanguageClientAware) server).connect(new Client(sync, extras.parser)); + } + if (isParameterAssistanceEnabled()) { + extras.popup = new SignaturePopup(textArea, provider::signatureHelp); + extras.popup.install(); + } + + // Keep the server's copy of the script up to date: opened now (which + // lets the servers get ready for it), changed with each edit. + extras.listener = new DocumentListener() { + + @Override + public void insertUpdate(final DocumentEvent e) { + changed(); + } + + @Override + public void removeUpdate(final DocumentEvent e) { + changed(); + } + + @Override + public void changedUpdate(final DocumentEvent e) { + // NB: Attributes only. + } + + private void changed() { + try { + sync.sync(textArea.getText()); + } + catch (final Exception | LinkageError exc) { + // NB: Never let a misbehaving server break the editor. + if (log != null) log.debug("Document sync failed", exc); + } + } + }; + textArea.getDocument().addDocumentListener(extras.listener); + installed.put(textArea, extras); + try { + sync.sync(textArea.getText()); + } + catch (final Exception | LinkageError exc) { + if (log != null) log.debug("Document sync failed", exc); + } + } + + @Override + public void uninstall(final RSyntaxTextArea textArea) { + uninstallImpl(textArea); + final Installed extras = installed.remove(textArea); + if (extras == null) return; + textArea.getDocument().removeDocumentListener(extras.listener); + if (textArea.getToolTipSupplier() instanceof HoverToolTipSupplier) { + textArea.setToolTipSupplier(null); + } + if (extras.parser != null) textArea.removeParser(extras.parser); + if (extras.popup != null) extras.popup.uninstall(); + // Let the servers release what they keep for this script. + try { + extras.sync.close(); + } + catch (final Exception | LinkageError exc) { + if (log != null) log.debug("Document close failed", exc); + } + } + + private static final class Installed { + + private final DocumentSync sync; + private DiagnosticsParser parser; + private SignaturePopup popup; + private DocumentListener listener; + + private Installed(final DocumentSync sync) { + this.sync = sync; + } + } + + /** Receives the server's messages: the script's problems, for the parser. */ + private final class Client implements LanguageClient { + + private final DocumentSync sync; + private final DiagnosticsParser parser; + + private Client(final DocumentSync sync, final DiagnosticsParser parser) { + this.sync = sync; + this.parser = parser; + } + + @Override + public void publishDiagnostics(final PublishDiagnosticsParams p) { + if (parser != null && p.getUri().equals(sync.uri())) { + parser.accept(p.getDiagnostics()); + } + } + + @Override + public void telemetryEvent(final Object object) {} + + @Override + public void showMessage(final MessageParams m) { + if (log != null) log.debug("Language server: " + m.getMessage()); + } + + @Override + public CompletableFuture<MessageActionItem> showMessageRequest( + final ShowMessageRequestParams r) + { + return CompletableFuture.completedFuture(null); + } + + @Override + public void logMessage(final MessageParams m) { + if (log != null) log.debug("Language server: " + m.getMessage()); + } + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java index 1e2d45c4..afb07900 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java @@ -33,101 +33,137 @@ import java.io.File; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -import javax.script.ScriptEngine; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.InsertTextFormat; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.MarkupKind; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SignatureHelp; +import org.eclipse.lsp4j.SignatureHelpParams; +import org.eclipse.lsp4j.SignatureInformation; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.services.LanguageServer; import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.CompletionCellRenderer; import org.fife.ui.autocomplete.DefaultCompletionProvider; -import org.fife.ui.autocomplete.ParameterChoicesProvider; import org.fife.ui.autocomplete.ParameterizedCompletion; +import org.scijava.code.api.CodeCompleter; +import org.scijava.code.lsp.RatedSignatureInformation; +import org.scijava.code.lsp.RatedSignatureInformation.Fit; +import org.scijava.code.lsp.UpdatingCompletionList; +import org.scijava.code.lsp.compat.CodeCompleterLanguageServer; import org.scijava.log.Logger; import org.scijava.script.ScriptLanguage; -import org.scijava.code.api.CodeCompleter; -import org.scijava.code.api.CompletionRequest; -import org.scijava.code.api.CompletionResult; -import org.scijava.code.api.ParameterChoices; -import org.scijava.code.api.SignatureHelp; -import org.scijava.code.api.SignatureHelp.Fit; /** - * The single bridge between SciJava's toolkit-agnostic code completion SPI - * ({@link CodeCompleter}, {@link org.scijava.code.api.Completion}) and - * RSyntaxTextArea's Swing-based completion machinery. + * The bridge between a language server (usually one standing for all of a + * script language's, see {@code org.scijava.code.lsp.LanguageServerService}) + * and RSyntaxTextArea's completion machinery. * <p> - * This provider delegates all language intelligence to a {@link CodeCompleter}, - * then translates the resulting neutral {@link CompletionResult} into RSTA - * {@link Completion}s. Language adapters therefore need no dependency on Swing or - * RSTA: they implement {@link CodeCompleter} (via a - * {@link org.scijava.code.api.CodeCompleterPlugin}) and the script editor - * renders the suggestions here. + * Completions with a snippet of parameters (e.g. {@code max(${1:a}, + * ${2:b})}) become function completions, for parameter assistance; their + * parameters' types come from the label details (e.g. {@code (double a, + * double b)}). The parameter tooltip lists the other overloads, as signature + * help rates them for the arguments typed (see + * {@link RatedSignatureInformation}). Documentation is resolved when shown. + * Slower completions (see {@link UpdatingCompletionList}) replace the first + * ones as they arrive. * </p> * * @author Curtis Rueden + * @author Gabriel Selzer */ public class SciJavaCompletionProvider extends DefaultCompletionProvider { - private final CodeCompleter completer; - private final ScriptLanguage language; + /** How long to wait for completions, in milliseconds. */ + private static final long REQUEST_TIMEOUT = 2000; - /** Optional logger, for reporting completer failures. */ - private Logger log; + /** How long the parameter tooltip waits for signature help, in ms. */ + private static final long HELP_BUDGET = 150; + + /** How long to wait for documentation, in milliseconds. */ + private static final long DESCRIBE_TIMEOUT = 1000; - /** Optional live engine, set when completing in an interpreter. */ - private ScriptEngine engine; + /** How many choices to offer for a parameter, at most. */ + private static final int CHOICES = 10; - /** Optional supplier of the edited script's file. */ - private Supplier<File> file; + /** A snippet placeholder: ${1:name}, ${1} or $1. */ + private static final Pattern PLACEHOLDER = Pattern.compile( + "\\$\\{(\\d+)(?::((?:[^}\\\\]|\\\\.)*))?\\}|\\$(\\d+)"); + + private final DocumentSync sync; + private final ScriptLanguage language; - // Cache so getAlreadyEnteredText() and getCompletionsImpl() — which RSTA - // calls in quick succession for the same caret — stay consistent and avoid - // recomputation. + /** Optional logger, for reporting server failures. */ + private Logger log; + + // Cache so getAlreadyEnteredText() and getCompletionsImpl(), which RSTA + // calls in quick succession for the same caret, stay consistent. private int cachedCaret = -1; private String cachedText; - private CompletionResult cachedResult; - - /** How long the parameter tooltip waits for better signature help, in ms. */ - private static final long HELP_BUDGET = 150; + private CompletionList cachedList; + private int cachedStart; // The signature help for the current text and caret. private String helpText; private int helpCaret = -1; private volatile SignatureHelp help; - /** Notified when a better result arrives for the current request. */ + /** Notified when better completions arrive for the current request. */ private volatile Consumer<Boolean> updateListener; - /** The parameter-choices provider installed for the cached result, if any. */ - private ParameterChoicesProvider choicesProvider; - - public SciJavaCompletionProvider(final CodeCompleter completer, + public SciJavaCompletionProvider(final LanguageServer server, final ScriptLanguage language) { - this.completer = completer; + this.sync = new DocumentSync(server, language); this.language = language; setParameterizedCompletionParams('(', ", ", ')'); // Show each callable's parameters and return type in the popup list. setListCellRenderer(new CompletionCellRenderer()); // Auto-activate after a letter, digit, '.' or '_'. setAutoActivationRules(true, "."); + setParameterChoicesProvider(this::parameterChoices); } - /** Sets a logger with which to report completer failures. */ + /** + * Uses a single code-api completer. TEMP: Until code-api is removed. + */ + public SciJavaCompletionProvider(final CodeCompleter completer, + final ScriptLanguage language) + { + this(new CodeCompleterLanguageServer(completer, language), language); + } + + /** The synchronization of the edited script with the server. */ + public DocumentSync sync() { + return sync; + } + + /** Sets a logger with which to report server failures. */ public void setLogger(final Logger log) { this.log = log; } @@ -137,12 +173,7 @@ public void setLogger(final Logger log) { * null, for an unsaved script). */ public void setFile(final Supplier<File> file) { - this.file = file; - } - - /** Sets a live script engine to enable variable/binding-based completion. */ - public void setEngine(final ScriptEngine engine) { - this.engine = engine; + sync.setFile(file); } @Override @@ -152,9 +183,9 @@ public boolean isValidChar(final char c) { @Override public String getAlreadyEnteredText(final JTextComponent comp) { - final CompletionResult result = compute(comp); + compute(comp); final int caret = comp.getCaretPosition(); - final int start = Math.min(Math.max(result.replaceStart(), 0), caret); + final int start = Math.min(Math.max(cachedStart, 0), caret); try { return comp.getText(start, caret - start); } @@ -165,192 +196,122 @@ public String getAlreadyEnteredText(final JTextComponent comp) { @Override public List<Completion> getCompletionsImpl(final JTextComponent comp) { - final CompletionResult result = compute(comp); - final List<org.scijava.code.api.Completion> source = - result.completions(); - // Group callables by name, so each can list its sibling overloads. - final Map<String, List<org.scijava.code.api.Completion>> overloads = - new HashMap<>(); - for (final org.scijava.code.api.Completion c : source) { - if (!c.isCallable()) continue; - overloads.computeIfAbsent(stripParens(c.insertionText()), - k -> new ArrayList<>()).add(c); - } - final int n = source.size(); - final List<Completion> out = new ArrayList<>(n); - for (int i = 0; i < n; i++) { - out.add(toRSTA(comp, source.get(i), n - i, overloads)); - } - return out; - } - - // -- Helper methods -- - - private CompletionResult compute(final JTextComponent comp) { - final int caret = comp.getCaretPosition(); - final String text; - try { - text = comp.getText(0, comp.getDocument().getLength()); - } - catch (final BadLocationException exc) { - return CompletionResult.EMPTY; - } - if (caret == cachedCaret && text.equals(cachedText) && - cachedResult != null) - { - return cachedResult; - } - CompletionResult result; - try { - final File f = file == null ? null : file.get(); - final CompletionRequest request = new CompletionRequest(text, caret, - language, engine, engine == null ? null : engine.getContext(), f == - null ? null : f.getPath()); - result = completer.complete(request); - if (result == null) result = CompletionResult.EMPTY; - } - catch (final Exception | LinkageError exc) { - // NB: Never let a misbehaving completer break the editor. - if (log != null) log.debug("Code completion failed", exc); - result = CompletionResult.EMPTY; - } - cachedCaret = caret; - cachedText = text; - adopt(result, comp); - return result; + compute(comp); + return convert(comp, cachedList, cachedText, cachedCaret, cachedStart); } /** - * Sets a listener to notify when a better result has arrived for the - * current request (see {@link CompletionResult#update()}), e.g. to refresh - * the completion popup. Called on the event dispatch thread. + * Sets a listener to notify when better completions have arrived for the + * current request (see {@link UpdatingCompletionList}), e.g. to refresh the + * completion popup. Called on the event dispatch thread. * - * @param listener Accepts whether the previous result had no completions - * (so that no popup may be showing for it). + * @param listener Accepts whether the previous completions were none (so + * that no popup may be showing for them). */ public void setUpdateListener(final Consumer<Boolean> listener) { this.updateListener = listener; } - /** Makes the given result current, and awaits its update, if any. */ - private void adopt(final CompletionResult result, final JTextComponent comp) { - cachedResult = result; - // Wire up (or clear) parameter assistance for this result's callables. - final ParameterChoices choices = result.parameterChoices(); - choicesProvider = - choices == null ? null : new NeutralChoicesProvider(choices); - setParameterChoicesProvider(choicesProvider); - - final CompletableFuture<CompletionResult> update = result.update(); - if (update == null) return; - final String text = cachedText; - final int caret = cachedCaret; - update.whenComplete((better, error) -> { - if (better == null) return; - SwingUtilities.invokeLater(() -> { - // NB: Only if nothing has changed since the request. - if (cachedResult != result || caret != comp.getCaretPosition() || - !text.equals(comp.getText())) return; - final boolean wasEmpty = result.completions().isEmpty(); - adopt(better, comp); - final Consumer<Boolean> listener = updateListener; - if (listener != null) listener.accept(wasEmpty); - }); - }); - } - /** - * Returns the candidate values for the given parameter, per the completer's - * {@link ParameterChoices} for the completion state at {@code comp}'s caret. - * Mirrors what RSTA's parameter assistance shows; also handy for non-popup - * front ends and tests. + * Gets choices for a parameter being filled in: what the server completes + * there, variables first (the server ranks those fitting best first). */ List<Completion> parameterChoices(final JTextComponent comp, final ParameterizedCompletion.Parameter param) { - compute(comp); - return choicesProvider == null ? Collections.emptyList() - : choicesProvider.getParameterChoices(comp, param); + final String text = comp.getText(); + final CompletionList list = request(text, comp.getCaretPosition()); + final List<Completion> out = new ArrayList<>(); + for (final CompletionItem item : sorted(list)) { + final CompletionItemKind k = item.getKind(); + if (k != null && k != CompletionItemKind.Variable && + k != CompletionItemKind.Field && k != CompletionItemKind.Constant && + k != CompletionItemKind.Value) continue; + final String name = plain(newText(item)); + out.add(new BasicCompletion(this, name, item.getDetail())); + if (out.size() >= CHOICES) break; + } + return out; } /** - * Translates a neutral completion into an RSTA completion. Callables become - * {@link org.fife.ui.autocomplete.FunctionCompletion}s (so parameter - * assistance and parameter choices engage); everything else becomes a - * {@link BasicCompletion}. Either may carry additional edits (e.g. - * auto-imports). The {@code orderRelevance} argument preserves the completer's - * ordering when the completion does not specify its own relevance. + * Gets signature help at the caret, waiting briefly. Remembered per text + * and caret; an answer arriving late is used when asked again. */ - private Completion toRSTA(final JTextComponent comp, - final org.scijava.code.api.Completion c, final int orderRelevance, - final Map<String, List<org.scijava.code.api.Completion>> overloads) - { - final Completion rsta = c.isCallable() // - ? functionCompletion(comp, c, overloads.get(stripParens(c - .insertionText()))) // - : basicCompletion(c); - final double rel = c.relevance(); - final int relevance = rel != 0 ? (int) Math.round(rel) : orderRelevance; - if (rsta instanceof BasicCompletion) { - ((BasicCompletion) rsta).setRelevance(relevance); + SignatureHelp signatureHelp(final JTextComponent comp) { + final String text; + try { + text = comp.getText(0, comp.getDocument().getLength()); } - return rsta; - } - - private Completion basicCompletion( - final org.scijava.code.api.Completion c) - { - // NB: The description may be expensive (computed on demand): ask for it - // only when RSTA shows it, i.e. when the completion is selected. - return new SciJavaCompletion(this, c.insertionText(), c.summary(), - c::description, c.additionalEdits()); - } - - private Completion functionCompletion(final JTextComponent comp, - final org.scijava.code.api.Completion c, - final List<org.scijava.code.api.Completion> overloads) - { - // FunctionCompletion appends the parameter template itself, so strip any - // trailing "()" the completer may have included in the insertion text. - final String name = stripParens(c.insertionText()); - final String returnType = c.returnType() == null ? "" : c.returnType(); - - final SciJavaFunctionCompletion fc = new SciJavaFunctionCompletion(this, - name, returnType, c.additionalEdits()); - fc.setReturnValueDescription(returnType); - fc.setShortDescription(c.summary()); - fc.setDescription(c::description); - fc.setParams(toRSTAParams(comp, c, overloads)); - return fc; + catch (final BadLocationException exc) { + return null; + } + final int caret = comp.getCaretPosition(); + if (caret == helpCaret && text.equals(helpText) && help != null) { + return help; + } + SignatureHelp h = null; + try { + final String uri = sync.sync(text); + final CompletableFuture<SignatureHelp> answer = sync.server() + .getTextDocumentService().signatureHelp(new SignatureHelpParams( + new TextDocumentIdentifier(uri), DocumentSync.position(text, + caret))); + try { + h = answer.get(HELP_BUDGET, TimeUnit.MILLISECONDS); + } + catch (final TimeoutException exc) { + // NB: Not in time: keep it for when RSTA asks again. + answer.thenAccept(late -> { + if (late != null && text.equals(helpText) && caret == helpCaret) { + help = late; + } + }); + } + } + catch (final Exception | LinkageError exc) { + // NB: Never let a misbehaving server break the editor. + if (log != null) log.debug("Signature help failed", exc); + } + helpText = text; + helpCaret = caret; + help = h; + return h; } /** - * Lists the parameters of {@code c}'s other overloads as HTML, one overload - * per line, for RSTA's parameter tooltip; or returns null if there are none. + * Lists the parameters of a callable's other overloads as HTML, one + * overload per line, for RSTA's parameter tooltip; or returns null if there + * are none. * <p> - * If the completer's signature help is for this call, its signatures are - * listed in its order, styled by how well they fit the arguments typed so - * far: matches plain, those needing a conversion greyed, mismatches struck - * out. Otherwise, {@code overloads} are listed, unstyled. + * If the signature help is for this call, its signatures are listed in its + * order, styled by how well they fit the arguments typed so far: matches + * plain, those needing a conversion greyed, mismatches struck out. + * Otherwise, {@code overloads} are listed, unstyled. * </p> + * + * @param name The callable's name. + * @param own Its parameters (e.g. {@code double a}). + * @param overloads The parameters of its overloads in the completions. + * @param help The signature help at the caret, or null. */ - static String otherOverloads(final org.scijava.code.api.Completion c, - final List<org.scijava.code.api.Completion> overloads, - final SignatureHelp help) + static String otherOverloads(final String name, final List<String> own, + final List<List<String>> overloads, final SignatureHelp help) { - final String own = parameterList(c); + final String ownList = parameterList(own); // NB: A map, since reflection can report the same signature twice. final Map<String, Fit> lines = new LinkedHashMap<>(); - if (help != null && isFor(help, c)) { - for (final SignatureHelp.Signature s : help.signatures()) { - final String sig = parameterList(s.callable()); - if (!sig.equals(own)) lines.putIfAbsent(sig, s.fit()); + if (help != null && isFor(help, name)) { + for (final SignatureInformation s : help.getSignatures()) { + final String sig = parameterList(SignatureLabel.of(s).parameters); + if (!sig.equals(ownList)) lines.putIfAbsent(sig, RatedSignatureInformation + .fitOf(s)); } } else if (overloads != null) { - for (final org.scijava.code.api.Completion o : overloads) { + for (final List<String> o : overloads) { final String sig = parameterList(o); - if (!sig.equals(own)) lines.putIfAbsent(sig, Fit.UNKNOWN); + if (!sig.equals(ownList)) lines.putIfAbsent(sig, Fit.UNKNOWN); } } if (lines.isEmpty()) return null; @@ -376,139 +337,195 @@ else if (overloads != null) { } /** - * True iff the help is about calls to {@code c}, rather than, e.g., a call - * nested in its arguments. + * The Look & Feel's color for disabled text, as an HTML color such as + * {@code #8c8c8c}. RSTA's parameter tooltip takes its other colors from the + * Look & Feel too. */ - private static boolean isFor(final SignatureHelp help, - final org.scijava.code.api.Completion c) - { - if (help.isEmpty()) return false; - final String name = simpleName(c); - for (final SignatureHelp.Signature s : help.signatures()) { - if (!simpleName(s.callable()).equals(name)) return false; - } - return true; + static String dimColor() { + Color c = UIManager.getColor("Label.disabledForeground"); + if (c == null) c = UIManager.getColor("textInactiveText"); + if (c == null) c = Color.GRAY; + return String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c + .getBlue()); } - private static String simpleName(final org.scijava.code.api.Completion c) { - final String name = stripParens(c.insertionText()); - return name.substring(name.lastIndexOf('.') + 1); + /** Converts documentation to HTML: Markdown as is (it allows HTML). */ + static String html(final Either<String, MarkupContent> doc) { + if (doc == null) return null; + final String text; + if (doc.isLeft()) text = "<pre>" + escapeHTML(doc.getLeft()) + "</pre>"; + else if (MarkupKind.MARKDOWN.equals(doc.getRight().getKind())) { + text = doc.getRight().getValue(); + } + else text = "<pre>" + escapeHTML(doc.getRight().getValue()) + "</pre>"; + return text == null || text.isEmpty() || text.equals("<pre></pre>") ? null + : text; } - /** - * Gets the completer's signature help at the caret (see - * {@link CodeCompleter#signatureHelp}), waiting briefly for a better - * answer, if one is announced. Remembered per text and caret. - */ - SignatureHelp signatureHelp(final JTextComponent comp) { + // -- Helper methods -- + + private void compute(final JTextComponent comp) { + final int caret = comp.getCaretPosition(); final String text; try { text = comp.getText(0, comp.getDocument().getLength()); } catch (final BadLocationException exc) { - return SignatureHelp.NONE; + return; } - final int caret = comp.getCaretPosition(); - if (caret == helpCaret && text.equals(helpText) && help != null) { - return help; + if (caret == cachedCaret && text.equals(cachedText) && cachedList != null) { + return; } - SignatureHelp h; + final CompletionList list = request(text, caret); + cachedCaret = caret; + cachedText = text; + adopt(list, comp); + } + + /** Asks the server for completions; none if it fails. */ + private CompletionList request(final String text, final int caret) { try { - final File f = file == null ? null : file.get(); - h = completer.signatureHelp(new CompletionRequest(text, caret, language, - engine, engine == null ? null : engine.getContext(), f == null ? null - : f.getPath())); - if (h == null) h = SignatureHelp.NONE; - if (h.update() != null) { - try { - final SignatureHelp better = h.update().get(HELP_BUDGET, - TimeUnit.MILLISECONDS); - if (better != null) h = better; - } - catch (final TimeoutException | ExecutionException exc) { - // NB: Not in time: use what there is, and keep the better help - // for when RSTA asks again (e.g. at the next parameter). - final SignatureHelp first = h; - h.update().thenAccept(better -> { - if (better != null && help == first) help = better; - }); - } - catch (final InterruptedException exc) { - Thread.currentThread().interrupt(); - } - } + final String uri = sync.sync(text); + final Either<List<CompletionItem>, CompletionList> answer = sync.server() + .getTextDocumentService().completion(new CompletionParams( + new TextDocumentIdentifier(uri), DocumentSync.position(text, caret))) + .get(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS); + if (answer == null) return new CompletionList(); + return answer.isRight() ? answer.getRight() : new CompletionList(answer + .getLeft()); } catch (final Exception | LinkageError exc) { - // NB: Never let a misbehaving completer break the editor. - if (log != null) log.debug("Signature help failed", exc); - h = SignatureHelp.NONE; + // NB: Never let a misbehaving server break the editor. + if (log != null) log.debug("Code completion failed", exc); + return new CompletionList(); } - helpText = text; - helpCaret = caret; - help = h; - return h; } - /** - * The Look & Feel's color for disabled text, as an HTML color such as - * {@code #8c8c8c}. RSTA's parameter tooltip takes its other colors from the - * Look & Feel too. - */ - static String dimColor() { - Color c = UIManager.getColor("Label.disabledForeground"); - if (c == null) c = UIManager.getColor("textInactiveText"); - if (c == null) c = Color.GRAY; - return String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c - .getBlue()); + /** Makes the given completions current, and awaits their update, if any. */ + private void adopt(final CompletionList list, final JTextComponent comp) { + cachedList = list; + cachedStart = replaceStart(list, cachedText, cachedCaret); + if (!(list instanceof UpdatingCompletionList)) return; + final CompletableFuture<CompletionList> update = + ((UpdatingCompletionList) list).update(); + if (update == null) return; + final String text = cachedText; + final int caret = cachedCaret; + update.whenComplete((better, error) -> { + if (better == null) return; + SwingUtilities.invokeLater(() -> { + // NB: Only if nothing has changed since the request. + if (cachedList != list || caret != comp.getCaretPosition() || !text + .equals(comp.getText())) return; + final boolean wasEmpty = list.getItems() == null || list.getItems() + .isEmpty(); + adopt(better, comp); + final Consumer<Boolean> listener = updateListener; + if (listener != null) listener.accept(wasEmpty); + }); + }); } /** - * A short parameter list such as {@code long[] pos, int d}, or {@code ()} if - * there are no parameters. + * Where the completions' replacement starts: the earliest of their edits' + * starts (completions without an edit replace the word before the caret). */ - private static String parameterList( - final org.scijava.code.api.Completion c) + private static int replaceStart(final CompletionList list, final String text, + final int caret) { - final StringBuilder sb = new StringBuilder(); - final List<org.scijava.code.api.Completion.Parameter> params = c - .parameters(); - for (int i = 0; i < params.size(); i++) { - if (i > 0) sb.append(", "); - final String type = params.get(i).type(); - if (type != null) sb.append(type.substring(type.lastIndexOf('.') + 1)); - if (params.get(i).name() != null) { - if (type != null) sb.append(' '); - sb.append(params.get(i).name()); - } + int start = caret; + for (final CompletionItem item : items(list)) { + start = Math.min(start, itemStart(item, text, caret)); } - return sb.length() == 0 ? "()" : sb.toString(); + return start; } - private static String escapeHTML(final String s) { - return s.replace("&", "&").replace("<", "<").replace(">", ">"); + private static int itemStart(final CompletionItem item, final String text, + final int caret) + { + if (item.getTextEdit() != null) { + final Range r = item.getTextEdit().isLeft() ? item.getTextEdit() + .getLeft().getRange() : item.getTextEdit().getRight().getInsert(); + return Math.min(caret, DocumentSync.offset(text, r.getStart())); + } + int start = Math.min(caret, text.length()); + while (start > 0 && Character.isJavaIdentifierPart(text.charAt(start - + 1))) start--; + return start; } - private static String stripParens(final String name) { - return name.endsWith("()") ? name.substring(0, name.length() - 2) : name; + /** Converts the server's completions to RSTA's. */ + private List<Completion> convert(final JTextComponent comp, + final CompletionList list, final String text, final int caret, + final int start) + { + final List<CompletionItem> items = sorted(list); + // Group callables by name, so each can list its sibling overloads. + final Map<String, List<List<String>>> overloads = new HashMap<>(); + final List<Object[]> converted = new ArrayList<>(); + for (final CompletionItem item : items) { + final String prefix = text.substring(start, Math.max(start, itemStart( + item, text, caret))); + final String inserted = newText(item); + final boolean callable = isSnippet(item) && inserted.indexOf('(') > 0; + if (callable) { + final String name = prefix + inserted.substring(0, inserted.indexOf( + '(')); + final List<String> params = parameters(item, inserted); + overloads.computeIfAbsent(name, k -> new ArrayList<>()).add(params); + converted.add(new Object[] { item, name, params }); + } + else converted.add(new Object[] { item, prefix + plain(inserted), null }); + } + final int n = converted.size(); + final List<Completion> out = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + final CompletionItem item = (CompletionItem) converted.get(i)[0]; + final String name = (String) converted.get(i)[1]; + @SuppressWarnings("unchecked") + final List<String> params = (List<String>) converted.get(i)[2]; + final Supplier<String> description = () -> describe(item); + final List<org.scijava.code.api.Completion.TextEdit> edits = edits(item, + text); + final Completion c; + if (params != null) { + final String returnType = item.getLabelDetails() == null ? null : item + .getLabelDetails().getDescription(); + final SciJavaFunctionCompletion fc = new SciJavaFunctionCompletion(this, + name, returnType == null ? "" : returnType, edits); + fc.setReturnValueDescription(returnType == null ? "" : returnType); + fc.setShortDescription(item.getDetail()); + fc.setDescription(description); + fc.setParams(rstaParams(comp, name, params, overloads.get(name))); + fc.setRelevance(n - i); + c = fc; + } + else { + final SciJavaCompletion sc = new SciJavaCompletion(this, name, item + .getDetail(), description, edits); + sc.setRelevance(n - i); + c = sc; + } + out.add(c); + } + return out; } /** - * Converts {@code c}'s neutral parameters to RSTA ones. RSTA's parameter + * Converts parameters (e.g. {@code double a}) to RSTA's. RSTA's parameter * tooltip shows the current parameter's description, as HTML, below the - * signature: there, we list {@code c}'s other overloads. + * signature: there, we list the other overloads. */ - private List<ParameterizedCompletion.Parameter> toRSTAParams( - final JTextComponent comp, final org.scijava.code.api.Completion c, - final List<org.scijava.code.api.Completion> overloads) + private List<ParameterizedCompletion.Parameter> rstaParams( + final JTextComponent comp, final String name, final List<String> params, + final List<List<String>> overloads) { - final List<org.scijava.code.api.Completion.Parameter> params = c - .parameters(); - final List<ParameterizedCompletion.Parameter> out = - new ArrayList<>(params.size()); - for (final org.scijava.code.api.Completion.Parameter p : params) { - // Pass the type as the parameter's "type object" so the choices - // provider can dispatch on it. - out.add(new ParameterizedCompletion.Parameter(p.type(), p.name()) { + final List<ParameterizedCompletion.Parameter> out = new ArrayList<>(); + for (final String p : params) { + final String[] typeAndName = SignatureLabel.typeAndName(p); + out.add(new ParameterizedCompletion.Parameter(typeAndName[0], + typeAndName[1]) + { /** * Computed when RSTA asks, so the list reflects the arguments typed @@ -517,7 +534,7 @@ private List<ParameterizedCompletion.Parameter> toRSTAParams( */ @Override public String getDescription() { - return otherOverloads(c, overloads, signatureHelp(comp)); + return otherOverloads(name, params, overloads, signatureHelp(comp)); } }); } @@ -525,41 +542,130 @@ public String getDescription() { } /** - * Bridges a neutral {@link ParameterChoices} to RSTA's - * {@link ParameterChoicesProvider}, translating between the two parameter and - * completion representations. + * A callable's parameters: from its label details (typed, e.g. + * {@code (double a, double b)}), if they agree with its snippet's + * placeholders; otherwise the placeholders' names. */ - private final class NeutralChoicesProvider implements - ParameterChoicesProvider + private static List<String> parameters(final CompletionItem item, + final String snippet) { + final List<String> names = new ArrayList<>(); + final Matcher m = PLACEHOLDER.matcher(snippet); + while (m.find()) { + final String name = m.group(2) != null ? m.group(2) : "arg" + names + .size(); + names.add(unescape(name)); + } + final String detail = item.getLabelDetails() == null ? null : item + .getLabelDetails().getDetail(); + if (detail != null) { + final List<String> typed = SignatureLabel.parameters(detail); + if (typed.size() == names.size()) return typed; + } + return names; + } - private final ParameterChoices choices; + /** Gets an item's documentation (resolving it first), as HTML. */ + private String describe(final CompletionItem item) { + if (item.getDocumentation() != null) return html(item.getDocumentation()); + try { + final CompletionItem resolved = sync.server().getTextDocumentService() + .resolveCompletionItem(item).get(DESCRIBE_TIMEOUT, + TimeUnit.MILLISECONDS); + return resolved == null ? null : html(resolved.getDocumentation()); + } + catch (final Exception exc) { + return null; // NB: Not in time, or failed. + } + } - NeutralChoicesProvider(final ParameterChoices choices) { - this.choices = choices; + private static List<org.scijava.code.api.Completion.TextEdit> edits( + final CompletionItem item, final String text) + { + if (item.getAdditionalTextEdits() == null) return Collections.emptyList(); + final List<org.scijava.code.api.Completion.TextEdit> out = + new ArrayList<>(); + for (final TextEdit e : item.getAdditionalTextEdits()) { + out.add(new org.scijava.code.api.Completion.TextEdit(DocumentSync.offset( + text, e.getRange().getStart()), DocumentSync.offset(text, e.getRange() + .getEnd()), e.getNewText())); } + return out; + } - @Override - public List<Completion> getParameterChoices(final JTextComponent tc, - final ParameterizedCompletion.Parameter param) - { - final org.scijava.code.api.Completion.Parameter neutral = - new org.scijava.code.api.Completion.Parameter(param.getName(), - param.getType()); - final List<org.scijava.code.api.Completion> result; - try { - result = choices.choicesFor(neutral); - } - catch (final Exception exc) { - return Collections.emptyList(); - } - if (result == null || result.isEmpty()) return Collections.emptyList(); - final List<Completion> out = new ArrayList<>(result.size()); - for (final org.scijava.code.api.Completion c : result) { - out.add(new BasicCompletion(SciJavaCompletionProvider.this, - c.insertionText(), c.summary(), c.description())); - } - return out; + private static List<CompletionItem> items(final CompletionList list) { + return list == null || list.getItems() == null ? Collections.emptyList() + : list.getItems(); + } + + /** The items in the server's order (by sortText, or else label). */ + private static List<CompletionItem> sorted(final CompletionList list) { + final List<CompletionItem> items = new ArrayList<>(items(list)); + items.sort(Comparator.comparing(i -> i.getSortText() != null ? i + .getSortText() : i.getLabel())); + return items; + } + + private static String newText(final CompletionItem item) { + if (item.getTextEdit() != null) { + return item.getTextEdit().isLeft() ? item.getTextEdit().getLeft() + .getNewText() : item.getTextEdit().getRight().getNewText(); } + return item.getInsertText() != null ? item.getInsertText() : item + .getLabel(); + } + + private static boolean isSnippet(final CompletionItem item) { + return item.getInsertTextFormat() == InsertTextFormat.Snippet; + } + + /** A snippet's text without its placeholders' markup. */ + private static String plain(final String snippet) { + final Matcher m = PLACEHOLDER.matcher(snippet); + final StringBuffer sb = new StringBuffer(); + while (m.find()) { + m.appendReplacement(sb, Matcher.quoteReplacement(m.group(2) == null ? "" + : unescape(m.group(2)))); + } + m.appendTail(sb); + return sb.toString(); + } + + private static String unescape(final String s) { + return s.replaceAll("\\\\(.)", "$1"); + } + + /** + * True iff the help is about calls to the named callable, rather than, + * e.g., a call nested in its arguments. + */ + private static boolean isFor(final SignatureHelp help, final String name) { + if (help.getSignatures() == null || help.getSignatures().isEmpty()) { + return false; + } + final String simple = name.substring(name.lastIndexOf('.') + 1); + for (final SignatureInformation s : help.getSignatures()) { + final String n = SignatureLabel.of(s).name; + if (!n.substring(n.lastIndexOf('.') + 1).equals(simple)) return false; + } + return true; + } + + /** + * A short parameter list such as {@code long[] pos, int d}, or {@code ()} if + * there are no parameters. + */ + private static String parameterList(final List<String> params) { + if (params.isEmpty()) return "()"; + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < params.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(SignatureLabel.simple(params.get(i))); + } + return sb.toString(); + } + + private static String escapeHTML(final String s) { + return s.replace("&", "&").replace("<", "<").replace(">", ">"); } } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SignatureLabel.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignatureLabel.java new file mode 100644 index 00000000..90f92e9c --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignatureLabel.java @@ -0,0 +1,144 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.lsp4j.ParameterInformation; +import org.eclipse.lsp4j.SignatureInformation; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.jsonrpc.messages.Tuple; + +/** + * A callable's signature as the editor shows it, read from a server's text: + * its name, its parameters (e.g. {@code double a}) and its return type; from + * a {@link SignatureInformation} (e.g. {@code max(double a, double b) -> + * double}) or a completion's label details (e.g. {@code (double a, double b)}). + * + * @author Gabriel Selzer + */ +final class SignatureLabel { + + final String name; + final List<String> parameters; + final String returnType; + + private SignatureLabel(final String name, final List<String> parameters, + final String returnType) + { + this.name = name; + this.parameters = parameters; + this.returnType = returnType; + } + + /** Reads a signature (e.g. {@code def max(a, b) -> int}). */ + static SignatureLabel of(final SignatureInformation s) { + final String label = s.getLabel(); + final int open = label.indexOf('('); + final String head = (open < 0 ? label : label.substring(0, open)).trim(); + final String name = head.substring(head.lastIndexOf(' ') + 1); + final List<String> params = new ArrayList<>(); + if (s.getParameters() != null && !s.getParameters().isEmpty()) { + for (final ParameterInformation p : s.getParameters()) { + params.add(label(label, p.getLabel())); + } + } + else if (open >= 0) params.addAll(parameters(label.substring(open))); + final int close = open < 0 ? -1 : closing(label, open); + String returns = close < 0 ? "" : label.substring(close + 1).trim(); + if (returns.startsWith("->")) returns = returns.substring(2).trim(); + else if (returns.startsWith(":")) returns = returns.substring(1).trim(); + return new SignatureLabel(name, params, returns.isEmpty() ? null : returns); + } + + /** + * Reads a parameter list, e.g. {@code (double a, Map<K, V> m)}: its + * parameters, split at the commas outside brackets. + */ + static List<String> parameters(final String list) { + final String s = list.trim(); + if (!s.startsWith("(")) return Collections.emptyList(); + final int close = closing(s, 0); + final String inner = s.substring(1, close < 0 ? s.length() : close); + final List<String> out = new ArrayList<>(); + int depth = 0, start = 0; + for (int i = 0; i < inner.length(); i++) { + final char c = inner.charAt(i); + if (c == '(' || c == '[' || c == '{' || c == '<') depth++; + else if (c == ')' || c == ']' || c == '}' || c == '>') depth--; + else if (c == ',' && depth == 0) { + out.add(inner.substring(start, i).trim()); + start = i + 1; + } + } + final String last = inner.substring(start).trim(); + if (!last.isEmpty() || !out.isEmpty()) out.add(last); + return out; + } + + /** + * A parameter (e.g. {@code java.lang.String name}) with simple type names + * ({@code String name}). + */ + static String simple(final String parameter) { + final int space = parameter.lastIndexOf(' '); + if (space < 0) return parameter; + final String type = parameter.substring(0, space); + return type.substring(type.lastIndexOf('.') + 1) + parameter.substring( + space); + } + + /** Splits a parameter into its type (or null) and its name. */ + static String[] typeAndName(final String parameter) { + final int space = parameter.lastIndexOf(' '); + return space < 0 ? new String[] { null, parameter } : new String[] { + parameter.substring(0, space).trim(), parameter.substring(space + 1) }; + } + + private static String label(final String signature, + final Either<String, Tuple.Two<Integer, Integer>> label) + { + if (label.isLeft()) return label.getLeft(); + return signature.substring(label.getRight().getFirst(), label.getRight() + .getSecond()); + } + + private static int closing(final String s, final int open) { + int depth = 0; + for (int i = open; i < s.length(); i++) { + final char c = s.charAt(i); + if (c == '(') depth++; + else if (c == ')' && --depth == 0) return i; + } + return -1; + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java index 58c1a9c5..b9bfd00b 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java @@ -53,12 +53,14 @@ import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; -import org.scijava.code.api.Completion; -import org.scijava.code.api.SignatureHelp; +import org.eclipse.lsp4j.SignatureHelp; +import org.eclipse.lsp4j.SignatureInformation; +import org.scijava.code.api.Signatures; +import org.scijava.code.lsp.RatedSignatureInformation; /** - * Shows the signatures of the call being typed (see - * {@link org.scijava.code.api.CodeCompleter#signatureHelp}), e.g. all + * Shows the signatures of the call being typed (the language server's + * signature help), e.g. all * overloads of {@code max} once {@code Math.max(} is typed: best fits first, * the parameter being typed in bold. It follows the caret through the call, * and closes once the caret leaves it, or on Escape. @@ -155,12 +157,15 @@ public void hide() { void refresh(final boolean opening) { if (!textArea.isShowing()) return; final SignatureHelp h = help.apply(textArea); - if (h == null || h.isEmpty()) { + if (h == null || h.getSignatures() == null || h.getSignatures() + .isEmpty()) + { hide(); if (opening) retry.restart(); return; } - show(html(h), h.callStart()); + show(html(h), Signatures.callStart(textArea.getText(), textArea + .getCaretPosition())); } /** Renders signatures as HTML, one per line, best fits first. */ @@ -169,11 +174,14 @@ static String html(final SignatureHelp help) { .dimColor() + "\">"; final StringBuilder sb = new StringBuilder("<html>"); boolean first = true; - for (final SignatureHelp.Signature s : help.signatures()) { + for (final SignatureInformation s : help.getSignatures()) { if (!first) sb.append("<br>"); first = false; - final String sig = signature(s.callable(), help.activeParameter()); - switch (s.fit()) { + final Integer active = s.getActiveParameter() != null ? s + .getActiveParameter() : help.getActiveParameter(); + final String sig = signature(SignatureLabel.of(s), active == null ? -1 + : active); + switch (RatedSignatureInformation.fitOf(s)) { case CONVERSION: sb.append(dim).append(sig).append("</font>"); break; @@ -188,28 +196,17 @@ static String html(final SignatureHelp help) { } /** Renders one signature, e.g. {@code max(double a, <b>double b</b>)}. */ - private static String signature(final Completion c, final int active) { - String name = c.insertionText(); - if (name.endsWith("()")) name = name.substring(0, name.length() - 2); - final StringBuilder sb = new StringBuilder(escape(name)).append('('); - final List<Completion.Parameter> params = c.parameters(); + private static String signature(final SignatureLabel s, final int active) { + final StringBuilder sb = new StringBuilder(escape(s.name)).append('('); + final List<String> params = s.parameters; for (int i = 0; i < params.size(); i++) { if (i > 0) sb.append(", "); - final Completion.Parameter p = params.get(i); - final String type = p.type(); - final StringBuilder param = new StringBuilder(); - if (type != null) param.append(type.substring(type.lastIndexOf('.') + - 1)); - if (p.name() != null) { - if (type != null) param.append(' '); - param.append(p.name()); - } - final String text = escape(param.toString()); + final String text = escape(SignatureLabel.simple(params.get(i))); sb.append(i == active ? "<b>" + text + "</b>" : text); } sb.append(')'); - if (c.returnType() != null) sb.append(" → ").append(escape(c - .returnType())); + if (s.returnType != null) sb.append(" \u2192 ").append(escape( + s.returnType)); return sb.toString(); } diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java index 938d958e..ee9012bc 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -48,13 +48,13 @@ import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; import org.junit.Test; import org.scijava.code.api.CodeCompleter; +import org.scijava.code.lsp.compat.CodeCompleterLanguageServer; import org.scijava.code.api.Completion; import org.scijava.code.api.CompletionRequest; import org.scijava.code.api.CompletionResult; import org.scijava.code.api.Diagnostic; import org.scijava.code.api.Hover; import org.scijava.code.api.SignatureHelp; -import org.scijava.code.api.SignatureHelp.Fit; /** * Tests {@link HoverToolTipSupplier}, {@link DiagnosticsParser} and @@ -78,8 +78,9 @@ public Hover hover(final CompletionRequest request) { at - 2) ? new Hover("<pre>Serialize.</pre>", 0, 5) : Hover.NONE; } }; - final HoverToolTipSupplier supplier = new HoverToolTipSupplier(completer, - null, null, null); + final HoverToolTipSupplier supplier = new HoverToolTipSupplier( + new DocumentSync(new CodeCompleterLanguageServer(completer, null), null), + null); final String text = "x.dumps(1)"; assertEquals("<html><pre>Serialize.</pre></html>", supplier.toolTip(text, 3)); @@ -108,27 +109,19 @@ public void testTruncate() { @Test public void testDiagnostics() throws Exception { - final CompletableFuture<List<Diagnostic>> found = new CompletableFuture<>(); - final CodeCompleter completer = new FakeCompleter() { - - @Override - public CompletableFuture<List<Diagnostic>> diagnose( - final CompletionRequest request) - { - return found; - } - }; final RSyntaxTextArea[] area = { null }; SwingUtilities.invokeAndWait(() -> area[0] = new RSyntaxTextArea( "x = 1\ny = (1 +\n")); - final DiagnosticsParser parser = new DiagnosticsParser(area[0], completer, - null, null, null); + final DiagnosticsParser parser = new DiagnosticsParser(area[0]); final RSyntaxDocument doc = (RSyntaxDocument) area[0].getDocument(); - // Asked; no answer yet: no problems. + // None published yet: no problems. assertTrue(parser.parse(doc, null).getNotices().isEmpty()); - // The answer: a problem on the second line. - found.complete(Collections.singletonList(new Diagnostic(10, 12, - Diagnostic.Severity.ERROR, "Unclosed (", "fake"))); + // Published: a problem on the second line. + final org.eclipse.lsp4j.Diagnostic d = new org.eclipse.lsp4j.Diagnostic( + new org.eclipse.lsp4j.Range(new org.eclipse.lsp4j.Position(1, 4), + new org.eclipse.lsp4j.Position(1, 6)), "Unclosed ("); + d.setSeverity(org.eclipse.lsp4j.DiagnosticSeverity.Error); + parser.accept(Collections.singletonList(d)); SwingUtilities.invokeAndWait(() -> {}); final ParseResult result = parser.parse(doc, null); assertEquals(1, result.getNotices().size()); @@ -149,15 +142,30 @@ public void testSignaturesHTML() { final Completion maxInt = Completion.builder("max").kind( Completion.Kind.METHOD).parameters(Arrays.asList(new Completion.Parameter( "a", "int"), new Completion.Parameter("b", "int"))).build(); - final String html = SignaturePopup.html(new SignatureHelp(Arrays.asList( - new SignatureHelp.Signature(max, Fit.MATCH), new SignatureHelp.Signature( - maxInt, Fit.MISMATCH)), 1, 8)); + final String html = SignaturePopup.html(new org.eclipse.lsp4j.SignatureHelp( + Arrays.asList(rated("max(double a, double b) -> double", + org.scijava.code.lsp.RatedSignatureInformation.Fit.MATCH, "double a", + "double b"), rated("max(int a, int b)", + org.scijava.code.lsp.RatedSignatureInformation.Fit.MISMATCH, "int a", + "int b")), 0, 1)); // The parameter being typed in bold; mismatches struck out. assertTrue(html, html.startsWith( "<html>max(double a, <b>double b</b>) → double<br>")); assertTrue(html, html.contains("<s>max(int a, <b>int b</b>)</s>")); } + private static org.eclipse.lsp4j.SignatureInformation rated( + final String label, final org.scijava.code.lsp.RatedSignatureInformation.Fit fit, + final String... params) + { + final List<org.eclipse.lsp4j.ParameterInformation> ps = + new java.util.ArrayList<>(); + for (final String p : params) { + ps.add(new org.eclipse.lsp4j.ParameterInformation(p)); + } + return new org.scijava.code.lsp.RatedSignatureInformation(label, ps, fit); + } + /** A completer completing nothing. */ private static class FakeCompleter implements CodeCompleter { diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index 3dd30f26..c8df5892 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -106,11 +106,11 @@ public void testAdditionalEditsSurfaceAsSciJavaCompletion() { @Test public void testCallablesBecomeFunctionCompletionsWithChoices() { final RSyntaxTextArea area = new RSyntaxTextArea(); - area.setText("addRoi("); + area.setText("addR"); area.setCaretPosition(area.getDocument().getLength()); - // A callable completion taking one ij.gui.Roi parameter, plus a - // parameter-choices resolver that offers an in-scope variable for it. + // A callable completion taking one ij.gui.Roi parameter; and, inside its + // argument list, an in-scope variable fitting it (and a method). final org.scijava.code.api.Completion.Parameter roiParam = new org.scijava.code.api.Completion.Parameter("roi", "ij.gui.Roi"); final org.scijava.code.api.Completion method = @@ -118,14 +118,13 @@ public void testCallablesBecomeFunctionCompletionsWithChoices() { .kind(org.scijava.code.api.Completion.Kind.METHOD) .parameters(java.util.Collections.singletonList(roiParam)) .returnType("void").build(); - final org.scijava.code.api.ParameterChoices choices = p -> // - "ij.gui.Roi".equals(p.type()) - ? java.util.Collections.singletonList( - org.scijava.code.api.Completion.of("myRoi")) - : java.util.Collections.emptyList(); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - request -> new CompletionResult(java.util.Collections.singletonList( - method), area.getDocument().getLength(), choices), + request -> request.lineToCaret().endsWith("(") // + ? new CompletionResult(Arrays.asList( // + org.scijava.code.api.Completion.builder("myRoi").kind( + org.scijava.code.api.Completion.Kind.VARIABLE).build(), method), + request.offset()) // + : new CompletionResult(java.util.Collections.singletonList(method), 0), null); // Callables surface as FunctionCompletions so parameter assistance works. @@ -134,7 +133,9 @@ public void testCallablesBecomeFunctionCompletionsWithChoices() { org.junit.Assert.assertTrue( completions.get(0) instanceof SciJavaFunctionCompletion); - // And the neutral ParameterChoices drives RSTA's parameter choices. + // Parameter choices: the variables completed at the parameter. + area.setText("addRoi("); + area.setCaretPosition(area.getDocument().getLength()); final List<Completion> roiChoices = provider.parameterChoices(area, new org.fife.ui.autocomplete.ParameterizedCompletion.Parameter( "ij.gui.Roi", "roi")); @@ -374,8 +375,8 @@ public void testPrepareOnInstall() { area.setText("import numpy\n"); final org.scijava.code.api.CompletionRequest[] prepared = { null }; final java.io.File file = new java.io.File("scripts", "blur.py"); - final CodeCompleterLanguageSupport support = - new CodeCompleterLanguageSupport(new org.scijava.code.api.CodeCompleter() + final LanguageServerLanguageSupport support = + new LanguageServerLanguageSupport(new org.scijava.code.api.CodeCompleter() { @Override @@ -394,7 +395,7 @@ public void prepare( }, null, null, () -> file); support.install(area); assertEquals("import numpy\n", prepared[0].text()); - assertEquals(file.getPath(), prepared[0].path()); + assertEquals(file.getAbsolutePath(), prepared[0].path()); support.uninstall(area); } @@ -433,7 +434,7 @@ public void testRequestCarriesScriptPath() { area.setText("xy"); area.setCaretPosition(2); provider.getCompletionsImpl(area); - assertEquals(file[0].getPath(), path[0]); + assertEquals(file[0].getAbsolutePath(), path[0]); } private static org.scijava.code.api.SignatureHelp.Signature signature( From bba9c03bb68dccb7500db9db78d34764655fac20 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Wed, 7 Oct 2026 13:22:08 -0500 Subject: [PATCH 11/21] Move off scijava-code-api, now removed Completions' additional edits are AdditionalEdits.Edit; the REPL prompt completes via the LanguageServerService (or a BindingsLanguageServer for languages without servers); the tests use fake language servers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- pom.xml | 14 +- .../scijava/ui/swing/script/EditorPane.java | 3 - .../scijava/ui/swing/script/PromptPane.java | 145 +++++- .../autocompletion/AdditionalEdits.java | 36 +- .../script/autocompletion/ClassUtil.java | 4 +- .../script/autocompletion/DocumentSync.java | 6 +- .../LanguageServerLanguageSupport.java | 13 - .../autocompletion/SciJavaAutoCompletion.java | 17 +- .../autocompletion/SciJavaCompletion.java | 14 +- .../SciJavaCompletionProvider.java | 19 +- .../SciJavaFunctionCompletion.java | 12 +- .../script/autocompletion/SignaturePopup.java | 2 +- .../CodeIntelligenceUITest.java | 53 +- .../SciJavaCompletionProviderTest.java | 471 ++++++++++-------- 14 files changed, 472 insertions(+), 337 deletions(-) diff --git a/pom.xml b/pom.xml index 4627aa59..822ee664 100644 --- a/pom.xml +++ b/pom.xml @@ -141,11 +141,10 @@ <properties> <scijava.jvm.version>11</scijava.jvm.version> - <!-- TEMP: in-development versions providing the code intelligence SPI. - scijava-code-api needs ParameterScriptProcessor#setLogger from - scijava-common. Remove once releases are pinned by pom-scijava. --> + <!-- TEMP: in-development versions providing code intelligence. + Remove once releases are pinned by pom-scijava. --> <scijava-common.version>2.100.2-SNAPSHOT</scijava-common.version> - <scijava-code-api.version>0.1.0-SNAPSHOT</scijava-code-api.version> + <scijava-code-lsp.version>0.1.0-SNAPSHOT</scijava-code-lsp.version> <main-class>org.scijava.ui.swing.script.Main</main-class> <package-name>org.scijava.ui.swing.script</package-name> <license.licenseName>bsd_2</license.licenseName> @@ -161,15 +160,10 @@ <groupId>org.scijava</groupId> <artifactId>scijava-common</artifactId> </dependency> - <dependency> - <groupId>org.scijava</groupId> - <artifactId>scijava-code-api</artifactId> - <version>${scijava-code-api.version}</version> - </dependency> <dependency> <groupId>org.scijava</groupId> <artifactId>scijava-code-lsp</artifactId> - <version>${scijava-code-api.version}</version> + <version>${scijava-code-lsp.version}</version> </dependency> <dependency> <groupId>org.scijava</groupId> diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index a900fc43..3f2c987a 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -94,7 +94,6 @@ import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptService; import org.scijava.ui.swing.script.vim.VimHandler; -import org.scijava.code.api.CodeCompletionService; import org.scijava.code.lsp.LanguageServerService; import org.scijava.ui.swing.script.autocompletion.LanguageServerLanguageSupport; import org.scijava.util.FileUtils; @@ -138,8 +137,6 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { @Parameter private LanguageSupportService languageSupportService; @Parameter - private CodeCompletionService codeCompletionService; - @Parameter private LanguageServerService languageServerService; @Parameter private ScriptService scriptService; diff --git a/src/main/java/org/scijava/ui/swing/script/PromptPane.java b/src/main/java/org/scijava/ui/swing/script/PromptPane.java index 7c97d122..63def684 100644 --- a/src/main/java/org/scijava/ui/swing/script/PromptPane.java +++ b/src/main/java/org/scijava/ui/swing/script/PromptPane.java @@ -38,20 +38,36 @@ import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.swing.JTextArea; import javax.swing.text.BadLocationException; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.InsertTextFormat; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentItem; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.services.TextDocumentService; import org.scijava.Context; import org.scijava.script.ScriptInterpreter; import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptREPL; -import org.scijava.code.api.CodeCompletionService; -import org.scijava.code.api.Completion; -import org.scijava.code.api.CompletionRequest; -import org.scijava.code.api.CompletionResult; +import org.scijava.code.lsp.BindingsLanguageServer; +import org.scijava.code.lsp.LanguageServerService; +import org.scijava.code.lsp.Positions; +import org.scijava.code.lsp.UpdatingCompletionList; import org.scijava.thread.ThreadService; import org.scijava.util.ClassUtils; import org.scijava.util.Types; @@ -65,6 +81,13 @@ */ public abstract class PromptPane implements UIComponent<JTextArea> { + /** The prompt's text, as the servers know it. */ + private static final String PROMPT_URI = "untitled:/prompt"; + + /** How long Tab waits for completions, in milliseconds. */ + private static final long COMPLETION_WAIT = 2000; + + private final ScriptREPL repl; private final VarsPane vars; private final TextArea textArea; @@ -204,57 +227,131 @@ private Context context() { } /** - * Tab-completion for the REPL prompt. Delegates to the language-agnostic - * {@link CodeCompletionService}, which inspects the live engine bindings, so - * every scripting language gets at least baseline completion here. + * Tab-completion for the REPL prompt: what the language's servers know (see + * {@link LanguageServerService}), with the interpreter's live variables; or, + * for languages without servers, its live variables alone (see + * {@link BindingsLanguageServer}). So every scripting language gets at + * least baseline completion here. */ private void complete() { final ScriptInterpreter interpreter = repl.getInterpreter(); if (interpreter == null) return; final ScriptEngine engine = interpreter.getEngine(); final ScriptLanguage language = interpreter.getLanguage(); + final ScriptContext live = engine == null ? null : engine.getContext(); final int caret = textArea.getCaretPosition(); final String text = textArea.getText(); - final CompletionResult result; + final List<CompletionItem> items; try { - result = context().service(CodeCompletionService.class).complete(// - new CompletionRequest(text, caret, language, engine, // - engine == null ? null : engine.getContext())); + items = items(language, live, text, caret); } catch (final Exception exc) { return; } - - final List<Completion> completions = result.completions(); - if (completions.isEmpty()) { + if (items.isEmpty()) { textArea.getToolkit().beep(); return; } - final int start = Math.min(Math.max(result.replaceStart(), 0), caret); + final int start = Math.min(Math.max(start(items.get(0), text, caret), 0), + caret); - if (completions.size() == 1) { - replaceRange(start, caret, completions.get(0).insertionText()); + if (items.size() == 1) { + replaceRange(start, caret, insertion(items.get(0))); return; } // Multiple candidates: insert their longest common prefix (if it extends // what is already typed), and list the options in the output pane. - final String lcp = longestCommonPrefix(completions); + final String lcp = longestCommonPrefix(items); final String current = text.substring(start, caret); if (lcp.length() > current.length()) { replaceRange(start, caret, lcp); } else { final StringBuilder sb = new StringBuilder(); - for (final Completion c : completions) { + for (final CompletionItem item : items) { if (sb.length() > 0) sb.append(" "); - sb.append(c.displayText()); + sb.append(item.getLabel()); + if (item.getLabelDetails() != null && item.getLabelDetails() + .getDetail() != null) sb.append(item.getLabelDetails().getDetail()); } output.append(sb.append("\n").toString()); } } + /** Asks the language's servers (or the live variables) for completions. */ + private List<CompletionItem> items(final ScriptLanguage language, + final ScriptContext live, final String text, final int caret) + throws Exception + { + final LanguageServerService servers = context().service( + LanguageServerService.class); + final LanguageServer server; + if (servers != null && language != null && servers.supports(language)) { + server = servers.server(language, live); + } + else if (live != null) server = new BindingsLanguageServer(live); + else return Collections.emptyList(); + + final TextDocumentService docs = server.getTextDocumentService(); + docs.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(PROMPT_URI, + language == null ? "" : language.getLanguageName(), 1, text))); + try { + CompletionList list = docs.completion(new CompletionParams( + new TextDocumentIdentifier(PROMPT_URI), Positions.position(text, + caret))).get(COMPLETION_WAIT, TimeUnit.MILLISECONDS).getRight(); + // NB: Tab asks once: wait a moment for slower servers. + if (list instanceof UpdatingCompletionList) { + try { + final CompletionList better = ((UpdatingCompletionList) list) + .update().get(COMPLETION_WAIT, TimeUnit.MILLISECONDS); + if (better != null) list = better; + } + catch (final Exception exc) { + // NB: Not in time: what there is. + } + } + final List<CompletionItem> items = new ArrayList<>(list.getItems()); + items.sort(Comparator.comparing(item -> item.getSortText() != null ? item + .getSortText() : item.getLabel())); + return items; + } + finally { + docs.didClose(new DidCloseTextDocumentParams(new TextDocumentIdentifier( + PROMPT_URI))); + } + } + + /** Where a completion's text starts: what it replaces, up to the caret. */ + private static int start(final CompletionItem item, final String text, + final int caret) + { + if (item.getTextEdit() == null) return caret; + final Range range = item.getTextEdit().isLeft() ? item.getTextEdit() + .getLeft().getRange() : item.getTextEdit().getRight().getInsert(); + return Positions.offset(text, range.getStart()); + } + + /** + * The text a completion inserts; for a callable (a snippet), up to its + * first parameter, e.g. {@code max(}. + */ + private static String insertion(final CompletionItem item) { + String text = item.getTextEdit() == null ? null : item.getTextEdit() + .isLeft() ? item.getTextEdit().getLeft().getNewText() : item + .getTextEdit().getRight().getNewText(); + if (text == null) text = item.getInsertText(); + if (text == null) text = item.getLabel(); + if (item.getInsertTextFormat() == InsertTextFormat.Snippet) { + final int stop = text.indexOf('$'); + if (stop >= 0) text = text.substring(0, stop); + text = text.replace("\\$", "$").replace("\\}", "}").replace("\\\\", + "\\"); + } + return text; + } + private void replaceRange(final int start, final int end, final String replacement) { @@ -267,10 +364,10 @@ private void replaceRange(final int start, final int end, } } - private static String longestCommonPrefix(final List<Completion> completions) { - String prefix = completions.get(0).insertionText(); - for (final Completion c : completions) { - final String s = c.insertionText(); + private static String longestCommonPrefix(final List<CompletionItem> items) { + String prefix = insertion(items.get(0)); + for (final CompletionItem item : items) { + final String s = insertion(item); int i = 0; final int max = Math.min(prefix.length(), s.length()); while (i < max && prefix.charAt(i) == s.charAt(i)) i++; diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java index 728c3fb5..04ef7671 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/AdditionalEdits.java @@ -31,12 +31,10 @@ import java.util.List; -import org.scijava.code.api.Completion.TextEdit; - /** - * Implemented by RSTA completions that carry - * {@link org.scijava.code.api.Completion#additionalEdits() additional - * edits} (e.g. auto-imports) to apply when accepted. Both the + * Implemented by RSTA completions that carry additional edits (e.g. + * auto-imports, a completion's {@code additionalTextEdits}) to apply when + * accepted. Both the * {@code BasicCompletion}-based and {@code FunctionCompletion}-based SciJava * completions implement this, so {@link SciJavaAutoCompletion} can apply the * edits regardless of the underlying RSTA completion type. @@ -46,5 +44,31 @@ public interface AdditionalEdits { /** Extra edits to apply when the completion is accepted; never null. */ - List<TextEdit> getAdditionalEdits(); + List<Edit> getAdditionalEdits(); + + /** Replaces the text between two offsets. */ + final class Edit { + + private final int start; + private final int end; + private final String newText; + + public Edit(final int start, final int end, final String newText) { + this.start = start; + this.end = end; + this.newText = newText; + } + + public int start() { + return start; + } + + public int end() { + return end; + } + + public String newText() { + return newText; + } + } } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java index ad23172e..cdfe0049 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java @@ -39,10 +39,10 @@ import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.CompletionProvider; -import org.scijava.code.api.ClassIndex; +import org.scijava.code.lsp.ClassIndex; /** - * Thin facade over the toolkit-agnostic {@link ClassIndex} in scijava-code-api, + * Thin facade over the toolkit-agnostic {@link ClassIndex} in scijava-code-lsp, * retained for the script editor's Swing-specific completion code. New code * should use {@link ClassIndex} directly; only the RSTA-specific * {@link #classUnavailableCompletions} helper is genuinely editor-side. diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java index 9604d940..bdebe618 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java @@ -44,7 +44,7 @@ import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.services.LanguageServer; -import org.scijava.code.lsp.LspClient; +import org.scijava.code.lsp.Positions; import org.scijava.script.ScriptLanguage; /** @@ -136,13 +136,13 @@ public synchronized String text() { /** The position of an offset of the given text, for requests. */ public static Position position(final String text, final int offset) { - return LspClient.position(text, Math.max(0, Math.min(offset, text + return Positions.position(text, Math.max(0, Math.min(offset, text .length()))); } /** The offset of a position in the given text. */ public static int offset(final String text, final Position position) { - return LspClient.offset(text, position); + return Positions.offset(text, position); } private String currentUri() { diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java index 8422642d..e303faee 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java @@ -48,8 +48,6 @@ import org.fife.rsta.ac.AbstractLanguageSupport; import org.fife.ui.autocomplete.AutoCompletion; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; -import org.scijava.code.api.CodeCompleter; -import org.scijava.code.lsp.compat.CodeCompleterLanguageServer; import org.scijava.log.Logger; import org.scijava.script.ScriptLanguage; @@ -99,17 +97,6 @@ public LanguageServerLanguageSupport(final LanguageServer server, setShowDescWindow(true); } - /** - * Uses a single code-api completer. TEMP: Until code-api is removed. - */ - public LanguageServerLanguageSupport(final CodeCompleter completer, - final ScriptLanguage language, final Logger log, - final Supplier<File> file) - { - this(new CodeCompleterLanguageServer(completer, language), language, log, - file); - } - /** * Sets whether to show what the server knows about the code under the * mouse. Applies to later installs. diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java index ff6403a5..6f0b2a2f 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaAutoCompletion.java @@ -39,13 +39,12 @@ import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.CompletionProvider; import org.fife.ui.rtextarea.RTextArea; -import org.scijava.code.api.Completion.TextEdit; +import org.scijava.ui.swing.script.autocompletion.AdditionalEdits.Edit; /** * An {@link org.fife.ui.autocomplete.AutoCompletion} that, in addition to - * inserting the chosen completion, applies any - * {@link org.scijava.code.api.Completion#additionalEdits() additional - * edits} the completion carries (see {@link SciJavaCompletion}). This is how + * inserting the chosen completion, applies any additional edits the + * completion carries (see {@link SciJavaCompletion}). This is how * language-neutral auto-imports are realized in the Swing editor. * * @author Curtis Rueden @@ -92,7 +91,7 @@ protected void insertCompletion(final Completion c, super.insertCompletion(c, typedParamListStartChar); return; } - final List<TextEdit> edits = ((AdditionalEdits) c).getAdditionalEdits(); + final List<Edit> edits = ((AdditionalEdits) c).getAdditionalEdits(); if (edits.isEmpty()) { super.insertCompletion(c, typedParamListStartChar); return; @@ -108,9 +107,9 @@ protected void insertCompletion(final Completion c, // Then apply additional edits. They lie before the completion point, // so applying them from highest to lowest offset keeps each offset // valid, and the caret (tracked by the document) shifts accordingly. - final List<TextEdit> sorted = new ArrayList<>(edits); - sorted.sort(Comparator.comparingInt(TextEdit::start).reversed()); - for (final TextEdit edit : sorted) { + final List<Edit> sorted = new ArrayList<>(edits); + sorted.sort(Comparator.comparingInt(Edit::start).reversed()); + for (final Edit edit : sorted) { applyEdit(comp, edit); } } @@ -119,7 +118,7 @@ protected void insertCompletion(final Completion c, } } - private void applyEdit(final JTextComponent comp, final TextEdit edit) { + private void applyEdit(final JTextComponent comp, final Edit edit) { try { final int len = edit.end() - edit.start(); if (len > 0) comp.getDocument().remove(edit.start(), len); diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java index d9a39f6f..b91bd5c3 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletion.java @@ -34,12 +34,10 @@ import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.CompletionProvider; -import org.scijava.code.api.Completion.TextEdit; /** - * An RSTA {@link BasicCompletion} that also carries - * {@link org.scijava.code.api.Completion#additionalEdits() additional - * edits} to apply on acceptance (e.g. auto-imports). The edits are applied by + * An RSTA {@link BasicCompletion} that also carries additional edits to apply + * on acceptance (e.g. auto-imports). The edits are applied by * {@link SciJavaAutoCompletion}. * * @author Curtis Rueden @@ -48,12 +46,12 @@ public class SciJavaCompletion extends BasicCompletion implements AdditionalEdits { - private final List<TextEdit> additionalEdits; + private final List<Edit> additionalEdits; private final Supplier<String> description; public SciJavaCompletion(final CompletionProvider provider, final String replacementText, final String shortDesc, final String summary, - final List<TextEdit> additionalEdits) + final List<Edit> additionalEdits) { this(provider, replacementText, shortDesc, () -> summary, additionalEdits); @@ -65,7 +63,7 @@ public SciJavaCompletion(final CompletionProvider provider, */ public SciJavaCompletion(final CompletionProvider provider, final String replacementText, final String shortDesc, - final Supplier<String> description, final List<TextEdit> additionalEdits) + final Supplier<String> description, final List<Edit> additionalEdits) { super(provider, replacementText, shortDesc, null); this.description = description; @@ -78,7 +76,7 @@ public String getSummary() { } @Override - public List<TextEdit> getAdditionalEdits() { + public List<Edit> getAdditionalEdits() { return additionalEdits; } } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java index afb07900..4dc610f9 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java @@ -71,11 +71,9 @@ import org.fife.ui.autocomplete.CompletionCellRenderer; import org.fife.ui.autocomplete.DefaultCompletionProvider; import org.fife.ui.autocomplete.ParameterizedCompletion; -import org.scijava.code.api.CodeCompleter; import org.scijava.code.lsp.RatedSignatureInformation; import org.scijava.code.lsp.RatedSignatureInformation.Fit; import org.scijava.code.lsp.UpdatingCompletionList; -import org.scijava.code.lsp.compat.CodeCompleterLanguageServer; import org.scijava.log.Logger; import org.scijava.script.ScriptLanguage; @@ -149,15 +147,6 @@ public SciJavaCompletionProvider(final LanguageServer server, setParameterChoicesProvider(this::parameterChoices); } - /** - * Uses a single code-api completer. TEMP: Until code-api is removed. - */ - public SciJavaCompletionProvider(final CodeCompleter completer, - final ScriptLanguage language) - { - this(new CodeCompleterLanguageServer(completer, language), language); - } - /** The synchronization of the edited script with the server. */ public DocumentSync sync() { return sync; @@ -485,7 +474,7 @@ private List<Completion> convert(final JTextComponent comp, @SuppressWarnings("unchecked") final List<String> params = (List<String>) converted.get(i)[2]; final Supplier<String> description = () -> describe(item); - final List<org.scijava.code.api.Completion.TextEdit> edits = edits(item, + final List<AdditionalEdits.Edit> edits = edits(item, text); final Completion c; if (params != null) { @@ -579,14 +568,14 @@ private String describe(final CompletionItem item) { } } - private static List<org.scijava.code.api.Completion.TextEdit> edits( + private static List<AdditionalEdits.Edit> edits( final CompletionItem item, final String text) { if (item.getAdditionalTextEdits() == null) return Collections.emptyList(); - final List<org.scijava.code.api.Completion.TextEdit> out = + final List<AdditionalEdits.Edit> out = new ArrayList<>(); for (final TextEdit e : item.getAdditionalTextEdits()) { - out.add(new org.scijava.code.api.Completion.TextEdit(DocumentSync.offset( + out.add(new AdditionalEdits.Edit(DocumentSync.offset( text, e.getRange().getStart()), DocumentSync.offset(text, e.getRange() .getEnd()), e.getNewText())); } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java index 82f63e78..610f5602 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaFunctionCompletion.java @@ -35,14 +35,12 @@ import org.fife.ui.autocomplete.CompletionProvider; import org.fife.ui.autocomplete.FunctionCompletion; -import org.scijava.code.api.Completion.TextEdit; /** * An RSTA {@link FunctionCompletion} for callable completions (methods, * functions, constructors), so parameter assistance and parameter choices - * engage. It also carries optional - * {@link org.scijava.code.api.Completion#additionalEdits() additional - * edits} (e.g. the auto-import needed by a constructor completion), applied by + * engage. It also carries optional additional edits (e.g. the auto-import + * needed by a constructor completion), applied by * {@link SciJavaAutoCompletion}. * * @author Curtis Rueden @@ -51,12 +49,12 @@ public class SciJavaFunctionCompletion extends FunctionCompletion implements AdditionalEdits { - private final List<TextEdit> additionalEdits; + private final List<Edit> additionalEdits; private Supplier<String> description; public SciJavaFunctionCompletion(final CompletionProvider provider, final String name, final String returnType, - final List<TextEdit> additionalEdits) + final List<Edit> additionalEdits) { super(provider, name, returnType); this.additionalEdits = additionalEdits == null ? Collections.emptyList() @@ -64,7 +62,7 @@ public SciJavaFunctionCompletion(final CompletionProvider provider, } @Override - public List<TextEdit> getAdditionalEdits() { + public List<Edit> getAdditionalEdits() { return additionalEdits; } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java index b9bfd00b..a812c5c8 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java @@ -55,7 +55,7 @@ import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SignatureInformation; -import org.scijava.code.api.Signatures; +import org.scijava.code.lsp.Signatures; import org.scijava.code.lsp.RatedSignatureInformation; /** diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java index ee9012bc..9f69ff71 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -42,23 +42,21 @@ import javax.swing.SwingUtilities; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.MarkupKind; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.parser.ParseResult; import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; import org.junit.Test; -import org.scijava.code.api.CodeCompleter; -import org.scijava.code.lsp.compat.CodeCompleterLanguageServer; -import org.scijava.code.api.Completion; -import org.scijava.code.api.CompletionRequest; -import org.scijava.code.api.CompletionResult; -import org.scijava.code.api.Diagnostic; -import org.scijava.code.api.Hover; -import org.scijava.code.api.SignatureHelp; +import org.scijava.code.lsp.Environment; +import org.scijava.code.lsp.ScriptLanguageServer; /** * Tests {@link HoverToolTipSupplier}, {@link DiagnosticsParser} and - * {@link SignaturePopup}, with a fake completer. + * {@link SignaturePopup}, with a fake server. * * @author Gabriel Selzer */ @@ -67,20 +65,22 @@ public class CodeIntelligenceUITest { @Test public void testHover() { final AtomicInteger asked = new AtomicInteger(); - final CodeCompleter completer = new FakeCompleter() { + final ScriptLanguageServer server = new FakeServer() { @Override - public Hover hover(final CompletionRequest request) { + protected Hover hover(final Document doc, final int at) { asked.incrementAndGet(); - final String text = request.text(); - final int at = request.offset(); - return text.startsWith("dumps", at - 1) || text.startsWith("dumps", - at - 2) ? new Hover("<pre>Serialize.</pre>", 0, 5) : Hover.NONE; + final String text = doc.text(); + if (!text.startsWith("dumps", at - 1) && !text.startsWith("dumps", at - + 2)) return null; + final Hover hover = new Hover(new MarkupContent(MarkupKind.MARKDOWN, + "<pre>Serialize.</pre>")); + hover.setRange(doc.range(0, 5)); + return hover; } }; final HoverToolTipSupplier supplier = new HoverToolTipSupplier( - new DocumentSync(new CodeCompleterLanguageServer(completer, null), null), - null); + new DocumentSync(server, null), null); final String text = "x.dumps(1)"; assertEquals("<html><pre>Serialize.</pre></html>", supplier.toolTip(text, 3)); @@ -135,13 +135,6 @@ public void testDiagnostics() throws Exception { @Test public void testSignaturesHTML() { - final Completion max = Completion.builder("max").kind( - Completion.Kind.METHOD).parameters(Arrays.asList(new Completion.Parameter( - "a", "double"), new Completion.Parameter("b", "double"))).returnType( - "double").build(); - final Completion maxInt = Completion.builder("max").kind( - Completion.Kind.METHOD).parameters(Arrays.asList(new Completion.Parameter( - "a", "int"), new Completion.Parameter("b", "int"))).build(); final String html = SignaturePopup.html(new org.eclipse.lsp4j.SignatureHelp( Arrays.asList(rated("max(double a, double b) -> double", org.scijava.code.lsp.RatedSignatureInformation.Fit.MATCH, "double a", @@ -166,12 +159,16 @@ private static org.eclipse.lsp4j.SignatureInformation rated( return new org.scijava.code.lsp.RatedSignatureInformation(label, ps, fit); } - /** A completer completing nothing. */ - private static class FakeCompleter implements CodeCompleter { + /** A server completing nothing. */ + private static class FakeServer extends ScriptLanguageServer { + + FakeServer() { + super(Environment.NONE); + } @Override - public CompletionResult complete(final CompletionRequest request) { - return CompletionResult.EMPTY; + protected CompletionList complete(final Document doc, final int offset) { + return new CompletionList(); } } } diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index c8df5892..9a03f10e 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -30,41 +30,70 @@ package org.scijava.ui.swing.script.autocompletion; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import java.io.File; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; - +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SignatureHelp; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.fife.ui.autocomplete.Completion; +import org.fife.ui.autocomplete.ParameterizedCompletion; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.junit.Test; -import org.scijava.code.api.CompletionResult; -import org.scijava.code.api.SignatureHelp.Fit; +import org.scijava.code.lsp.Callable; +import org.scijava.code.lsp.Environment; +import org.scijava.code.lsp.Positions; +import org.scijava.code.lsp.RatedSignatureInformation.Fit; +import org.scijava.code.lsp.ScriptLanguageServer; +import org.scijava.code.lsp.UpdatingCompletionList; /** - * Tests that {@link SciJavaCompletionProvider} faithfully bridges neutral - * {@link org.scijava.code.api.Completion}s to RSTA completions, including - * honoring the completer's reported replacement span. + * Tests that {@link SciJavaCompletionProvider} faithfully bridges a language + * server's completions to RSTA completions, including honoring the range each + * completion replaces. * * @author Curtis Rueden + * @author Gabriel Selzer */ public class SciJavaCompletionProviderTest { @Test - public void testBridgesNeutralCompletions() { + public void testBridgesCompletions() { final RSyntaxTextArea area = new RSyntaxTextArea(); area.setText("foo.ba"); area.setCaretPosition(area.getDocument().getLength()); // offset 6 - // A completer that, regardless of input, suggests two members of "foo", + // A server that, regardless of input, suggests two members of "foo", // replacing the partial token "ba" (which starts at offset 4). - final int replaceStart = 4; + final FakeServer server = new FakeServer(); + server.complete = (doc, caret) -> { + final CompletionItem bar = new CompletionItem("bar"); + bar.setDetail("the bar member"); + return FakeServer.items(doc, 4, caret, bar, new CompletionItem("baz")); + }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - request -> new CompletionResult(Arrays.asList( - org.scijava.code.api.Completion.builder("bar") - .summary("the bar member").build(), - org.scijava.code.api.Completion.builder("baz").build()), - replaceStart), - null); + server, null); // The provider must report the already-entered text as the replaced span. assertEquals("ba", provider.getAlreadyEnteredText(area)); @@ -83,24 +112,27 @@ public void testAdditionalEditsSurfaceAsSciJavaCompletion() { area.setCaretPosition(area.getDocument().getLength()); // A class-name completion that also auto-imports at the top of the file. - final org.scijava.code.api.Completion.TextEdit importEdit = - org.scijava.code.api.Completion.TextEdit.insert(0, - "from ij.gui import Roi\n"); + final FakeServer server = new FakeServer(); + server.complete = (doc, caret) -> { + final CompletionItem roi = new CompletionItem("Roi"); + roi.setAdditionalTextEdits(Collections.singletonList(new TextEdit( + doc.range(0, 0), "from ij.gui import Roi\n"))); + return FakeServer.items(doc, 0, caret, roi); + }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - request -> new CompletionResult(java.util.Collections.singletonList( - org.scijava.code.api.Completion.builder("Roi") - .additionalEdits(java.util.Collections.singletonList(importEdit)) - .build()), - 0), - null); + server, null); final List<Completion> completions = provider.getCompletionsImpl(area); assertEquals(1, completions.size()); // Completions carrying extra edits must be SciJavaCompletions so the // SciJavaAutoCompletion applies their auto-import on acceptance. final Completion c = completions.get(0); - org.junit.Assert.assertTrue(c instanceof SciJavaCompletion); - assertEquals(1, ((SciJavaCompletion) c).getAdditionalEdits().size()); + assertTrue(c instanceof SciJavaCompletion); + final List<AdditionalEdits.Edit> edits = ((SciJavaCompletion) c) + .getAdditionalEdits(); + assertEquals(1, edits.size()); + assertEquals(0, edits.get(0).start()); + assertEquals("from ij.gui import Roi\n", edits.get(0).newText()); } @Test @@ -111,34 +143,31 @@ public void testCallablesBecomeFunctionCompletionsWithChoices() { // A callable completion taking one ij.gui.Roi parameter; and, inside its // argument list, an in-scope variable fitting it (and a method). - final org.scijava.code.api.Completion.Parameter roiParam = - new org.scijava.code.api.Completion.Parameter("roi", "ij.gui.Roi"); - final org.scijava.code.api.Completion method = - org.scijava.code.api.Completion.builder("addRoi") - .kind(org.scijava.code.api.Completion.Kind.METHOD) - .parameters(java.util.Collections.singletonList(roiParam)) - .returnType("void").build(); + final Callable addRoi = new Callable("addRoi", Collections.singletonList( + new Callable.Param("roi", "ij.gui.Roi")), "void").kind( + CompletionItemKind.Method); + final FakeServer server = new FakeServer(); + server.complete = (doc, caret) -> { + if (!doc.lineTo(caret).endsWith("(")) { + return FakeServer.items(doc, 0, caret, addRoi.toItem()); + } + final CompletionItem myRoi = new CompletionItem("myRoi"); + myRoi.setKind(CompletionItemKind.Variable); + return FakeServer.items(doc, caret, caret, myRoi, addRoi.toItem()); + }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - request -> request.lineToCaret().endsWith("(") // - ? new CompletionResult(Arrays.asList( // - org.scijava.code.api.Completion.builder("myRoi").kind( - org.scijava.code.api.Completion.Kind.VARIABLE).build(), method), - request.offset()) // - : new CompletionResult(java.util.Collections.singletonList(method), 0), - null); + server, null); // Callables surface as FunctionCompletions so parameter assistance works. final List<Completion> completions = provider.getCompletionsImpl(area); assertEquals(1, completions.size()); - org.junit.Assert.assertTrue( - completions.get(0) instanceof SciJavaFunctionCompletion); + assertTrue(completions.get(0) instanceof SciJavaFunctionCompletion); // Parameter choices: the variables completed at the parameter. area.setText("addRoi("); area.setCaretPosition(area.getDocument().getLength()); final List<Completion> roiChoices = provider.parameterChoices(area, - new org.fife.ui.autocomplete.ParameterizedCompletion.Parameter( - "ij.gui.Roi", "roi")); + new ParameterizedCompletion.Parameter("ij.gui.Roi", "roi")); assertEquals(1, roiChoices.size()); assertEquals("myRoi", roiChoices.get(0).getReplacementText()); } @@ -150,25 +179,19 @@ public void testCallablesRenderWithSignatures() { area.setText(completionText); area.setCaretPosition(area.getDocument().getLength()); - final org.scijava.code.api.Completion method = - org.scijava.code.api.Completion.builder(completionText) - .kind(org.scijava.code.api.Completion.Kind.METHOD) - .parameters(java.util.Collections.singletonList( - new org.scijava.code.api.Completion.Parameter("pos", "long[]"))) - .returnType("java.lang.Object").build(); + final FakeServer server = new FakeServer(); + server.complete = (doc, caret) -> FakeServer.items(doc, 0, caret, method( + completionText, "long[]").toItem()); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - request -> new CompletionResult(java.util.Collections.singletonList( - method), 0), - null); + server, null); // The popup list must show a callable's parameters, not just its name. final Completion c = provider.getCompletionsImpl(area).get(0); - final javax.swing.JLabel label = (javax.swing.JLabel) provider - .getListCellRenderer().getListCellRendererComponent( - new javax.swing.JList<>(), c, 0, false, false); + final JLabel label = (JLabel) provider.getListCellRenderer() + .getListCellRendererComponent(new JList<>(), c, 0, false, false); final String text = label.getText(); - org.junit.Assert.assertTrue(text, text.contains("long[]")); - org.junit.Assert.assertTrue(text, text.contains("pos")); + assertTrue(text, text.contains("long[]")); + assertTrue(text, text.contains("p0")); } @Test @@ -177,33 +200,33 @@ public void testParameterTooltipListsOtherOverloads() { area.setText("img.getAt"); area.setCaretPosition(area.getDocument().getLength()); + final FakeServer server = new FakeServer(); + server.complete = (doc, caret) -> FakeServer.items(doc, 0, caret, // + method("img.getAt", "int[]").toItem(), // + method("img.getAt", "net.imglib2.Localizable").toItem(), // + method("img.getAt", "int[]").toItem(), // e.g. a bridge method + method("img.getAtX", "long[]").toItem()); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - request -> new CompletionResult(Arrays.asList( // - method("img.getAt", "int[]"), // - method("img.getAt", "net.imglib2.Localizable"), // - method("img.getAt", "int[]"), // e.g. a bridge method - method("img.getAtX", "long[]")), 0), - null); + server, null); final List<Completion> completions = provider.getCompletionsImpl(area); // Each overload's tooltip lists the others, once each, even before RSTA // inserts the argument list... - assertEquals("<hr>Localizable p0", - ((org.fife.ui.autocomplete.ParameterizedCompletion) completions.get(0)) - .getParam(0).getDescription()); + assertEquals("<hr>Localizable p0", ((ParameterizedCompletion) completions + .get(0)).getParam(0).getDescription()); // ...but the side description window does not. final String summary = completions.get(0).getSummary(); - org.junit.Assert.assertFalse(summary, summary.contains("Localizable")); + assertFalse(summary, summary != null && summary.contains("Localizable")); // Inside the argument list, likewise. area.setText("img.getAt(p0)"); area.setCaretPosition(10); - final String desc = ((org.fife.ui.autocomplete.ParameterizedCompletion) // - completions.get(0)).getParam(0).getDescription(); + final String desc = ((ParameterizedCompletion) completions.get(0)) + .getParam(0).getDescription(); assertEquals("<hr>Localizable p0", desc); // ...and a callable with no other overloads gets no description. - org.junit.Assert.assertNull(((org.fife.ui.autocomplete.ParameterizedCompletion) // - completions.get(3)).getParam(0).getDescription()); + assertNull(((ParameterizedCompletion) completions.get(completions.size() - + 1)).getParam(0).getDescription()); } @Test @@ -212,45 +235,33 @@ public void testParameterTooltipShowsSignatureHelp() { area.setText("a"); area.setCaretPosition(1); - final org.scijava.code.api.Completion ints = method("a", "int", "int"); - final org.scijava.code.api.Completion strings = method("a", - "java.lang.String"); - final org.scijava.code.api.Completion floats = method("a", "float", - "float"); - final org.scijava.code.api.Completion longs = method("a", "long", "long"); - final org.scijava.code.api.CompletionRequest[] asked = { null }; + final Callable ints = method("a", "int", "int"); + final Callable strings = method("a", "java.lang.String"); + final Callable floats = method("a", "float", "float"); + final Callable longs = method("a", "long", "long"); + final String[] askedText = { null }; + final int[] askedCaret = { -1 }; + final FakeServer server = new FakeServer(); + server.complete = (doc, caret) -> FakeServer.items(doc, 0, caret, ints + .toItem(), strings.toItem(), floats.toItem(), longs.toItem()); + server.help = (doc, caret) -> { + askedText[0] = doc.text(); + askedCaret[0] = caret; + // The server rates and orders the signatures: here, as for a float + // typed as the first argument. + return new SignatureHelp(Arrays.asList(floats.toSignature(Fit.MATCH), + ints.toSignature(Fit.MISMATCH), longs.toSignature(Fit.CONVERSION), + strings.toSignature(Fit.MISMATCH)), 0, 1); + }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new org.scijava.code.api.CodeCompleter() { - - @Override - public CompletionResult complete( - final org.scijava.code.api.CompletionRequest request) - { - return new CompletionResult(Arrays.asList(ints, strings, floats, - longs), 0); - } - - @Override - public org.scijava.code.api.SignatureHelp signatureHelp( - final org.scijava.code.api.CompletionRequest request) - { - asked[0] = request; - // The completer rates and orders the signatures: here, as for a - // float typed as the first argument. - return new org.scijava.code.api.SignatureHelp(Arrays.asList( - signature(floats, Fit.MATCH), // - signature(ints, Fit.MISMATCH), // - signature(longs, Fit.CONVERSION), // - signature(strings, Fit.MISMATCH)), 1, 1); - } - }, null); + server, null); final List<Completion> completions = provider.getCompletionsImpl(area); - final org.fife.ui.autocomplete.ParameterizedCompletion intOverload = - (org.fife.ui.autocomplete.ParameterizedCompletion) completions.get(0); + final ParameterizedCompletion intOverload = + (ParameterizedCompletion) completions.get(0); final String gray = "<font color=\"" + SciJavaCompletionProvider .dimColor() + "\">"; - // Accepted a(int, int), typed a float, and tabbed onward: the completer's + // Accepted a(int, int), typed a float, and tabbed onward: the server's // order, styled by fit, without the accepted overload. area.setText("a(1.5, p1)"); area.setCaretPosition(7); @@ -258,9 +269,9 @@ public org.scijava.code.api.SignatureHelp signatureHelp( "<hr>" + gray + "long p0, long p1</font>" + // "<hr>" + gray + "<s>String p0</s></font>", // intOverload.getParam(1).getDescription()); - // The completer was asked about the caret. - assertEquals(7, asked[0].offset()); - assertEquals("a(1.5, p1)", asked[0].text()); + // The server was asked about the caret. + assertEquals(7, askedCaret[0]); + assertEquals("a(1.5, p1)", askedText[0]); } @Test @@ -268,31 +279,18 @@ public void testParameterTooltipIgnoresOtherCalls() { final RSyntaxTextArea area = new RSyntaxTextArea(); area.setText("a"); area.setCaretPosition(1); - final org.scijava.code.api.Completion ints = method("a", "int", "int"); - final org.scijava.code.api.Completion floats = method("a", "float", - "float"); + final Callable ints = method("a", "int", "int"); + final Callable floats = method("a", "float", "float"); + final FakeServer server = new FakeServer(); + server.complete = (doc, caret) -> FakeServer.items(doc, 0, caret, ints + .toItem(), floats.toItem()); + // The caret is in a call nested in a's arguments: b's signatures. + server.help = (doc, caret) -> new SignatureHelp(Collections.singletonList( + method("b", "double").toSignature(Fit.MATCH)), 0, 0); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new org.scijava.code.api.CodeCompleter() { - - @Override - public CompletionResult complete( - final org.scijava.code.api.CompletionRequest request) - { - return new CompletionResult(Arrays.asList(ints, floats), 0); - } - - @Override - public org.scijava.code.api.SignatureHelp signatureHelp( - final org.scijava.code.api.CompletionRequest request) - { - // The caret is in a call nested in a's arguments: b's signatures. - return new org.scijava.code.api.SignatureHelp(Arrays.asList( - signature(method("b", "double"), Fit.MATCH)), 0, 2); - } - }, null); - final org.fife.ui.autocomplete.ParameterizedCompletion intOverload = - (org.fife.ui.autocomplete.ParameterizedCompletion) provider - .getCompletionsImpl(area).get(0); + server, null); + final ParameterizedCompletion intOverload = + (ParameterizedCompletion) provider.getCompletionsImpl(area).get(0); area.setText("a(b(|), p1)"); area.setCaretPosition(4); // Not b's: a's other overloads, unranked. @@ -305,29 +303,28 @@ public void testLazyDescriptions() { final RSyntaxTextArea area = new RSyntaxTextArea(); area.setText("np.ze"); area.setCaretPosition(5); - final java.util.concurrent.atomic.AtomicInteger asked = - new java.util.concurrent.atomic.AtomicInteger(); - final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - request -> new CompletionResult(Arrays.asList( // - org.scijava.code.api.Completion.builder("zeros").kind( - org.scijava.code.api.Completion.Kind.METHOD).parameters(Arrays - .asList(new org.scijava.code.api.Completion.Parameter("shape", - "int"))).lazyDescription(() -> { - asked.incrementAndGet(); - return "Return a new array of zeros."; - }).build(), // - org.scijava.code.api.Completion.builder("zeta").lazyDescription( - () -> { + final AtomicInteger asked = new AtomicInteger(); + final FakeServer server = new FakeServer(); + server.complete = (doc, caret) -> FakeServer.items(doc, 3, caret, // + server.described(new Callable("zeros", Collections.singletonList( + new Callable.Param("shape", "int")), null).kind( + CompletionItemKind.Method).toItem(), () -> { asked.incrementAndGet(); - return "The zeta function."; - }).build()), 3), null); + return "Return a new array of zeros."; + }), // + server.described(new CompletionItem("zeta"), () -> { + asked.incrementAndGet(); + return "The zeta function."; + })); + final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( + server, null); final List<Completion> completions = provider.getCompletionsImpl(area); // Nothing is described until RSTA shows a completion's description... assertEquals(0, asked.get()); // ...and then it is shown, for functions as for everything else. - org.junit.Assert.assertTrue(completions.get(0).getSummary().contains( + assertTrue(completions.get(0).getSummary().contains( "Return a new array of zeros.")); - assertEquals("The zeta function.", completions.get(1).getSummary()); + assertTrue(completions.get(1).getSummary().contains("The zeta function.")); assertEquals(2, asked.get()); } @@ -336,79 +333,63 @@ public void testResultUpdates() throws Exception { final RSyntaxTextArea area = new RSyntaxTextArea(); area.setText("np.z"); area.setCaretPosition(4); - final java.util.concurrent.CompletableFuture<CompletionResult> later = - new java.util.concurrent.CompletableFuture<>(); + final CompletableFuture<CompletionList> later = new CompletableFuture<>(); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - request -> CompletionResult.EMPTY.withUpdate(later), null); + new UpdatingServer(later), null); final Boolean[] notified = { null }; provider.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); // At first, nothing; then the better result arrives. assertEquals(0, provider.getCompletionsImpl(area).size()); - later.complete(new CompletionResult(Arrays.asList( - org.scijava.code.api.Completion.of("zeros")), 3)); - javax.swing.SwingUtilities.invokeAndWait(() -> {}); + later.complete(zeros("np.z")); + SwingUtilities.invokeAndWait(() -> {}); assertEquals(Boolean.TRUE, notified[0]); final List<Completion> completions = provider.getCompletionsImpl(area); assertEquals(1, completions.size()); assertEquals("zeros", completions.get(0).getReplacementText()); // An update for a request the user has typed past is ignored. - final java.util.concurrent.CompletableFuture<CompletionResult> stale = - new java.util.concurrent.CompletableFuture<>(); + final CompletableFuture<CompletionList> stale = new CompletableFuture<>(); final SciJavaCompletionProvider provider2 = new SciJavaCompletionProvider( - request -> CompletionResult.EMPTY.withUpdate(stale), null); + new UpdatingServer(stale), null); notified[0] = null; provider2.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); + area.setText("np.z"); + area.setCaretPosition(4); provider2.getCompletionsImpl(area); area.setText("np.ze"); area.setCaretPosition(5); - stale.complete(new CompletionResult(Arrays.asList( - org.scijava.code.api.Completion.of("zeros")), 3)); - javax.swing.SwingUtilities.invokeAndWait(() -> {}); - org.junit.Assert.assertNull(notified[0]); + stale.complete(zeros("np.z")); + SwingUtilities.invokeAndWait(() -> {}); + assertNull(notified[0]); } @Test - public void testPrepareOnInstall() { + public void testOpenOnInstall() { final RSyntaxTextArea area = new RSyntaxTextArea(); area.setText("import numpy\n"); - final org.scijava.code.api.CompletionRequest[] prepared = { null }; - final java.io.File file = new java.io.File("scripts", "blur.py"); + final File file = new File("scripts", "blur.py"); + final FakeServer server = new FakeServer(); final LanguageServerLanguageSupport support = - new LanguageServerLanguageSupport(new org.scijava.code.api.CodeCompleter() - { - - @Override - public CompletionResult complete( - final org.scijava.code.api.CompletionRequest request) - { - return CompletionResult.EMPTY; - } - - @Override - public void prepare( - final org.scijava.code.api.CompletionRequest request) - { - prepared[0] = request; - } - }, null, null, () -> file); + new LanguageServerLanguageSupport(server, null, null, () -> file); support.install(area); - assertEquals("import numpy\n", prepared[0].text()); - assertEquals(file.getAbsolutePath(), prepared[0].path()); + assertEquals(1, server.opened.size()); + assertEquals("import numpy\n", server.opened.get(0).text()); + assertEquals(file.getAbsolutePath(), server.opened.get(0).path()); support.uninstall(area); + assertEquals(1, server.closed.size()); } @Test public void testDimColorFollowsLookAndFeel() { - final Object old = javax.swing.UIManager.get("Label.disabledForeground"); + final Object old = UIManager.get("Label.disabledForeground"); try { - javax.swing.UIManager.put("Label.disabledForeground", - new java.awt.Color(0x12, 0x34, 0xab)); + UIManager.put("Label.disabledForeground", new java.awt.Color(0x12, 0x34, + 0xab)); assertEquals("#1234ab", SciJavaCompletionProvider.dimColor()); } finally { - javax.swing.UIManager.put("Label.disabledForeground", old); + UIManager.put("Label.disabledForeground", old); } } @@ -418,18 +399,20 @@ public void testRequestCarriesScriptPath() { area.setText("x"); area.setCaretPosition(1); final String[] path = { "unset" }; + final FakeServer server = new FakeServer(); + server.complete = (doc, caret) -> { + path[0] = doc.path(); + return new CompletionList(); + }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - request -> { - path[0] = request.path(); - return CompletionResult.EMPTY; - }, null); + server, null); // Without a file supplier (e.g. in an interpreter): no path. provider.getCompletionsImpl(area); - org.junit.Assert.assertNull(path[0]); + assertNull(path[0]); // With one: the script's path, even if it changes (e.g. Save As). - final java.io.File[] file = { new java.io.File("scripts", "a.py") }; + final File[] file = { new File("scripts", "a.py") }; provider.setFile(() -> file[0]); area.setText("xy"); area.setCaretPosition(2); @@ -437,23 +420,95 @@ public void testRequestCarriesScriptPath() { assertEquals(file[0].getAbsolutePath(), path[0]); } - private static org.scijava.code.api.SignatureHelp.Signature signature( - final org.scijava.code.api.Completion callable, final Fit fit) - { - return new org.scijava.code.api.SignatureHelp.Signature(callable, fit); - } + // -- Helper methods -- - private static org.scijava.code.api.Completion method(final String name, + private static Callable method(final String name, final String... paramTypes) { - final List<org.scijava.code.api.Completion.Parameter> params = - new java.util.ArrayList<>(); + final List<Callable.Param> params = new ArrayList<>(); for (int i = 0; i < paramTypes.length; i++) { - params.add(new org.scijava.code.api.Completion.Parameter("p" + i, - paramTypes[i])); + params.add(new Callable.Param("p" + i, paramTypes[i])); + } + return new Callable(name, params, "java.lang.Object").kind( + CompletionItemKind.Method); + } + + /** "zeros", replacing what follows the dot, up to the end of the text. */ + private static CompletionList zeros(final String text) { + final CompletionItem zeros = new CompletionItem("zeros"); + zeros.setTextEdit(Either.forLeft(new TextEdit(new Range(Positions.position( + text, text.indexOf('.') + 1), Positions.position(text, text.length())), + "zeros"))); + return new CompletionList(false, Collections.singletonList(zeros)); + } + + // -- Helper classes -- + + /** A server answering as its functions say. */ + private static class FakeServer extends ScriptLanguageServer { + + BiFunction<Document, Integer, CompletionList> complete = (doc, + caret) -> new CompletionList(); + BiFunction<Document, Integer, SignatureHelp> help = (doc, caret) -> null; + final List<Document> opened = new ArrayList<>(); + final List<Document> closed = new ArrayList<>(); + + FakeServer() { + super(Environment.NONE); + } + + /** Completions replacing the text from {@code start} to the caret. */ + static CompletionList items(final Document doc, final int start, + final int caret, final CompletionItem... items) + { + return list(doc, start, caret, new ArrayList<>(Arrays.asList(items))); + } + + /** A completion documented on demand. */ + CompletionItem described(final CompletionItem item, + final Supplier<String> html) + { + return describe(item, html); + } + + @Override + protected CompletionList complete(final Document doc, final int offset) { + return complete.apply(doc, offset); + } + + @Override + protected SignatureHelp signatureHelp(final Document doc, + final int offset) + { + return help.apply(doc, offset); + } + + @Override + protected void opened(final Document doc) { + opened.add(doc); + } + + @Override + protected void closed(final Document doc) { + closed.add(doc); + } + } + + /** A server with nothing at first, and more to come. */ + private static class UpdatingServer extends FakeServer { + + private final CompletableFuture<CompletionList> update; + + UpdatingServer(final CompletableFuture<CompletionList> update) { + this.update = update; + } + + @Override + public CompletableFuture<Either<List<CompletionItem>, CompletionList>> + completion(final CompletionParams params) + { + return CompletableFuture.completedFuture(Either.forRight( + new UpdatingCompletionList(Collections.emptyList(), update))); } - return org.scijava.code.api.Completion.builder(name).kind( - org.scijava.code.api.Completion.Kind.METHOD).parameters(params) - .returnType("java.lang.Object").build(); } } From 262c6c7f6d64c88b9c0925644e7c2918fbf89300 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Wed, 7 Oct 2026 15:56:36 -0500 Subject: [PATCH 12/21] Ask language servers through script sessions DocumentSync is replaced by code-lsp's ScriptSession: one per text area (completion, signature help, hover and problems), and one per REPL prompt, kept while its interpreter is the REPL's and closed with the interpreter pane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../scijava/ui/swing/script/EditorPane.java | 5 +- .../ui/swing/script/InterpreterPane.java | 1 + .../scijava/ui/swing/script/PromptPane.java | 102 ++++++----- .../autocompletion/DiagnosticsParser.java | 5 +- .../script/autocompletion/DocumentSync.java | 158 ------------------ .../autocompletion/HoverToolTipSupplier.java | 15 +- .../LanguageServerLanguageSupport.java | 100 +++-------- .../SciJavaCompletionProvider.java | 65 +++---- .../CodeIntelligenceUITest.java | 3 +- .../SciJavaCompletionProviderTest.java | 28 ++-- 10 files changed, 123 insertions(+), 359 deletions(-) delete mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 3f2c987a..50ac8351 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -815,9 +815,10 @@ protected void setLanguage(final ScriptLanguage language, // Tier 2: language servers for this language. if (support == null && languageServerService.supports(currentLanguage)) { + final ScriptLanguage served = currentLanguage; final LanguageServerLanguageSupport serverSupport = - new LanguageServerLanguageSupport(languageServerService.server( - currentLanguage), currentLanguage, log, this::getFile); + new LanguageServerLanguageSupport(() -> languageServerService.session( + served), log, this::getFile); serverSupport.setHoverEnabled(hoverEnabled); serverSupport.setDiagnosticsEnabled(diagnosticsEnabled); support = serverSupport; diff --git a/src/main/java/org/scijava/ui/swing/script/InterpreterPane.java b/src/main/java/org/scijava/ui/swing/script/InterpreterPane.java index 4e5de1b5..580790cc 100644 --- a/src/main/java/org/scijava/ui/swing/script/InterpreterPane.java +++ b/src/main/java/org/scijava/ui/swing/script/InterpreterPane.java @@ -172,6 +172,7 @@ public void print(final String string) { public void dispose() { output.close(); + prompt.dispose(); } // -- UIComponent methods -- diff --git a/src/main/java/org/scijava/ui/swing/script/PromptPane.java b/src/main/java/org/scijava/ui/swing/script/PromptPane.java index 63def684..eb0beca4 100644 --- a/src/main/java/org/scijava/ui/swing/script/PromptPane.java +++ b/src/main/java/org/scijava/ui/swing/script/PromptPane.java @@ -44,29 +44,21 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.swing.JTextArea; import javax.swing.text.BadLocationException; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionList; -import org.eclipse.lsp4j.CompletionParams; -import org.eclipse.lsp4j.DidCloseTextDocumentParams; -import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.InsertTextFormat; import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.TextDocumentIdentifier; -import org.eclipse.lsp4j.TextDocumentItem; -import org.eclipse.lsp4j.services.LanguageServer; -import org.eclipse.lsp4j.services.TextDocumentService; import org.scijava.Context; import org.scijava.script.ScriptInterpreter; -import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptREPL; import org.scijava.code.lsp.BindingsLanguageServer; import org.scijava.code.lsp.LanguageServerService; import org.scijava.code.lsp.Positions; +import org.scijava.code.lsp.ScriptSession; import org.scijava.code.lsp.UpdatingCompletionList; import org.scijava.thread.ThreadService; import org.scijava.util.ClassUtils; @@ -81,9 +73,6 @@ */ public abstract class PromptPane implements UIComponent<JTextArea> { - /** The prompt's text, as the servers know it. */ - private static final String PROMPT_URI = "untitled:/prompt"; - /** How long Tab waits for completions, in milliseconds. */ private static final long COMPLETION_WAIT = 2000; @@ -95,6 +84,10 @@ public abstract class PromptPane implements UIComponent<JTextArea> { private boolean executing; + /** The session about the prompt's text, and the interpreter it is for. */ + private ScriptSession session; + private ScriptInterpreter sessionInterpreter; + public PromptPane(final ScriptREPL repl, final VarsPane vars, final OutputPane output) { @@ -236,15 +229,12 @@ private Context context() { private void complete() { final ScriptInterpreter interpreter = repl.getInterpreter(); if (interpreter == null) return; - final ScriptEngine engine = interpreter.getEngine(); - final ScriptLanguage language = interpreter.getLanguage(); - final ScriptContext live = engine == null ? null : engine.getContext(); final int caret = textArea.getCaretPosition(); final String text = textArea.getText(); final List<CompletionItem> items; try { - items = items(language, live, text, caret); + items = items(interpreter, text, caret); } catch (final Exception exc) { return; @@ -281,46 +271,54 @@ private void complete() { } /** Asks the language's servers (or the live variables) for completions. */ - private List<CompletionItem> items(final ScriptLanguage language, - final ScriptContext live, final String text, final int caret) - throws Exception + private List<CompletionItem> items(final ScriptInterpreter interpreter, + final String text, final int caret) throws Exception + { + final ScriptSession session = session(interpreter); + if (session == null) return Collections.emptyList(); + CompletionList list = session.completion(text, caret).get(COMPLETION_WAIT, + TimeUnit.MILLISECONDS); + // NB: Tab asks once: wait a moment for slower servers. + if (list instanceof UpdatingCompletionList) { + try { + final CompletionList better = ((UpdatingCompletionList) list).update() + .get(COMPLETION_WAIT, TimeUnit.MILLISECONDS); + if (better != null) list = better; + } + catch (final Exception exc) { + // NB: Not in time: what there is. + } + } + final List<CompletionItem> items = new ArrayList<>(list.getItems()); + items.sort(Comparator.comparing(item -> item.getSortText() != null ? item + .getSortText() : item.getLabel())); + return items; + } + + /** + * The session about the prompt's text, for the given interpreter: kept + * while it is the REPL's; or null if completion is unavailable. + */ + private synchronized ScriptSession session( + final ScriptInterpreter interpreter) { + if (session != null && sessionInterpreter == interpreter) return session; + dispose(); final LanguageServerService servers = context().service( LanguageServerService.class); - final LanguageServer server; - if (servers != null && language != null && servers.supports(language)) { - server = servers.server(language, live); - } - else if (live != null) server = new BindingsLanguageServer(live); - else return Collections.emptyList(); + final ScriptEngine engine = interpreter.getEngine(); + if (servers == null || interpreter.getLanguage() == null) return null; + session = servers.session(interpreter.getLanguage(), engine == null ? null + : engine.getContext()); + sessionInterpreter = interpreter; + return session; + } - final TextDocumentService docs = server.getTextDocumentService(); - docs.didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(PROMPT_URI, - language == null ? "" : language.getLanguageName(), 1, text))); - try { - CompletionList list = docs.completion(new CompletionParams( - new TextDocumentIdentifier(PROMPT_URI), Positions.position(text, - caret))).get(COMPLETION_WAIT, TimeUnit.MILLISECONDS).getRight(); - // NB: Tab asks once: wait a moment for slower servers. - if (list instanceof UpdatingCompletionList) { - try { - final CompletionList better = ((UpdatingCompletionList) list) - .update().get(COMPLETION_WAIT, TimeUnit.MILLISECONDS); - if (better != null) list = better; - } - catch (final Exception exc) { - // NB: Not in time: what there is. - } - } - final List<CompletionItem> items = new ArrayList<>(list.getItems()); - items.sort(Comparator.comparing(item -> item.getSortText() != null ? item - .getSortText() : item.getLabel())); - return items; - } - finally { - docs.didClose(new DidCloseTextDocumentParams(new TextDocumentIdentifier( - PROMPT_URI))); - } + /** Lets the language servers release what they keep for the prompt. */ + public synchronized void dispose() { + if (session != null) session.close(); + session = null; + sessionInterpreter = null; } /** Where a completion's text starts: what it replaces, up to the caret. */ diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java index 1445be58..0466a7a8 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/DiagnosticsParser.java @@ -45,6 +45,7 @@ import org.fife.ui.rsyntaxtextarea.parser.DefaultParserNotice; import org.fife.ui.rsyntaxtextarea.parser.ParseResult; import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; +import org.scijava.code.lsp.Positions; /** * Shows the problems language servers publish for the script (e.g. syntax @@ -87,8 +88,8 @@ public ParseResult parse(final RSyntaxDocument doc, final String style) { } final Element root = doc.getDefaultRootElement(); for (final Diagnostic d : problems) { - int start = DocumentSync.offset(text, d.getRange().getStart()); - final int end = DocumentSync.offset(text, d.getRange().getEnd()); + int start = Positions.offset(text, d.getRange().getStart()); + final int end = Positions.offset(text, d.getRange().getEnd()); if (start >= text.length() && text.length() > 0) { start = text.length() - 1; // NB: E.g. "unexpected end of file". } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java deleted file mode 100644 index bdebe618..00000000 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/DocumentSync.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * #%L - * Script Editor and Interpreter for SciJava script languages. - * %% - * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; - -import java.io.File; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Supplier; - -import org.eclipse.lsp4j.DidChangeTextDocumentParams; -import org.eclipse.lsp4j.DidCloseTextDocumentParams; -import org.eclipse.lsp4j.DidOpenTextDocumentParams; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.TextDocumentContentChangeEvent; -import org.eclipse.lsp4j.TextDocumentIdentifier; -import org.eclipse.lsp4j.TextDocumentItem; -import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; -import org.eclipse.lsp4j.services.LanguageServer; -import org.scijava.code.lsp.Positions; -import org.scijava.script.ScriptLanguage; - -/** - * Keeps a language server's copy of an edited script up to date: opens it, - * sends its changes (the whole text), and closes it. Its URI is its file's, - * if saved; otherwise an {@code untitled:} one. Saving it under another name - * closes the old document and opens the new one. - * - * @author Gabriel Selzer - */ -public class DocumentSync { - - private static final AtomicInteger UNTITLED = new AtomicInteger(); - - private final LanguageServer server; - private final ScriptLanguage language; - private final String untitled; - private volatile Supplier<File> file; - - private String uri; - private String sent; - private int version; - - public DocumentSync(final LanguageServer server, - final ScriptLanguage language) - { - this.server = server; - this.language = language; - final List<String> extensions = language == null ? Collections - .emptyList() : language.getExtensions(); - untitled = "untitled:/script-" + UNTITLED.incrementAndGet() + (extensions - .isEmpty() ? "" : "." + extensions.get(0)); - } - - /** The server documents are synchronized with. */ - public LanguageServer server() { - return server; - } - - /** Sets a supplier of the script's file (which may supply null). */ - public void setFile(final Supplier<File> file) { - this.file = file; - } - - /** - * Brings the server's copy up to date with the given text (opening it, if - * needed). - * - * @return The document's URI. - */ - public synchronized String sync(final String text) { - final String current = currentUri(); - if (uri != null && !uri.equals(current)) close(); - if (uri == null) { - uri = current; - version = 1; - sent = text; - server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams( - new TextDocumentItem(uri, languageId(), version, text))); - } - else if (!text.equals(sent)) { - sent = text; - version++; - server.getTextDocumentService().didChange(new DidChangeTextDocumentParams( - new VersionedTextDocumentIdentifier(uri, version), Collections - .singletonList(new TextDocumentContentChangeEvent(text)))); - } - return uri; - } - - /** Closes the server's copy, if open. */ - public synchronized void close() { - if (uri == null) return; - server.getTextDocumentService().didClose(new DidCloseTextDocumentParams( - new TextDocumentIdentifier(uri))); - uri = null; - sent = null; - } - - /** The open document's URI, or null if none. */ - public synchronized String uri() { - return uri; - } - - /** The text last sent, or null if none. */ - public synchronized String text() { - return sent; - } - - /** The position of an offset of the given text, for requests. */ - public static Position position(final String text, final int offset) { - return Positions.position(text, Math.max(0, Math.min(offset, text - .length()))); - } - - /** The offset of a position in the given text. */ - public static int offset(final String text, final Position position) { - return Positions.offset(text, position); - } - - private String currentUri() { - final Supplier<File> f = file; - final File path = f == null ? null : f.get(); - return path == null ? untitled : path.getAbsoluteFile().toURI() - .toString(); - } - - private String languageId() { - return language == null ? "plaintext" : language.getLanguageName(); - } -} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java index 39c0ac54..35c02959 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/HoverToolTipSupplier.java @@ -34,13 +34,12 @@ import java.util.concurrent.TimeUnit; import org.eclipse.lsp4j.Hover; -import org.eclipse.lsp4j.HoverParams; -import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.MarkedString; -import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.fife.ui.rtextarea.RTextArea; import org.fife.ui.rtextarea.ToolTipSupplier; +import org.scijava.code.lsp.ScriptSession; import org.scijava.log.Logger; /** @@ -62,7 +61,7 @@ public class HoverToolTipSupplier implements ToolTipSupplier { /** How much of an answer to show, at most, in lines. */ private static final int MAX_LINES = 25; - private final DocumentSync sync; + private final ScriptSession session; private final Logger log; /** The last question (text and word), and its answer. */ @@ -70,8 +69,8 @@ public class HoverToolTipSupplier implements ToolTipSupplier { private int askedWord = -1; private CompletableFuture<Hover> answer; - public HoverToolTipSupplier(final DocumentSync sync, final Logger log) { - this.sync = sync; + public HoverToolTipSupplier(final ScriptSession session, final Logger log) { + this.session = session; this.log = log; } @@ -104,9 +103,7 @@ String toolTip(final String text, final int offset) { private CompletableFuture<Hover> ask(final String text, final int offset) { try { - final String uri = sync.sync(text); - return sync.server().getTextDocumentService().hover(new HoverParams( - new TextDocumentIdentifier(uri), DocumentSync.position(text, offset))); + return session.hover(text, offset); } catch (final Exception | LinkageError exc) { // NB: Never let a misbehaving server break the editor. diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java index e303faee..f7593f69 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java @@ -32,41 +32,31 @@ import java.io.File; import java.util.Map; import java.util.WeakHashMap; -import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; -import org.eclipse.lsp4j.MessageActionItem; -import org.eclipse.lsp4j.MessageParams; -import org.eclipse.lsp4j.PublishDiagnosticsParams; -import org.eclipse.lsp4j.ShowMessageRequestParams; -import org.eclipse.lsp4j.services.LanguageClient; -import org.eclipse.lsp4j.services.LanguageClientAware; -import org.eclipse.lsp4j.services.LanguageServer; import org.fife.rsta.ac.AbstractLanguageSupport; import org.fife.ui.autocomplete.AutoCompletion; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.scijava.code.lsp.ScriptSession; import org.scijava.log.Logger; -import org.scijava.script.ScriptLanguage; /** - * RSyntaxTextArea language support from a language server (usually one - * standing for all of a script language's, see + * RSyntaxTextArea language support from language servers, via a + * {@link ScriptSession} per text area (usually from * {@code org.scijava.code.lsp.LanguageServerService}): completion with * parameter assistance, documentation on hover, problems as squiggles, and - * signatures as a call is typed. The edited script is kept in sync with the - * server (see {@link DocumentSync}): opened on install, changed with each - * edit, closed on uninstall. + * signatures as a call is typed. The session is told the script's text on + * install and with each edit, and closed on uninstall. * * @author Curtis Rueden * @author Gabriel Selzer */ public class LanguageServerLanguageSupport extends AbstractLanguageSupport { - private final LanguageServer server; - private final ScriptLanguage language; + private final Supplier<ScriptSession> sessions; private final Logger log; private final Supplier<File> file; private boolean hoverEnabled = true; @@ -77,19 +67,17 @@ public class LanguageServerLanguageSupport extends AbstractLanguageSupport { new WeakHashMap<>(); /** - * @param server The language server (one per support: it is told about the - * script, and publishes its problems to this support). - * @param language The script's language. + * @param sessions Starts a session about the script, for each text area + * this is installed in (e.g. + * {@code () -> languageServerService.session(language)}). * @param log Where to report problems, or null. * @param file Supplies the file of the script being edited (or null if * unsaved). */ - public LanguageServerLanguageSupport(final LanguageServer server, - final ScriptLanguage language, final Logger log, - final Supplier<File> file) + public LanguageServerLanguageSupport(final Supplier<ScriptSession> sessions, + final Logger log, final Supplier<File> file) { - this.server = server; - this.language = language; + this.sessions = sessions; this.log = log; this.file = file; setAutoCompleteEnabled(true); @@ -115,11 +103,10 @@ public void setDiagnosticsEnabled(final boolean enabled) { @Override public void install(final RSyntaxTextArea textArea) { + final ScriptSession session = sessions.get().setFile(file).setLogger(log); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - server, language); + session); provider.setLogger(log); - provider.setFile(file); - final DocumentSync sync = provider.sync(); final AutoCompletion ac = new SciJavaAutoCompletion(provider); ac.setAutoCompleteEnabled(isAutoCompleteEnabled()); ac.setAutoActivationEnabled(isAutoActivationEnabled()); @@ -129,16 +116,14 @@ public void install(final RSyntaxTextArea textArea) { installImpl(textArea, ac); // Documentation on hover, problems as squiggles, signatures as typed. - final Installed extras = new Installed(sync); + final Installed extras = new Installed(session); if (hoverEnabled) { - textArea.setToolTipSupplier(new HoverToolTipSupplier(sync, log)); + textArea.setToolTipSupplier(new HoverToolTipSupplier(session, log)); } if (diagnosticsEnabled) { extras.parser = new DiagnosticsParser(textArea); textArea.addParser(extras.parser); - } - if (server instanceof LanguageClientAware) { - ((LanguageClientAware) server).connect(new Client(sync, extras.parser)); + session.setDiagnosticsListener(extras.parser::accept); } if (isParameterAssistanceEnabled()) { extras.popup = new SignaturePopup(textArea, provider::signatureHelp); @@ -166,7 +151,7 @@ public void changedUpdate(final DocumentEvent e) { private void changed() { try { - sync.sync(textArea.getText()); + session.update(textArea.getText()); } catch (final Exception | LinkageError exc) { // NB: Never let a misbehaving server break the editor. @@ -177,7 +162,7 @@ private void changed() { textArea.getDocument().addDocumentListener(extras.listener); installed.put(textArea, extras); try { - sync.sync(textArea.getText()); + session.update(textArea.getText()); } catch (final Exception | LinkageError exc) { if (log != null) log.debug("Document sync failed", exc); @@ -197,7 +182,7 @@ public void uninstall(final RSyntaxTextArea textArea) { if (extras.popup != null) extras.popup.uninstall(); // Let the servers release what they keep for this script. try { - extras.sync.close(); + extras.session.close(); } catch (final Exception | LinkageError exc) { if (log != null) log.debug("Document close failed", exc); @@ -206,52 +191,13 @@ public void uninstall(final RSyntaxTextArea textArea) { private static final class Installed { - private final DocumentSync sync; + private final ScriptSession session; private DiagnosticsParser parser; private SignaturePopup popup; private DocumentListener listener; - private Installed(final DocumentSync sync) { - this.sync = sync; - } - } - - /** Receives the server's messages: the script's problems, for the parser. */ - private final class Client implements LanguageClient { - - private final DocumentSync sync; - private final DiagnosticsParser parser; - - private Client(final DocumentSync sync, final DiagnosticsParser parser) { - this.sync = sync; - this.parser = parser; - } - - @Override - public void publishDiagnostics(final PublishDiagnosticsParams p) { - if (parser != null && p.getUri().equals(sync.uri())) { - parser.accept(p.getDiagnostics()); - } - } - - @Override - public void telemetryEvent(final Object object) {} - - @Override - public void showMessage(final MessageParams m) { - if (log != null) log.debug("Language server: " + m.getMessage()); - } - - @Override - public CompletableFuture<MessageActionItem> showMessageRequest( - final ShowMessageRequestParams r) - { - return CompletableFuture.completedFuture(null); - } - - @Override - public void logMessage(final MessageParams m) { - if (log != null) log.debug("Language server: " + m.getMessage()); + private Installed(final ScriptSession session) { + this.session = session; } } } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java index 4dc610f9..640213a2 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java @@ -30,7 +30,6 @@ package org.scijava.ui.swing.script.autocompletion; import java.awt.Color; -import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -54,28 +53,25 @@ import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionItemKind; import org.eclipse.lsp4j.CompletionList; -import org.eclipse.lsp4j.CompletionParams; import org.eclipse.lsp4j.InsertTextFormat; import org.eclipse.lsp4j.MarkupContent; import org.eclipse.lsp4j.MarkupKind; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SignatureHelp; -import org.eclipse.lsp4j.SignatureHelpParams; import org.eclipse.lsp4j.SignatureInformation; -import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.eclipse.lsp4j.services.LanguageServer; import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.CompletionCellRenderer; import org.fife.ui.autocomplete.DefaultCompletionProvider; import org.fife.ui.autocomplete.ParameterizedCompletion; -import org.scijava.code.lsp.RatedSignatureInformation; +import org.scijava.code.lsp.Positions; import org.scijava.code.lsp.RatedSignatureInformation.Fit; +import org.scijava.code.lsp.RatedSignatureInformation; +import org.scijava.code.lsp.ScriptSession; import org.scijava.code.lsp.UpdatingCompletionList; import org.scijava.log.Logger; -import org.scijava.script.ScriptLanguage; /** * The bridge between a language server (usually one standing for all of a @@ -113,8 +109,7 @@ public class SciJavaCompletionProvider extends DefaultCompletionProvider { private static final Pattern PLACEHOLDER = Pattern.compile( "\\$\\{(\\d+)(?::((?:[^}\\\\]|\\\\.)*))?\\}|\\$(\\d+)"); - private final DocumentSync sync; - private final ScriptLanguage language; + private final ScriptSession session; /** Optional logger, for reporting server failures. */ private Logger log; @@ -134,11 +129,9 @@ public class SciJavaCompletionProvider extends DefaultCompletionProvider { /** Notified when better completions arrive for the current request. */ private volatile Consumer<Boolean> updateListener; - public SciJavaCompletionProvider(final LanguageServer server, - final ScriptLanguage language) - { - this.sync = new DocumentSync(server, language); - this.language = language; + /** @param session Where to ask about the script being edited. */ + public SciJavaCompletionProvider(final ScriptSession session) { + this.session = session; setParameterizedCompletionParams('(', ", ", ')'); // Show each callable's parameters and return type in the popup list. setListCellRenderer(new CompletionCellRenderer()); @@ -147,9 +140,9 @@ public SciJavaCompletionProvider(final LanguageServer server, setParameterChoicesProvider(this::parameterChoices); } - /** The synchronization of the edited script with the server. */ - public DocumentSync sync() { - return sync; + /** Where this asks about the script being edited. */ + public ScriptSession session() { + return session; } /** Sets a logger with which to report server failures. */ @@ -157,14 +150,6 @@ public void setLogger(final Logger log) { this.log = log; } - /** - * Sets a supplier of the file of the script being edited (which may supply - * null, for an unsaved script). - */ - public void setFile(final Supplier<File> file) { - sync.setFile(file); - } - @Override public boolean isValidChar(final char c) { return Character.isLetterOrDigit(c) || c == '.' || c == '_'; @@ -241,11 +226,8 @@ SignatureHelp signatureHelp(final JTextComponent comp) { } SignatureHelp h = null; try { - final String uri = sync.sync(text); - final CompletableFuture<SignatureHelp> answer = sync.server() - .getTextDocumentService().signatureHelp(new SignatureHelpParams( - new TextDocumentIdentifier(uri), DocumentSync.position(text, - caret))); + final CompletableFuture<SignatureHelp> answer = session.signatureHelp( + text, caret); try { h = answer.get(HELP_BUDGET, TimeUnit.MILLISECONDS); } @@ -374,14 +356,8 @@ private void compute(final JTextComponent comp) { /** Asks the server for completions; none if it fails. */ private CompletionList request(final String text, final int caret) { try { - final String uri = sync.sync(text); - final Either<List<CompletionItem>, CompletionList> answer = sync.server() - .getTextDocumentService().completion(new CompletionParams( - new TextDocumentIdentifier(uri), DocumentSync.position(text, caret))) - .get(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS); - if (answer == null) return new CompletionList(); - return answer.isRight() ? answer.getRight() : new CompletionList(answer - .getLeft()); + return session.completion(text, caret).get(REQUEST_TIMEOUT, + TimeUnit.MILLISECONDS); } catch (final Exception | LinkageError exc) { // NB: Never let a misbehaving server break the editor. @@ -435,7 +411,7 @@ private static int itemStart(final CompletionItem item, final String text, if (item.getTextEdit() != null) { final Range r = item.getTextEdit().isLeft() ? item.getTextEdit() .getLeft().getRange() : item.getTextEdit().getRight().getInsert(); - return Math.min(caret, DocumentSync.offset(text, r.getStart())); + return Math.min(caret, Positions.offset(text, r.getStart())); } int start = Math.min(caret, text.length()); while (start > 0 && Character.isJavaIdentifierPart(text.charAt(start - @@ -558,9 +534,8 @@ private static List<String> parameters(final CompletionItem item, private String describe(final CompletionItem item) { if (item.getDocumentation() != null) return html(item.getDocumentation()); try { - final CompletionItem resolved = sync.server().getTextDocumentService() - .resolveCompletionItem(item).get(DESCRIBE_TIMEOUT, - TimeUnit.MILLISECONDS); + final CompletionItem resolved = session.resolve(item).get( + DESCRIBE_TIMEOUT, TimeUnit.MILLISECONDS); return resolved == null ? null : html(resolved.getDocumentation()); } catch (final Exception exc) { @@ -575,9 +550,9 @@ private static List<AdditionalEdits.Edit> edits( final List<AdditionalEdits.Edit> out = new ArrayList<>(); for (final TextEdit e : item.getAdditionalTextEdits()) { - out.add(new AdditionalEdits.Edit(DocumentSync.offset( - text, e.getRange().getStart()), DocumentSync.offset(text, e.getRange() - .getEnd()), e.getNewText())); + out.add(new AdditionalEdits.Edit(Positions.offset(text, e.getRange() + .getStart()), Positions.offset(text, e.getRange().getEnd()), e + .getNewText())); } return out; } diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java index 9f69ff71..d05ce295 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -53,6 +53,7 @@ import org.junit.Test; import org.scijava.code.lsp.Environment; import org.scijava.code.lsp.ScriptLanguageServer; +import org.scijava.code.lsp.ScriptSession; /** * Tests {@link HoverToolTipSupplier}, {@link DiagnosticsParser} and @@ -80,7 +81,7 @@ protected Hover hover(final Document doc, final int at) { } }; final HoverToolTipSupplier supplier = new HoverToolTipSupplier( - new DocumentSync(server, null), null); + ScriptSession.owning(server, null), null); final String text = "x.dumps(1)"; assertEquals("<html><pre>Serialize.</pre></html>", supplier.toolTip(text, 3)); diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index 9a03f10e..e2c3e58d 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -66,6 +66,7 @@ import org.scijava.code.lsp.Positions; import org.scijava.code.lsp.RatedSignatureInformation.Fit; import org.scijava.code.lsp.ScriptLanguageServer; +import org.scijava.code.lsp.ScriptSession; import org.scijava.code.lsp.UpdatingCompletionList; /** @@ -93,7 +94,7 @@ public void testBridgesCompletions() { return FakeServer.items(doc, 4, caret, bar, new CompletionItem("baz")); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - server, null); + ScriptSession.owning(server, null)); // The provider must report the already-entered text as the replaced span. assertEquals("ba", provider.getAlreadyEnteredText(area)); @@ -120,7 +121,7 @@ public void testAdditionalEditsSurfaceAsSciJavaCompletion() { return FakeServer.items(doc, 0, caret, roi); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - server, null); + ScriptSession.owning(server, null)); final List<Completion> completions = provider.getCompletionsImpl(area); assertEquals(1, completions.size()); @@ -156,7 +157,7 @@ public void testCallablesBecomeFunctionCompletionsWithChoices() { return FakeServer.items(doc, caret, caret, myRoi, addRoi.toItem()); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - server, null); + ScriptSession.owning(server, null)); // Callables surface as FunctionCompletions so parameter assistance works. final List<Completion> completions = provider.getCompletionsImpl(area); @@ -183,7 +184,7 @@ public void testCallablesRenderWithSignatures() { server.complete = (doc, caret) -> FakeServer.items(doc, 0, caret, method( completionText, "long[]").toItem()); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - server, null); + ScriptSession.owning(server, null)); // The popup list must show a callable's parameters, not just its name. final Completion c = provider.getCompletionsImpl(area).get(0); @@ -207,7 +208,7 @@ public void testParameterTooltipListsOtherOverloads() { method("img.getAt", "int[]").toItem(), // e.g. a bridge method method("img.getAtX", "long[]").toItem()); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - server, null); + ScriptSession.owning(server, null)); final List<Completion> completions = provider.getCompletionsImpl(area); // Each overload's tooltip lists the others, once each, even before RSTA @@ -254,7 +255,7 @@ public void testParameterTooltipShowsSignatureHelp() { strings.toSignature(Fit.MISMATCH)), 0, 1); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - server, null); + ScriptSession.owning(server, null)); final List<Completion> completions = provider.getCompletionsImpl(area); final ParameterizedCompletion intOverload = (ParameterizedCompletion) completions.get(0); @@ -288,7 +289,7 @@ public void testParameterTooltipIgnoresOtherCalls() { server.help = (doc, caret) -> new SignatureHelp(Collections.singletonList( method("b", "double").toSignature(Fit.MATCH)), 0, 0); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - server, null); + ScriptSession.owning(server, null)); final ParameterizedCompletion intOverload = (ParameterizedCompletion) provider.getCompletionsImpl(area).get(0); area.setText("a(b(|), p1)"); @@ -317,7 +318,7 @@ public void testLazyDescriptions() { return "The zeta function."; })); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - server, null); + ScriptSession.owning(server, null)); final List<Completion> completions = provider.getCompletionsImpl(area); // Nothing is described until RSTA shows a completion's description... assertEquals(0, asked.get()); @@ -335,7 +336,7 @@ public void testResultUpdates() throws Exception { area.setCaretPosition(4); final CompletableFuture<CompletionList> later = new CompletableFuture<>(); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new UpdatingServer(later), null); + ScriptSession.owning(new UpdatingServer(later), null)); final Boolean[] notified = { null }; provider.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); @@ -351,7 +352,7 @@ public void testResultUpdates() throws Exception { // An update for a request the user has typed past is ignored. final CompletableFuture<CompletionList> stale = new CompletableFuture<>(); final SciJavaCompletionProvider provider2 = new SciJavaCompletionProvider( - new UpdatingServer(stale), null); + ScriptSession.owning(new UpdatingServer(stale), null)); notified[0] = null; provider2.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); area.setText("np.z"); @@ -371,7 +372,8 @@ public void testOpenOnInstall() { final File file = new File("scripts", "blur.py"); final FakeServer server = new FakeServer(); final LanguageServerLanguageSupport support = - new LanguageServerLanguageSupport(server, null, null, () -> file); + new LanguageServerLanguageSupport(() -> ScriptSession.owning(server, null), + null, () -> file); support.install(area); assertEquals(1, server.opened.size()); assertEquals("import numpy\n", server.opened.get(0).text()); @@ -405,7 +407,7 @@ public void testRequestCarriesScriptPath() { return new CompletionList(); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - server, null); + ScriptSession.owning(server, null)); // Without a file supplier (e.g. in an interpreter): no path. provider.getCompletionsImpl(area); @@ -413,7 +415,7 @@ public void testRequestCarriesScriptPath() { // With one: the script's path, even if it changes (e.g. Save As). final File[] file = { new File("scripts", "a.py") }; - provider.setFile(() -> file[0]); + provider.session().setFile(() -> file[0]); area.setText("xy"); area.setCaretPosition(2); provider.getCompletionsImpl(area); From 7b4ab38191cdf9e2e02ab228e659b7941e96c9da Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Wed, 7 Oct 2026 16:12:32 -0500 Subject: [PATCH 13/21] Minor cleanup Cleaned up some javadoc and deduped a few lines --- .../scijava/ui/swing/script/EditorPane.java | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 50ac8351..0f5dabf7 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -726,15 +726,6 @@ protected void setLanguage(final ScriptLanguage language) { setLanguage(language, false); } - /** - * Set the language of this {@link EditorPane}, optionally adding a header. - * TODO: What is this header? - * - * @param language {@link ScriptLanguage} to set the editors language to. - * @param addHeader set to <code>true</code> to add a header. - * @see #setLanguageByFileName(String) - * @see #setLanguage(ScriptLanguage) - */ /** * Uninstalls the language support (e.g. code completion), as the editor * is closed. @@ -746,14 +737,20 @@ void releaseLanguageSupport() { } } + /** + * Set the language of this {@link EditorPane}, optionally adding a header. + * TODO: What is this header? + * + * @param language {@link ScriptLanguage} to set the editors language to. + * @param addHeader set to <code>true</code> to add a header. + * @see #setLanguageByFileName(String) + * @see #setLanguage(ScriptLanguage) + */ protected void setLanguage(final ScriptLanguage language, final boolean addHeader) { // uninstall existing language support. - if (installedSupport != null) { - installedSupport.uninstall(this); - installedSupport = null; - } + releaseLanguageSupport(); LanguageSupport support; String languageName; @@ -871,8 +868,8 @@ public void setAutoCompletion(final boolean enabled) { * @param enabled Whether Java should be enabled as fallback language for * auto-completion */ - void setFallbackAutoCompletion(final boolean value) { - autoCompletionJavaFallback = value; + void setFallbackAutoCompletion(final boolean enabled) { + autoCompletionJavaFallback = enabled; if (autoCompletionEnabled && currentLanguage != null) setLanguage(currentLanguage); } From 2ea0c9ede9cd85f8bff567c3cb3ca446687220da Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Thu, 8 Oct 2026 13:10:41 -0500 Subject: [PATCH 14/21] Follow code-lsp's packages by purpose BindingsLanguageServer is now internal to code-lsp, so the prompt's javadoc no longer links to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- src/main/java/org/scijava/ui/swing/script/PromptPane.java | 6 ++---- .../scijava/ui/swing/script/autocompletion/ClassUtil.java | 2 +- .../ui/swing/script/autocompletion/SignaturePopup.java | 2 +- .../swing/script/autocompletion/CodeIntelligenceUITest.java | 2 +- .../autocompletion/SciJavaCompletionProviderTest.java | 4 ++-- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/PromptPane.java b/src/main/java/org/scijava/ui/swing/script/PromptPane.java index eb0beca4..e7c5f805 100644 --- a/src/main/java/org/scijava/ui/swing/script/PromptPane.java +++ b/src/main/java/org/scijava/ui/swing/script/PromptPane.java @@ -55,7 +55,6 @@ import org.scijava.Context; import org.scijava.script.ScriptInterpreter; import org.scijava.script.ScriptREPL; -import org.scijava.code.lsp.BindingsLanguageServer; import org.scijava.code.lsp.LanguageServerService; import org.scijava.code.lsp.Positions; import org.scijava.code.lsp.ScriptSession; @@ -222,9 +221,8 @@ private Context context() { /** * Tab-completion for the REPL prompt: what the language's servers know (see * {@link LanguageServerService}), with the interpreter's live variables; or, - * for languages without servers, its live variables alone (see - * {@link BindingsLanguageServer}). So every scripting language gets at - * least baseline completion here. + * for languages without servers, its live variables alone. So every + * scripting language gets at least baseline completion here. */ private void complete() { final ScriptInterpreter interpreter = repl.getInterpreter(); diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java index cdfe0049..095503f3 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java @@ -39,7 +39,7 @@ import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.CompletionProvider; -import org.scijava.code.lsp.ClassIndex; +import org.scijava.code.lsp.jvm.ClassIndex; /** * Thin facade over the toolkit-agnostic {@link ClassIndex} in scijava-code-lsp, diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java index a812c5c8..76f88c22 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java @@ -55,7 +55,7 @@ import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SignatureInformation; -import org.scijava.code.lsp.Signatures; +import org.scijava.code.lsp.jvm.Signatures; import org.scijava.code.lsp.RatedSignatureInformation; /** diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java index d05ce295..1ac1dbc4 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -52,7 +52,7 @@ import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; import org.junit.Test; import org.scijava.code.lsp.Environment; -import org.scijava.code.lsp.ScriptLanguageServer; +import org.scijava.code.lsp.jvm.ScriptLanguageServer; import org.scijava.code.lsp.ScriptSession; /** diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index e2c3e58d..ef7fa76c 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -61,11 +61,11 @@ import org.fife.ui.autocomplete.ParameterizedCompletion; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.junit.Test; -import org.scijava.code.lsp.Callable; +import org.scijava.code.lsp.jvm.Callable; import org.scijava.code.lsp.Environment; import org.scijava.code.lsp.Positions; import org.scijava.code.lsp.RatedSignatureInformation.Fit; -import org.scijava.code.lsp.ScriptLanguageServer; +import org.scijava.code.lsp.jvm.ScriptLanguageServer; import org.scijava.code.lsp.ScriptSession; import org.scijava.code.lsp.UpdatingCompletionList; From 7b99213f1886302ad10685aed4def12b539a8710 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Thu, 8 Oct 2026 13:10:41 -0500 Subject: [PATCH 15/21] Use ClassIndex directly, not the deprecated ClassUtil ClassUtil only forwards to code-lsp's ClassIndex; it stays, for code outside the script editor still calling it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- src/main/java/org/scijava/ui/swing/script/TextEditor.java | 4 ++-- .../ui/swing/script/autocompletion/CompletionText.java | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/TextEditor.java b/src/main/java/org/scijava/ui/swing/script/TextEditor.java index 8486a80a..06ebfb51 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -133,6 +133,7 @@ import org.scijava.Context; import org.scijava.app.AppService; import org.scijava.batch.BatchService; +import org.scijava.code.lsp.jvm.ClassIndex; import org.scijava.command.CommandService; import org.scijava.event.ContextDisposingEvent; import org.scijava.event.EventHandler; @@ -155,7 +156,6 @@ import org.scijava.thread.ThreadService; import org.scijava.ui.CloseConfirmable; import org.scijava.ui.UIService; -import org.scijava.ui.swing.script.autocompletion.ClassUtil; import org.scijava.ui.swing.script.commands.ChooseFontSize; import org.scijava.ui.swing.script.commands.ChooseTabSize; import org.scijava.ui.swing.script.commands.GitGrep; @@ -3309,7 +3309,7 @@ public void run() { setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); final HashMap<String, ArrayList<String>> matches; try { - matches = ClassUtil.findDocumentationForClass(text); + matches = ClassIndex.findDocumentationForClass(text); } finally { setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/CompletionText.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/CompletionText.java index e06f31f7..985aa9a5 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/CompletionText.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/CompletionText.java @@ -39,6 +39,7 @@ import org.fife.ui.autocomplete.AbstractCompletion; import org.fife.ui.autocomplete.BasicCompletion; import org.fife.ui.autocomplete.CompletionProvider; +import org.scijava.code.lsp.jvm.ClassIndex; public class CompletionText { @@ -59,17 +60,17 @@ public CompletionText(final String replacementText, final String summary, final } public CompletionText(final String replacementText, final Class<?> c, final Field f) { - this(replacementText, ClassUtil.getSummaryCompletion(f, c), null); + this(replacementText, ClassIndex.getSummaryCompletion(f, c), null); } public CompletionText(final String replacementText, final Class<?> c, final Method m) { - this(replacementText, ClassUtil.getSummaryCompletion(m, c), null); + this(replacementText, ClassIndex.getSummaryCompletion(m, c), null); this.method_args = Arrays.asList(m.getParameters()); this.method_returnType = m.getReturnType().getCanonicalName(); } public CompletionText(final String replacementText, final Class<?> c, final Constructor<?> constructor) { - this(replacementText, ClassUtil.getSummaryCompletion(constructor, c), null); + this(replacementText, ClassIndex.getSummaryCompletion(constructor, c), null); this.method_args = Arrays.asList(constructor.getParameters()); this.method_returnType = c.getCanonicalName(); } From 5d708bb79674b24dac7b7f592427a87f9a83e5be Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Thu, 8 Oct 2026 14:27:46 -0500 Subject: [PATCH 16/21] Ask for completions asynchronously code-lsp no longer answers with completions now and better ones later, nor rates signatures. The editor waits a moment for completions, and shows slower ones as they arrive; signatures show in the server's order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../scijava/ui/swing/script/PromptPane.java | 16 +- .../SciJavaCompletionProvider.java | 154 ++++++++---------- .../script/autocompletion/SignaturePopup.java | 19 +-- .../CodeIntelligenceUITest.java | 21 +-- .../SciJavaCompletionProviderTest.java | 56 ++----- 5 files changed, 102 insertions(+), 164 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/PromptPane.java b/src/main/java/org/scijava/ui/swing/script/PromptPane.java index e7c5f805..f2982276 100644 --- a/src/main/java/org/scijava/ui/swing/script/PromptPane.java +++ b/src/main/java/org/scijava/ui/swing/script/PromptPane.java @@ -58,7 +58,6 @@ import org.scijava.code.lsp.LanguageServerService; import org.scijava.code.lsp.Positions; import org.scijava.code.lsp.ScriptSession; -import org.scijava.code.lsp.UpdatingCompletionList; import org.scijava.thread.ThreadService; import org.scijava.util.ClassUtils; import org.scijava.util.Types; @@ -274,19 +273,8 @@ private List<CompletionItem> items(final ScriptInterpreter interpreter, { final ScriptSession session = session(interpreter); if (session == null) return Collections.emptyList(); - CompletionList list = session.completion(text, caret).get(COMPLETION_WAIT, - TimeUnit.MILLISECONDS); - // NB: Tab asks once: wait a moment for slower servers. - if (list instanceof UpdatingCompletionList) { - try { - final CompletionList better = ((UpdatingCompletionList) list).update() - .get(COMPLETION_WAIT, TimeUnit.MILLISECONDS); - if (better != null) list = better; - } - catch (final Exception exc) { - // NB: Not in time: what there is. - } - } + final CompletionList list = session.completion(text, caret).get( + COMPLETION_WAIT, TimeUnit.MILLISECONDS); final List<CompletionItem> items = new ArrayList<>(list.getItems()); items.sort(Comparator.comparing(item -> item.getSortText() != null ? item .getSortText() : item.getLabel())); diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java index 640213a2..778fedfb 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProvider.java @@ -29,14 +29,14 @@ package org.scijava.ui.swing.script.autocompletion; -import java.awt.Color; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -46,7 +46,6 @@ import java.util.regex.Pattern; import javax.swing.SwingUtilities; -import javax.swing.UIManager; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; @@ -67,10 +66,7 @@ import org.fife.ui.autocomplete.DefaultCompletionProvider; import org.fife.ui.autocomplete.ParameterizedCompletion; import org.scijava.code.lsp.Positions; -import org.scijava.code.lsp.RatedSignatureInformation.Fit; -import org.scijava.code.lsp.RatedSignatureInformation; import org.scijava.code.lsp.ScriptSession; -import org.scijava.code.lsp.UpdatingCompletionList; import org.scijava.log.Logger; /** @@ -81,11 +77,9 @@ * Completions with a snippet of parameters (e.g. {@code max(${1:a}, * ${2:b})}) become function completions, for parameter assistance; their * parameters' types come from the label details (e.g. {@code (double a, - * double b)}). The parameter tooltip lists the other overloads, as signature - * help rates them for the arguments typed (see - * {@link RatedSignatureInformation}). Documentation is resolved when shown. - * Slower completions (see {@link UpdatingCompletionList}) replace the first - * ones as they arrive. + * double b)}). The parameter tooltip lists the other overloads, in signature + * help's order. Documentation is resolved when shown. Completions slower than + * a moment are shown as they arrive. * </p> * * @author Curtis Rueden @@ -96,6 +90,12 @@ public class SciJavaCompletionProvider extends DefaultCompletionProvider { /** How long to wait for completions, in milliseconds. */ private static final long REQUEST_TIMEOUT = 2000; + /** + * How long the editor waits for completions as the user types, in + * milliseconds; those slower are shown as they arrive. + */ + private static final long ANSWER_WAIT = 50; + /** How long the parameter tooltip waits for signature help, in ms. */ private static final long HELP_BUDGET = 150; @@ -175,9 +175,9 @@ public List<Completion> getCompletionsImpl(final JTextComponent comp) { } /** - * Sets a listener to notify when better completions have arrived for the - * current request (see {@link UpdatingCompletionList}), e.g. to refresh the - * completion popup. Called on the event dispatch thread. + * Sets a listener to notify when completions have arrived late for the + * current request, e.g. to refresh the completion popup. Called on the + * event dispatch thread. * * @param listener Accepts whether the previous completions were none (so * that no popup may be showing for them). @@ -256,9 +256,8 @@ SignatureHelp signatureHelp(final JTextComponent comp) { * are none. * <p> * If the signature help is for this call, its signatures are listed in its - * order, styled by how well they fit the arguments typed so far: matches - * plain, those needing a conversion greyed, mismatches struck out. - * Otherwise, {@code overloads} are listed, unstyled. + * order (servers list those fitting the arguments typed so far first). + * Otherwise, {@code overloads} are listed. * </p> * * @param name The callable's name. @@ -270,56 +269,26 @@ static String otherOverloads(final String name, final List<String> own, final List<List<String>> overloads, final SignatureHelp help) { final String ownList = parameterList(own); - // NB: A map, since reflection can report the same signature twice. - final Map<String, Fit> lines = new LinkedHashMap<>(); + // NB: A set, since reflection can report the same signature twice. + final Set<String> lines = new LinkedHashSet<>(); if (help != null && isFor(help, name)) { for (final SignatureInformation s : help.getSignatures()) { - final String sig = parameterList(SignatureLabel.of(s).parameters); - if (!sig.equals(ownList)) lines.putIfAbsent(sig, RatedSignatureInformation - .fitOf(s)); + lines.add(parameterList(SignatureLabel.of(s).parameters)); } } else if (overloads != null) { - for (final List<String> o : overloads) { - final String sig = parameterList(o); - if (!sig.equals(ownList)) lines.putIfAbsent(sig, Fit.UNKNOWN); - } + for (final List<String> o : overloads) lines.add(parameterList(o)); } + lines.remove(ownList); if (lines.isEmpty()) return null; - // NB: Read the color now, so the tooltip follows Look & Feel changes. - final String dim = "<font color=\"" + dimColor() + "\">"; final StringBuilder sb = new StringBuilder(); - for (final Map.Entry<String, Fit> line : lines.entrySet()) { + for (final String line : lines) { // NB: A rule between overloads (and after RSTA's own line). - sb.append("<hr>"); - final String sig = escapeHTML(line.getKey()); - switch (line.getValue()) { - case CONVERSION: - sb.append(dim).append(sig).append("</font>"); - break; - case MISMATCH: - sb.append(dim).append("<s>").append(sig).append("</s></font>"); - break; - default: - sb.append(sig); - } + sb.append("<hr>").append(escapeHTML(line)); } return sb.toString(); } - /** - * The Look & Feel's color for disabled text, as an HTML color such as - * {@code #8c8c8c}. RSTA's parameter tooltip takes its other colors from the - * Look & Feel too. - */ - static String dimColor() { - Color c = UIManager.getColor("Label.disabledForeground"); - if (c == null) c = UIManager.getColor("textInactiveText"); - if (c == null) c = Color.GRAY; - return String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c - .getBlue()); - } - /** Converts documentation to HTML: Markdown as is (it allows HTML). */ static String html(final Either<String, MarkupContent> doc) { if (doc == null) return null; @@ -347,48 +316,67 @@ private void compute(final JTextComponent comp) { if (caret == cachedCaret && text.equals(cachedText) && cachedList != null) { return; } - final CompletionList list = request(text, caret); + final CompletableFuture<CompletionList> answer = ask(text, caret); cachedCaret = caret; cachedText = text; - adopt(list, comp); + final CompletionList now = await(answer, ANSWER_WAIT); + adopt(now == null ? new CompletionList() : now); + if (now != null) return; + // NB: Not in time: show the completions when they arrive, unless + // anything has changed since the request. + final CompletionList pending = cachedList; + answer.thenAccept(later -> SwingUtilities.invokeLater(() -> { + if (cachedList != pending || caret != comp.getCaretPosition() || !text + .equals(comp.getText())) return; + adopt(later); + final Consumer<Boolean> listener = updateListener; + if (listener != null) listener.accept(true); + })); } - /** Asks the server for completions; none if it fails. */ + /** Asks the server for completions, waiting for them; none if it fails. */ private CompletionList request(final String text, final int caret) { + final CompletionList list = await(ask(text, caret), REQUEST_TIMEOUT); + return list == null ? new CompletionList() : list; + } + + /** Asks the server for completions; none if it fails. */ + private CompletableFuture<CompletionList> ask(final String text, + final int caret) + { try { - return session.completion(text, caret).get(REQUEST_TIMEOUT, - TimeUnit.MILLISECONDS); + return session.completion(text, caret).exceptionally(exc -> { + failed(exc); + return new CompletionList(); + }); } catch (final Exception | LinkageError exc) { - // NB: Never let a misbehaving server break the editor. - if (log != null) log.debug("Code completion failed", exc); - return new CompletionList(); + failed(exc); + return CompletableFuture.completedFuture(new CompletionList()); + } + } + + /** Waits a while for completions; null if they are not done by then. */ + private static CompletionList await( + final CompletableFuture<CompletionList> answer, final long timeout) + { + try { + return answer.get(timeout, TimeUnit.MILLISECONDS); + } + catch (final Exception exc) { + return null; } } - /** Makes the given completions current, and awaits their update, if any. */ - private void adopt(final CompletionList list, final JTextComponent comp) { + private void failed(final Throwable exc) { + // NB: Never let a misbehaving server break the editor. + if (log != null) log.debug("Code completion failed", exc); + } + + /** Makes the given completions current. */ + private void adopt(final CompletionList list) { cachedList = list; cachedStart = replaceStart(list, cachedText, cachedCaret); - if (!(list instanceof UpdatingCompletionList)) return; - final CompletableFuture<CompletionList> update = - ((UpdatingCompletionList) list).update(); - if (update == null) return; - final String text = cachedText; - final int caret = cachedCaret; - update.whenComplete((better, error) -> { - if (better == null) return; - SwingUtilities.invokeLater(() -> { - // NB: Only if nothing has changed since the request. - if (cachedList != list || caret != comp.getCaretPosition() || !text - .equals(comp.getText())) return; - final boolean wasEmpty = list.getItems() == null || list.getItems() - .isEmpty(); - adopt(better, comp); - final Consumer<Boolean> listener = updateListener; - if (listener != null) listener.accept(wasEmpty); - }); - }); } /** diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java index 76f88c22..06082995 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/SignaturePopup.java @@ -56,7 +56,6 @@ import org.eclipse.lsp4j.SignatureHelp; import org.eclipse.lsp4j.SignatureInformation; import org.scijava.code.lsp.jvm.Signatures; -import org.scijava.code.lsp.RatedSignatureInformation; /** * Shows the signatures of the call being typed (the language server's @@ -168,10 +167,8 @@ void refresh(final boolean opening) { .getCaretPosition())); } - /** Renders signatures as HTML, one per line, best fits first. */ + /** Renders signatures as HTML, one per line, in the server's order. */ static String html(final SignatureHelp help) { - final String dim = "<font color=\"" + SciJavaCompletionProvider - .dimColor() + "\">"; final StringBuilder sb = new StringBuilder("<html>"); boolean first = true; for (final SignatureInformation s : help.getSignatures()) { @@ -179,18 +176,8 @@ static String html(final SignatureHelp help) { first = false; final Integer active = s.getActiveParameter() != null ? s .getActiveParameter() : help.getActiveParameter(); - final String sig = signature(SignatureLabel.of(s), active == null ? -1 - : active); - switch (RatedSignatureInformation.fitOf(s)) { - case CONVERSION: - sb.append(dim).append(sig).append("</font>"); - break; - case MISMATCH: - sb.append(dim).append("<s>").append(sig).append("</s></font>"); - break; - default: - sb.append(sig); - } + sb.append(signature(SignatureLabel.of(s), active == null ? -1 + : active)); } return sb.append("</html>").toString(); } diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java index 1ac1dbc4..55c382e2 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -137,27 +137,22 @@ public void testDiagnostics() throws Exception { @Test public void testSignaturesHTML() { final String html = SignaturePopup.html(new org.eclipse.lsp4j.SignatureHelp( - Arrays.asList(rated("max(double a, double b) -> double", - org.scijava.code.lsp.RatedSignatureInformation.Fit.MATCH, "double a", - "double b"), rated("max(int a, int b)", - org.scijava.code.lsp.RatedSignatureInformation.Fit.MISMATCH, "int a", - "int b")), 0, 1)); - // The parameter being typed in bold; mismatches struck out. - assertTrue(html, html.startsWith( - "<html>max(double a, <b>double b</b>) → double<br>")); - assertTrue(html, html.contains("<s>max(int a, <b>int b</b>)</s>")); + Arrays.asList(signature("max(double a, double b) -> double", "double a", + "double b"), signature("max(int a, int b)", "int a", "int b")), 0, 1)); + // One per line, in the server's order; the parameter being typed in bold. + assertEquals("<html>max(double a, <b>double b</b>) → double<br>" + + "max(int a, <b>int b</b>)</html>", html); } - private static org.eclipse.lsp4j.SignatureInformation rated( - final String label, final org.scijava.code.lsp.RatedSignatureInformation.Fit fit, - final String... params) + private static org.eclipse.lsp4j.SignatureInformation signature( + final String label, final String... params) { final List<org.eclipse.lsp4j.ParameterInformation> ps = new java.util.ArrayList<>(); for (final String p : params) { ps.add(new org.eclipse.lsp4j.ParameterInformation(p)); } - return new org.scijava.code.lsp.RatedSignatureInformation(label, ps, fit); + return new org.eclipse.lsp4j.SignatureInformation(label, (String) null, ps); } /** A server completing nothing. */ diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index ef7fa76c..30266686 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -47,7 +47,6 @@ import javax.swing.JLabel; import javax.swing.JList; import javax.swing.SwingUtilities; -import javax.swing.UIManager; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionItemKind; @@ -64,10 +63,8 @@ import org.scijava.code.lsp.jvm.Callable; import org.scijava.code.lsp.Environment; import org.scijava.code.lsp.Positions; -import org.scijava.code.lsp.RatedSignatureInformation.Fit; import org.scijava.code.lsp.jvm.ScriptLanguageServer; import org.scijava.code.lsp.ScriptSession; -import org.scijava.code.lsp.UpdatingCompletionList; /** * Tests that {@link SciJavaCompletionProvider} faithfully bridges a language @@ -248,27 +245,24 @@ public void testParameterTooltipShowsSignatureHelp() { server.help = (doc, caret) -> { askedText[0] = doc.text(); askedCaret[0] = caret; - // The server rates and orders the signatures: here, as for a float - // typed as the first argument. - return new SignatureHelp(Arrays.asList(floats.toSignature(Fit.MATCH), - ints.toSignature(Fit.MISMATCH), longs.toSignature(Fit.CONVERSION), - strings.toSignature(Fit.MISMATCH)), 0, 1); + // The server orders the signatures, best fits first: here, as for a + // float typed as the first argument. + return new SignatureHelp(Arrays.asList(floats.toSignature(), ints + .toSignature(), longs.toSignature(), strings.toSignature()), 0, 1); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( ScriptSession.owning(server, null)); final List<Completion> completions = provider.getCompletionsImpl(area); final ParameterizedCompletion intOverload = (ParameterizedCompletion) completions.get(0); - final String gray = "<font color=\"" + SciJavaCompletionProvider - .dimColor() + "\">"; // Accepted a(int, int), typed a float, and tabbed onward: the server's - // order, styled by fit, without the accepted overload. + // order, without the accepted overload. area.setText("a(1.5, p1)"); area.setCaretPosition(7); assertEquals("<hr>float p0, float p1" + // - "<hr>" + gray + "long p0, long p1</font>" + // - "<hr>" + gray + "<s>String p0</s></font>", // + "<hr>long p0, long p1" + // + "<hr>String p0", // intOverload.getParam(1).getDescription()); // The server was asked about the caret. assertEquals(7, askedCaret[0]); @@ -287,7 +281,7 @@ public void testParameterTooltipIgnoresOtherCalls() { .toItem(), floats.toItem()); // The caret is in a call nested in a's arguments: b's signatures. server.help = (doc, caret) -> new SignatureHelp(Collections.singletonList( - method("b", "double").toSignature(Fit.MATCH)), 0, 0); + method("b", "double").toSignature()), 0, 0); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( ScriptSession.owning(server, null)); final ParameterizedCompletion intOverload = @@ -336,11 +330,11 @@ public void testResultUpdates() throws Exception { area.setCaretPosition(4); final CompletableFuture<CompletionList> later = new CompletableFuture<>(); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(new UpdatingServer(later), null)); + ScriptSession.owning(new LateServer(later), null)); final Boolean[] notified = { null }; provider.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); - // At first, nothing; then the better result arrives. + // At first, nothing (not in time); then the result arrives. assertEquals(0, provider.getCompletionsImpl(area).size()); later.complete(zeros("np.z")); SwingUtilities.invokeAndWait(() -> {}); @@ -349,10 +343,10 @@ public void testResultUpdates() throws Exception { assertEquals(1, completions.size()); assertEquals("zeros", completions.get(0).getReplacementText()); - // An update for a request the user has typed past is ignored. + // A result for a request the user has typed past is ignored. final CompletableFuture<CompletionList> stale = new CompletableFuture<>(); final SciJavaCompletionProvider provider2 = new SciJavaCompletionProvider( - ScriptSession.owning(new UpdatingServer(stale), null)); + ScriptSession.owning(new LateServer(stale), null)); notified[0] = null; provider2.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); area.setText("np.z"); @@ -382,19 +376,6 @@ public void testOpenOnInstall() { assertEquals(1, server.closed.size()); } - @Test - public void testDimColorFollowsLookAndFeel() { - final Object old = UIManager.get("Label.disabledForeground"); - try { - UIManager.put("Label.disabledForeground", new java.awt.Color(0x12, 0x34, - 0xab)); - assertEquals("#1234ab", SciJavaCompletionProvider.dimColor()); - } - finally { - UIManager.put("Label.disabledForeground", old); - } - } - @Test public void testRequestCarriesScriptPath() { final RSyntaxTextArea area = new RSyntaxTextArea(); @@ -496,21 +477,20 @@ protected void closed(final Document doc) { } } - /** A server with nothing at first, and more to come. */ - private static class UpdatingServer extends FakeServer { + /** A server whose completions come when they are completed. */ + private static class LateServer extends FakeServer { - private final CompletableFuture<CompletionList> update; + private final CompletableFuture<CompletionList> later; - UpdatingServer(final CompletableFuture<CompletionList> update) { - this.update = update; + LateServer(final CompletableFuture<CompletionList> later) { + this.later = later; } @Override public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(final CompletionParams params) { - return CompletableFuture.completedFuture(Either.forRight( - new UpdatingCompletionList(Collections.emptyList(), update))); + return later.thenApply(Either::forRight); } } } From 6ccfefa5762dc4a072beb56c9750b81d993ce1b2 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Thu, 15 Oct 2026 17:57:40 -0500 Subject: [PATCH 17/21] Follow AbstractLanguageServer, as code-lsp renamed it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../swing/script/autocompletion/CodeIntelligenceUITest.java | 6 +++--- .../autocompletion/SciJavaCompletionProviderTest.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java index 55c382e2..45beb5f7 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -52,7 +52,7 @@ import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; import org.junit.Test; import org.scijava.code.lsp.Environment; -import org.scijava.code.lsp.jvm.ScriptLanguageServer; +import org.scijava.code.lsp.jvm.AbstractLanguageServer; import org.scijava.code.lsp.ScriptSession; /** @@ -66,7 +66,7 @@ public class CodeIntelligenceUITest { @Test public void testHover() { final AtomicInteger asked = new AtomicInteger(); - final ScriptLanguageServer server = new FakeServer() { + final AbstractLanguageServer server = new FakeServer() { @Override protected Hover hover(final Document doc, final int at) { @@ -156,7 +156,7 @@ private static org.eclipse.lsp4j.SignatureInformation signature( } /** A server completing nothing. */ - private static class FakeServer extends ScriptLanguageServer { + private static class FakeServer extends AbstractLanguageServer { FakeServer() { super(Environment.NONE); diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index 30266686..88e20708 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -63,7 +63,7 @@ import org.scijava.code.lsp.jvm.Callable; import org.scijava.code.lsp.Environment; import org.scijava.code.lsp.Positions; -import org.scijava.code.lsp.jvm.ScriptLanguageServer; +import org.scijava.code.lsp.jvm.AbstractLanguageServer; import org.scijava.code.lsp.ScriptSession; /** @@ -428,7 +428,7 @@ private static CompletionList zeros(final String text) { // -- Helper classes -- /** A server answering as its functions say. */ - private static class FakeServer extends ScriptLanguageServer { + private static class FakeServer extends AbstractLanguageServer { BiFunction<Document, Integer, CompletionList> complete = (doc, caret) -> new CompletionList(); From 56dfdf3456500c21743b81824431f09c18f32d53 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Mon, 19 Oct 2026 15:45:42 -0500 Subject: [PATCH 18/21] Follow scijava-code-lsp's ScriptSession constructor and NO_STATE Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../scijava/ui/swing/script/PromptPane.java | 5 ++-- .../CodeIntelligenceUITest.java | 3 ++- .../SciJavaCompletionProviderTest.java | 25 ++++++++++--------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/PromptPane.java b/src/main/java/org/scijava/ui/swing/script/PromptPane.java index f2982276..c7ad2642 100644 --- a/src/main/java/org/scijava/ui/swing/script/PromptPane.java +++ b/src/main/java/org/scijava/ui/swing/script/PromptPane.java @@ -55,6 +55,7 @@ import org.scijava.Context; import org.scijava.script.ScriptInterpreter; import org.scijava.script.ScriptREPL; +import org.scijava.code.lsp.Environment; import org.scijava.code.lsp.LanguageServerService; import org.scijava.code.lsp.Positions; import org.scijava.code.lsp.ScriptSession; @@ -294,8 +295,8 @@ private synchronized ScriptSession session( LanguageServerService.class); final ScriptEngine engine = interpreter.getEngine(); if (servers == null || interpreter.getLanguage() == null) return null; - session = servers.session(interpreter.getLanguage(), engine == null ? null - : engine.getContext()); + session = servers.session(interpreter.getLanguage(), engine == null + ? Environment.NO_STATE : engine.getContext()); sessionInterpreter = interpreter; return session; } diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java index 45beb5f7..e12b656b 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -52,6 +52,7 @@ import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; import org.junit.Test; import org.scijava.code.lsp.Environment; +import org.scijava.code.lsp.ScriptDocument; import org.scijava.code.lsp.jvm.AbstractLanguageServer; import org.scijava.code.lsp.ScriptSession; @@ -81,7 +82,7 @@ protected Hover hover(final Document doc, final int at) { } }; final HoverToolTipSupplier supplier = new HoverToolTipSupplier( - ScriptSession.owning(server, null), null); + new ScriptSession(server, ScriptDocument::of), null); final String text = "x.dumps(1)"; assertEquals("<html><pre>Serialize.</pre></html>", supplier.toolTip(text, 3)); diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index 88e20708..1eb6211d 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -60,6 +60,7 @@ import org.fife.ui.autocomplete.ParameterizedCompletion; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.junit.Test; +import org.scijava.code.lsp.ScriptDocument; import org.scijava.code.lsp.jvm.Callable; import org.scijava.code.lsp.Environment; import org.scijava.code.lsp.Positions; @@ -91,7 +92,7 @@ public void testBridgesCompletions() { return FakeServer.items(doc, 4, caret, bar, new CompletionItem("baz")); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(server, null)); + new ScriptSession(server, ScriptDocument::of)); // The provider must report the already-entered text as the replaced span. assertEquals("ba", provider.getAlreadyEnteredText(area)); @@ -118,7 +119,7 @@ public void testAdditionalEditsSurfaceAsSciJavaCompletion() { return FakeServer.items(doc, 0, caret, roi); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(server, null)); + new ScriptSession(server, ScriptDocument::of)); final List<Completion> completions = provider.getCompletionsImpl(area); assertEquals(1, completions.size()); @@ -154,7 +155,7 @@ public void testCallablesBecomeFunctionCompletionsWithChoices() { return FakeServer.items(doc, caret, caret, myRoi, addRoi.toItem()); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(server, null)); + new ScriptSession(server, ScriptDocument::of)); // Callables surface as FunctionCompletions so parameter assistance works. final List<Completion> completions = provider.getCompletionsImpl(area); @@ -181,7 +182,7 @@ public void testCallablesRenderWithSignatures() { server.complete = (doc, caret) -> FakeServer.items(doc, 0, caret, method( completionText, "long[]").toItem()); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(server, null)); + new ScriptSession(server, ScriptDocument::of)); // The popup list must show a callable's parameters, not just its name. final Completion c = provider.getCompletionsImpl(area).get(0); @@ -205,7 +206,7 @@ public void testParameterTooltipListsOtherOverloads() { method("img.getAt", "int[]").toItem(), // e.g. a bridge method method("img.getAtX", "long[]").toItem()); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(server, null)); + new ScriptSession(server, ScriptDocument::of)); final List<Completion> completions = provider.getCompletionsImpl(area); // Each overload's tooltip lists the others, once each, even before RSTA @@ -251,7 +252,7 @@ public void testParameterTooltipShowsSignatureHelp() { .toSignature(), longs.toSignature(), strings.toSignature()), 0, 1); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(server, null)); + new ScriptSession(server, ScriptDocument::of)); final List<Completion> completions = provider.getCompletionsImpl(area); final ParameterizedCompletion intOverload = (ParameterizedCompletion) completions.get(0); @@ -283,7 +284,7 @@ public void testParameterTooltipIgnoresOtherCalls() { server.help = (doc, caret) -> new SignatureHelp(Collections.singletonList( method("b", "double").toSignature()), 0, 0); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(server, null)); + new ScriptSession(server, ScriptDocument::of)); final ParameterizedCompletion intOverload = (ParameterizedCompletion) provider.getCompletionsImpl(area).get(0); area.setText("a(b(|), p1)"); @@ -312,7 +313,7 @@ public void testLazyDescriptions() { return "The zeta function."; })); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(server, null)); + new ScriptSession(server, ScriptDocument::of)); final List<Completion> completions = provider.getCompletionsImpl(area); // Nothing is described until RSTA shows a completion's description... assertEquals(0, asked.get()); @@ -330,7 +331,7 @@ public void testResultUpdates() throws Exception { area.setCaretPosition(4); final CompletableFuture<CompletionList> later = new CompletableFuture<>(); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(new LateServer(later), null)); + new ScriptSession(new LateServer(later), ScriptDocument::of)); final Boolean[] notified = { null }; provider.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); @@ -346,7 +347,7 @@ public void testResultUpdates() throws Exception { // A result for a request the user has typed past is ignored. final CompletableFuture<CompletionList> stale = new CompletableFuture<>(); final SciJavaCompletionProvider provider2 = new SciJavaCompletionProvider( - ScriptSession.owning(new LateServer(stale), null)); + new ScriptSession(new LateServer(stale), ScriptDocument::of)); notified[0] = null; provider2.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); area.setText("np.z"); @@ -366,7 +367,7 @@ public void testOpenOnInstall() { final File file = new File("scripts", "blur.py"); final FakeServer server = new FakeServer(); final LanguageServerLanguageSupport support = - new LanguageServerLanguageSupport(() -> ScriptSession.owning(server, null), + new LanguageServerLanguageSupport(() -> new ScriptSession(server, ScriptDocument::of), null, () -> file); support.install(area); assertEquals(1, server.opened.size()); @@ -388,7 +389,7 @@ public void testRequestCarriesScriptPath() { return new CompletionList(); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - ScriptSession.owning(server, null)); + new ScriptSession(server, ScriptDocument::of)); // Without a file supplier (e.g. in an interpreter): no path. provider.getCompletionsImpl(area); From 0530536a9ac33c243dd16fab3ea6a92d1d6a47e8 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Tue, 20 Oct 2026 09:50:46 -0500 Subject: [PATCH 19/21] Follow scijava-code-lsp: sessions are made with the service Sessions tell their servers off the caller's thread, so a test waits for the server to hear of the script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../scijava/ui/swing/script/EditorPane.java | 5 +-- .../scijava/ui/swing/script/PromptPane.java | 4 +-- .../LanguageServerLanguageSupport.java | 2 +- .../CodeIntelligenceUITest.java | 2 +- .../SciJavaCompletionProviderTest.java | 33 ++++++++++++------- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/EditorPane.java b/src/main/java/org/scijava/ui/swing/script/EditorPane.java index 0f5dabf7..8bd5185a 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -95,6 +95,7 @@ import org.scijava.script.ScriptService; import org.scijava.ui.swing.script.vim.VimHandler; import org.scijava.code.lsp.LanguageServerService; +import org.scijava.code.lsp.ScriptSession; import org.scijava.ui.swing.script.autocompletion.LanguageServerLanguageSupport; import org.scijava.util.FileUtils; @@ -814,8 +815,8 @@ protected void setLanguage(final ScriptLanguage language, if (support == null && languageServerService.supports(currentLanguage)) { final ScriptLanguage served = currentLanguage; final LanguageServerLanguageSupport serverSupport = - new LanguageServerLanguageSupport(() -> languageServerService.session( - served), log, this::getFile); + new LanguageServerLanguageSupport(() -> new ScriptSession( + languageServerService, served), log, this::getFile); serverSupport.setHoverEnabled(hoverEnabled); serverSupport.setDiagnosticsEnabled(diagnosticsEnabled); support = serverSupport; diff --git a/src/main/java/org/scijava/ui/swing/script/PromptPane.java b/src/main/java/org/scijava/ui/swing/script/PromptPane.java index c7ad2642..88ef5879 100644 --- a/src/main/java/org/scijava/ui/swing/script/PromptPane.java +++ b/src/main/java/org/scijava/ui/swing/script/PromptPane.java @@ -295,8 +295,8 @@ private synchronized ScriptSession session( LanguageServerService.class); final ScriptEngine engine = interpreter.getEngine(); if (servers == null || interpreter.getLanguage() == null) return null; - session = servers.session(interpreter.getLanguage(), engine == null - ? Environment.NO_STATE : engine.getContext()); + session = new ScriptSession(servers, interpreter.getLanguage(), + engine == null ? Environment.NO_STATE : engine.getContext()); sessionInterpreter = interpreter; return session; } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java index f7593f69..714607f5 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java @@ -69,7 +69,7 @@ public class LanguageServerLanguageSupport extends AbstractLanguageSupport { /** * @param sessions Starts a session about the script, for each text area * this is installed in (e.g. - * {@code () -> languageServerService.session(language)}). + * {@code () -> new ScriptSession(servers, language)}). * @param log Where to report problems, or null. * @param file Supplies the file of the script being edited (or null if * unsaved). diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java index e12b656b..af1a333b 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -82,7 +82,7 @@ protected Hover hover(final Document doc, final int at) { } }; final HoverToolTipSupplier supplier = new HoverToolTipSupplier( - new ScriptSession(server, ScriptDocument::of), null); + new ScriptSession(e -> server, ScriptDocument::of), null); final String text = "x.dumps(1)"; assertEquals("<html><pre>Serialize.</pre></html>", supplier.toolTip(text, 3)); diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index 1eb6211d..58e1d8b9 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -92,7 +92,7 @@ public void testBridgesCompletions() { return FakeServer.items(doc, 4, caret, bar, new CompletionItem("baz")); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(server, ScriptDocument::of)); + new ScriptSession(e -> server, ScriptDocument::of)); // The provider must report the already-entered text as the replaced span. assertEquals("ba", provider.getAlreadyEnteredText(area)); @@ -119,7 +119,7 @@ public void testAdditionalEditsSurfaceAsSciJavaCompletion() { return FakeServer.items(doc, 0, caret, roi); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(server, ScriptDocument::of)); + new ScriptSession(e -> server, ScriptDocument::of)); final List<Completion> completions = provider.getCompletionsImpl(area); assertEquals(1, completions.size()); @@ -155,7 +155,7 @@ public void testCallablesBecomeFunctionCompletionsWithChoices() { return FakeServer.items(doc, caret, caret, myRoi, addRoi.toItem()); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(server, ScriptDocument::of)); + new ScriptSession(e -> server, ScriptDocument::of)); // Callables surface as FunctionCompletions so parameter assistance works. final List<Completion> completions = provider.getCompletionsImpl(area); @@ -182,7 +182,7 @@ public void testCallablesRenderWithSignatures() { server.complete = (doc, caret) -> FakeServer.items(doc, 0, caret, method( completionText, "long[]").toItem()); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(server, ScriptDocument::of)); + new ScriptSession(e -> server, ScriptDocument::of)); // The popup list must show a callable's parameters, not just its name. final Completion c = provider.getCompletionsImpl(area).get(0); @@ -206,7 +206,7 @@ public void testParameterTooltipListsOtherOverloads() { method("img.getAt", "int[]").toItem(), // e.g. a bridge method method("img.getAtX", "long[]").toItem()); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(server, ScriptDocument::of)); + new ScriptSession(e -> server, ScriptDocument::of)); final List<Completion> completions = provider.getCompletionsImpl(area); // Each overload's tooltip lists the others, once each, even before RSTA @@ -252,7 +252,7 @@ public void testParameterTooltipShowsSignatureHelp() { .toSignature(), longs.toSignature(), strings.toSignature()), 0, 1); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(server, ScriptDocument::of)); + new ScriptSession(e -> server, ScriptDocument::of)); final List<Completion> completions = provider.getCompletionsImpl(area); final ParameterizedCompletion intOverload = (ParameterizedCompletion) completions.get(0); @@ -284,7 +284,7 @@ public void testParameterTooltipIgnoresOtherCalls() { server.help = (doc, caret) -> new SignatureHelp(Collections.singletonList( method("b", "double").toSignature()), 0, 0); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(server, ScriptDocument::of)); + new ScriptSession(e -> server, ScriptDocument::of)); final ParameterizedCompletion intOverload = (ParameterizedCompletion) provider.getCompletionsImpl(area).get(0); area.setText("a(b(|), p1)"); @@ -313,7 +313,7 @@ public void testLazyDescriptions() { return "The zeta function."; })); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(server, ScriptDocument::of)); + new ScriptSession(e -> server, ScriptDocument::of)); final List<Completion> completions = provider.getCompletionsImpl(area); // Nothing is described until RSTA shows a completion's description... assertEquals(0, asked.get()); @@ -331,7 +331,7 @@ public void testResultUpdates() throws Exception { area.setCaretPosition(4); final CompletableFuture<CompletionList> later = new CompletableFuture<>(); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(new LateServer(later), ScriptDocument::of)); + new ScriptSession(e -> new LateServer(later), ScriptDocument::of)); final Boolean[] notified = { null }; provider.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); @@ -347,7 +347,7 @@ public void testResultUpdates() throws Exception { // A result for a request the user has typed past is ignored. final CompletableFuture<CompletionList> stale = new CompletableFuture<>(); final SciJavaCompletionProvider provider2 = new SciJavaCompletionProvider( - new ScriptSession(new LateServer(stale), ScriptDocument::of)); + new ScriptSession(e -> new LateServer(stale), ScriptDocument::of)); notified[0] = null; provider2.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); area.setText("np.z"); @@ -367,9 +367,18 @@ public void testOpenOnInstall() { final File file = new File("scripts", "blur.py"); final FakeServer server = new FakeServer(); final LanguageServerLanguageSupport support = - new LanguageServerLanguageSupport(() -> new ScriptSession(server, ScriptDocument::of), + new LanguageServerLanguageSupport(() -> new ScriptSession(e -> server, ScriptDocument::of), null, () -> file); support.install(area); + // NB: Sessions tell their servers off the caller's thread. + for (int i = 0; i < 500 && server.opened.isEmpty(); i++) { + try { + Thread.sleep(10); + } + catch (final InterruptedException exc) { + Thread.currentThread().interrupt(); + } + } assertEquals(1, server.opened.size()); assertEquals("import numpy\n", server.opened.get(0).text()); assertEquals(file.getAbsolutePath(), server.opened.get(0).path()); @@ -389,7 +398,7 @@ public void testRequestCarriesScriptPath() { return new CompletionList(); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(server, ScriptDocument::of)); + new ScriptSession(e -> server, ScriptDocument::of)); // Without a file supplier (e.g. in an interpreter): no path. provider.getCompletionsImpl(area); From 7e6a4af1e57eb1970842fa4b165ec46bbbd6d222 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Tue, 20 Oct 2026 13:22:59 -0500 Subject: [PATCH 20/21] Make test sessions with a fake LanguageServerService ScriptSession is made with a service only: FakeServers gives each test session a fake server of its own, which sees scripts as they are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../CodeIntelligenceUITest.java | 4 +- .../script/autocompletion/FakeServers.java | 145 ++++++++++++++++++ .../SciJavaCompletionProviderTest.java | 26 ++-- 3 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 src/test/java/org/scijava/ui/swing/script/autocompletion/FakeServers.java diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java index af1a333b..e752786c 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -52,9 +52,7 @@ import org.fife.ui.rsyntaxtextarea.parser.ParserNotice; import org.junit.Test; import org.scijava.code.lsp.Environment; -import org.scijava.code.lsp.ScriptDocument; import org.scijava.code.lsp.jvm.AbstractLanguageServer; -import org.scijava.code.lsp.ScriptSession; /** * Tests {@link HoverToolTipSupplier}, {@link DiagnosticsParser} and @@ -82,7 +80,7 @@ protected Hover hover(final Document doc, final int at) { } }; final HoverToolTipSupplier supplier = new HoverToolTipSupplier( - new ScriptSession(e -> server, ScriptDocument::of), null); + FakeServers.session(server), null); final String text = "x.dumps(1)"; assertEquals("<html><pre>Serialize.</pre></html>", supplier.toolTip(text, 3)); diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/FakeServers.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/FakeServers.java new file mode 100644 index 00000000..74cdbf29 --- /dev/null +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/FakeServers.java @@ -0,0 +1,145 @@ +/* + * #%L + * Script Editor and Interpreter for SciJava script languages. + * %% + * Copyright (C) 2009 - 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.ui.swing.script.autocompletion; + +import java.util.Collections; +import java.util.Map; + +import javax.script.ScriptEngine; + +import org.eclipse.lsp4j.services.LanguageServer; +import org.scijava.Context; +import org.scijava.code.lsp.Environment; +import org.scijava.code.lsp.LanguageServerService; +import org.scijava.code.lsp.ScriptDialect; +import org.scijava.code.lsp.ScriptDocument; +import org.scijava.code.lsp.ScriptSession; +import org.scijava.script.AbstractScriptLanguage; +import org.scijava.script.ScriptLanguage; +import org.scijava.service.AbstractService; + +/** + * A {@link LanguageServerService} serving every script with one fake server, + * which sees scripts as they are: for tests of what editors do with sessions. + * + * @author Gabriel Selzer + */ +final class FakeServers extends AbstractService implements + LanguageServerService +{ + + /** A script language: any will do, as all are served alike here. */ + private static final ScriptLanguage LANGUAGE = + new AbstractScriptLanguage() + { + + @Override + public ScriptEngine getScriptEngine() { + return null; + } + }; + + /** Shows scripts as they are. */ + private static final ScriptDialect AS_IS = new ScriptDialect() { + + @Override + public boolean supports(final ScriptLanguage language) { + return true; + } + + @Override + public String languageId() { + return "plaintext"; + } + + @Override + public Header header(final Map<String, Class<?>> parameters) { + return new Header("", Collections.emptySet()); + } + }; + + /** Where sessions look for a log service: there is none. */ + private static final Context EMPTY = new Context(true); + + private final LanguageServer server; + + private FakeServers(final LanguageServer server) { + this.server = server; + } + + @Override + public Context context() { + return EMPTY; + } + + @Override + public Context getContext() { + return EMPTY; + } + + /** A session whose server is the given one. */ + static ScriptSession session(final LanguageServer server) { + return new ScriptSession(new FakeServers(server), LANGUAGE); + } + + @Override + public ScriptDialect dialect(final ScriptLanguage language) { + return AS_IS; + } + + @Override + public ScriptDocument document(final String script, + final ScriptDialect dialect) + { + return ScriptDocument.of(script); + } + + @Override + public boolean supports(final ScriptLanguage language) { + return true; + } + + @Override + public boolean supports(final String languageId) { + return true; + } + + @Override + public LanguageServer launch(final String languageId, + final Environment environment) + { + return server; + } + + @Override + public Map<String, Class<?>> scriptParameters(final String text) { + return Collections.emptyMap(); + } +} diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index 58e1d8b9..a1c4d2f7 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -60,12 +60,10 @@ import org.fife.ui.autocomplete.ParameterizedCompletion; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.junit.Test; -import org.scijava.code.lsp.ScriptDocument; import org.scijava.code.lsp.jvm.Callable; import org.scijava.code.lsp.Environment; import org.scijava.code.lsp.Positions; import org.scijava.code.lsp.jvm.AbstractLanguageServer; -import org.scijava.code.lsp.ScriptSession; /** * Tests that {@link SciJavaCompletionProvider} faithfully bridges a language @@ -92,7 +90,7 @@ public void testBridgesCompletions() { return FakeServer.items(doc, 4, caret, bar, new CompletionItem("baz")); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(e -> server, ScriptDocument::of)); + FakeServers.session(server)); // The provider must report the already-entered text as the replaced span. assertEquals("ba", provider.getAlreadyEnteredText(area)); @@ -119,7 +117,7 @@ public void testAdditionalEditsSurfaceAsSciJavaCompletion() { return FakeServer.items(doc, 0, caret, roi); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(e -> server, ScriptDocument::of)); + FakeServers.session(server)); final List<Completion> completions = provider.getCompletionsImpl(area); assertEquals(1, completions.size()); @@ -155,7 +153,7 @@ public void testCallablesBecomeFunctionCompletionsWithChoices() { return FakeServer.items(doc, caret, caret, myRoi, addRoi.toItem()); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(e -> server, ScriptDocument::of)); + FakeServers.session(server)); // Callables surface as FunctionCompletions so parameter assistance works. final List<Completion> completions = provider.getCompletionsImpl(area); @@ -182,7 +180,7 @@ public void testCallablesRenderWithSignatures() { server.complete = (doc, caret) -> FakeServer.items(doc, 0, caret, method( completionText, "long[]").toItem()); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(e -> server, ScriptDocument::of)); + FakeServers.session(server)); // The popup list must show a callable's parameters, not just its name. final Completion c = provider.getCompletionsImpl(area).get(0); @@ -206,7 +204,7 @@ public void testParameterTooltipListsOtherOverloads() { method("img.getAt", "int[]").toItem(), // e.g. a bridge method method("img.getAtX", "long[]").toItem()); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(e -> server, ScriptDocument::of)); + FakeServers.session(server)); final List<Completion> completions = provider.getCompletionsImpl(area); // Each overload's tooltip lists the others, once each, even before RSTA @@ -252,7 +250,7 @@ public void testParameterTooltipShowsSignatureHelp() { .toSignature(), longs.toSignature(), strings.toSignature()), 0, 1); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(e -> server, ScriptDocument::of)); + FakeServers.session(server)); final List<Completion> completions = provider.getCompletionsImpl(area); final ParameterizedCompletion intOverload = (ParameterizedCompletion) completions.get(0); @@ -284,7 +282,7 @@ public void testParameterTooltipIgnoresOtherCalls() { server.help = (doc, caret) -> new SignatureHelp(Collections.singletonList( method("b", "double").toSignature()), 0, 0); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(e -> server, ScriptDocument::of)); + FakeServers.session(server)); final ParameterizedCompletion intOverload = (ParameterizedCompletion) provider.getCompletionsImpl(area).get(0); area.setText("a(b(|), p1)"); @@ -313,7 +311,7 @@ public void testLazyDescriptions() { return "The zeta function."; })); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(e -> server, ScriptDocument::of)); + FakeServers.session(server)); final List<Completion> completions = provider.getCompletionsImpl(area); // Nothing is described until RSTA shows a completion's description... assertEquals(0, asked.get()); @@ -331,7 +329,7 @@ public void testResultUpdates() throws Exception { area.setCaretPosition(4); final CompletableFuture<CompletionList> later = new CompletableFuture<>(); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(e -> new LateServer(later), ScriptDocument::of)); + FakeServers.session(new LateServer(later))); final Boolean[] notified = { null }; provider.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); @@ -347,7 +345,7 @@ public void testResultUpdates() throws Exception { // A result for a request the user has typed past is ignored. final CompletableFuture<CompletionList> stale = new CompletableFuture<>(); final SciJavaCompletionProvider provider2 = new SciJavaCompletionProvider( - new ScriptSession(e -> new LateServer(stale), ScriptDocument::of)); + FakeServers.session(new LateServer(stale))); notified[0] = null; provider2.setUpdateListener(wasEmpty -> notified[0] = wasEmpty); area.setText("np.z"); @@ -367,7 +365,7 @@ public void testOpenOnInstall() { final File file = new File("scripts", "blur.py"); final FakeServer server = new FakeServer(); final LanguageServerLanguageSupport support = - new LanguageServerLanguageSupport(() -> new ScriptSession(e -> server, ScriptDocument::of), + new LanguageServerLanguageSupport(() -> FakeServers.session(server), null, () -> file); support.install(area); // NB: Sessions tell their servers off the caller's thread. @@ -398,7 +396,7 @@ public void testRequestCarriesScriptPath() { return new CompletionList(); }; final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( - new ScriptSession(e -> server, ScriptDocument::of)); + FakeServers.session(server)); // Without a file supplier (e.g. in an interpreter): no path. provider.getCompletionsImpl(area); From fd5c88407cffec5f0d403cebd8684bafc3a51fb0 Mon Sep 17 00:00:00 2001 From: Gabriel Selzer <gjselzer@wisc.edu> Date: Wed, 21 Oct 2026 11:27:16 -0500 Subject: [PATCH 21/21] Follow scijava-code-lsp's sessions and environments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- src/main/java/org/scijava/ui/swing/script/PromptPane.java | 6 +++--- .../autocompletion/LanguageServerLanguageSupport.java | 3 ++- .../swing/script/autocompletion/CodeIntelligenceUITest.java | 2 +- .../scijava/ui/swing/script/autocompletion/FakeServers.java | 5 ----- .../autocompletion/SciJavaCompletionProviderTest.java | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/PromptPane.java b/src/main/java/org/scijava/ui/swing/script/PromptPane.java index 88ef5879..c2e1852a 100644 --- a/src/main/java/org/scijava/ui/swing/script/PromptPane.java +++ b/src/main/java/org/scijava/ui/swing/script/PromptPane.java @@ -55,7 +55,6 @@ import org.scijava.Context; import org.scijava.script.ScriptInterpreter; import org.scijava.script.ScriptREPL; -import org.scijava.code.lsp.Environment; import org.scijava.code.lsp.LanguageServerService; import org.scijava.code.lsp.Positions; import org.scijava.code.lsp.ScriptSession; @@ -295,8 +294,9 @@ private synchronized ScriptSession session( LanguageServerService.class); final ScriptEngine engine = interpreter.getEngine(); if (servers == null || interpreter.getLanguage() == null) return null; - session = new ScriptSession(servers, interpreter.getLanguage(), - engine == null ? Environment.NO_STATE : engine.getContext()); + session = engine == null ? new ScriptSession(servers, interpreter + .getLanguage()) : new ScriptSession(servers, interpreter.getLanguage(), + engine.getContext()); sessionInterpreter = interpreter; return session; } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java index 714607f5..f9633012 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/LanguageServerLanguageSupport.java @@ -103,7 +103,8 @@ public void setDiagnosticsEnabled(final boolean enabled) { @Override public void install(final RSyntaxTextArea textArea) { - final ScriptSession session = sessions.get().setFile(file).setLogger(log); + final ScriptSession session = sessions.get(); + session.setFile(file); final SciJavaCompletionProvider provider = new SciJavaCompletionProvider( session); provider.setLogger(log); diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java index e752786c..60fa930a 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/CodeIntelligenceUITest.java @@ -158,7 +158,7 @@ private static org.eclipse.lsp4j.SignatureInformation signature( private static class FakeServer extends AbstractLanguageServer { FakeServer() { - super(Environment.NONE); + super(new Environment()); } @Override diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/FakeServers.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/FakeServers.java index 74cdbf29..7b80fc73 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/FakeServers.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/FakeServers.java @@ -137,9 +137,4 @@ public LanguageServer launch(final String languageId, { return server; } - - @Override - public Map<String, Class<?>> scriptParameters(final String text) { - return Collections.emptyMap(); - } } diff --git a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java index a1c4d2f7..9fae7b8e 100644 --- a/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java +++ b/src/test/java/org/scijava/ui/swing/script/autocompletion/SciJavaCompletionProviderTest.java @@ -445,7 +445,7 @@ private static class FakeServer extends AbstractLanguageServer { final List<Document> closed = new ArrayList<>(); FakeServer() { - super(Environment.NONE); + super(new Environment()); } /** Completions replacing the text from {@code start} to the caret. */