diff --git a/pom.xml b/pom.xml
index 9107571b..b05408f7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -127,7 +127,7 @@
scm:git:git://github.com/scijava/script-editor
scm:git:git@github.com:scijava/script-editor
- HEAD
+ script-editor-0.5.9
https://github.com/scijava/script-editor
diff --git a/release.properties b/release.properties
new file mode 100644
index 00000000..d4adcd00
--- /dev/null
+++ b/release.properties
@@ -0,0 +1,19 @@
+#release configuration
+#Wed Dec 16 10:56:44 CET 2020
+project.scm.org.scijava\:script-editor.developerConnection=scm\:git\:git@github.com\:scijava/script-editor
+scm.tagNameFormat=@{project.artifactId}-@{project.version}
+scm.tag=script-editor-0.5.9
+pushChanges=false
+scm.url=scm\:git\:git\://github.com/scijava/script-editor
+preparationGoals=clean verify
+project.scm.org.scijava\:script-editor.tag=HEAD
+project.scm.org.scijava\:script-editor.url=https\://github.com/scijava/script-editor
+remoteTagging=true
+projectVersionPolicyId=default
+scm.commentPrefix=[maven-release-plugin]
+project.scm.org.scijava\:script-editor.connection=scm\:git\:git\://github.com/scijava/script-editor
+project.dev.org.scijava\:script-editor=0.5.10-SNAPSHOT
+exec.snapshotReleasePluginAllowed=false
+exec.additionalArguments=-Dgpg.skip\=true -P deploy-to-scijava
+completedPhase=end-release
+project.rel.org.scijava\:script-editor=0.5.9
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 1cad39e8..8076bd76 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
@@ -30,6 +30,9 @@
import java.io.File;
import java.io.IOException;
+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;
@@ -46,9 +49,13 @@
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;
+
public class ClassUtil {
- static private final String scijava_javadoc_URL = "https://javadoc.scijava.org/"; // with ending slash
+ 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<>();
@@ -335,4 +342,103 @@ static public final ArrayList findSimpleClassNamesStartingWith(final Str
}
return matches;
}
+
+ 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("%s", url, name);
+ }
+
+ private static String getDocPackage(final String classCanonicalName) {
+ //TODO: Do this programatically
+ 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;
+ }
+
+ /**
+ * 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("").append(field.getName()).append("");
+ summary.append(" (").append(field.getType().getName()).append(")");
+ summary.append("");
+ summary.append("- Defined in:");
+ summary.append("
- ").append(getJavaDocLink(c));
+ summary.append("
");
+ return summary.toString();
+ }
+
+ /**
+ * 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(", ");
+ }
+ replacementHeader.setLength(replacementHeader.length() - 2); // remove trailing ', ';
+ }
+ replacementHeader.append(")");
+ replacementHeader.replace(bIndex, bIndex + 1, "("); // In header, highlight only method name for extra
+ // contrast
+ summary.append("").append(replacementHeader);
+ summary.append("");
+ summary.append("- Returns:");
+ summary.append("
- ").append(method.getReturnType().getSimpleName());
+ summary.append("
- Defined in:");
+ summary.append("
- ").append(getJavaDocLink(c));
+ summary.append("
");
+ return summary.toString();
+ }
+
+ static List classUnavailableCompletions(final CompletionProvider provider, final String pre) {
+ // placeholder completions to warn users class was not available (repeated to force pop-up display)
+ final List list = new ArrayList<>();
+ final String summary = "Class not found or invalid import. See "
+ + String.format("SciJavaDocs", scijava_javadoc_URL)
+ + " or search for help";
+ 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/CompletionText.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/CompletionText.java
new file mode 100644
index 00000000..b88926a1
--- /dev/null
+++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/CompletionText.java
@@ -0,0 +1,67 @@
+package org.scijava.ui.swing.script.autocompletion;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import org.fife.ui.autocomplete.AbstractCompletion;
+import org.fife.ui.autocomplete.BasicCompletion;
+import org.fife.ui.autocomplete.CompletionProvider;
+
+public class CompletionText {
+
+ private String replacementText;
+ private String description;
+ private String summary;
+
+ public CompletionText(final String replacementText) {
+ this(replacementText, (String)null, (String)null);
+ }
+
+ public CompletionText(final String replacementText, final String summary, final String description) {
+ this.replacementText = replacementText;
+ this.summary = summary;
+ this.description = description;
+ }
+
+ public CompletionText(final String replacementText, final Class> c, final Field f) {
+ this(replacementText, ClassUtil.getSummaryCompletion(f, c), null);
+ }
+
+ public CompletionText(final String replacementText, final Class> c, final Method m) {
+ this(replacementText, ClassUtil.getSummaryCompletion(m, c), null);
+ }
+
+ public String getReplacementText() {
+ return replacementText;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getSummary() {
+ return summary;
+ }
+
+ public AbstractCompletion getCompletion(final CompletionProvider provider, final String replacementText) {
+ return new BasicCompletion(provider, replacementText, description, summary);
+ }
+
+ public void setReplacementText(final String replacementText) {
+ this.replacementText = replacementText;
+ }
+
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ public void setSummary(final String summary) {
+ this.summary = summary;
+ }
+
+ @Override
+ public String toString() {
+ return replacementText + " | " + description + " | " + summary;
+ }
+
+}
diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutoCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutoCompletion.java
index adfbcee4..e5abf56d 100644
--- a/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutoCompletion.java
+++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutoCompletion.java
@@ -47,7 +47,8 @@ public JythonAutoCompletion(final CompletionProvider provider) {
}
static private final Pattern importPattern = Pattern.compile("^(from[ \\t]+([a-zA-Z_][a-zA-Z0-9._]*)[ \\t]+|)import[ \\t]+([a-zA-Z_][a-zA-Z0-9_]*[ \\ta-zA-Z0-9_,]*)[ \\t]*([\\\\]*|)[ \\t]*(#.*|)$"),
- tripleQuotePattern = Pattern.compile("\"\"\"");
+ tripleQuotePattern = Pattern.compile("\"\"\""),
+ variableDeclarationPattern = Pattern.compile("([a-zA-Z_][a-zA-Z0-9._]*)[ \\t]*=[ \\t]*([A-Z_][a-zA-Z0-9._]*)(?:\\()"); // E.g., in 'imp=ImagePlus()' group1: imp; group2: ImagePlus
static public class Import {
final public String className,
@@ -65,7 +66,26 @@ public Import(final String packageName, final String[] parts, final int lineNumb
this(packageName + "." + parts[0], 3 == parts.length ? parts[2] : null, lineNumber);
}
}
-
+
+ static public final String findClassAliasOfVariable(final String variable, String inputText) {
+ final String[] lines = inputText.split("\n");
+ for (int i = 0; i < lines.length; ++i) {
+ final String line = lines[i];
+ final Matcher matcher = variableDeclarationPattern.matcher(line);
+ if (matcher.find()) {
+ // a line containing a variable declaration
+// System.out.println("Queried variable: " + variable);
+// System.out.println("Hit: line #" + i + ": " + line);
+// System.out.println("Matcher g1: " + matcher.group(1));
+// System.out.println("Matcher g2: " + matcher.group(2));
+ if (variable.equals(matcher.group(1))) {
+ return matcher.group(2);
+ }
+ }
+ }
+ return null;
+ }
+
static public final HashMap findImportedClasses(final String text) {
final HashMap importedClasses = new HashMap<>();
String packageName = "";
diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutocompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutocompletionProvider.java
index 1be15159..d1389adb 100644
--- a/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutocompletionProvider.java
+++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutocompletionProvider.java
@@ -30,6 +30,7 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Vector;
import java.util.regex.Matcher;
@@ -82,6 +83,7 @@ public boolean isValidChar(final char c) {
return Character.isLetterOrDigit(c) || '.' == c || ' ' == c;
}
+ @SuppressWarnings("unused")
static private final Pattern
fromImport = Pattern.compile("^((from|import)[ \\t]+)([a-zA-Z_][a-zA-Z0-9._]*)$"),
fastImport = Pattern.compile("^(from[ \\t]+)([a-zA-Z_][a-zA-Z0-9._]*)[ \\t]+$"),
@@ -148,12 +150,12 @@ public List getCompletions(final String text) {
final Matcher m1f = fastImport.matcher(text);
if (m1f.find())
return asCompletionList(ClassUtil.findClassNamesForPackage(m1f.group(2)).map(formatter::singleToImportStatement), "");
-
+
// E.g. "from ij.gui import Roi, Po" to expand to PolygonRoi, PointRoi for Jython
final Matcher m2 = importStatement.matcher(text);
if (m2.find()) {
- String packageName = m2.group(3),
- className = m2.group(4); // incomplete or empty, or multiple separated by commas with the last one incomplete or empty
+ final String packageName = m2.group(3);
+ String className = m2.group(4); // incomplete or empty, or multiple separated by commas with the last one incomplete or empty
System.out.println("m2 matches className: " + className);
final String[] bycomma = className.split(",");
@@ -189,33 +191,105 @@ public List getCompletions(final String text) {
/* Covered by listener from jython-completions
final Matcher m4 = staticMethodOrField.matcher(text);
- if (m4.find()) {
- try {
- final String simpleClassName = m4.group(3), // expected complete, e.g. ImagePlus
- methodOrFieldSeed = m4.group(4).toLowerCase(); // incomplete: e.g. "GR", a string to search for in the class declared fields or methods
+ try {
+
+ String simpleClassName;
+ String methodOrFieldSeed;
+ String pre;
+ boolean isStatic;
+
+ if (m4.find()) {
+
+ // a call to a static class
+ pre = m4.group(1);
+ simpleClassName = m4.group(3); // expected complete, e.g. ImagePlus
+ methodOrFieldSeed = m4.group(4).toLowerCase(); // incomplete: e.g. "GR", a string to search for in the class declared fields or methods
+ isStatic = true;
+
+ } else {
+
+ // a call to an instantiated class
+ final String[] varAndSeed = getVariableAnSeedAtCaretLocation();
+ if (varAndSeed == null) return Collections.emptyList();
+
+ simpleClassName = JythonAutoCompletion.findClassAliasOfVariable(varAndSeed[0], text_area.getText());
+ if (simpleClassName == null) return Collections.emptyList();
+
+ pre = varAndSeed[0] + ".";
+ methodOrFieldSeed = varAndSeed[1];
+ isStatic = false;
+
+// System.out.println("simpleClassName: " + simpleClassName);
+// System.out.println("methodOrFieldSeed: " + methodOrFieldSeed);
- // Scan the script, parse the imports, find first one matching
- final Import im = JythonAutoCompletion.findImportedClasses(text_area.getText()).get(simpleClassName);
- if (null != im) {
+ }
+
+ // Retrieve all methods and fields, if the seed is empty
+ final boolean includeAll = methodOrFieldSeed.trim().isEmpty();
+
+ // Scan the script, parse the imports, find first one matching
+ final Import im = JythonAutoCompletion.findImportedClasses(text_area.getText()).get(simpleClassName);
+ if (null != im) {
+ try {
final Class> c = Class.forName(im.className);
- final ArrayList matches = new ArrayList<>();
+ final ArrayList completions = new ArrayList<>();
for (final Field f: c.getFields()) {
- if (Modifier.isStatic(f.getModifiers()) && f.getName().toLowerCase().startsWith(methodOrFieldSeed))
- matches.add(f.getName());
+ if (isStatic == Modifier.isStatic(f.getModifiers()) &&
+ (includeAll || f.getName().toLowerCase().contains(methodOrFieldSeed)))
+ completions.add(ClassUtil.getCompletion(this, pre, f, c));
}
for (final Method m: c.getMethods()) {
- if (Modifier.isStatic(m.getModifiers()) && m.getName().toLowerCase().startsWith(methodOrFieldSeed))
- matches.add(m.getName() + "(");
+ if (isStatic == Modifier.isStatic(m.getModifiers()) &&
+ (includeAll || m.getName().toLowerCase().contains(methodOrFieldSeed)))
+ completions.add(ClassUtil.getCompletion(this, pre, m, c));
}
- return asCompletionList(matches.stream(), m4.group(1));
+
+ Collections.sort(completions, new Comparator() {
+ int prefix1Index = Integer.MAX_VALUE;
+ int prefix2Index = Integer.MAX_VALUE;
+ @Override
+ public int compare(final Completion o1, final Completion o2) {
+ prefix1Index = Integer.MAX_VALUE;
+ prefix2Index = Integer.MAX_VALUE;
+ if (o1.getReplacementText().startsWith(pre))
+ prefix1Index = 0;
+ if (o2.getReplacementText().startsWith(pre))
+ prefix2Index = 0;
+ if (prefix1Index == prefix2Index)
+ return o1.compareTo(o2);
+ else
+ return prefix1Index - prefix2Index;
+ }
+ });
+
+ return completions;
+ } catch (final ClassNotFoundException ignored) {
+ return ClassUtil.classUnavailableCompletions(this, simpleClassName + ".");
}
- } catch (Exception e) {
- e.printStackTrace();
}
+ } catch (final Exception e) {
+ e.printStackTrace();
}
*/
return Collections.emptyList();
}
+
+ @SuppressWarnings("unused")
+ private String[] getVariableAnSeedAtCaretLocation() {
+ try {
+ final int caretOffset = text_area.getCaretPosition();
+ final int lineNumber = text_area.getLineOfOffset(caretOffset);
+ final int startOffset = text_area.getLineStartOffset(lineNumber);
+ final String lineUpToCaret = text_area.getText(startOffset, caretOffset - startOffset);
+ final String[] words = lineUpToCaret.split("\\s+");
+ final String[] varAndSeed = words[words.length - 1].split("\\.");
+ return (varAndSeed.length == 2) ? varAndSeed : new String[] { varAndSeed[varAndSeed.length - 1], "" };
+ } catch (final BadLocationException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
}