Code suggestions refactoring

- fixed threading
- support for array types
- support for static access
- support for local classes
This commit is contained in:
Jakub Valtar
2015-09-21 15:48:43 -04:00
parent 6845c3b904
commit 52a2724aca
3 changed files with 571 additions and 357 deletions

View File

@@ -35,7 +35,6 @@ import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -43,7 +42,6 @@ import java.util.Map;
import java.util.Stack;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import javax.swing.BorderFactory;
@@ -157,7 +155,6 @@ public class ASTGenerator {
//addCompletionPopupListner();
addListeners();
//loadJavaDoc();
predictionOngoing = new AtomicBoolean(false);
}
@@ -398,7 +395,7 @@ public class ASTGenerator {
}
jdocMap.put(methodName, msg);
}
System.out.println("JDoc loaded "+jdocMap.size());
System.out.println("JDoc loaded " + jdocMap.size());
}
@@ -539,11 +536,42 @@ public class ASTGenerator {
if(decl != null){
// see if locally defined
log(getNodeAsString(astNode)+" found decl -> " + getNodeAsString(decl));
{
if (decl.getNodeType() == ASTNode.TYPE_DECLARATION) {
TypeDeclaration td = (TypeDeclaration) decl;
return new ClassMember(td);
}
}
{ // Handle "array." x "array[1]."
Type type = extracTypeInfo2(decl);
if (type != null && type.isArrayType() &&
astNode.getParent().getNodeType() != ASTNode.ARRAY_ACCESS) {
// No array access, we want members of the array itself
Type elementType = ((ArrayType) type).getElementType();
// Get name of the element class
String name = "";
if (elementType.isSimpleType()) {
Class<?> c = findClassIfExists(elementType.toString());
if (c != null) name = c.getName();
} else if (elementType.isPrimitiveType()) {
name = ((PrimitiveType) elementType).getPrimitiveTypeCode().toString();
}
// Convert element class to array class
Class<?> arrayClass = getArrayClass(name);
return arrayClass == null ? null : new ClassMember(arrayClass);
}
}
return new ClassMember(extracTypeInfo(decl));
}
else {
// or in a predefined class?
Class<?> tehClass = findClassIfExists(((SimpleName) astNode).toString());
Class<?> tehClass = findClassIfExists(astNode.toString());
if (tehClass != null) {
return new ClassMember(tehClass);
}
@@ -596,6 +624,30 @@ public class ASTGenerator {
if(temp instanceof MethodDeclaration){
// method is locally defined
log(mi.getName() + " was found locally," + getNodeAsString(extracTypeInfo(temp)));
{ // Handle "array." x "array[1]."
Type type = extracTypeInfo2(temp);
if (type != null && type.isArrayType() &&
astNode.getParent().getNodeType() != ASTNode.ARRAY_ACCESS) {
// No array access, we want members of the array itself
Type elementType = ((ArrayType) type).getElementType();
// Get name of the element class
String name = "";
if (elementType.isSimpleType()) {
Class<?> c = findClassIfExists(elementType.toString());
if (c != null) name = c.getName();
} else if (elementType.isPrimitiveType()) {
name = ((PrimitiveType) elementType).getPrimitiveTypeCode().toString();
}
// Convert element class to array class
Class<?> arrayClass = getArrayClass(name);
return arrayClass == null ? null : new ClassMember(arrayClass);
}
}
return new ClassMember(extracTypeInfo(temp));
}
if (mi.getExpression() == null) {
@@ -605,38 +657,46 @@ public class ASTGenerator {
return null;
} else {
if (mi.getExpression() instanceof SimpleName) {
stp = extracTypeInfo(findDeclaration2((SimpleName) mi.getExpression(),
nearestNode));
if(stp == null){
ASTNode decl = findDeclaration2((SimpleName) mi.getExpression(),
nearestNode);
if (decl != null) {
if (decl.getNodeType() == ASTNode.TYPE_DECLARATION) {
TypeDeclaration td = (TypeDeclaration) decl;
return new ClassMember(td);
}
stp = extracTypeInfo(decl);
if(stp == null){
/*The type wasn't found in local code, so it might be something like
* System.console()., or maybe belonging to super class, etc.
*/
Class<?> tehClass = findClassIfExists(((SimpleName)mi.getExpression()).toString());
if (tehClass != null) {
// Method Expression is a simple name and wasn't located locally, but found in a class
// so look for method in this class.
return definedIn3rdPartyClass(new ClassMember(tehClass), mi
.getName().toString());
Class<?> tehClass = findClassIfExists(((SimpleName)mi.getExpression()).toString());
if (tehClass != null) {
// Method Expression is a simple name and wasn't located locally, but found in a class
// so look for method in this class.
return definedIn3rdPartyClass(new ClassMember(tehClass), mi
.getName().toString());
}
log("MI resolve 3rd par, Can't resolve " + mi.getExpression());
return null;
}
log("MI resolve 3rd par, Can't resolve " + mi.getExpression());
return null;
}
log("MI, SN Type " + getNodeAsString(stp));
ASTNode typeDec = findDeclaration2(stp.getName(),nearestNode);
if(typeDec == null){
log(stp.getName() + " couldn't be found locally..");
Class<?> tehClass = findClassIfExists(stp.getName().toString());
if (tehClass != null) {
// Method Expression is a simple name and wasn't located locally, but found in a class
// so look for method in this class.
return definedIn3rdPartyClass(new ClassMember(tehClass), mi
.getName().toString());
log("MI, SN Type " + getNodeAsString(stp));
ASTNode typeDec = findDeclaration2(stp.getName(),nearestNode);
if(typeDec == null){
log(stp.getName() + " couldn't be found locally..");
Class<?> tehClass = findClassIfExists(stp.getName().toString());
if (tehClass != null) {
// Method Expression is a simple name and wasn't located locally, but found in a class
// so look for method in this class.
return definedIn3rdPartyClass(new ClassMember(tehClass), mi
.getName().toString());
}
//return new ClassMember(findClassIfExists(stp.getName().toString()));
}
//return new ClassMember(findClassIfExists(stp.getName().toString()));
//scopeParent = definedIn3rdPartyClass(stp.getName().toString(), "THIS");
return definedIn3rdPartyClass(new ClassMember(typeDec), mi
.getName().toString());
}
//scopeParent = definedIn3rdPartyClass(stp.getName().toString(), "THIS");
return definedIn3rdPartyClass(new ClassMember(typeDec), mi
.getName().toString());
} else {
log("MI EXP.."+getNodeAsString(mi.getExpression()));
// return null;
@@ -711,6 +771,36 @@ public class ASTGenerator {
return null;
}
public Class<?> getArrayClass(String elementClass) {
String name;
if (elementClass.startsWith("[")) {
// just add a leading "["
name = "[" + elementClass;
} else if (elementClass.equals("boolean")) {
name = "[Z";
} else if (elementClass.equals("byte")) {
name = "[B";
} else if (elementClass.equals("char")) {
name = "[C";
} else if (elementClass.equals("double")) {
name = "[D";
} else if (elementClass.equals("float")) {
name = "[F";
} else if (elementClass.equals("int")) {
name = "[I";
} else if (elementClass.equals("long")) {
name = "[J";
} else if (elementClass.equals("short")) {
name = "[S";
} else {
// must be an object non-array class
name = "[L" + elementClass + ";";
}
return loadClass(name);
}
/**
* For a().abc.a123 this would return a123
*
@@ -753,151 +843,113 @@ public class ASTGenerator {
return null;
}
protected void trimCandidates(String newWord) {
ArrayList<CompletionCandidate> newCandidate = new ArrayList<CompletionCandidate>();
protected static List<CompletionCandidate> trimCandidates(String newWord, List<CompletionCandidate> candidates) {
ArrayList<CompletionCandidate> newCandidate = new ArrayList<>();
newWord = newWord.toLowerCase();
for (CompletionCandidate comp : candidates) {
if(comp.getNoHtmlLabel().toLowerCase().startsWith(newWord)){
newCandidate.add(comp);
}
}
candidates = newCandidate;
return newCandidate;
}
protected List<CompletionCandidate> candidates;
protected String lastPredictedWord = " ";
protected int predictionMinLength = 2;
private AtomicBoolean predictionOngoing;
protected String lastPredictedPhrase = " ";
protected static final int predictionMinLength = 2;
/**
* The main function that calculates possible code completion candidates
*
* @param word
* @param pdePhrase
* @param line
* @param lineStartNonWSOffset
*/
public void preparePredictions(final String word, final int line,
final int lineStartNonWSOffset) {
// EventQueue.invokeLater(new Runnable() {
// public void run() {
// preparePredictions2(word, line, lineStartNonWSOffset);
// }
// });
// }
//
// public void preparePredictions2(final String word, final int line,
// final int lineStartNonWSOffset) {
// System.out.println(EventQueue.isDispatchThread() + " " + predictionOngoing.get() + " " + (!JavaMode.codeCompletionsEnabled) + " " + (word.length() < predictionMinLength));
// new Exception("preparing predictions " + EventQueue.isDispatchThread() + " " + predictionOngoing.get() + " " + (!JavaMode.codeCompletionsEnabled) + " " + (word.length() < predictionMinLength)).printStackTrace(System.out);
if (predictionOngoing.get()) return;
if (!JavaMode.codeCompletionsEnabled) return;
if (word.length() < predictionMinLength) return;
predictionOngoing.set(true);
// This method is called from TextArea.fetchPhrase, which is called via a SwingWorker instance
// in TextArea.processKeyEvent
if (caretWithinLineComment()) {
log("No predictions.");
predictionOngoing.set(false);
return;
}
// presumably this was removed because the caller is running from a SwingWorker [fry]
// SwingWorker worker = new SwingWorker() {
//
// @Override
// protected Object doInBackground() throws Exception {
// return null;
// }
//
// protected void done() {
public List<CompletionCandidate> preparePredictions(final String pdePhrase,
final int line) {
ErrorCheckerService errorCheckerService = editor.getErrorChecker();
ASTNode astRootNode = (ASTNode) errorCheckerService.getLatestCU().types().get(0);
// If the parsed code contains pde enhancements, take 'em out.
String word2 = ASTNodeWrapper.getJavaCode(word);
String phrase = ASTNodeWrapper.getJavaCode(pdePhrase);
//After typing 'arg.' all members of arg type are to be listed. This one is a flag for it
boolean noCompare = false;
if (word2.endsWith(".")) {
// return all matches
word2 = word2.substring(0, word2.length() - 1);
noCompare = true;
boolean noCompare = phrase.endsWith(".");
if (noCompare) {
phrase = phrase.substring(0, phrase.length() - 1);
}
if (word2.length() >= predictionMinLength && !noCompare
&& word2.length() > lastPredictedWord.length()) {
if (word2.startsWith(lastPredictedWord)) {
log(word + " starts with " + lastPredictedWord);
log("Don't recalc");
if (word2.contains(".")) {
int x = word2.lastIndexOf('.');
trimCandidates(word2.substring(x + 1));
} else {
trimCandidates(word2);
}
showPredictions(word);
lastPredictedWord = word2;
predictionOngoing.set(false);
return;
boolean incremental = !noCompare &&
phrase.length() > lastPredictedPhrase.length() &&
phrase.startsWith(lastPredictedPhrase);
if (incremental) {
log(pdePhrase + " starts with " + lastPredictedPhrase);
log("Don't recalc");
if (phrase.contains(".")) {
int x = phrase.lastIndexOf('.');
candidates = trimCandidates(phrase.substring(x + 1), candidates);
} else {
candidates = trimCandidates(phrase, candidates);
}
lastPredictedPhrase = phrase;
return candidates;
}
int lineNumber = line;
// Adjust line number for tabbed sketches
if (errorCheckerService != null) {
editor = errorCheckerService.getEditor();
int codeIndex = editor.getSketch().getCodeIndex(editor.getCurrentTab());
if (codeIndex > 0) {
for (int i = 0; i < codeIndex; i++) {
SketchCode sc = editor.getSketch().getCode(i);
int len = Util.countLines(sc.getProgram()) + 1;
lineNumber += len;
}
int codeIndex = editor.getSketch().getCodeIndex(editor.getCurrentTab());
if (codeIndex > 0) {
for (int i = 0; i < codeIndex; i++) {
SketchCode sc = editor.getSketch().getCode(i);
int len = Util.countLines(sc.getProgram()) + 1;
lineNumber += len;
}
}
// Ensure that we're not inside a comment. TODO: Binary search
// Ensure that we're not inside a comment. TODO: Binary search
/*for (Comment comm : getCodeComments()) {
int commLineNo = PdeToJavaLineNumber(compilationUnit
.getLineNumber(comm.getStartPosition()));
if(commLineNo == lineNumber){
log("Found a comment line " + comm);
log("Comment LSO "
+ javaCodeOffsetToLineStartOffset(compilationUnit
.getLineNumber(comm.getStartPosition()),
comm.getStartPosition()));
break;
}
}*/
/*for (Comment comm : getCodeComments()) {
int commLineNo = PdeToJavaLineNumber(compilationUnit
.getLineNumber(comm.getStartPosition()));
if(commLineNo == lineNumber){
log("Found a comment line " + comm);
log("Comment LSO "
+ javaCodeOffsetToLineStartOffset(compilationUnit
.getLineNumber(comm.getStartPosition()),
comm.getStartPosition()));
break;
}
}*/
// Now parse the expression into an ASTNode object
ASTNode nearestNode = null;
ASTParser parser = ASTParser.newParser(AST.JLS8);
parser.setKind(ASTParser.K_EXPRESSION);
parser.setSource(word2.toCharArray());
parser.setSource(phrase.toCharArray());
ASTNode testnode = parser.createAST(null);
//Base.loge("PREDICTION PARSER PROBLEMS: " + parser);
// Find closest ASTNode of the document to this word
Messages.loge("Typed: " + word2 + "|" + " temp Node type: " + testnode.getClass().getSimpleName());
Messages.loge("Typed: " + phrase + "|" + " temp Node type: " + testnode.getClass().getSimpleName());
if(testnode instanceof MethodInvocation){
MethodInvocation mi = (MethodInvocation)testnode;
log(mi.getName() + "," + mi.getExpression() + "," + mi.typeArguments().size());
}
// find nearest ASTNode
nearestNode = findClosestNode(lineNumber, (ASTNode) errorCheckerService.getLastCorrectCU().types()
.get(0));
nearestNode = findClosestNode(lineNumber, astRootNode);
if (nearestNode == null) {
// Make sure nearestNode is not NULL if couldn't find a closeset node
nearestNode = (ASTNode) errorCheckerService.getLastCorrectCU().types().get(0);
nearestNode = astRootNode;
}
Messages.loge(lineNumber + " Nearest ASTNode to PRED "
+ getNodeAsString(nearestNode));
candidates = new ArrayList<CompletionCandidate>();
lastPredictedWord = word2;
candidates = new ArrayList<>();
lastPredictedPhrase = phrase;
// Determine the expression typed
if (testnode instanceof SimpleName && !noCompare) {
@@ -916,7 +968,7 @@ public class ASTGenerator {
SimpleType st = (SimpleType) td.getStructuralProperty(TypeDeclaration.SUPERCLASS_TYPE_PROPERTY);
log("Superclass " + st.getName());
ArrayList<CompletionCandidate> tempCandidates =
getMembersForType(st.getName().toString(), word2, noCompare, false);
getMembersForType(st.getName().toString(), phrase, noCompare, false);
for (CompletionCandidate can : tempCandidates) {
candidates.add(can);
}
@@ -933,7 +985,7 @@ public class ASTGenerator {
CompletionCandidate[] types = checkForTypes(cnode);
if (types != null) {
for (int i = 0; i < types.length; i++) {
if (types[i].getElementName().toLowerCase().startsWith(word2.toLowerCase()))
if (types[i].getElementName().toLowerCase().startsWith(phrase.toLowerCase()))
candidates.add(types[i]);
}
}
@@ -946,7 +998,7 @@ public class ASTGenerator {
CompletionCandidate[] types = checkForTypes(clnode);
if (types != null) {
for (int i = 0; i < types.length; i++) {
if (types[i].getElementName().toLowerCase().startsWith(word2.toLowerCase()))
if (types[i].getElementName().toLowerCase().startsWith(phrase.toLowerCase()))
candidates.add(types[i]);
}
}
@@ -957,11 +1009,11 @@ public class ASTGenerator {
}
// We're seeing a simple name that's not defined locally or in
// the parent class. So most probably a pre-defined type.
log("Empty can. " + word2);
log("Empty can. " + phrase);
if (classPath != null) {
RegExpResourceFilter regExpResourceFilter =
new RegExpResourceFilter(Pattern.compile(".*"),
Pattern.compile(word2 + "[a-zA-Z_0-9]*.class",
Pattern.compile(phrase + "[a-zA-Z_0-9]*.class",
Pattern.CASE_INSENSITIVE));
String[] resources = classPath.findResources("", regExpResourceFilter);
@@ -988,62 +1040,33 @@ public class ASTGenerator {
ASTNode childExpr = getChildExpression(testnode);
log("Parent expression : " + getParentExpression(testnode));
log("Child expression : " + childExpr);
if (childExpr != null) {
if (!noCompare) {
log("Original testnode " + getNodeAsString(testnode));
testnode = getParentExpression(testnode);
log("Corrected testnode " + getNodeAsString(testnode));
}
ClassMember expr =
if (!noCompare) {
log("Original testnode " + getNodeAsString(testnode));
testnode = getParentExpression(testnode);
log("Corrected testnode " + getNodeAsString(testnode));
}
ClassMember expr =
resolveExpression3rdParty(nearestNode, testnode, noCompare);
if (expr == null) {
log("Expr is null");
} else {
log("Expr is " + expr.toString());
candidates = getMembersForType(expr, childExpr.toString(),
noCompare, false);
}
if (expr == null) {
log("Expr is null");
} else {
log("ChildExpr is null");
boolean isArray = expr.thisclass != null && expr.thisclass.isArray();
boolean isSimpleType = (expr.astNode != null) &&
expr.astNode.getNodeType() == ASTNode.SIMPLE_TYPE;
boolean isMethod = expr.method != null;
boolean staticOnly = !isMethod && !isArray && !isSimpleType;
log("Expr is " + expr.toString());
String lookFor = (noCompare || (childExpr == null)) ?
"" : childExpr.toString();
candidates = getMembersForType(expr, lookFor, noCompare, staticOnly);
}
}
showPredictions(word);
predictionOngoing.set(false);
// }
// };
//
// worker.execute();
return candidates;
}
protected void showPredictions(final String word) {
if (sketchOutline != null && sketchOutline.isVisible()) {
// don't show completions when the outline is visible
return;
}
Collections.sort(candidates);
// CompletionCandidate[][] candi = new CompletionCandidate[candidates.size()][1];
// DefaultListModel<CompletionCandidate> defListModel = new DefaultListModel<CompletionCandidate>();
//
// for (int i = 0; i < candidates.size(); i++) {
//// candi[i][0] = candidates.get(i);
// defListModel.addElement(candidates.get(i));
// }
// log("Total preds = " + candidates.size());
DefaultListModel<CompletionCandidate> defListModel = filterPredictions();
// DefaultTableModel tm = new DefaultTableModel(candi,
// new String[] { "Suggestions" });
// if (tableAuto.isVisible()) {
// tableAuto.setModel(tm);
// tableAuto.validate();
// tableAuto.repaint();
// }
errorCheckerService.getEditor().getJavaTextArea().showSuggestion(defListModel, word);
}
private DefaultListModel<CompletionCandidate> filterPredictions(){
DefaultListModel<CompletionCandidate> defListModel = new DefaultListModel<CompletionCandidate>();
protected static DefaultListModel<CompletionCandidate> filterPredictions(List<CompletionCandidate> candidates){
DefaultListModel<CompletionCandidate> defListModel = new DefaultListModel<>();
if (candidates.isEmpty())
return defListModel;
// check if first & last CompCandidate are the same methods, only then show all overloaded methods
@@ -1117,7 +1140,7 @@ public class ASTGenerator {
boolean noCompare,
boolean staticOnly) {
String child = childToLookFor.toLowerCase();
ArrayList<CompletionCandidate> candidates = new ArrayList<CompletionCandidate>();
ArrayList<CompletionCandidate> candidates = new ArrayList<>();
log("getMemFoType-> Looking for match " + child.toString()
+ " inside " + tehClass + " noCompare " + noCompare + " staticOnly "
+ staticOnly);
@@ -1126,28 +1149,34 @@ public class ASTGenerator {
}
// tehClass will either be a TypeDecl defined locally
if(tehClass.getDeclaringNode() instanceof TypeDeclaration){
TypeDeclaration td = (TypeDeclaration) tehClass.getDeclaringNode();
for (int i = 0; i < td.getFields().length; i++) {
List<VariableDeclarationFragment> vdfs = td.getFields()[i]
.fragments();
for (VariableDeclarationFragment vdf : vdfs) {
if (noCompare) {
candidates
.add(new CompletionCandidate(vdf));
} else if (vdf.getName().toString().toLowerCase()
.startsWith(child))
candidates
.add(new CompletionCandidate(vdf));
{
FieldDeclaration[] fields = td.getFields();
for (int i = 0; i < fields.length; i++) {
if (staticOnly && !isStatic(fields[i].modifiers())) {
continue;
}
List<VariableDeclarationFragment> vdfs = fields[i].fragments();
for (VariableDeclarationFragment vdf : vdfs) {
if (noCompare) {
candidates.add(new CompletionCandidate(vdf));
} else if (vdf.getName().toString().toLowerCase().startsWith(child))
candidates.add(new CompletionCandidate(vdf));
}
}
}
for (int i = 0; i < td.getMethods().length; i++) {
if (noCompare) {
candidates.add(new CompletionCandidate(td.getMethods()[i]));
} else if (td.getMethods()[i].getName().toString().toLowerCase()
.startsWith(child))
candidates.add(new CompletionCandidate(td.getMethods()[i]));
{
MethodDeclaration[] methods = td.getMethods();
for (int i = 0; i < methods.length; i++) {
if (staticOnly && !isStatic(methods[i].modifiers())) {
continue;
}
if (noCompare) {
candidates.add(new CompletionCandidate(methods[i]));
} else if (methods[i].getName().toString().toLowerCase()
.startsWith(child))
candidates.add(new CompletionCandidate(methods[i]));
}
}
ArrayList<CompletionCandidate> superClassCandidates = new ArrayList<CompletionCandidate>();
@@ -1209,9 +1238,35 @@ public class ASTGenerator {
candidates.add(new CompletionCandidate(field));
}
}
if (probableClass.isArray() && !staticOnly) {
// add array members manually, they can't be fetched through code
String className = probableClass.getSimpleName();
if (noCompare || "clone()".startsWith(child)) {
String methodLabel = "<html>clone() : " + className +
" - <font color=#777777>" + className + "</font></html>";
candidates.add(new CompletionCandidate("clone()", methodLabel, "clone()",
CompletionCandidate.PREDEF_METHOD));
}
if ("length".startsWith(child)) {
String fieldLabel = "<html>length : int - <font color=#777777>" +
className + "</font></html>";
candidates.add(new CompletionCandidate("length", fieldLabel, "length",
CompletionCandidate.PREDEF_FIELD));
}
}
return candidates;
}
private static boolean isStatic(List<org.eclipse.jdt.core.dom.Modifier> modifiers) {
for (org.eclipse.jdt.core.dom.Modifier m : modifiers) {
if (m.isStatic()) return true;
}
return false;
}
public String getPDESourceCodeLine(int javaLineNumber) {
int res[] = errorCheckerService
.calculateTabIndexAndLineNumber(javaLineNumber);
@@ -3022,19 +3077,6 @@ public class ASTGenerator {
}
protected boolean caretWithinLineComment() {
final JEditTextArea ta = editor.getTextArea();
String pdeLine = editor.getLineText(ta.getCaretLine()).trim();
int caretPos = ta.getCaretPosition() - ta.getLineStartNonWhiteSpaceOffset(ta.getCaretLine());
int x = pdeLine.indexOf("//");
if (x >= 0 && caretPos > x) {
return true;
}
return false;
}
/**
* A wrapper for java.lang.reflect types.
* Will have to see if the usage turns out to be internal only here or not

View File

@@ -112,7 +112,7 @@ public class CompletionPanel {
this.textarea = (JavaTextArea) textarea;
this.editor = editor;
this.insertionPosition = position;
if (subWord.indexOf('.') != -1) {
if (subWord.indexOf('.') != -1 && subWord.indexOf('.') != subWord.length()-1) {
this.subWord = subWord.substring(subWord.lastIndexOf('.') + 1);
} else {
this.subWord = subWord;
@@ -398,13 +398,7 @@ public class CompletionPanel {
if(mouseClickOnOverloadedMethods) {
// See #2755
SwingWorker<Object, Object> worker = new SwingWorker<Object, Object>() {
protected Object doInBackground() throws Exception {
((JavaTextArea) editor.getTextArea()).fetchPhrase(null);
return null;
}
};
worker.execute();
((JavaTextArea) editor.getTextArea()).fetchPhrase();
}
return true;

View File

@@ -20,6 +20,7 @@ along with this program; if not, write to the Free Software Foundation, Inc.
package processing.mode.java.pdex;
import processing.core.PVector;
import processing.mode.java.JavaInputHandler;
import processing.mode.java.JavaMode;
import processing.mode.java.JavaEditor;
@@ -28,12 +29,22 @@ import processing.mode.java.tweak.Handle;
import java.awt.*;
import java.awt.event.*;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.DefaultListModel;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.text.BadLocationException;
import processing.app.Messages;
import processing.app.Mode;
@@ -222,7 +233,8 @@ public class JavaTextArea extends JEditTextArea {
if (!editor.hasJavaTabs()) {
if (evt.getID() == KeyEvent.KEY_TYPED) {
processCompletionKeys(evt);
} else if (!Platform.isMacOS() && evt.getID() == KeyEvent.KEY_RELEASED) {
processCompletionKeys(evt);
} else if (Platform.isMacOS() && evt.getID() == KeyEvent.KEY_RELEASED) {
processControlSpace(evt);
}
@@ -234,28 +246,24 @@ public class JavaTextArea extends JEditTextArea {
// https://github.com/processing/processing/issues/2699
private void processControlSpace(final KeyEvent event) {
if (event.getKeyCode() == KeyEvent.VK_SPACE && event.isControlDown()) {
SwingWorker<Object, Object> worker = new SwingWorker<Object, Object>() {
protected Object doInBackground() throws Exception {
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled) {
Messages.log("[KeyEvent]" + KeyEvent.getKeyText(event.getKeyCode()) + " |Prediction started");
Messages.log("Typing: " + fetchPhrase(event));
}
return null;
}
};
worker.execute();
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled) {
Messages.log("[KeyEvent]" + KeyEvent.getKeyText(event.getKeyCode()) + " |Prediction started");
fetchPhrase();
}
}
}
private void processCompletionKeys(final KeyEvent event) {
char keyChar = event.getKeyChar();
int keyCode = event.getKeyCode();
if (keyChar == KeyEvent.VK_ENTER ||
keyChar == KeyEvent.VK_ESCAPE ||
keyChar == KeyEvent.VK_TAB ||
keyChar == KeyEvent.CHAR_UNDEFINED) {
(event.getID() == KeyEvent.KEY_RELEASED &&
keyCode != KeyEvent.VK_LEFT && keyCode != KeyEvent.VK_RIGHT)) {
// ignore
} else if (keyChar == ')') {
// https://github.com/processing/processing/issues/2741
hideSuggestion();
@@ -263,28 +271,33 @@ public class JavaTextArea extends JEditTextArea {
} else if (keyChar == '.') {
if (JavaMode.codeCompletionsEnabled) {
Messages.log("[KeyEvent]" + KeyEvent.getKeyText(event.getKeyCode()) + " |Prediction started");
Messages.log("Typing: " + fetchPhrase(event));
fetchPhrase();
}
} else if (keyChar == ' ') { // Trigger on Ctrl-Space
if (!Platform.isMacOS() && JavaMode.codeCompletionsEnabled &&
(event.isControlDown() || event.isMetaDown())) {
SwingWorker<Object, Object> worker = new SwingWorker<Object, Object>() {
protected Object doInBackground() throws Exception {
//SwingWorker<Object, Object> worker = new SwingWorker<Object, Object>() {
// protected Object doInBackground() throws Exception {
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled) {
getDocument().remove(getCaretPosition() - 1, 1); // Remove the typed space
Messages.log("[KeyEvent]" + event.getKeyChar() + " |Prediction started");
Messages.log("Typing: " + fetchPhrase(event));
try {
getDocument().remove(getCaretPosition() - 1, 1); // Remove the typed space
Messages.log("[KeyEvent]" + event.getKeyChar() + " |Prediction started");
fetchPhrase();
} catch (BadLocationException e) {
e.printStackTrace();
}
}
return null;
}
};
worker.execute();
// return null;
// }
//};
//worker.execute();
} else {
hideSuggestion(); // hide on spacebar
}
} else {
if (JavaMode.codeCompletionsEnabled) {
//fetchPhrase();
prepareSuggestions(event);
}
}
@@ -293,17 +306,13 @@ public class JavaTextArea extends JEditTextArea {
/** Kickstart auto-complete suggestions */
private void prepareSuggestions(final KeyEvent evt) {
new SwingWorker<Object, Object>() {
protected Object doInBackground() throws Exception {
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled &&
(JavaMode.ccTriggerEnabled || suggestion.isVisible())) {
Messages.log("[KeyEvent]" + evt.getKeyChar() + " |Prediction started");
Messages.log("Typing: " + fetchPhrase(evt));
}
return null;
}
}.execute();
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled &&
(JavaMode.ccTriggerEnabled ||
(suggestion != null && suggestion.isVisible()))) {
Messages.log("[KeyEvent]" + evt.getKeyChar() + " |Prediction started");
fetchPhrase();
}
}
@@ -377,128 +386,300 @@ public class JavaTextArea extends JEditTextArea {
}
SwingWorker<Void, Void> suggestionWorker = null;
int lastCaretPosition = 0;
String lastPhrase = "";
volatile boolean suggestionRunning = false;
volatile boolean suggestionRequested = false;
/**
* Retrieves the current word typed just before the caret.
* Then triggers code completion for that word.
* @param evt - the KeyEvent which triggered this method
*/
protected String fetchPhrase(KeyEvent evt) {
int off = getCaretPosition();
Messages.log("off " + off);
if (off < 0)
return null;
int line = getCaretLine();
if (line < 0)
return null;
String s = getLineText(line);
Messages.log(" line " + line);
protected void fetchPhrase() {
//log2(s + " len " + s.length());
int x = getCaretPosition() - getLineStartOffset(line) - 1, x1 = x - 1;
if(x >= s.length() || x < 0) {
//log("X is " + x + ". Returning null");
hideSuggestion();
return null; //TODO: Does this check cause problems? Verify.
if (suggestionRunning) {
suggestionRequested = true;
return;
}
Messages.log(" x char: " + s.charAt(x));
suggestionRunning = true;
suggestionRequested = false;
if (!(Character.isLetterOrDigit(s.charAt(x)) || s.charAt(x) == '_'
|| s.charAt(x) == '(' || s.charAt(x) == '.')) {
//log("Char before caret isn't a letter/digit/_(. so no predictions");
hideSuggestion();
return null;
} else if (x > 0 && (s.charAt(x - 1) == ' ' || s.charAt(x - 1) == '(')
&& Character.isDigit(s.charAt(x))) {
//log("Char before caret isn't a letter, but ' ' or '(', so no predictions");
hideSuggestion(); // See #2755, Option 2 comment
return null;
} else if (x == 0){
//log("X is zero");
hideSuggestion();
return null;
final String text;
final int caretLineIndex;
final int caretLinePosition;
{
// Get caret position
int caretPosition = getCaretPosition();
if (caretPosition < 0) {
suggestionRunning = false;
return;
}
// Get line index
caretLineIndex = getCaretLine();
if (caretLineIndex < 0) {
suggestionRunning = false;
return;
}
// Get text of the line
String lineText = getLineText(caretLineIndex);
if (lineText == null) {
suggestionRunning = false;
return;
}
// Get caret position on the line
caretLinePosition = getCaretPosition() - getLineStartOffset(caretLineIndex);
if (caretLinePosition <= 0) {
suggestionRunning = false;
return;
}
// Get part of the line to the left of the caret
if (caretLinePosition > lineText.length()) {
suggestionRunning = false;
return;
}
text = lineText.substring(0, caretLinePosition);
}
//int xLS = off - getLineStartNonWhiteSpaceOffset(line);
suggestionWorker = new SwingWorker<Void, Void>() {
String word = (x < s.length() ? s.charAt(x) : "") + "";
if (s.trim().length() == 1) {
// word = ""
// + (keyChar == KeyEvent.CHAR_UNDEFINED ? s.charAt(x - 1) : keyChar);
//word = (x < s.length()?s.charAt(x):"") + "";
word = word.trim();
if (word.endsWith("."))
word = word.substring(0, word.length() - 1);
String phrase = null;
DefaultListModel<CompletionCandidate> defListModel = null;
editor.getErrorChecker().getASTGenerator().preparePredictions(word, line + editor.getErrorChecker().mainClassOffset,0);
return word;
}
@Override
protected Void doInBackground() throws Exception {
Messages.log("phrase parse start");
phrase = parsePhrase(text, caretLinePosition);
Messages.log("phrase: " + phrase);
if (phrase == null) return null;
int i = 0;
int closeB = 0;
List<CompletionCandidate> candidates = null;
while (true) {
i++;
//TODO: currently works on single line only. "a. <new line> b()" won't be detected
if (x1 >= 0) {
// if (s.charAt(x1) != ';' && s.charAt(x1) != ',' && s.charAt(x1) != '(')
if (Character.isLetterOrDigit(s.charAt(x1)) || s.charAt(x1) == '_'
|| s.charAt(x1) == '.' || s.charAt(x1) == ')' || s.charAt(x1) == ']') {
ASTGenerator astGenerator = editor.getErrorChecker().getASTGenerator();
synchronized (astGenerator) {
int lineOffset = caretLineIndex +
editor.getErrorChecker().mainClassOffset;
if (s.charAt(x1) == ')') {
word = s.charAt(x1--) + word;
closeB++;
while (x1 >= 0 && closeB > 0) {
word = s.charAt(x1) + word;
if (s.charAt(x1) == '(')
closeB--;
if (s.charAt(x1) == ')')
closeB++;
x1--;
candidates = astGenerator.preparePredictions(phrase, lineOffset);
}
if (suggestionRequested) return null;
// don't show completions when the outline is visible
boolean showSuggestions = astGenerator.sketchOutline == null ||
!astGenerator.sketchOutline.isVisible();
if (showSuggestions && phrase != null &&
candidates != null && !candidates.isEmpty()) {
Collections.sort(candidates);
defListModel = ASTGenerator.filterPredictions(candidates);
Messages.log("Got: " + candidates.size() + " candidates, " + defListModel.size() + " filtered");
}
return null;
}
@Override
protected void done() {
try {
get();
} catch (ExecutionException e) {
Messages.loge("error while preparing suggestions", e.getCause());
} catch (InterruptedException e) {
// don't care
}
suggestionRunning = false;
if (suggestionRequested) {
Messages.log("completion invalidated");
hideSuggestion();
fetchPhrase();
return;
}
Messages.log("completion finishing");
if (defListModel != null) {
showSuggestion(defListModel, phrase);
} else {
hideSuggestion();
}
}
};
suggestionWorker.execute();
}
protected static String parsePhrase(String lineText, int caretLinePosition) {
boolean overloading = false;
{ // Check if we can provide suggestions for this phrase ending
String trimmedLineText = lineText.trim();
if (trimmedLineText.length() == 0) return null;
int lastCodePoint = trimmedLineText.codePointAt(trimmedLineText.length() - 1);
if (lastCodePoint == '.') {
trimmedLineText = trimmedLineText.substring(0, trimmedLineText.length() - 1).trim();
if (trimmedLineText.length() == 0) return null;
lastCodePoint = trimmedLineText.codePointAt(trimmedLineText.length() - 1);
switch (lastCodePoint) {
case ')':
case ']':
case '"':
break; // We can suggest for these
default:
if (!Character.isJavaIdentifierPart(lastCodePoint)) {
return null; // Not something we can suggest
}
}
else if (s.charAt(x1) == ']') {
word = s.charAt(x1--) + word;
closeB++;
while (x1 >= 0 && closeB > 0) {
word = s.charAt(x1) + word;
if (s.charAt(x1) == '[')
closeB--;
if (s.charAt(x1) == ']')
closeB++;
x1--;
}
}
else {
word = s.charAt(x1--) + word;
break;
}
} else if (lastCodePoint == '(') {
overloading = true; // We can suggest overloaded methods
} else if (!Character.isJavaIdentifierPart(lastCodePoint)) {
return null; // Not something we can suggest
}
}
final int currentCharIndex = caretLinePosition - 1;
{ // Check if the caret is in the comment
int commentStart = lineText.indexOf("//", 0);
if (commentStart >= 0 && currentCharIndex > commentStart) {
return null;
}
}
// Index the line
BitSet isInLiteral = new BitSet(lineText.length());
BitSet isInBrackets = new BitSet(lineText.length());
{ // Mark parts in literals
boolean inString = false;
boolean inChar = false;
boolean inEscaped = false;
for (int i = 0; i < lineText.length(); i++) {
if (!inEscaped) {
switch (lineText.codePointAt(i)) {
case '\"':
if (!inChar) inString = !inString;
break;
case '\'':
if (!inString) inChar = !inChar;
break;
case '\\':
if (inString || inChar) {
inEscaped = true;
}
break;
}
} else {
break;
inEscaped = false;
}
} else {
break;
}
if (i > 200) {
// time out!
break;
isInLiteral.set(i, inString || inChar);
}
}
if (Character.isDigit(word.charAt(0)))
return null;
word = word.trim();
// if (word.endsWith("."))
// word = word.substring(0, word.length() - 1);
int lineStartNonWSOffset = 0;
if (word.length() >= JavaMode.codeCompletionTriggerLength) {
editor.getErrorChecker().getASTGenerator()
.preparePredictions(word, line + editor.getErrorChecker().mainClassOffset,
lineStartNonWSOffset);
}
return word;
if (isInLiteral.get(currentCharIndex)) return null;
{ // Mark parts in top level brackets
int depth = overloading ? 1 : 0;
int bracketStart = overloading ? lineText.length() : 0;
int squareDepth = 0;
int squareBracketStart = 0;
bracketLoop: for (int i = lineText.length() - 1; i >= 0; i--) {
if (!isInLiteral.get(i)) {
switch (lineText.codePointAt(i)) {
case ')':
if (depth == 0) bracketStart = i;
depth++;
break;
case '(':
depth--;
if (depth == 0) {
isInBrackets.set(i, bracketStart);
} else if (depth < 0) {
break bracketLoop;
}
break;
case ']':
if (squareDepth == 0) squareBracketStart = i;
squareDepth++;
break;
case '[':
squareDepth--;
if (squareDepth == 0) {
isInBrackets.set(i, squareBracketStart);
} else if (squareDepth < 0) {
break bracketLoop;
}
break;
}
}
}
if (depth > 0) isInBrackets.set(0, bracketStart);
if (squareDepth > 0) isInBrackets.set(0, squareBracketStart);
}
// Walk the line from the end until it makes sense
int position = currentCharIndex;
parseLoop: while (position >= 0) {
int codePoint = lineText.codePointAt(position);
switch (codePoint) {
case '.': // Grab it
position--;
break;
case '[':
break parseLoop; // End of scope
case ']': // Grab the whole region in square brackets
position = isInBrackets.previousClearBit(position-1);
break;
case '(':
if (isInBrackets.get(position)) {
position--; // This checks for first bracket while overloading
break;
}
break parseLoop; // End of scope
case ')': // Grab the whole region in brackets
position = isInBrackets.previousClearBit(position-1);
break;
case '"': // Grab the whole literal and quit
position = isInLiteral.previousClearBit(position - 1);
break parseLoop;
default:
if (Character.isJavaIdentifierPart(codePoint)) {
position--; // Grab the identifier
} else if (Character.isWhitespace(codePoint)) {
position--; // Grab whitespace too
} else {
break parseLoop; // Got a char ending the phrase
}
break;
}
}
position++;
// Extract phrase
String phrase = lineText.substring(position, caretLinePosition).trim();
Messages.log(phrase);
if (phrase.length() == 0 || Character.isDigit(phrase.codePointAt(0))) {
return null; // Can't suggest for numbers or empty phrases
}
return phrase;
}
@@ -799,9 +980,6 @@ public class JavaTextArea extends JEditTextArea {
return;
}
if (subWord.length() < 2) {
return;
}
suggestion = new CompletionPanel(this, position, subWord,
listModel, location, editor);
requestFocusInWindow();
@@ -814,7 +992,7 @@ public class JavaTextArea extends JEditTextArea {
if (suggestion != null) {
suggestion.setInvisible();
//log("Suggestion hidden.");
suggestion = null;
suggestion = null; // TODO: check if we dispose the window properly
}
}