|
| 1 | +package sc.fiji.jython.autocompletion; |
| 2 | + |
| 3 | +import java.util.Collections; |
| 4 | +import java.util.List; |
| 5 | +import java.util.regex.Matcher; |
| 6 | +import java.util.regex.Pattern; |
| 7 | +import java.util.stream.Collectors; |
| 8 | + |
| 9 | +import org.fife.ui.autocomplete.BasicCompletion; |
| 10 | +import org.fife.ui.autocomplete.Completion; |
| 11 | +import org.fife.ui.autocomplete.CompletionProvider; |
| 12 | +import org.scijava.ui.swing.script.autocompletion.AutoCompletionListener; |
| 13 | +import org.scijava.ui.swing.script.autocompletion.JythonAutocompletionProvider; |
| 14 | + |
| 15 | +public class JythonAutoCompletions implements AutoCompletionListener |
| 16 | +{ |
| 17 | + static { |
| 18 | + JythonAutocompletionProvider.addAutoCompletionListener(new JythonAutoCompletions()); |
| 19 | + } |
| 20 | + |
| 21 | + static private final Pattern assign = Pattern.compile("^([ \\t]*)(([a-zA-Z_][a-zA-Z0-9_ \\t,]*)[ \\t]+=[ \\t]+(.*))$"), |
| 22 | + nameToken = Pattern.compile("^(.*?[ \\t]+|)([a-zA-Z_][a-zA-Z0-9_]+)$"), |
| 23 | + dotNameToken = Pattern.compile("^(.*?[ \\t]+|)([a-zA-Z0-9_\\.\\[\\](){}]+)\\.([a-zA-Z0-9_]*)$"); |
| 24 | + |
| 25 | + @Override |
| 26 | + public List<Completion> completionsFor(final CompletionProvider provider, final String codeWithoutLastLine, final String lastLine) { |
| 27 | + // Query the lastLine to find out what needs autocompletion |
| 28 | + |
| 29 | + // Preconditions: can't expand when ending with any of: "()[]{},; " |
| 30 | + final char lastChar = lastLine.charAt(lastLine.length() -1); |
| 31 | + if (0 == lastLine.length() || "()[]{},; ".indexOf(lastChar) > -1) |
| 32 | + return Collections.emptyList(); |
| 33 | + |
| 34 | + // Two situations to autocomplete: |
| 35 | + // 1) a plain name: delimited with space (or none) to the left, and without parentheses. |
| 36 | + // 2) a method or field: none or some text after a period. |
| 37 | + |
| 38 | + final Matcher m1 = nameToken.matcher(lastLine); |
| 39 | + if (m1.find()) |
| 40 | + return JythonScriptParser.parseAST(codeWithoutLastLine).getLast().findStartsWith(m1.group(2)).stream() |
| 41 | + .map(s -> new BasicCompletion(provider, lastLine + s.substring(m1.group(2).length()))) |
| 42 | + .collect(Collectors.toList()); |
| 43 | + |
| 44 | + final Matcher m2 = dotNameToken.matcher(lastLine); |
| 45 | + if (m2.find()) { |
| 46 | + final String seed = m2.group(3); // can be empty |
| 47 | + // Expand fields and methods of previous class |
| 48 | + // Assume code is correct up to the dot |
| 49 | + // Python has multiple assignment: find out the class of the last left var |
| 50 | + final String code, |
| 51 | + varName; |
| 52 | + final Matcher m3 = assign.matcher(lastLine); |
| 53 | + if (m3.find()) { |
| 54 | + // An assignment, e.g. "ip1, ip2 = imp1.getProcessor(), imp2." |
| 55 | + final String[] assignment = lastLine.split("="); |
| 56 | + final String[] names = assignment[0].split(","); |
| 57 | + varName = names[names.length -1].trim(); |
| 58 | + code = codeWithoutLastLine + lastLine.substring(0, lastLine.length() -1); // without the ending dot |
| 59 | + } else { |
| 60 | + // Not an assignment, i.e. "imp.getImage()." |
| 61 | + // Find first non-whitespace char |
| 62 | + int start = 0; |
| 63 | + while (Character.isWhitespace(lastLine.charAt(start++))); |
| 64 | + varName = "____GRAB____"; // an injected var to capture the class |
| 65 | + code = codeWithoutLastLine + lastLine.substring(0, start) + varName + " = " + lastLine.substring(start); |
| 66 | + } |
| 67 | + final DotAutocompletions da = JythonScriptParser.parseAST(code).getLast().find(varName, DotAutocompletions.EMPTY); |
| 68 | + return da.get().stream() |
| 69 | + .filter(s -> s.startsWith(seed)) |
| 70 | + .map(s -> new BasicCompletion(provider, lastLine + s, da.getClassname(), "From class " + da.getClassname())) |
| 71 | + .collect(Collectors.toList()); |
| 72 | + } |
| 73 | + |
| 74 | + return Collections.emptyList(); |
| 75 | + } |
| 76 | +} |
0 commit comments