Skip to content

Commit cc6e1d6

Browse files
committed
Autocompletion for python (jython) using the LanguageSupportService.
1 parent 4316bc7 commit cc6e1d6

9 files changed

+221
-23
lines changed

src/main/java/org/scijava/ui/swing/script/EditorPane.java

+3-4
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@
7171
import org.scijava.script.ScriptHeaderService;
7272
import org.scijava.script.ScriptLanguage;
7373
import org.scijava.script.ScriptService;
74-
import org.scijava.ui.swing.script.autocompletion.AutoCompletionProxy;
75-
import org.scijava.ui.swing.script.autocompletion.AutocompletionProvider;
74+
import org.scijava.ui.swing.script.autocompletion.JythonAutoCompletion;
7675
import org.scijava.util.FileUtils;
7776

7877
/**
@@ -108,6 +107,8 @@ public class EditorPane extends RSyntaxTextArea implements DocumentListener {
108107
private PrefService prefService;
109108
@Parameter
110109
private LogService log;
110+
111+
private JythonAutoCompletion autoCompletionProxy;
111112

112113
/**
113114
* Constructor.
@@ -126,8 +127,6 @@ public EditorPane() {
126127
wordMovement(-1, true));
127128
ToolTipManager.sharedInstance().registerComponent(this);
128129
getDocument().addDocumentListener(this);
129-
130-
new AutoCompletionProxy(new AutocompletionProvider(this)).install(this);
131130
}
132131

133132
@Override

src/main/java/org/scijava/ui/swing/script/TextEditor.java

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
import org.scijava.thread.ThreadService;
159159
import org.scijava.ui.CloseConfirmable;
160160
import org.scijava.ui.UIService;
161+
import org.scijava.ui.swing.script.autocompletion.ClassUtil;
161162
import org.scijava.ui.swing.script.commands.ChooseFontSize;
162163
import org.scijava.ui.swing.script.commands.ChooseTabSize;
163164
import org.scijava.ui.swing.script.commands.GitGrep;

src/main/java/org/scijava/ui/swing/script/ClassUtil.java src/main/java/org/scijava/ui/swing/script/autocompletion/ClassUtil.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.scijava.ui.swing.script;
1+
package org.scijava.ui.swing.script.autocompletion;
22

33
import java.io.File;
44
import java.io.IOException;
@@ -290,6 +290,11 @@ static public final Stream<String> findClassNamesContaining(final String text) {
290290
return class_urls.keySet().stream().filter(s -> s.contains(text));
291291
}
292292

293+
/**
294+
* Find simple class names starting with "text", returning the fully qualified class names.
295+
* @param text
296+
* @return
297+
*/
293298
static public final ArrayList<String> findSimpleClassNamesStartingWith(final String text) {
294299
ensureCache();
295300
final ArrayList<String> matches = new ArrayList<>();
@@ -298,7 +303,7 @@ static public final ArrayList<String> findSimpleClassNamesStartingWith(final Str
298303
for (final String classname: class_urls.keySet()) {
299304
final int idot = classname.lastIndexOf('.');
300305
final String simplename = -1 == idot ? classname : classname.substring(idot + 1);
301-
if (simplename.startsWith(text)) matches.add(simplename);
306+
if (simplename.startsWith(text)) matches.add(classname);
302307
}
303308
return matches;
304309
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.scijava.ui.swing.script.autocompletion;
2+
3+
import org.fife.ui.autocomplete.BasicCompletion;
4+
import org.fife.ui.autocomplete.CompletionProvider;
5+
6+
public class ImportCompletion extends BasicCompletion
7+
{
8+
protected final String importStatement,
9+
className;
10+
11+
public ImportCompletion(final CompletionProvider provider, final String replacementText, final String className, final String importStatement) {
12+
super(provider, replacementText);
13+
this.className = className;
14+
this.importStatement = importStatement;
15+
}
16+
17+
@Override
18+
public String getSummary() {
19+
return importStatement;
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.scijava.ui.swing.script.autocompletion;
2+
3+
public interface ImportFormat
4+
{
5+
/** Given a fully-qualified class name, return a String with the class formatted as an import statement. */
6+
public String singleToImportStatement(String className);
7+
8+
public String dualToImportStatement(String packageName, String simpleClassName);
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package org.scijava.ui.swing.script.autocompletion;
2+
3+
import java.util.HashSet;
4+
import java.util.regex.Matcher;
5+
import java.util.regex.Pattern;
6+
7+
import javax.swing.text.BadLocationException;
8+
9+
import org.fife.ui.autocomplete.AutoCompletion;
10+
import org.fife.ui.autocomplete.Completion;
11+
import org.fife.ui.autocomplete.CompletionProvider;
12+
import org.scijava.script.ScriptLanguage;
13+
import org.scijava.ui.swing.script.EditorPane;
14+
15+
public class JythonAutoCompletion extends AutoCompletion {
16+
17+
public JythonAutoCompletion(final CompletionProvider provider) {
18+
super(provider);
19+
}
20+
21+
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]*(#.*|)$");
22+
23+
@Override
24+
protected void insertCompletion(final Completion c, final boolean typedParamListStartChar) {
25+
if (c instanceof ImportCompletion) {
26+
final EditorPane editor = (EditorPane) super.getTextComponent();
27+
editor.beginAtomicEdit();
28+
try {
29+
super.insertCompletion(c, typedParamListStartChar);
30+
final ImportCompletion cc = (ImportCompletion)c;
31+
final HashSet<String> classNames = new HashSet<>();
32+
String packageName = "";
33+
boolean endingBackslash = false;
34+
int insertAtLine = 0;
35+
36+
// Scan the whole file for imports
37+
final String[] lines = editor.getText().split("\n");
38+
for (int i=0; i<lines.length; ++i) {
39+
final String line = lines[i];
40+
41+
// Handle classes imported in a truncated import statement
42+
if (endingBackslash) {
43+
String importLine = line;
44+
final int backslash = line.lastIndexOf('\\');
45+
if (backslash > -1) importLine = importLine.substring(0, backslash);
46+
else {
47+
final int sharp = importLine.lastIndexOf('#');
48+
if (sharp > -1) importLine = importLine.substring(0, sharp);
49+
}
50+
for (final String simpleClassname : importLine.split(",")) {
51+
classNames.add(packageName + "." + simpleClassname.trim());
52+
}
53+
endingBackslash = -1 != backslash; // otherwise there is another line with classes of the same package
54+
insertAtLine = i;
55+
continue;
56+
}
57+
final Matcher m = importPattern.matcher(line);
58+
if (m.find()) {
59+
packageName = null == m.group(2) ? "" : m.group(2);
60+
for (final String simpleClassName : m.group(3).split(",")) {
61+
classNames.add(packageName + "." + simpleClassName.trim());
62+
}
63+
endingBackslash = null != m.group(4) && m.group(4).length() > 0 && '\\' == m.group(4).charAt(0);
64+
insertAtLine = i;
65+
}
66+
}
67+
for (final String className : classNames) {
68+
System.out.println(className);
69+
}
70+
// Insert import statement after the last import, if not there already
71+
if (!classNames.contains(cc.className))
72+
try {
73+
editor.insert(cc.importStatement + "\n", editor.getLineStartOffset(insertAtLine + 1));
74+
} catch (BadLocationException e) {
75+
e.printStackTrace();
76+
}
77+
} finally {
78+
editor.endAtomicEdit();
79+
}
80+
} else {
81+
super.insertCompletion(c, typedParamListStartChar);
82+
}
83+
}
84+
}

src/main/java/org/scijava/ui/swing/script/autocompletion/AutocompletionProvider.java src/main/java/org/scijava/ui/swing/script/autocompletion/JythonAutocompletionProvider.java

+17-17
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
import org.fife.ui.autocomplete.Completion;
1919
import org.fife.ui.autocomplete.DefaultCompletionProvider;
2020
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
21-
import org.scijava.ui.swing.script.ClassUtil;
2221

23-
public class AutocompletionProvider extends DefaultCompletionProvider {
22+
public class JythonAutocompletionProvider extends DefaultCompletionProvider {
2423

2524
private final RSyntaxTextArea text_area;
25+
private final ImportFormat formatter;
2626

27-
public AutocompletionProvider(final RSyntaxTextArea text_area) {
27+
public JythonAutocompletionProvider(final RSyntaxTextArea text_area, final ImportFormat formatter) {
2828
this.text_area = text_area;
29+
this.formatter = formatter;
2930
new Thread(new Runnable() {
3031
@Override
3132
public void run() {
@@ -58,7 +59,7 @@ public boolean isValidChar(final char c) {
5859

5960
private final List<Completion> asCompletionList(final Stream<String> stream, final String pre) {
6061
return stream
61-
.map((s) -> new BasicCompletion(AutocompletionProvider.this, pre + s))
62+
.map((s) -> new BasicCompletion(JythonAutocompletionProvider.this, pre + s))
6263
.collect(Collectors.toList());
6364
}
6465

@@ -72,20 +73,11 @@ public List<Completion> getCompletionsImpl(final JTextComponent comp) {
7273
// E.g. "from ij" to expand to a package name and class like ij or ij.gui or ij.plugin
7374
final Matcher m1 = fromImport.matcher(text);
7475
if (m1.find())
75-
return asCompletionList(ClassUtil.findClassNamesContaining(m1.group(3))
76-
.map(new Function<String, String>() {
77-
@Override
78-
public final String apply(final String s) {
79-
final int idot = s.lastIndexOf('.');
80-
return "from " + s.substring(0, Math.max(0, idot)) + " import " + s.substring(idot +1);
81-
}
82-
}),
83-
"");
76+
return asCompletionList(ClassUtil.findClassNamesContaining(m1.group(3)).map(formatter::singleToImportStatement), "");
8477

8578
final Matcher m1f = fastImport.matcher(text);
8679
if (m1f.find())
87-
return asCompletionList(ClassUtil.findClassNamesForPackage(m1f.group(2)).map(s -> s.substring(m1f.group(2).length() + 1)),
88-
m1f.group(0) + "import ");
80+
return asCompletionList(ClassUtil.findClassNamesForPackage(m1f.group(2)).map(formatter::singleToImportStatement), "");
8981

9082
// E.g. "from ij.gui import Roi, Po" to expand to PolygonRoi, PointRoi for Jython
9183
// 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) {
113105
}
114106

115107
final Matcher m3 = simpleClassName.matcher(text);
116-
if (m3.find())
117-
return asCompletionList(ClassUtil.findSimpleClassNamesStartingWith(m3.group(2)).stream(), m3.group(1));
108+
if (m3.find()) {
109+
// Side effect: insert the import at the top of the file if necessary
110+
//return asCompletionList(ClassUtil.findSimpleClassNamesStartingWith(m3.group(2)).stream(), m3.group(1));
111+
return ClassUtil.findSimpleClassNamesStartingWith(m3.group(2)).stream()
112+
.map(className -> new ImportCompletion(JythonAutocompletionProvider.this,
113+
m3.group(1) + className.substring(className.lastIndexOf('.') + 1),
114+
className,
115+
formatter.singleToImportStatement(className)))
116+
.collect(Collectors.toList());
117+
}
118118

119119
final Matcher m4 = staticMethodOrField.matcher(text);
120120
if (m4.find()) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.scijava.ui.swing.script.autocompletion;
2+
3+
public class JythonImportFormat implements ImportFormat
4+
{
5+
@Override
6+
public final String singleToImportStatement(final String className) {
7+
final int idot = className.lastIndexOf('.');
8+
if (-1 == idot)
9+
return "import " + className;
10+
return dualToImportStatement(className.substring(0, idot), className.substring(idot + 1));
11+
}
12+
13+
@Override
14+
public String dualToImportStatement(final String packageName, final String simpleClassName) {
15+
return "from " + packageName + " import " + simpleClassName;
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.scijava.ui.swing.script.languagesupport;
2+
3+
import org.fife.rsta.ac.AbstractLanguageSupport;
4+
import org.fife.ui.autocomplete.AutoCompletion;
5+
import org.fife.ui.autocomplete.CompletionProvider;
6+
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
7+
import org.scijava.plugin.Plugin;
8+
import org.scijava.ui.swing.script.LanguageSupportPlugin;
9+
import org.scijava.ui.swing.script.LanguageSupportService;
10+
import org.scijava.ui.swing.script.autocompletion.JythonAutocompletionProvider;
11+
import org.scijava.ui.swing.script.autocompletion.JythonImportFormat;
12+
import org.scijava.ui.swing.script.autocompletion.JythonAutoCompletion;
13+
14+
/**
15+
* {@link LanguageSupportPlugin} for the jython language.
16+
*
17+
* @author Albert Cardona
18+
*
19+
* @see LanguageSupportService
20+
*/
21+
@Plugin(type = LanguageSupportPlugin.class)
22+
public class JythonLanguageSupportPlugin extends AbstractLanguageSupport implements LanguageSupportPlugin
23+
{
24+
25+
private AutoCompletion ac;
26+
private RSyntaxTextArea text_area;
27+
28+
public JythonLanguageSupportPlugin() {
29+
setAutoCompleteEnabled(true);
30+
setShowDescWindow(true);
31+
}
32+
33+
@Override
34+
public String getLanguageName() {
35+
return "python";
36+
}
37+
38+
@Override
39+
public void install(final RSyntaxTextArea textArea) {
40+
this.text_area = textArea;
41+
this.ac = this.createAutoCompletion(null);
42+
this.ac.install(textArea);
43+
// store upstream
44+
super.installImpl(textArea, this.ac);
45+
}
46+
47+
@Override
48+
public void uninstall(final RSyntaxTextArea textArea) {
49+
if (textArea == this.text_area) {
50+
super.uninstallImpl(textArea); // will call this.acp.uninstall();
51+
}
52+
}
53+
54+
/**
55+
* Ignores the argument.
56+
*/
57+
@Override
58+
protected AutoCompletion createAutoCompletion(CompletionProvider p) {
59+
return new JythonAutoCompletion(new JythonAutocompletionProvider(text_area, new JythonImportFormat()));
60+
}
61+
62+
}

0 commit comments

Comments
 (0)