From 7507734c711156b8428ba4c23f06f157301f449c Mon Sep 17 00:00:00 2001 From: Albert Cardona Date: Thu, 12 Nov 2020 21:31:09 +0000 Subject: [PATCH 01/16] Prevent Script Editor window from being larger than the desktop. This can happen when using a networked account across different computers or with different monitors. --- src/main/java/org/scijava/ui/swing/script/TextEditor.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 07323382..a2299315 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -988,7 +988,12 @@ public void loadPreferences() { final int windowWidth = prefService.getInt(getClass(), WINDOW_WIDTH, dim.width); final int windowHeight = prefService.getInt(getClass(), WINDOW_HEIGHT, dim.height); - setPreferredSize(new Dimension(windowWidth, windowHeight)); + // Avoid creating a window larger than the desktop + final Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); + if (windowWidth > screen.getWidth() || windowHeight > screen.getHeight()) + setPreferredSize(new Dimension(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)); + else + setPreferredSize(new Dimension(windowWidth, windowHeight)); final int mainDivLocation = prefService.getInt(getClass(), MAIN_DIV_LOCATION, body.getDividerLocation()); body.setDividerLocation(mainDivLocation); From 0a1b4436e0d1d19cc3460973940cc17da18198a8 Mon Sep 17 00:00:00 2001 From: Albert Cardona Date: Fri, 13 Nov 2020 23:36:04 +0000 Subject: [PATCH 02/16] ClassUtil: inspect all jar files in search of all class names, and collect info and URLs from their pom.xml --- .../scijava/ui/swing/script/ClassUtil.java | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 src/main/java/org/scijava/ui/swing/script/ClassUtil.java diff --git a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java new file mode 100644 index 00000000..2c6ba062 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java @@ -0,0 +1,233 @@ +package org.scijava.ui.swing.script; + +import java.io.File; +import java.io.IOException; +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.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; + +public class ClassUtil { + + static private 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<>(); + + /** Cache of subURL javadoc at https://javadoc.scijava.org */ + static private final HashMap scijava_javadoc_URLs = new HashMap<>(); + + static private final void ensureCache() { + synchronized (class_urls) { + if (class_urls.isEmpty()) + class_urls.putAll(findAllClasses()); + } + } + + 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 java8 = Pattern.compile("^(java|javax|org.omg|org.w3c|org.xml|org.ietf.jgss)\\..*$"); + + 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 (java8.matcher(classname).matches()) { + urls.add(scijava_javadoc_URLs.get("java8") + 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") + final int ispace = props.name.indexOf(' '); + if (-1 != ispace) { + scijava_javadoc_url = scijava_javadoc_URLs.get(props.name.toLowerCase().substring(0, ispace)); + } + } + 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; + } + } + + static public final HashMap<String, JarProperties> findAllClasses() { + // Find all jar files + final ArrayList<String> jarFilePaths = new ArrayList<String>(); + final LinkedList<String> dirs = new LinkedList<>(); + dirs.add(System.getProperty("java.home")); + dirs.add(System.getProperty("ij.dir")); + final HashSet<String> seenDirs = new HashSet<>(); + while (!dirs.isEmpty()) { + final String filepath = dirs.removeFirst(); + final File file = new File(filepath); + seenDirs.add(file.getAbsolutePath()); + if (file.exists()) { + if (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; + } +} From 80fd9dd7369fa3dce348527675bb7ab8a1fb4e96 Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Sat, 14 Nov 2020 01:39:36 +0000 Subject: [PATCH 03/16] Create new menu item "Source or javadoc for class or package..." which fixes or replaces the "Open Help" menu item, by using ClassUtils and showing a UI that lists all matching classes with buttons to open their source code in github or gitlab and the javadoc when available (will guess the URL, load it, and not show a link to it when 404). Can match partial text (the list of possible classes may be longer) and can also match package names or partial package names. Any substring of a fully qualified class name will be matched, which is fabulous. (Compare with current implementatio nof "Open Help..." which requires a fully qualified class name to work, which is not useful as often one does not know it or not have it handy in a script.) --- .../scijava/ui/swing/script/TextEditor.java | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) 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 a2299315..671f62fa 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -30,6 +30,7 @@ package org.scijava.ui.swing.script; import java.awt.Color; +import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.GridBagConstraints; @@ -108,6 +109,7 @@ import javax.swing.JCheckBoxMenuItem; import javax.swing.JFileChooser; import javax.swing.JFrame; +import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; @@ -214,7 +216,7 @@ public class TextEditor extends JFrame implements ActionListener, openMacroFunctions, decreaseFontSize, increaseFontSize, chooseFontSize, chooseTabSize, gitGrep, openInGitweb, replaceTabsWithSpaces, replaceSpacesWithTabs, toggleWhiteSpaceLabeling, zapGremlins, - savePreferences, toggleAutoCompletionMenu; + savePreferences, toggleAutoCompletionMenu, openClassOrPackageHelp; private RecentFilesMenuItem openRecent; private JMenu gitMenu, tabsMenu, fontSizeMenu, tabSizeMenu, toolsMenu, runMenu, whiteSpaceMenu; @@ -483,6 +485,8 @@ public TextEditor(final Context context) { openHelp = addToMenu(toolsMenu, "Open Help for Class (with frames)...", 0, 0); openHelp.setMnemonic(KeyEvent.VK_P); + openClassOrPackageHelp = addToMenu(toolsMenu, "Source or javadoc for class or package...", 0, 0); + openClassOrPackageHelp.setMnemonic(KeyEvent.VK_S); openMacroFunctions = addToMenu(toolsMenu, "Open Help on Macro Functions...", 0, 0); openMacroFunctions.setMnemonic(KeyEvent.VK_H); @@ -1400,6 +1404,7 @@ else if (source == savePreferences) { } else if (source == openHelp) openHelp(null); else if (source == openHelpWithoutFrames) openHelp(null, false); + else if (source == openClassOrPackageHelp) openClassOrPackageHelp(null); else if (source == openMacroFunctions) try { new MacroFunctions(this).openHelp(getTextArea().getSelectedText()); } @@ -2664,6 +2669,79 @@ public void openHelp(String className, final boolean withFrames) { handleException(e); } } + + /** + * @param text Either a classname, or a partial class name, or package name or any part of the fully qualified class name. + */ + public void openClassOrPackageHelp(String text) { + if (text == null) + text = getSelectedClassNameOrAsk(); + if (null == text) return; + new Thread(new FindClassSourceAndJavadoc(text)).start(); // fork away from event dispatch thread + } + + public class FindClassSourceAndJavadoc implements Runnable { + private final String text; + public FindClassSourceAndJavadoc(final String text) { + this.text = text; + } + @Override + public void run() { + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + final HashMap<String, ArrayList<String>> matches; + try { + matches = ClassUtil.findDocumentationForClass(text); + } finally { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + if (matches.isEmpty()) { + JOptionPane.showMessageDialog(getEditorPane(), "No info found for:\n'" + text +'"'); + return; + } + final JPanel panel = new JPanel(); + final GridBagLayout gridbag = new GridBagLayout(); + final GridBagConstraints c = new GridBagConstraints(); + panel.setLayout(gridbag); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + final List<String> keys = new ArrayList<String>(matches.keySet()); + Collections.sort(keys); + c.gridy = 0; + for (final String classname: keys) { + c.gridx = 0; + c.anchor = GridBagConstraints.EAST; + final JLabel class_label = new JLabel(classname); + gridbag.setConstraints(class_label, c); + panel.add(class_label); + for (final String url: matches.get(classname)) { + c.gridx += 1; + c.anchor = GridBagConstraints.WEST; + final JButton link = new JButton(url.endsWith("java") ? "Source" : "JavaDoc"); + gridbag.setConstraints(link, c); + panel.add(link); + link.addActionListener(new ActionListener() { + public void actionPerformed(final ActionEvent event) { + try { + platformService.open(new URL(url)); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + c.gridy += 1; + } + final JScrollPane jsp = new JScrollPane(panel); + //jsp.setPreferredSize(new Dimension(800, 500)); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + final JFrame frame = new JFrame(text); + frame.getContentPane().add(jsp); + frame.pack(); + frame.setVisible(true); + } + }); + } + } public void extractSourceJar() { final File file = openWithDialog(null); From 5cf2a7af418905cbf176f9d16a30507940b556e1 Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Sat, 14 Nov 2020 10:36:52 +0000 Subject: [PATCH 04/16] Ensure the correct java version is used, although the list of package suffixes comes from java 8. --- .../java/org/scijava/ui/swing/script/ClassUtil.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java index 2c6ba062..f123aa0d 100644 --- a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java @@ -74,16 +74,21 @@ static public HashMap<String, ArrayList<String>> findDocumentationForClass(final final HashMap<String, JarProperties> matches = findClassDocumentationURLs(s); ensureSciJavaSubURLCache(); - final Pattern java8 = Pattern.compile("^(java|javax|org.omg|org.w3c|org.xml|org.ietf.jgss)\\..*$"); - + 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<String, ArrayList<String>> class_urls = new HashMap<>(); for (final Map.Entry<String, JarProperties> entry: matches.entrySet()) { final String classname = entry.getKey(); final ArrayList<String> urls = new ArrayList<>(); class_urls.put(classname, urls); - if (java8.matcher(classname).matches()) { - urls.add(scijava_javadoc_URLs.get("java8") + classname.replace('.', '/') + ".html"); + 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 From 4583ccfecf7c3002f85c14966efcf1b4de8f9cac Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Sat, 14 Nov 2020 16:25:20 +0000 Subject: [PATCH 05/16] ClassUtil: generalize static method findAllClasses so that it an be used for other directories. --- .../scijava/ui/swing/script/ClassUtil.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java index f123aa0d..3c2f5e0f 100644 --- a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java @@ -9,6 +9,7 @@ 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; @@ -28,8 +29,12 @@ public class ClassUtil { static private final void ensureCache() { synchronized (class_urls) { - if (class_urls.isEmpty()) - class_urls.putAll(findAllClasses()); + if (class_urls.isEmpty()) { + final ArrayList<String> dirs = new ArrayList<>(); + dirs.add(System.getProperty("java.home")); + dirs.add(System.getProperty("ij.dir")); + class_urls.putAll(findAllClasses(dirs)); + } } } @@ -161,25 +166,22 @@ public JarProperties(final String name, final ArrayList<String> urls) { } } - static public final HashMap<String, JarProperties> findAllClasses() { + 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<>(); - dirs.add(System.getProperty("java.home")); - dirs.add(System.getProperty("ij.dir")); + 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()) { - if (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); - } + 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); } } } From 3020432a9ca54bc894a981a9ea5e5e6fb222e21f Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Sat, 14 Nov 2020 16:34:23 +0000 Subject: [PATCH 06/16] If a classname doesn't have any URLS associated with it, then search in DuckDuckGo. --- .../java/org/scijava/ui/swing/script/TextEditor.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 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 671f62fa..57423696 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -2712,10 +2712,18 @@ public void run() { final JLabel class_label = new JLabel(classname); gridbag.setConstraints(class_label, c); panel.add(class_label); - for (final String url: matches.get(classname)) { + ArrayList<String> urls = matches.get(classname); + if (urls.isEmpty()) { + urls = new ArrayList<String>(); + urls.add("https://duckduckgo.com/?q=" + classname); + } + for (final String url: urls) { c.gridx += 1; c.anchor = GridBagConstraints.WEST; - final JButton link = new JButton(url.endsWith("java") ? "Source" : "JavaDoc"); + String title = "JavaDoc"; + if (url.endsWith(".java")) title = "Source"; + else if (url.contains("duckduckgo")) title = "Search..."; + final JButton link = new JButton(title); gridbag.setConstraints(link, c); panel.add(link); link.addActionListener(new ActionListener() { From d8b7d3277852e43d5d71eb6aa2d9e6a39af62a53 Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Mon, 16 Nov 2020 09:41:06 +0000 Subject: [PATCH 07/16] ClassUtil: search among the multiple words in the repository name for a name that matches a subURL of javadoc.scijava.org --- src/main/java/org/scijava/ui/swing/script/ClassUtil.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java index 3c2f5e0f..03dfe576 100644 --- a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java @@ -121,9 +121,9 @@ static public HashMap<String, ArrayList<String>> findDocumentationForClass(final 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") - final int ispace = props.name.indexOf(' '); - if (-1 != ispace) { - scijava_javadoc_url = scijava_javadoc_URLs.get(props.name.toLowerCase().substring(0, ispace)); + 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) { From e1b1f12d7b6f42829918a5853a6a2e5fef4df190 Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Thu, 19 Nov 2020 00:05:47 +0000 Subject: [PATCH 08/16] ClassUtil methods to pattern-match against package and class names. --- .../scijava/ui/swing/script/ClassUtil.java | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java index 03dfe576..dfb9060f 100644 --- a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java @@ -16,6 +16,7 @@ import java.util.jar.JarFile; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Stream; public class ClassUtil { @@ -24,20 +25,37 @@ public class ClassUtil { /** 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<String, JarProperties> class_urls = new HashMap<>(); + static private final Map<String, JarProperties> package_urls = new HashMap<>(); + + static private boolean ready = false; + /** Cache of subURL javadoc at https://javadoc.scijava.org */ static private final HashMap<String, String> scijava_javadoc_URLs = new HashMap<>(); - static private final void ensureCache() { + static public final void ensureCache() { synchronized (class_urls) { if (class_urls.isEmpty()) { final ArrayList<String> 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<String, JarProperties> 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; @@ -237,4 +255,30 @@ static public final HashMap<String, JarProperties> findAllClasses(final List<Str } return class_urls; } + + static public final Stream<String> findPackageNamesStartingWith(final String text) { + ensureCache(); + return package_urls.keySet().stream().filter(s -> s.startsWith(text)); + } + + static public final Stream<String> findClassNamesForPackage(final String packageName) { + ensureCache(); + return class_urls.keySet().stream().filter(s -> s.startsWith(packageName) && -1 == s.indexOf('.', packageName.length() + 2)); + } + + static public final Stream<String> findClassNamesStartingWith(final String text) { + ensureCache(); + return class_urls.keySet().stream().filter(s -> s.startsWith(text)); + } + + static public final ArrayList<String> findSimpleClassNamesStartingWith(final String text) { + ensureCache(); + final ArrayList<String> matches = new ArrayList<>(); + 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(simplename); + } + return matches; + } } From 156d16b809b66d712b2c179a6d2be9c401a685d7 Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Thu, 19 Nov 2020 00:07:09 +0000 Subject: [PATCH 09/16] New class AutocompletionProvider to provide completions for class, package and fields and methods of imported classes. --- .../swing/script/AutocompletionProvider.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 src/main/java/org/scijava/ui/swing/script/AutocompletionProvider.java diff --git a/src/main/java/org/scijava/ui/swing/script/AutocompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/AutocompletionProvider.java new file mode 100644 index 00000000..01176b1a --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/AutocompletionProvider.java @@ -0,0 +1,161 @@ +package org.scijava.ui.swing.script; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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.rsyntaxtextarea.RSyntaxTextArea; + +public class AutocompletionProvider extends DefaultCompletionProvider { + + private final RSyntaxTextArea text_area; + + public AutocompletionProvider(final RSyntaxTextArea text_area) { + this.text_area = text_area; + new Thread(new Runnable() { + @Override + public void run() { + ClassUtil.ensureCache(); + } + }).start(); + } + + /** + * Override parent implementation to allow letters, digits, the period and a space, to be able to match e.g.: + * + * "from " + * "from ij" + * "from ij.Im" + * etc. + * + * @param c + */ + @Override + public boolean isValidChar(final char c) { + return Character.isLetterOrDigit(c) || '.' == c || ' ' == c; + } + + static private final Pattern + fromImport = Pattern.compile("^(from[ \\t]+)([a-zA-Z][a-zA-Z0-9._]*)$"), + fastImport = Pattern.compile("^(from[ \\t]+)([a-zA-Z][a-zA-Z0-9._]*)[ \\t]+$"), + importStatement = Pattern.compile("^((from[ \\t]+([a-zA-Z0-9._]+)[ \\t]+|[ \\t]*)import(Class\\(|[ \\t]+))([a-zA-Z0-9_., \\t]*)$"), + simpleClassName = Pattern.compile("^(.*[ \\t]+|)([A-Z_][a-zA-Z0-9_]+)$"), + staticMethodOrField = Pattern.compile("^((.*[ \\t]+|)([A-Z_][a-zA-Z0-9_]*)\\.)([a-zA-Z0-9_]*)$"); + + private final List<Completion> asCompletionList(final Stream<String> stream, final String pre) { + return stream + .map((s) -> new BasicCompletion(AutocompletionProvider.this, pre + s)) + .collect(Collectors.toList()); + } + + @Override + public List<Completion> getCompletionsImpl(final JTextComponent comp) { + // don't block + if (!ClassUtil.isCacheReady()) return Collections.emptyList(); + + final String text = this.getAlreadyEnteredText(comp); + + // E.g. "from ij" to expand to a package name like ij or ij.gui or ij.plugin + final Matcher m1 = fromImport.matcher(text); + if (m1.find()) + return asCompletionList(ClassUtil.findPackageNamesStartingWith(m1.group(2)), m1.group(1)); + + final Matcher m1f = fastImport.matcher(text); + if (m1f.find()) + return asCompletionList(ClassUtil.findClassNamesForPackage(m1f.group(2)).map(s -> s.substring(m1f.group(2).length() + 1)), + m1f.group(0) + "import "); + + // E.g. "from ij.gui import Roi, Po" to expand to PolygonRoi, PointRoi for Jython + // or e.g. "importClass(Package.ij" to expand to a fully qualified class name for Javascript + final Matcher m2 = importStatement.matcher(text); + if (m2.find()) { + String packageName = m2.group(3), + className = m2.group(5); // 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(","); + String precomma = ""; + if (bycomma.length > 1) { + className = bycomma[bycomma.length -1].trim(); // last one + for (int i=0; i<bycomma.length -1; ++i) + precomma += bycomma[0] + ", "; + } + Stream<String> stream; + if (className.length() > 0) + stream = ClassUtil.findClassNamesStartingWith(null == packageName ? className : packageName + "." + className); + else + stream = ClassUtil.findClassNamesForPackage(packageName); + if (!m2.group(4).equals("Class(Package")) + stream = stream.map((s) -> s.substring(Math.max(0, s.lastIndexOf('.') + 1))); // simple class name for Jython + return asCompletionList(stream, m2.group(1) + precomma); + } + + final Matcher m3 = simpleClassName.matcher(text); + if (m3.find()) + return asCompletionList(ClassUtil.findSimpleClassNamesStartingWith(m3.group(2)).stream(), m3.group(1)); + + 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 + // Scan the script, parse the imports, find first one matching + String packageName = null; + lines: for (final String line: text_area.getText().split("\n")) { + System.out.println(line); + final String[] comma = line.split(","); + final Matcher m = importStatement.matcher(comma[0]); + if (m.find()) { + final String first = m.group(5); + if (m.group(4).equals("Class(Package")) { + // Javascript import + final int lastdot = Math.max(0, first.lastIndexOf('.')); + if (simpleClassName.equals(first.substring(lastdot + 1))) { + packageName = first.substring(0, lastdot); + break lines; + } + } else { + // Jython import + comma[0] = first; + for (int i=0; i<comma.length; ++i) + if (simpleClassName.equals(comma[i].trim())) { + packageName = m.group(3); + break lines; + } + } + } + } + System.out.println("package name: " + packageName); + if (null != packageName) { + final Class<?> c = Class.forName(packageName + "." + simpleClassName); + final ArrayList<String> matches = new ArrayList<>(); + for (final Field f: c.getFields()) { + if (Modifier.isStatic(f.getModifiers()) && f.getName().toLowerCase().startsWith(methodOrFieldSeed)) + matches.add(f.getName()); + } + for (final Method m: c.getMethods()) { + if (Modifier.isStatic(m.getModifiers()) && m.getName().toLowerCase().startsWith(methodOrFieldSeed)) + matches.add(m.getName() + "("); + } + return asCompletionList(matches.stream(), m4.group(1)); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + return Collections.emptyList(); + } +} From c447129c04608155b930b1dd9932cb0901eb817e Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Thu, 19 Nov 2020 00:07:51 +0000 Subject: [PATCH 10/16] Enable autocompletion. --- src/main/java/org/scijava/ui/swing/script/EditorPane.java | 3 +++ 1 file changed, 3 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 debea1e9..08b3deee 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -54,6 +54,7 @@ import javax.swing.text.DefaultEditorKit; import org.fife.rsta.ac.LanguageSupport; +import org.fife.ui.autocomplete.AutoCompletion; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.Style; @@ -124,6 +125,8 @@ public EditorPane() { wordMovement(-1, true)); ToolTipManager.sharedInstance().registerComponent(this); getDocument().addDocumentListener(this); + + new AutoCompletion(new AutocompletionProvider(this)).install(this); } @Override From 0e697949b66d1b2a52a892b0fe6b989280266adc Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Fri, 20 Nov 2020 15:10:01 +0000 Subject: [PATCH 11/16] New package for autocompletion classes --- src/main/java/org/scijava/ui/swing/script/EditorPane.java | 5 +++-- .../script/{ => autocompletion}/AutocompletionProvider.java | 0 2 files changed, 3 insertions(+), 2 deletions(-) rename src/main/java/org/scijava/ui/swing/script/{ => autocompletion}/AutocompletionProvider.java (100%) 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 08b3deee..9ed9c15b 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -54,7 +54,6 @@ import javax.swing.text.DefaultEditorKit; import org.fife.rsta.ac.LanguageSupport; -import org.fife.ui.autocomplete.AutoCompletion; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.Style; @@ -72,6 +71,8 @@ import org.scijava.script.ScriptHeaderService; import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptService; +import org.scijava.ui.swing.script.autocompletion.AutoCompletionProxy; +import org.scijava.ui.swing.script.autocompletion.AutocompletionProvider; import org.scijava.util.FileUtils; /** @@ -126,7 +127,7 @@ public EditorPane() { ToolTipManager.sharedInstance().registerComponent(this); getDocument().addDocumentListener(this); - new AutoCompletion(new AutocompletionProvider(this)).install(this); + new AutoCompletionProxy(new AutocompletionProvider(this)).install(this); } @Override diff --git a/src/main/java/org/scijava/ui/swing/script/AutocompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java similarity index 100% rename from src/main/java/org/scijava/ui/swing/script/AutocompletionProvider.java rename to src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java From e5ae00c46ef52e2aae5b18876b02a8db6bff7d84 Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Fri, 20 Nov 2020 15:11:09 +0000 Subject: [PATCH 12/16] New ClassUtil.findClassNamesContaining method, to enable matching class names on any substring rather than only the beginning. --- .../org/scijava/ui/swing/script/ClassUtil.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java index dfb9060f..db0a1d65 100644 --- a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java @@ -266,11 +266,26 @@ static public final Stream<String> findClassNamesForPackage(final String package return class_urls.keySet().stream().filter(s -> s.startsWith(packageName) && -1 == s.indexOf('.', packageName.length() + 2)); } + /** + * + * @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(); return class_urls.keySet().stream().filter(s -> s.startsWith(text)); } + /** + * + * @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)); + } + static public final ArrayList<String> findSimpleClassNamesStartingWith(final String text) { ensureCache(); final ArrayList<String> matches = new ArrayList<>(); From a34b8a060edee420d0f9e5c645be548ebe7ebd27 Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Fri, 20 Nov 2020 15:12:23 +0000 Subject: [PATCH 13/16] When autocompleting "import <text>" enable matching anywhere on the class name, not just the beginning. --- .../autocompletion/AutocompletionProvider.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java index 01176b1a..93488e0c 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java @@ -1,4 +1,4 @@ -package org.scijava.ui.swing.script; +package org.scijava.ui.swing.script.autocompletion; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -17,6 +18,7 @@ import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.DefaultCompletionProvider; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.scijava.ui.swing.script.ClassUtil; public class AutocompletionProvider extends DefaultCompletionProvider { @@ -67,10 +69,18 @@ public List<Completion> getCompletionsImpl(final JTextComponent comp) { final String text = this.getAlreadyEnteredText(comp); - // E.g. "from ij" to expand to a package name like ij or ij.gui or ij.plugin + // E.g. "from ij" to expand to a package name and class like ij or ij.gui or ij.plugin final Matcher m1 = fromImport.matcher(text); if (m1.find()) - return asCompletionList(ClassUtil.findPackageNamesStartingWith(m1.group(2)), m1.group(1)); + return asCompletionList(ClassUtil.findClassNamesContaining(m1.group(2)) + .map(new Function<String, String>() { + @Override + public final String apply(final String s) { + final int idot = s.lastIndexOf('.'); + return s.substring(0, Math.max(0, idot)) + " import " + s.substring(idot +1); + } + }), + m1.group(1)); final Matcher m1f = fastImport.matcher(text); if (m1f.find()) From b1b4c6423c76dc8dfbdd8941cbd02eb527e138a6 Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Fri, 27 Nov 2020 15:34:00 +0000 Subject: [PATCH 14/16] ClassUtil: handle empty strings for matching. --- src/main/java/org/scijava/ui/swing/script/ClassUtil.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java index db0a1d65..f6ed8c62 100644 --- a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/ClassUtil.java @@ -263,6 +263,8 @@ static public final Stream<String> findPackageNamesStartingWith(final String tex static public final Stream<String> findClassNamesForPackage(final String packageName) { ensureCache(); + if (packageName.length() == 0) + return class_urls.keySet().stream(); return class_urls.keySet().stream().filter(s -> s.startsWith(packageName) && -1 == s.indexOf('.', packageName.length() + 2)); } @@ -273,6 +275,8 @@ static public final Stream<String> findClassNamesForPackage(final String package */ 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)); } @@ -289,6 +293,8 @@ static public final Stream<String> findClassNamesContaining(final String text) { 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); From 4316bc7ae67915cf8756b2a5c77e65746136ec7b Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Fri, 27 Nov 2020 15:34:44 +0000 Subject: [PATCH 15/16] Autocompletion: enable autocompleting "from" and "import". --- .../script/autocompletion/AutocompletionProvider.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java index 93488e0c..9a55f65e 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java @@ -50,7 +50,7 @@ public boolean isValidChar(final char c) { } static private final Pattern - fromImport = Pattern.compile("^(from[ \\t]+)([a-zA-Z][a-zA-Z0-9._]*)$"), + 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]+$"), importStatement = Pattern.compile("^((from[ \\t]+([a-zA-Z0-9._]+)[ \\t]+|[ \\t]*)import(Class\\(|[ \\t]+))([a-zA-Z0-9_., \\t]*)$"), simpleClassName = Pattern.compile("^(.*[ \\t]+|)([A-Z_][a-zA-Z0-9_]+)$"), @@ -72,15 +72,15 @@ public List<Completion> getCompletionsImpl(final JTextComponent comp) { // E.g. "from ij" to expand to a package name and class like ij or ij.gui or ij.plugin final Matcher m1 = fromImport.matcher(text); if (m1.find()) - return asCompletionList(ClassUtil.findClassNamesContaining(m1.group(2)) + return asCompletionList(ClassUtil.findClassNamesContaining(m1.group(3)) .map(new Function<String, String>() { @Override public final String apply(final String s) { final int idot = s.lastIndexOf('.'); - return s.substring(0, Math.max(0, idot)) + " import " + s.substring(idot +1); + return "from " + s.substring(0, Math.max(0, idot)) + " import " + s.substring(idot +1); } }), - m1.group(1)); + ""); final Matcher m1f = fastImport.matcher(text); if (m1f.find()) From 167ab74ee7a53bff563d5ead8e858d399fa7ff6b Mon Sep 17 00:00:00 2001 From: Albert Cardona <sapristi@gmail.com> Date: Sat, 28 Nov 2020 17:27:57 +0000 Subject: [PATCH 16/16] Autocompletion for python (jython) using the LanguageSupportService. --- .../scijava/ui/swing/script/EditorPane.java | 7 +- .../scijava/ui/swing/script/TextEditor.java | 1 + .../{ => autocompletion}/ClassUtil.java | 9 +- .../autocompletion/ImportCompletion.java | 21 +++++ .../script/autocompletion/ImportFormat.java | 9 ++ .../autocompletion/JythonAutoCompletion.java | 85 +++++++++++++++++++ ...java => JythonAutocompletionProvider.java} | 34 ++++---- .../autocompletion/JythonImportFormat.java | 17 ++++ .../JythonLanguageSupportPlugin.java | 62 ++++++++++++++ 9 files changed, 222 insertions(+), 23 deletions(-) rename src/main/java/org/scijava/ui/swing/script/{ => autocompletion}/ClassUtil.java (97%) create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/ImportCompletion.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/ImportFormat.java create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutoCompletion.java rename src/main/java/org/scijava/ui/swing/script/autocompletion/{AutocompletionProvider.java => JythonAutocompletionProvider.java} (85%) create mode 100644 src/main/java/org/scijava/ui/swing/script/autocompletion/JythonImportFormat.java create mode 100644 src/main/java/org/scijava/ui/swing/script/languagesupport/JythonLanguageSupportPlugin.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 9ed9c15b..c8a01ec0 100644 --- a/src/main/java/org/scijava/ui/swing/script/EditorPane.java +++ b/src/main/java/org/scijava/ui/swing/script/EditorPane.java @@ -71,8 +71,7 @@ import org.scijava.script.ScriptHeaderService; import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptService; -import org.scijava.ui.swing.script.autocompletion.AutoCompletionProxy; -import org.scijava.ui.swing.script.autocompletion.AutocompletionProvider; +import org.scijava.ui.swing.script.autocompletion.JythonAutoCompletion; import org.scijava.util.FileUtils; /** @@ -108,6 +107,8 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { private PrefService prefService; @Parameter private LogService log; + + private JythonAutoCompletion autoCompletionProxy; /** * Constructor. @@ -126,8 +127,6 @@ public EditorPane() { wordMovement(-1, true)); ToolTipManager.sharedInstance().registerComponent(this); getDocument().addDocumentListener(this); - - new AutoCompletionProxy(new AutocompletionProvider(this)).install(this); } @Override 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 57423696..6e80ab0c 100644 --- a/src/main/java/org/scijava/ui/swing/script/TextEditor.java +++ b/src/main/java/org/scijava/ui/swing/script/TextEditor.java @@ -158,6 +158,7 @@ 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; diff --git a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java similarity index 97% rename from src/main/java/org/scijava/ui/swing/script/ClassUtil.java rename to src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java index f6ed8c62..03f5b330 100644 --- a/src/main/java/org/scijava/ui/swing/script/ClassUtil.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java @@ -1,4 +1,4 @@ -package org.scijava.ui.swing.script; +package org.scijava.ui.swing.script.autocompletion; import java.io.File; import java.io.IOException; @@ -290,6 +290,11 @@ static public final Stream<String> findClassNamesContaining(final String text) { return class_urls.keySet().stream().filter(s -> s.contains(text)); } + /** + * 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<>(); @@ -298,7 +303,7 @@ static public final ArrayList<String> findSimpleClassNamesStartingWith(final Str 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(simplename); + if (simplename.startsWith(text)) matches.add(classname); } return matches; } diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/ImportCompletion.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/ImportCompletion.java new file mode 100644 index 00000000..5748dd4e --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/ImportCompletion.java @@ -0,0 +1,21 @@ +package org.scijava.ui.swing.script.autocompletion; + +import org.fife.ui.autocomplete.BasicCompletion; +import org.fife.ui.autocomplete.CompletionProvider; + +public class ImportCompletion extends BasicCompletion +{ + protected final String importStatement, + className; + + public ImportCompletion(final CompletionProvider provider, final String replacementText, final String className, final String importStatement) { + super(provider, replacementText); + this.className = className; + this.importStatement = importStatement; + } + + @Override + public String getSummary() { + return importStatement; + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/ImportFormat.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/ImportFormat.java new file mode 100644 index 00000000..9423979f --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/ImportFormat.java @@ -0,0 +1,9 @@ +package org.scijava.ui.swing.script.autocompletion; + +public interface ImportFormat +{ + /** Given a fully-qualified class name, return a String with the class formatted as an import statement. */ + public String singleToImportStatement(String className); + + public String dualToImportStatement(String packageName, String simpleClassName); +} \ No newline at end of file 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 new file mode 100644 index 00000000..05b3e518 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutoCompletion.java @@ -0,0 +1,85 @@ +package org.scijava.ui.swing.script.autocompletion; + +import java.util.HashSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.text.BadLocationException; + +import org.fife.ui.autocomplete.AutoCompletion; +import org.fife.ui.autocomplete.Completion; +import org.fife.ui.autocomplete.CompletionProvider; +import org.scijava.script.ScriptLanguage; +import org.scijava.ui.swing.script.EditorPane; + +public class JythonAutoCompletion extends AutoCompletion { + + public JythonAutoCompletion(final CompletionProvider provider) { + super(provider); + this.setShowDescWindow(true); + } + + 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]*(#.*|)$"); + + @Override + protected void insertCompletion(final Completion c, final boolean typedParamListStartChar) { + if (c instanceof ImportCompletion) { + final EditorPane editor = (EditorPane) super.getTextComponent(); + editor.beginAtomicEdit(); + try { + super.insertCompletion(c, typedParamListStartChar); + final ImportCompletion cc = (ImportCompletion)c; + final HashSet<String> classNames = new HashSet<>(); + String packageName = ""; + boolean endingBackslash = false; + int insertAtLine = 0; + + // Scan the whole file for imports + final String[] lines = editor.getText().split("\n"); + for (int i=0; i<lines.length; ++i) { + final String line = lines[i]; + + // Handle classes imported in a truncated import statement + if (endingBackslash) { + String importLine = line; + final int backslash = line.lastIndexOf('\\'); + if (backslash > -1) importLine = importLine.substring(0, backslash); + else { + final int sharp = importLine.lastIndexOf('#'); + if (sharp > -1) importLine = importLine.substring(0, sharp); + } + for (final String simpleClassname : importLine.split(",")) { + classNames.add(packageName + "." + simpleClassname.trim()); + } + endingBackslash = -1 != backslash; // otherwise there is another line with classes of the same package + insertAtLine = i; + continue; + } + final Matcher m = importPattern.matcher(line); + if (m.find()) { + packageName = null == m.group(2) ? "" : m.group(2); + for (final String simpleClassName : m.group(3).split(",")) { + classNames.add(packageName + "." + simpleClassName.trim()); + } + endingBackslash = null != m.group(4) && m.group(4).length() > 0 && '\\' == m.group(4).charAt(0); + insertAtLine = i; + } + } + for (final String className : classNames) { + System.out.println(className); + } + // Insert import statement after the last import, if not there already + if (!classNames.contains(cc.className)) + try { + editor.insert(cc.importStatement + "\n", editor.getLineStartOffset(insertAtLine + 1)); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } finally { + editor.endAtomicEdit(); + } + } else { + super.insertCompletion(c, typedParamListStartChar); + } + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutocompletionProvider.java similarity index 85% rename from src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java rename to src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutocompletionProvider.java index 9a55f65e..060a9829 100644 --- a/src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutocompletionProvider.java @@ -18,14 +18,15 @@ import org.fife.ui.autocomplete.Completion; import org.fife.ui.autocomplete.DefaultCompletionProvider; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; -import org.scijava.ui.swing.script.ClassUtil; -public class AutocompletionProvider extends DefaultCompletionProvider { +public class JythonAutocompletionProvider extends DefaultCompletionProvider { private final RSyntaxTextArea text_area; + private final ImportFormat formatter; - public AutocompletionProvider(final RSyntaxTextArea text_area) { + public JythonAutocompletionProvider(final RSyntaxTextArea text_area, final ImportFormat formatter) { this.text_area = text_area; + this.formatter = formatter; new Thread(new Runnable() { @Override public void run() { @@ -58,7 +59,7 @@ public boolean isValidChar(final char c) { private final List<Completion> asCompletionList(final Stream<String> stream, final String pre) { return stream - .map((s) -> new BasicCompletion(AutocompletionProvider.this, pre + s)) + .map((s) -> new BasicCompletion(JythonAutocompletionProvider.this, pre + s)) .collect(Collectors.toList()); } @@ -72,20 +73,11 @@ public List<Completion> getCompletionsImpl(final JTextComponent comp) { // E.g. "from ij" to expand to a package name and class like ij or ij.gui or ij.plugin final Matcher m1 = fromImport.matcher(text); if (m1.find()) - return asCompletionList(ClassUtil.findClassNamesContaining(m1.group(3)) - .map(new Function<String, String>() { - @Override - public final String apply(final String s) { - final int idot = s.lastIndexOf('.'); - return "from " + s.substring(0, Math.max(0, idot)) + " import " + s.substring(idot +1); - } - }), - ""); + return asCompletionList(ClassUtil.findClassNamesContaining(m1.group(3)).map(formatter::singleToImportStatement), ""); final Matcher m1f = fastImport.matcher(text); if (m1f.find()) - return asCompletionList(ClassUtil.findClassNamesForPackage(m1f.group(2)).map(s -> s.substring(m1f.group(2).length() + 1)), - m1f.group(0) + "import "); + 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 // or e.g. "importClass(Package.ij" to expand to a fully qualified class name for Javascript @@ -113,8 +105,16 @@ public final String apply(final String s) { } final Matcher m3 = simpleClassName.matcher(text); - if (m3.find()) - return asCompletionList(ClassUtil.findSimpleClassNamesStartingWith(m3.group(2)).stream(), m3.group(1)); + if (m3.find()) { + // Side effect: insert the import at the top of the file if necessary + //return asCompletionList(ClassUtil.findSimpleClassNamesStartingWith(m3.group(2)).stream(), m3.group(1)); + return ClassUtil.findSimpleClassNamesStartingWith(m3.group(2)).stream() + .map(className -> new ImportCompletion(JythonAutocompletionProvider.this, + m3.group(1) + className.substring(className.lastIndexOf('.') + 1), + className, + formatter.singleToImportStatement(className))) + .collect(Collectors.toList()); + } final Matcher m4 = staticMethodOrField.matcher(text); if (m4.find()) { diff --git a/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonImportFormat.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonImportFormat.java new file mode 100644 index 00000000..bc427c58 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonImportFormat.java @@ -0,0 +1,17 @@ +package org.scijava.ui.swing.script.autocompletion; + +public class JythonImportFormat implements ImportFormat +{ + @Override + public final String singleToImportStatement(final String className) { + final int idot = className.lastIndexOf('.'); + if (-1 == idot) + return "import " + className; + return dualToImportStatement(className.substring(0, idot), className.substring(idot + 1)); + } + + @Override + public String dualToImportStatement(final String packageName, final String simpleClassName) { + return "from " + packageName + " import " + simpleClassName; + } +} diff --git a/src/main/java/org/scijava/ui/swing/script/languagesupport/JythonLanguageSupportPlugin.java b/src/main/java/org/scijava/ui/swing/script/languagesupport/JythonLanguageSupportPlugin.java new file mode 100644 index 00000000..322fd168 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/languagesupport/JythonLanguageSupportPlugin.java @@ -0,0 +1,62 @@ +package org.scijava.ui.swing.script.languagesupport; + +import org.fife.rsta.ac.AbstractLanguageSupport; +import org.fife.ui.autocomplete.AutoCompletion; +import org.fife.ui.autocomplete.CompletionProvider; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.scijava.plugin.Plugin; +import org.scijava.ui.swing.script.LanguageSupportPlugin; +import org.scijava.ui.swing.script.LanguageSupportService; +import org.scijava.ui.swing.script.autocompletion.JythonAutocompletionProvider; +import org.scijava.ui.swing.script.autocompletion.JythonImportFormat; +import org.scijava.ui.swing.script.autocompletion.JythonAutoCompletion; + +/** + * {@link LanguageSupportPlugin} for the jython language. + * + * @author Albert Cardona + * + * @see LanguageSupportService + */ +@Plugin(type = LanguageSupportPlugin.class) +public class JythonLanguageSupportPlugin extends AbstractLanguageSupport implements LanguageSupportPlugin +{ + + private AutoCompletion ac; + private RSyntaxTextArea text_area; + + public JythonLanguageSupportPlugin() { + setAutoCompleteEnabled(true); + setShowDescWindow(true); + } + + @Override + public String getLanguageName() { + return "python"; + } + + @Override + public void install(final RSyntaxTextArea textArea) { + this.text_area = textArea; + this.ac = this.createAutoCompletion(null); + this.ac.install(textArea); + // store upstream + super.installImpl(textArea, this.ac); + } + + @Override + public void uninstall(final RSyntaxTextArea textArea) { + if (textArea == this.text_area) { + super.uninstallImpl(textArea); // will call this.acp.uninstall(); + } + } + + /** + * Ignores the argument. + */ + @Override + protected AutoCompletion createAutoCompletion(CompletionProvider p) { + return new JythonAutoCompletion(new JythonAutocompletionProvider(text_area, new JythonImportFormat())); + } + +}