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