|
| 1 | +package org.scijava.ui.swing.script; |
| 2 | + |
| 3 | +import java.lang.reflect.Field; |
| 4 | +import java.lang.reflect.Method; |
| 5 | +import java.lang.reflect.Modifier; |
| 6 | +import java.util.ArrayList; |
| 7 | +import java.util.Collections; |
| 8 | +import java.util.List; |
| 9 | +import java.util.regex.Matcher; |
| 10 | +import java.util.regex.Pattern; |
| 11 | +import java.util.stream.Collectors; |
| 12 | +import java.util.stream.Stream; |
| 13 | + |
| 14 | +import javax.swing.text.JTextComponent; |
| 15 | + |
| 16 | +import org.fife.ui.autocomplete.BasicCompletion; |
| 17 | +import org.fife.ui.autocomplete.Completion; |
| 18 | +import org.fife.ui.autocomplete.DefaultCompletionProvider; |
| 19 | +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; |
| 20 | + |
| 21 | +public class AutocompletionProvider extends DefaultCompletionProvider { |
| 22 | + |
| 23 | + private final RSyntaxTextArea text_area; |
| 24 | + |
| 25 | + public AutocompletionProvider(final RSyntaxTextArea text_area) { |
| 26 | + this.text_area = text_area; |
| 27 | + new Thread(new Runnable() { |
| 28 | + @Override |
| 29 | + public void run() { |
| 30 | + ClassUtil.ensureCache(); |
| 31 | + } |
| 32 | + }).start(); |
| 33 | + } |
| 34 | + |
| 35 | + /** |
| 36 | + * Override parent implementation to allow letters, digits, the period and a space, to be able to match e.g.: |
| 37 | + * |
| 38 | + * "from " |
| 39 | + * "from ij" |
| 40 | + * "from ij.Im" |
| 41 | + * etc. |
| 42 | + * |
| 43 | + * @param c |
| 44 | + */ |
| 45 | + @Override |
| 46 | + public boolean isValidChar(final char c) { |
| 47 | + return Character.isLetterOrDigit(c) || '.' == c || ' ' == c; |
| 48 | + } |
| 49 | + |
| 50 | + static private final Pattern |
| 51 | + fromImport = Pattern.compile("^(from[ \\t]+)([a-zA-Z][a-zA-Z0-9._]*)$"), |
| 52 | + fastImport = Pattern.compile("^(from[ \\t]+)([a-zA-Z][a-zA-Z0-9._]*)[ \\t]+$"), |
| 53 | + importStatement = Pattern.compile("^((from[ \\t]+([a-zA-Z0-9._]+)[ \\t]+|[ \\t]*)import(Class\\(|[ \\t]+))([a-zA-Z0-9_., \\t]*)$"), |
| 54 | + simpleClassName = Pattern.compile("^(.*[ \\t]+|)([A-Z_][a-zA-Z0-9_]+)$"), |
| 55 | + staticMethodOrField = Pattern.compile("^((.*[ \\t]+|)([A-Z_][a-zA-Z0-9_]*)\\.)([a-zA-Z0-9_]*)$"); |
| 56 | + |
| 57 | + private final List<Completion> asCompletionList(final Stream<String> stream, final String pre) { |
| 58 | + return stream |
| 59 | + .map((s) -> new BasicCompletion(AutocompletionProvider.this, pre + s)) |
| 60 | + .collect(Collectors.toList()); |
| 61 | + } |
| 62 | + |
| 63 | + @Override |
| 64 | + public List<Completion> getCompletionsImpl(final JTextComponent comp) { |
| 65 | + // don't block |
| 66 | + if (!ClassUtil.isCacheReady()) return Collections.emptyList(); |
| 67 | + |
| 68 | + final String text = this.getAlreadyEnteredText(comp); |
| 69 | + |
| 70 | + // E.g. "from ij" to expand to a package name like ij or ij.gui or ij.plugin |
| 71 | + final Matcher m1 = fromImport.matcher(text); |
| 72 | + if (m1.find()) |
| 73 | + return asCompletionList(ClassUtil.findPackageNamesStartingWith(m1.group(2)), m1.group(1)); |
| 74 | + |
| 75 | + final Matcher m1f = fastImport.matcher(text); |
| 76 | + if (m1f.find()) |
| 77 | + return asCompletionList(ClassUtil.findClassNamesForPackage(m1f.group(2)).map(s -> s.substring(m1f.group(2).length() + 1)), |
| 78 | + m1f.group(0) + "import "); |
| 79 | + |
| 80 | + // E.g. "from ij.gui import Roi, Po" to expand to PolygonRoi, PointRoi for Jython |
| 81 | + // or e.g. "importClass(Package.ij" to expand to a fully qualified class name for Javascript |
| 82 | + final Matcher m2 = importStatement.matcher(text); |
| 83 | + if (m2.find()) { |
| 84 | + String packageName = m2.group(3), |
| 85 | + className = m2.group(5); // incomplete or empty, or multiple separated by commas with the last one incomplete or empty |
| 86 | + |
| 87 | + System.out.println("m2 matches className: " + className); |
| 88 | + final String[] bycomma = className.split(","); |
| 89 | + String precomma = ""; |
| 90 | + if (bycomma.length > 1) { |
| 91 | + className = bycomma[bycomma.length -1].trim(); // last one |
| 92 | + for (int i=0; i<bycomma.length -1; ++i) |
| 93 | + precomma += bycomma[0] + ", "; |
| 94 | + } |
| 95 | + Stream<String> stream; |
| 96 | + if (className.length() > 0) |
| 97 | + stream = ClassUtil.findClassNamesStartingWith(null == packageName ? className : packageName + "." + className); |
| 98 | + else |
| 99 | + stream = ClassUtil.findClassNamesForPackage(packageName); |
| 100 | + if (!m2.group(4).equals("Class(Package")) |
| 101 | + stream = stream.map((s) -> s.substring(Math.max(0, s.lastIndexOf('.') + 1))); // simple class name for Jython |
| 102 | + return asCompletionList(stream, m2.group(1) + precomma); |
| 103 | + } |
| 104 | + |
| 105 | + final Matcher m3 = simpleClassName.matcher(text); |
| 106 | + if (m3.find()) |
| 107 | + return asCompletionList(ClassUtil.findSimpleClassNamesStartingWith(m3.group(2)).stream(), m3.group(1)); |
| 108 | + |
| 109 | + final Matcher m4 = staticMethodOrField.matcher(text); |
| 110 | + if (m4.find()) { |
| 111 | + try { |
| 112 | + final String simpleClassName = m4.group(3), // expected complete, e.g. ImagePlus |
| 113 | + methodOrFieldSeed = m4.group(4).toLowerCase(); // incomplete: e.g. "GR", a string to search for in the class declared fields or methods |
| 114 | + // Scan the script, parse the imports, find first one matching |
| 115 | + String packageName = null; |
| 116 | + lines: for (final String line: text_area.getText().split("\n")) { |
| 117 | + System.out.println(line); |
| 118 | + final String[] comma = line.split(","); |
| 119 | + final Matcher m = importStatement.matcher(comma[0]); |
| 120 | + if (m.find()) { |
| 121 | + final String first = m.group(5); |
| 122 | + if (m.group(4).equals("Class(Package")) { |
| 123 | + // Javascript import |
| 124 | + final int lastdot = Math.max(0, first.lastIndexOf('.')); |
| 125 | + if (simpleClassName.equals(first.substring(lastdot + 1))) { |
| 126 | + packageName = first.substring(0, lastdot); |
| 127 | + break lines; |
| 128 | + } |
| 129 | + } else { |
| 130 | + // Jython import |
| 131 | + comma[0] = first; |
| 132 | + for (int i=0; i<comma.length; ++i) |
| 133 | + if (simpleClassName.equals(comma[i].trim())) { |
| 134 | + packageName = m.group(3); |
| 135 | + break lines; |
| 136 | + } |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + System.out.println("package name: " + packageName); |
| 141 | + if (null != packageName) { |
| 142 | + final Class<?> c = Class.forName(packageName + "." + simpleClassName); |
| 143 | + final ArrayList<String> matches = new ArrayList<>(); |
| 144 | + for (final Field f: c.getFields()) { |
| 145 | + if (Modifier.isStatic(f.getModifiers()) && f.getName().toLowerCase().startsWith(methodOrFieldSeed)) |
| 146 | + matches.add(f.getName()); |
| 147 | + } |
| 148 | + for (final Method m: c.getMethods()) { |
| 149 | + if (Modifier.isStatic(m.getModifiers()) && m.getName().toLowerCase().startsWith(methodOrFieldSeed)) |
| 150 | + matches.add(m.getName() + "("); |
| 151 | + } |
| 152 | + return asCompletionList(matches.stream(), m4.group(1)); |
| 153 | + } |
| 154 | + } catch (Exception e) { |
| 155 | + e.printStackTrace(); |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + return Collections.emptyList(); |
| 160 | + } |
| 161 | +} |
0 commit comments