Skip to content

Commit ba95b3c

Browse files
committed
Support method autocompletion with arguments, with suggestions picked from
existing suitable known vars in scope.
1 parent d8115f7 commit ba95b3c

5 files changed

Lines changed: 159 additions & 26 deletions

File tree

‎src/main/java/org/scijava/plugins/scripteditor/jython/JythonAutoCompletion.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class JythonAutoCompletion extends AutoCompletion {
4444

4545
public JythonAutoCompletion(final CompletionProvider provider) {
4646
super(provider);
47+
this.setParameterAssistanceEnabled(true);
4748
this.setShowDescWindow(true);
4849
}
4950

‎src/main/java/org/scijava/plugins/scripteditor/jython/JythonAutoCompletions.java‎

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import java.io.File;
3232
import java.io.IOException;
33+
import java.lang.reflect.Parameter;
3334
import java.nio.file.FileVisitOption;
3435
import java.nio.file.Files;
3536
import java.nio.file.Path;
@@ -44,10 +45,16 @@
4445
import java.util.regex.Pattern;
4546
import java.util.stream.Collectors;
4647

48+
import javax.swing.text.JTextComponent;
49+
4750
import org.fife.ui.autocomplete.BasicCompletion;
4851
import org.fife.ui.autocomplete.Completion;
4952
import org.fife.ui.autocomplete.CompletionProvider;
53+
import org.fife.ui.autocomplete.FunctionCompletion;
54+
import org.fife.ui.autocomplete.ParameterChoicesProvider;
55+
import org.fife.ui.autocomplete.ParameterizedCompletion;
5056
import org.python.indexer.types.NModuleType;
57+
import org.scijava.ui.swing.script.autocompletion.CompletionText;
5158

5259
public class JythonAutoCompletions {
5360

@@ -82,17 +89,15 @@ public List<String> apply(final Path filepath) {
8289
.collect(Collectors.toList());
8390
return modules;
8491
} catch (Exception e) {
85-
System.out.println("Failed to load jython module");
86-
if (JythonAutocompletionProvider.debug >= 2) e.printStackTrace();
92+
JythonDev.print("Failed to load jython module", e);
8793
} finally {
8894
if (null != jar) try { jar.close(); } catch (Exception ee) {}
8995
}
9096
return Collections.emptyList();
9197
}
9298
}).findFirst().orElse(Collections.emptyList());
9399
} catch (IOException e) {
94-
System.out.println("Cannot find jython-slim-*.jar file");
95-
if (JythonAutocompletionProvider.debug >= 2) e.printStackTrace();
100+
JythonDev.print("Cannot find jython-slim-*.jar file", e);
96101
} finally {
97102
jython_jar_modules = ls;
98103
}
@@ -101,7 +106,7 @@ public List<String> apply(final Path filepath) {
101106

102107
public JythonAutoCompletions() {}
103108

104-
public List<Completion> completionsFor(final CompletionProvider provider, String codeWithoutLastLine, final String lastLine, final String alreadyEnteredText) {
109+
public List<Completion> completionsFor(final JythonAutocompletionProvider provider, String codeWithoutLastLine, final String lastLine, final String alreadyEnteredText) {
105110

106111
// Replacing of text will start at crop, given the already entered text that is considered for replacement
107112
final int crop = lastLine.length() - alreadyEnteredText.length();
@@ -118,14 +123,14 @@ public List<Completion> completionsFor(final CompletionProvider provider, String
118123
// so that the ParserFacade can work
119124
boolean add_pass = false;
120125
if (codeWithoutLastLine.endsWith("\n")) {
121-
JythonScriptParser.print("codeWithoutLastLine ends with line break");
126+
JythonDev.printTrace("codeWithoutLastLine ends with line break");
122127
final int priorLineBreak = codeWithoutLastLine.lastIndexOf('\n', codeWithoutLastLine.length() - 2);
123128
final String endingLine = codeWithoutLastLine.substring(priorLineBreak + 1);
124129
final Matcher me = endingCode.matcher(endingLine);
125130
if (me.find()) {
126131
codeWithoutLastLine = codeWithoutLastLine.substring(0, priorLineBreak + 1) + me.group(1) + me.group(2);
127132
add_pass = true;
128-
JythonScriptParser.print("changed code to: \n" + codeWithoutLastLine + "\n###");
133+
JythonDev.printTrace("changed code to: \n" + codeWithoutLastLine + "\n###");
129134
}
130135
}
131136

@@ -137,10 +142,9 @@ public List<Completion> completionsFor(final CompletionProvider provider, String
137142
if (path.exists() && path.isDirectory() && !Scope.indexer.getLoadPath().stream().filter(s -> path.equals(new File(s))).findFirst().isPresent())
138143
Scope.indexer.addPath(path.getAbsolutePath());
139144
}
140-
JythonScriptParser.print("PYTHONPATH:\n" + String.join("\n", Scope.indexer.getLoadPath()));
145+
JythonDev.printTrace("PYTHONPATH:\n" + String.join("\n", Scope.indexer.getLoadPath()));
141146
} catch (Exception e) {
142-
System.out.println("Failed to add path from sys.path.append expression.");
143-
if (JythonAutocompletionProvider.debug >= 2) e.printStackTrace();
147+
JythonDev.print("Failed to add path from sys.path.append expression.", e);
144148
}
145149

146150
// Situations to autocomplete:
@@ -172,8 +176,7 @@ public List<Completion> completionsFor(final CompletionProvider provider, String
172176
: s.substring(dir.length(), s.length() -3)) // remove ending ".py"
173177
.replace('/', '.'));
174178
} catch (IOException e) {
175-
System.out.println("Failed to read jython module file.");
176-
if (JythonAutocompletionProvider.debug >= 2) e.printStackTrace();
179+
JythonDev.print("Failed to read jython module file.", e);
177180
}
178181
return null;
179182
}).flatMap(Function.identity())
@@ -247,27 +250,94 @@ public List<Completion> completionsFor(final CompletionProvider provider, String
247250
String suffix = "";
248251
if (add_pass) {
249252
add_pass = false; // don't
250-
JythonScriptParser.print("Removed ' pass'");
253+
JythonDev.printTrace("Removed ' pass'");
251254
suffix = ":\n ";
252255
}
253256
varName = "____GRAB____"; // an injected var to capture the returned class
254257
code = codeWithoutLastLine + (add_pass ? ": pass" : "") + suffix + lastLine.substring(0, start) + varName + " = " + lastLine.substring(start, lastLine.length() - 1 - seed.length());
255-
JythonScriptParser.print("codeWithoutLastLine:\n" + codeWithoutLastLine);
258+
JythonDev.printTrace("codeWithoutLastLine:\n" + codeWithoutLastLine);
256259
}
257-
final DotAutocompletions da = JythonScriptParser.parseAST(code).getLast().find(varName, DotAutocompletions.EMPTY);
260+
final Scope scope = JythonScriptParser.parseAST(code);
261+
final DotAutocompletions da = scope.getLast().find(varName, DotAutocompletions.EMPTY);
258262
final String fullPre = lastLine.substring(crop);
259263
final String pre = fullPre.substring(0, fullPre.lastIndexOf(seed));
260264
final String lowerCaseSeed = seed.toLowerCase();
265+
266+
// Depends on scope
267+
provider.setParameterChoicesProvider(new ParameterChoicesProvider() {
268+
@Override
269+
public List<Completion> getParameterChoices(JTextComponent tc,
270+
org.fife.ui.autocomplete.ParameterizedCompletion.Parameter param) {
271+
final Object typeObj = param.getTypeObject();
272+
Class<?> clazz = null;
273+
if (null != typeObj && typeObj instanceof Class) {
274+
clazz = (Class<?>)typeObj;
275+
JythonDev.printTrace("class: " + clazz + ", " + clazz.getCanonicalName());
276+
}
277+
switch (param.getType()) {
278+
case "int": clazz = Integer.class; break;
279+
case "long": clazz = Long.class; break;
280+
case "float": clazz = Float.class; break;
281+
case "double": clazz = Double.class; break;
282+
case "boolean": clazz = Boolean.class; break;
283+
case "char": clazz = Character.class; break;
284+
case "short": clazz = Short.class; break;
285+
case "byte": clazz = Byte.class; break;
286+
}
287+
if (null != clazz) try {
288+
JythonDev.printTrace("type is: " + param.getType());
289+
JythonDev.printTrace("class is: " + clazz.getCanonicalName());
290+
final List<Completion> bc = scope
291+
.findVarsByType(param.getType(), clazz)
292+
.map(varName -> new BasicCompletion(provider, varName))
293+
.collect(Collectors.toList());
294+
for (int i=0; i<bc.size(); ++i) {
295+
((BasicCompletion)bc.get(i)).setRelevance(bc.size() - i); // ensure sorted from innermost scope to outer, rather than alphabetical
296+
}
297+
JythonDev.printTrace("found bc: " + bc.size());
298+
return bc;
299+
} catch (Exception e) {
300+
JythonDev.printError(e);
301+
}
302+
return Collections.emptyList();
303+
}
304+
});
305+
261306
List<Completion> list = da.get().stream()
262307
.filter(s -> s.getReplacementText().toLowerCase().contains(lowerCaseSeed))
263-
.map(s -> s.getCompletion(provider, pre + s.getReplacementText(), s.getReplacementText().startsWith(seed) ? 1 : 0))
308+
//.map(s -> s.getCompletion(provider, pre + s.getReplacementText(), s.getReplacementText().startsWith(seed) ? 1 : 0))
309+
.map(s -> makeDotCompletion(pre, lowerCaseSeed, scope, s, provider))
264310
.collect(Collectors.toList());
265311
sortCompletions(list, seed);
266312
return list;
267313
}
268314

269315
return Collections.emptyList();
270316
}
317+
318+
private static Completion makeDotCompletion(final String pre, final String seed, final Scope scope, final CompletionText ct, final CompletionProvider provider) {
319+
List<Parameter> ps = ct.getMethodArgs();
320+
if (null != ps && !ps.isEmpty()) {
321+
JythonDev.printTrace("using FunctionCompletion: ps is " + ps.stream().map(Parameter::toString).collect(Collectors.joining(", ", "[", "]")));
322+
final FunctionCompletion fc = new CustomFunctionCompletion(
323+
provider,
324+
pre + ct.getReplacementText().substring(0, ct.getReplacementText().length() -2), // remove trailing parentheses
325+
ct.getReturnType());
326+
fc.setReturnValueDescription(ct.getReturnType());
327+
fc.setShortDescription(ct.getSummary()); // CompletionText.getDescription returns null
328+
//fc.setSummary(ct.getSummary()); // useless: later, FunctionCompletion doesn't use the summary from its superclass
329+
final ArrayList<ParameterizedCompletion.Parameter> params = new ArrayList<>();
330+
for (int i=0; i<ps.size(); ++i) {
331+
params.add(new ParameterizedCompletion.Parameter(ps.get(i).getType().getCanonicalName(), ps.get(i).getName(), false)); // can't mark last one as isEndParam = true, would not allow choosing it for some reason
332+
}
333+
fc.setParams(params);
334+
fc.setRelevance(fc.getReplacementText().startsWith(seed) ? 1 : 0);
335+
return fc;
336+
}
337+
JythonDev.printTrace("not FunctionCompletion: replacement text is " + ct.getReplacementText());
338+
return ct.getCompletion(provider, pre + ct.getReplacementText(), ct.getReplacementText().startsWith(seed) ? 1 : 0);
339+
}
340+
271341

272342
@SuppressWarnings("unused")
273343
private static String removeLastOptionalDot(final String s) {

‎src/main/java/org/scijava/plugins/scripteditor/jython/JythonAutocompletionProvider.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public class JythonAutocompletionProvider extends DefaultCompletionProvider {
6565
public JythonAutocompletionProvider(final RSyntaxTextArea text_area, final ImportFormat formatter) {
6666
this.text_area = text_area;
6767
this.formatter = formatter;
68+
this.setParameterizedCompletionParams('(', ", ", ')'); // for methods and functions
6869
new Thread(ClassUtil::ensureCache).start();
6970
}
7071

‎src/main/java/org/scijava/plugins/scripteditor/jython/JythonLanguageSupportPlugin.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public class JythonLanguageSupportPlugin extends AbstractLanguageSupport implem
5353

5454
public JythonLanguageSupportPlugin() {
5555
setAutoCompleteEnabled(true);
56+
setParameterAssistanceEnabled(true);
5657
setShowDescWindow(true);
5758
}
5859

@@ -65,6 +66,7 @@ public String getLanguageName() {
6566
public void install(final RSyntaxTextArea textArea) {
6667
this.text_area = textArea;
6768
this.ac = this.createAutoCompletion(null);
69+
this.ac.setParameterAssistanceEnabled(true);
6870
this.ac.install(textArea);
6971
// store upstream
7072
super.installImpl(textArea, this.ac);

‎src/main/java/org/scijava/plugins/scripteditor/jython/Scope.java‎

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@
4141
import java.util.List;
4242
import java.util.Map;
4343
import java.util.stream.Collectors;
44+
import java.util.stream.Stream;
4445

4546
import org.python.indexer.Indexer;
4647
import org.python.indexer.types.NModuleType;
4748
import org.scijava.ui.swing.script.autocompletion.CompletionText;
4849

4950

51+
5052
public class Scope {
5153
final Scope parent;
5254
final String className; // if any
@@ -78,7 +80,7 @@ public void run() {
7880
return;
7981
}
8082
if (keys.containsKey(key)) {
81-
System.out.println("Python module at:\n" + keys.get(key) + "\n ... was updated. Clearing indexer cache.");
83+
JythonDev.print("Python module at:\n" + keys.get(key) + "\n ... was updated. Clearing indexer cache.");
8284
// One of the files changed: unload all, given that parent modules would have been loaded as well
8385
// and it gets complicated quickly to find out which need to be reloaded and which don't.
8486
keys.clear();
@@ -92,7 +94,7 @@ public void run() {
9294
module_watcher.setPriority(Thread.NORM_PRIORITY);
9395
module_watcher.start();
9496
} catch (Exception e ){
95-
System.out.println("Failed to start filesystem watcher service for python modules");
97+
JythonDev.print("Failed to start filesystem watcher service for python modules", e);
9698
}
9799
}
98100

@@ -112,8 +114,7 @@ static NModuleType loadPythonModule(final String qname) {
112114
mod = indexer.loadModule(qname);
113115
if (null == mod) return null;
114116
} catch (Exception e) {
115-
if (JythonAutocompletionProvider.debug >= 1) System.out.println("Could not load python module named " + qname);
116-
if (JythonAutocompletionProvider.debug >= 2) e.printStackTrace();
117+
JythonDev.print("Could not load python module named " + qname, e);
117118
return null;
118119
}
119120
try {
@@ -128,14 +129,10 @@ static NModuleType loadPythonModule(final String qname) {
128129
StandardWatchEventKinds.ENTRY_DELETE);
129130
keys.put(key, path);
130131
} else {
131-
System.out.println("Python module " + qname + " doesn't have an associated file path.");
132+
JythonDev.print("Python module " + qname + " doesn't have an associated file path.");
132133
}
133134
} catch (Exception e) {
134-
if (JythonAutocompletionProvider.debug >= 1) {
135-
System.out.println("Could not load python module named " + qname);
136-
System.out.println(e.getMessage());
137-
}
138-
if (JythonAutocompletionProvider.debug >= 2) e.printStackTrace();
135+
JythonDev.print("Could not load python module named " + qname, e);
139136
}
140137
return mod;
141138
}
@@ -199,6 +196,68 @@ public List<String> findStartsWith(final String name) {
199196
return completions;
200197
}
201198

199+
200+
/** Find vars by type, recursively upstream the nested scopes, listing first those of the innermost scope.
201+
*
202+
* @param clazz
203+
*/
204+
public Stream<String> findVarsByType(final String type, final Class<?> clazz) {
205+
Stream<String> varNames = new ArrayList<String>().stream();
206+
Scope scope = this;
207+
JythonDev.printTrace("Scope.findVarsByType: searching for type " + type + " and class " + clazz.getCanonicalName());
208+
while (null != scope) {
209+
varNames = Stream.concat(varNames,
210+
scope.vars.entrySet().stream()
211+
.filter(e -> {
212+
try {
213+
JythonDev.printTrace("Scope.findVarsByType, testing: " + e.getKey() + " :: " + e.getValue() + " with class " + e.getValue().getClassname());
214+
if (e.getKey().startsWith("____")) return false; // injected variables in JythonAutoCompletions.completionsFor
215+
final String classname = e.getValue().getClassname();
216+
if (null == classname) return false;
217+
if (type.equals(classname)) {
218+
JythonDev.printTrace("type == classname: " + type);
219+
return true;
220+
}
221+
// Handle compatible numeric arguments
222+
if (Number.class.isAssignableFrom(clazz)) {
223+
// Python only has long or float
224+
if (classname.equals("float")) {
225+
return true; // all numeric types will fit
226+
}
227+
if (classname.equals("long")) {
228+
// or "long", but that was exact-matched earlier
229+
return type.equals("int")
230+
|| type.equals("short")
231+
|| type.equals("byte")
232+
|| clazz.isAssignableFrom(Long.class)
233+
|| clazz.isAssignableFrom(Integer.class)
234+
|| clazz.isAssignableFrom(Short.class)
235+
|| clazz.isAssignableFrom(Byte.class);
236+
}
237+
}
238+
// Fix class when it's a primitive number
239+
Class<?> c = null;
240+
switch (classname) {
241+
case "long": c = Long.class; break;
242+
case "float": c = Float.class; break;
243+
default: c = Class.forName(classname);
244+
}
245+
// Search for subclass or interface
246+
return clazz.isAssignableFrom(c);
247+
248+
// TODO: search for methods of non-matching classes that return a matching type
249+
250+
} catch (ClassNotFoundException e1) {
251+
JythonDev.print("Cannot load class " + e.getValue(), e1);
252+
}
253+
return false;
254+
})
255+
.map(e -> e.getKey()));
256+
scope = scope.parent;
257+
}
258+
return varNames;
259+
}
260+
202261

203262
public HashMap<String, DotAutocompletions> getImports() {
204263
final HashMap<String, DotAutocompletions> imports = new HashMap<>(this.imports);

0 commit comments

Comments
 (0)