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..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,6 +71,7 @@ import org.scijava.script.ScriptHeaderService; import org.scijava.script.ScriptLanguage; import org.scijava.script.ScriptService; +import org.scijava.ui.swing.script.autocompletion.JythonAutoCompletion; import org.scijava.util.FileUtils; /** @@ -106,6 +107,8 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener { private PrefService prefService; @Parameter private LogService log; + + private JythonAutoCompletion autoCompletionProxy; /** * Constructor. 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..6e80ab0c 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; @@ -156,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; @@ -214,7 +217,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 +486,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); @@ -988,7 +993,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); @@ -1395,6 +1405,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()); } @@ -2659,6 +2670,87 @@ 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> 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 keys = new ArrayList(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); + ArrayList urls = matches.get(classname); + if (urls.isEmpty()) { + urls = new ArrayList(); + urls.add("https://duckduckgo.com/?q=" + classname); + } + for (final String url: urls) { + c.gridx += 1; + c.anchor = GridBagConstraints.WEST; + 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() { + 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); 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 new file mode 100644 index 00000000..03f5b330 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java @@ -0,0 +1,310 @@ +package org.scijava.ui.swing.script.autocompletion; + +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.List; +import java.util.Map; +import java.util.Scanner; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +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<>(); + + static private final Map package_urls = new HashMap<>(); + + static private boolean ready = false; + + /** Cache of subURL javadoc at https://javadoc.scijava.org */ + static private final HashMap scijava_javadoc_URLs = new HashMap<>(); + + static public final void ensureCache() { + synchronized (class_urls) { + if (class_urls.isEmpty()) { + final ArrayList dirs = new ArrayList<>(); + dirs.add(System.getProperty("java.home")); + dirs.add(System.getProperty("ij.dir")); + class_urls.putAll(findAllClasses(dirs)); + // Soft attempt at getting all packages (will get them wrong if multiple jars have the same packages) + for (final Map.Entry entry: class_urls.entrySet()) { + final int idot = entry.getKey().lastIndexOf('.'); + if (-1 == idot) continue; // no package + final String package_name = entry.getKey().substring(0, idot); + if (package_urls.containsKey(package_name)) continue; + package_urls.put(package_name, entry.getValue()); + } + ready = true; + } + } + } + + static public final boolean isCacheReady() { + return ready; + } + + static public final void ensureSciJavaSubURLCache() { + synchronized (scijava_javadoc_URLs) { + if (!scijava_javadoc_URLs.isEmpty()) return; + Scanner scanner = null; + try { + final Pattern pattern = Pattern.compile("
"); + final URLConnection connection = new URL(scijava_javadoc_URL).openConnection(); + scanner = new Scanner(connection.getInputStream()); + while (scanner.hasNext()) { + final Matcher matcher = pattern.matcher(scanner.nextLine()); + if (matcher.find()) { + String name = matcher.group(1).toLowerCase(); + if (name.endsWith("/")) name = name.substring(0, name.length() -1); + scijava_javadoc_URLs.put(name, scijava_javadoc_URL + matcher.group(1)); + } + } + scanner.close(); + } catch ( Exception e ) { + e.printStackTrace(); + } finally { + if (null != scanner) scanner.close(); + } + } + } + + static public HashMap findClassDocumentationURLs(final String s) { + ensureCache(); + final HashMap matches = new HashMap<>(); + for (final Map.Entry entry: class_urls.entrySet()) { + if (entry.getKey().contains(s)) { + final JarProperties props = entry.getValue(); + matches.put(entry.getKey(), new JarProperties(props.name, new ArrayList(props.urls))); + } + } + return matches; + } + + static public HashMap> findDocumentationForClass(final String s) { + final HashMap matches = findClassDocumentationURLs(s); + ensureSciJavaSubURLCache(); + + final Pattern javaPackages = Pattern.compile("^(java|javax|org\\.omg|org\\.w3c|org\\.xml|org\\.ietf\\.jgss)\\..*$"); + final String version = System.getProperty("java.version"); + final String majorVersion = version.startsWith("1.") ? + version.substring(2, version.indexOf('.', 2)) + : version.substring(0, version.indexOf('.')); + final String javaDoc = "java" + majorVersion; + + final HashMap> class_urls = new HashMap<>(); + + for (final Map.Entry entry: matches.entrySet()) { + final String classname = entry.getKey(); + final ArrayList urls = new ArrayList<>(); + class_urls.put(classname, urls); + if (javaPackages.matcher(classname).matches()) { + urls.add(scijava_javadoc_URLs.get(javaDoc) + classname.replace('.', '/') + ".html"); + } else { + final JarProperties props = entry.getValue(); + // Find the first URL with git in it + for (final String url : props.urls) { + final boolean github = url.contains("/github.com"), + gitlab = url.contains("/gitlab.com"); + if (github || gitlab) { + // Find the 5th slash, e.g. https://github.com/imglib/imglib2/ + int count = 0; + int last = 0; + while (count < 5) { + last = url.indexOf('/', last + 1); + if (-1 == last) break; // less than 5 found + ++count; + } + String urlbase = url; + if (5 == count) urlbase = url.substring(0, last); // without the ending slash + // Assume maven, since these URLs were found in a pom.xml: src/main/java/ + urls.add(urlbase + (gitlab ? "/-" : "") + "/blob/master/src/main/java/" + classname.replace('.', '/') + ".java"); + break; + } + } + // Try to find a javadoc in the scijava website + if (null != props.name) { + String scijava_javadoc_url = scijava_javadoc_URLs.get(props.name.toLowerCase()); + if (null == scijava_javadoc_url) { + // Try cropping name at the first whitespace if any (e.g. "ImgLib2 Core Library" to "ImgLib2") + for (final String word: props.name.split(" ")) { + scijava_javadoc_url = scijava_javadoc_URLs.get(word.toLowerCase()); + if (null != scijava_javadoc_url) break; // found a valid one + } + } + if (null != scijava_javadoc_url) { + urls.add(scijava_javadoc_url + classname.replace('.', '/') + ".html"); + } else { + // Try Fiji: could be a plugin + Scanner scanner = null; + try { + final String url = scijava_javadoc_URL + "Fiji/" + classname.replace('.', '/') + ".html"; + final URLConnection c = new URL(url).openConnection(); + scanner = new Scanner(c.getInputStream()); + while (scanner.hasNext()) { + final String line = scanner.nextLine(); + if (line.contains("")) { + if (!line.contains("<title>404")) { + urls.add(url); + } + break; + } + } + } catch (Exception e) { + // Ignore: 404 that wasn't redirected to an error page + } finally { + if (null != scanner) scanner.close(); + } + } + } + } + } + + return class_urls; + } + + static public final class JarProperties { + public final ArrayList<String> urls; + public String name = null; + public JarProperties(final String name, final ArrayList<String> urls) { + this.name = name; + this.urls = urls; + } + } + + static public final HashMap<String, JarProperties> findAllClasses(final List<String> jar_folders) { + // Find all jar files + final ArrayList<String> jarFilePaths = new ArrayList<String>(); + final LinkedList<String> dirs = new LinkedList<>(jar_folders); + final HashSet<String> seenDirs = new HashSet<>(); + while (!dirs.isEmpty()) { + final String filepath = dirs.removeFirst(); + if (null == filepath) continue; + final File file = new File(filepath); + seenDirs.add(file.getAbsolutePath()); + if (file.exists() && file.isDirectory()) { + for (final File child : file.listFiles()) { + final String childfilepath = child.getAbsolutePath(); + if (seenDirs.contains(childfilepath)) continue; + if (child.isDirectory()) dirs.add(childfilepath); + else if (childfilepath.endsWith(".jar")) jarFilePaths.add(childfilepath); + } + } + } + // Find all classes from all jar files + final HashMap<String, JarProperties> class_urls = new HashMap<>(); + final Pattern urlpattern = Pattern.compile(">(http.*?)<"); + final Pattern namepattern = Pattern.compile("<name>(.*?)<"); + for (final String jarpath : jarFilePaths) { + JarFile jar = null; + try { + jar = new JarFile(jarpath); + final Enumeration<JarEntry> entries = jar.entries(); + final ArrayList<String> urls = new ArrayList<>(); + final JarProperties props = new JarProperties(null, urls); + // For every filepath in the jar zip archive + while (entries.hasMoreElements()) { + final JarEntry entry = entries.nextElement(); + if (entry.isDirectory()) continue; + if (entry.getName().endsWith(".class")) { + String classname = entry.getName().replace('/', '.'); + final int idollar = classname.indexOf('$'); + if (-1 != idollar) { + classname = classname.substring(0, idollar); // truncate at the first dollar sign + } else { + classname = classname.substring(0, classname.length() - 6); // without .class + } + class_urls.put(classname, props); + } else if (entry.getName().endsWith("/pom.xml")) { + final Scanner scanner = new Scanner(jar.getInputStream(entry)); + while (scanner.hasNext()) { + final String line = scanner.nextLine(); + final Matcher matcher1 = urlpattern.matcher(line); + if (matcher1.find()) { + urls.add(matcher1.group(1)); + } + if (null == props.name) { + final Matcher matcher2 = namepattern.matcher(line); + if (matcher2.find()) { + props.name = matcher2.group(1); + } + } + } + scanner.close(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (null != jar) try { + jar.close(); + } catch (IOException e) { e.printStackTrace(); } + } + } + return class_urls; + } + + 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(); + 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)); + } + + /** + * + * @param text A left-justified substring of a fully qualified class name, with the package. + * @return + */ + static public final Stream<String> findClassNamesStartingWith(final String text) { + ensureCache(); + if (text.length() == 0) + return class_urls.keySet().stream(); + return class_urls.keySet().stream().filter(s -> s.startsWith(text)); + } + + /** + * + * @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)); + } + + /** + * Find simple class names starting with "text", returning the fully qualified class names. + * @param text + * @return + */ + static public final ArrayList<String> findSimpleClassNamesStartingWith(final String text) { + ensureCache(); + final ArrayList<String> matches = new ArrayList<>(); + if (0 == text.length()) + return matches; + for (final String classname: class_urls.keySet()) { + final int idot = classname.lastIndexOf('.'); + final String simplename = -1 == idot ? classname : classname.substring(idot + 1); + if (simplename.startsWith(text)) matches.add(classname); + } + return matches; + } +} 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/JythonAutocompletionProvider.java b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutocompletionProvider.java new file mode 100644 index 00000000..060a9829 --- /dev/null +++ b/src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutocompletionProvider.java @@ -0,0 +1,171 @@ +package org.scijava.ui.swing.script.autocompletion; + +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.function.Function; +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 JythonAutocompletionProvider extends DefaultCompletionProvider { + + private final RSyntaxTextArea text_area; + private final ImportFormat formatter; + + 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() { + 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|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_]+)$"), + 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(JythonAutocompletionProvider.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 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(formatter::singleToImportStatement), ""); + + 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 + // 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()) { + // 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()) { + 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(); + } +} 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())); + } + +}