diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 33efb289c..03526e69e 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -31,6 +31,7 @@ import processing.app.ui.Toolkit; import processing.mode.java.debug.LineBreakpoint; import processing.mode.java.debug.LineHighlight; import processing.mode.java.debug.LineID; +import processing.mode.java.pdex.ASTGenerator; import processing.mode.java.pdex.ErrorCheckerService; import processing.mode.java.pdex.LineMarker; import processing.mode.java.pdex.ErrorMessageSimplifier; @@ -2674,14 +2675,20 @@ public class JavaEditor extends Editor { /** Handle refactor operation */ private void handleRefactor() { Messages.log("Caret at:" + textarea.getLineText(textarea.getCaretLine())); - errorCheckerService.getASTGenerator().handleRefactor(); + ASTGenerator astGenerator = errorCheckerService.getASTGenerator(); + synchronized (astGenerator) { + astGenerator.handleRefactor(); + } } /** Handle show usage operation */ private void handleShowUsage() { Messages.log("Caret at:" + textarea.getLineText(textarea.getCaretLine())); - errorCheckerService.getASTGenerator().handleShowUsage(); + ASTGenerator astGenerator = errorCheckerService.getASTGenerator(); + synchronized (astGenerator) { + astGenerator.handleShowUsage(); + } } diff --git a/java/src/processing/mode/java/pdex/ASTGenerator.java b/java/src/processing/mode/java/pdex/ASTGenerator.java index 297e6851c..a4d126de9 100644 --- a/java/src/processing/mode/java/pdex/ASTGenerator.java +++ b/java/src/processing/mode/java/pdex/ASTGenerator.java @@ -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); } @@ -222,7 +219,7 @@ public class ASTGenerator { protected DefaultMutableTreeNode buildAST(String source, CompilationUnit cu) { if (cu == null) { - ASTParser parser = ASTParser.newParser(AST.JLS4); + ASTParser parser = ASTParser.newParser(AST.JLS8); parser.setSource(source.toCharArray()); parser.setKind(ASTParser.K_COMPILATION_UNIT); @@ -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 newCandidate = new ArrayList(); + protected static List trimCandidates(String newWord, List candidates) { + ArrayList 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 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 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.JLS4); + 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(); - 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 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); @@ -969,7 +1021,7 @@ public class ASTGenerator { matchedClass2 = matchedClass2.replace('/', '.'); //package name String matchedClass = matchedClass2.substring(0, matchedClass2.length() - 6); int d = matchedClass.lastIndexOf('.'); - if (!ignorableImport(matchedClass,matchedClass.substring(d + 1))) { + if (!errorCheckerService.ignorableSuggestionImport(matchedClass)) { matchedClass = matchedClass.substring(d + 1); //class name // display package name in grey String html = "" + matchedClass + " : " + @@ -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 defListModel = new DefaultListModel(); -// -// 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 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 filterPredictions(){ - DefaultListModel defListModel = new DefaultListModel(); + protected static DefaultListModel filterPredictions(List candidates){ + DefaultListModel 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 candidates = new ArrayList(); + ArrayList 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 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 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 superClassCandidates = new ArrayList(); @@ -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 = "clone() : " + className + + " - " + className + ""; + candidates.add(new CompletionCandidate("clone()", methodLabel, "clone()", + CompletionCandidate.PREDEF_METHOD)); + } + + if ("length".startsWith(child)) { + String fieldLabel = "length : int - " + + className + ""; + candidates.add(new CompletionCandidate("length", fieldLabel, "length", + CompletionCandidate.PREDEF_FIELD)); + } + } return candidates; } + private static boolean isStatic(List 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); @@ -1752,7 +1807,7 @@ public class ASTGenerator { //errorCheckerService.highlightNode(simpName2); ASTNodeWrapper declWrap = new ASTNodeWrapper(simpName2, nodeLabel); //errorCheckerService.highlightNode(declWrap); - if (!declWrap.highlightNode(this)) { + if (!declWrap.highlightNode(editor)) { Messages.loge("Highlighting failed."); } } @@ -1885,7 +1940,7 @@ public class ASTGenerator { .getLastSelectedPathComponent(); if (tnode.getUserObject() instanceof ASTNodeWrapper) { ASTNodeWrapper awrap = (ASTNodeWrapper) tnode.getUserObject(); - awrap.highlightNode(thisASTGenerator); + awrap.highlightNode(editor); // errorCheckerService.highlightNode(awrap); //-- @@ -1984,7 +2039,7 @@ public class ASTGenerator { if (tnode.getUserObject() instanceof ASTNodeWrapper) { ASTNodeWrapper awrap = (ASTNodeWrapper) tnode.getUserObject(); //errorCheckerService.highlightNode(awrap); - awrap.highlightNode(thisASTGenerator); + awrap.highlightNode(editor); } } }; @@ -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 @@ -3161,9 +3203,9 @@ public class ASTGenerator { return null; } else if (t instanceof ArrayType) { ArrayType at = (ArrayType) t; - log(at.getComponentType() + " <-comp type, ele type-> " - + at.getElementType() + ", " - + at.getElementType().getClass().getName()); + log("ele type " + + at.getElementType() + ", " + + at.getElementType().getClass().getName()); if (at.getElementType() instanceof PrimitiveType) { return null; } else if (at.getElementType() instanceof SimpleType) { @@ -3521,43 +3563,6 @@ public class ASTGenerator { } - protected boolean ignorableImport(String impName, String fullClassName) { - for (ImportStatement impS : errorCheckerService.getProgramImports()) { - if (impName.toLowerCase().startsWith(impS.getPackageName().toLowerCase())) { - return false; - } - } - - for (ImportStatement impS : errorCheckerService.codeFolderImports) { - if (impName.toLowerCase().startsWith(impS.getPackageName().toLowerCase())) { - return false; - } - } - - if (JavaMode.suggestionsMap == null - || JavaMode.suggestionsMap.keySet().size() == 0) { - log("SuggestionsMap is null or empty, won't be able to trim class names"); - return true; - } - final String include = "include"; - final String exclude = "exclude"; - - if (impName.startsWith("processing")) { - if (JavaMode.suggestionsMap.get(include).contains(impName)) { - return false; - } else if (JavaMode.suggestionsMap.get(exclude).contains(impName)) { - return true; - } - } else if (impName.startsWith("java")) { - if (JavaMode.suggestionsMap.get(include).contains(impName)) { - return false; - } - } - - return true; - } - - public static boolean isAddableASTNode(ASTNode node) { switch (node.getNodeType()) { // case ASTNode.STRING_LITERAL: diff --git a/java/src/processing/mode/java/pdex/ASTNodeWrapper.java b/java/src/processing/mode/java/pdex/ASTNodeWrapper.java index d9d4dec29..2a2426935 100644 --- a/java/src/processing/mode/java/pdex/ASTNodeWrapper.java +++ b/java/src/processing/mode/java/pdex/ASTNodeWrapper.java @@ -46,6 +46,7 @@ import org.eclipse.jdt.core.dom.TypeDeclaration; import processing.app.Base; import processing.app.Messages; +import processing.mode.java.JavaEditor; /** @@ -54,7 +55,7 @@ import processing.app.Messages; * */ public class ASTNodeWrapper { - private ASTNode Node; + private ASTNode node; private String label; private int lineNumber; @@ -72,7 +73,7 @@ public class ASTNodeWrapper { if (node == null){ return; } - this.Node = node; + this.node = node; label = getNodeAsString(node); if (label == null) label = node.toString(); @@ -85,7 +86,7 @@ public class ASTNodeWrapper { if (node == null){ return; } - this.Node = node; + this.node = node; if(label != null) this.label = label; else{ @@ -105,10 +106,10 @@ public class ASTNodeWrapper { * node length} */ public int[] getJavaCodeOffsets(ErrorCheckerService ecs) { - int nodeOffset = Node.getStartPosition(), nodeLength = Node + int nodeOffset = node.getStartPosition(), nodeLength = node .getLength(); Messages.log("0.nodeOffset " + nodeOffset); - ASTNode thisNode = Node; + ASTNode thisNode = node; while (thisNode.getParent() != null) { if (getLineNumber(thisNode.getParent()) == lineNumber) { thisNode = thisNode.getParent(); @@ -178,12 +179,12 @@ public class ASTNodeWrapper { flag = false; } else { - if (cnode == Node) { + if (cnode == node) { // loop only till the current node. break; } // We've located the first node in the line. - // Now normalize offsets till Node + // Now normalize offsets till node //altStartPos += normalizeOffsets(cnode); } @@ -356,7 +357,7 @@ public class ASTNodeWrapper { * @param source * @return int[0] - java code offsets, int[1] = pde code offsets */ - public int[][] getOffsetMapping(ErrorCheckerService ecs, String source){ + public int[][] getOffsetMapping(ErrorCheckerService ecs, String source) { /* * This is some tricky shiz. So detailed explanation follows: @@ -390,7 +391,10 @@ public class ASTNodeWrapper { // Instead of converting pde into java, how can I simply extract the same source // from the java code? Think. TODO String sourceAlt = new String(source); - String sourceJava = ecs.astGenerator.getJavaSourceCodeLine(lineNumber); + String sourceJava; + synchronized (ecs.astGenerator) { + sourceJava = ecs.astGenerator.getJavaSourceCodeLine(lineNumber); + } TreeMap offsetmap = new TreeMap(); if(sourceJava.trim().startsWith("public") && !source.startsWith("public")){ @@ -538,21 +542,20 @@ public class ASTNodeWrapper { /** * Highlight the ASTNode in the editor, if it's of type * SimpleName - * @param astGenerator + * @param editor * @return - true if highlighting was successful */ - public boolean highlightNode(ASTGenerator astGenerator){ - if (!(Node instanceof SimpleName)) { + public boolean highlightNode(JavaEditor editor){ + if (!(node instanceof SimpleName)) { return false; } - SimpleName nodeName = (SimpleName) Node; + SimpleName nodeName = (SimpleName) node; try { //TODO: Redundant code. See ASTGenerator.getJavaSourceCodeline() int javaLineNumber = getLineNumber(nodeName); - int pdeOffs[] = astGenerator.errorCheckerService - .calculateTabIndexAndLineNumber(javaLineNumber); + int pdeOffs[] = editor.getErrorChecker().calculateTabIndexAndLineNumber(javaLineNumber); PlainDocument javaSource = new PlainDocument(); - javaSource.insertString(0, astGenerator.errorCheckerService.sourceCode, null); + javaSource.insertString(0, editor.getErrorChecker().sourceCode, null); Element lineElement = javaSource.getDefaultRootElement() .getElement(javaLineNumber-1); if(lineElement == null) { @@ -563,8 +566,8 @@ public class ASTNodeWrapper { String javaLine = javaSource.getText(lineElement.getStartOffset(), lineElement.getEndOffset() - lineElement.getStartOffset()); - astGenerator.editor.getSketch().setCurrentCode(pdeOffs[0]); - String pdeLine = astGenerator.editor.getLineText(pdeOffs[1]); + editor.getSketch().setCurrentCode(pdeOffs[0]); + String pdeLine = editor.getLineText(pdeOffs[1]); String lookingFor = nodeName.toString(); Messages.log(lookingFor + ", " + nodeName.getStartPosition()); Messages.log(javaLineNumber +" JL " + javaLine + " LSO " + lineElement.getStartOffset() + "," @@ -584,9 +587,9 @@ public class ASTNodeWrapper { "Please file a bug report."); return false; } - int lso = astGenerator.editor.getTextArea().getLineStartOffset(pdeOffs[1]); + int lso = editor.getTextArea().getLineStartOffset(pdeOffs[1]); highlightStart += lso; - astGenerator.editor.setSelection(highlightStart, highlightStart + editor.setSelection(highlightStart, highlightStart + nodeName.getLength()); /* // First find the name in the java line, and marks its index @@ -649,18 +652,18 @@ public class ASTNodeWrapper { * int[3] are on TODO */ public int[] getPDECodeOffsets(ErrorCheckerService ecs) { - return ecs.JavaToPdeOffsets(lineNumber + 1, Node.getStartPosition()); + return ecs.JavaToPdeOffsets(lineNumber + 1, node.getStartPosition()); } public int getPDECodeOffsetForSN(ASTGenerator astGen){ - if (Node instanceof SimpleName) { + if (node instanceof SimpleName) { Element lineElement = astGen.getJavaSourceCodeElement(lineNumber); Messages.log("Line element off " + lineElement.getStartOffset()); OffsetMatcher ofm = new OffsetMatcher(astGen.getPDESourceCodeLine(lineNumber), astGen.getJavaSourceCodeLine(lineNumber)); //log(""); - int pdeOffset = ofm.getPdeOffForJavaOff(Node.getStartPosition() - - lineElement.getStartOffset(), Node.toString().length()); + int pdeOffset = ofm.getPdeOffForJavaOff(node.getStartPosition() + - lineElement.getStartOffset(), node.toString().length()); return pdeOffset; } return -1; @@ -671,7 +674,7 @@ public class ASTNodeWrapper { } public ASTNode getNode() { - return Node; + return node; } public String getLabel() { @@ -679,7 +682,7 @@ public class ASTNodeWrapper { } public int getNodeType() { - return Node.getNodeType(); + return node.getNodeType(); } public int getLineNumber() { diff --git a/java/src/processing/mode/java/pdex/CompletionPanel.java b/java/src/processing/mode/java/pdex/CompletionPanel.java index e1f0e8cdb..bd760c614 100644 --- a/java/src/processing/mode/java/pdex/CompletionPanel.java +++ b/java/src/processing/mode/java/pdex/CompletionPanel.java @@ -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; @@ -129,8 +129,11 @@ public class CompletionPanel { scrollPane.setViewportView(completionList = createSuggestionList(position, items)); popupMenu.add(scrollPane, BorderLayout.CENTER); popupMenu.setPopupSize(calcWidth(), calcHeight(items.getSize())); //TODO: Eradicate this evil - editor.getErrorChecker().getASTGenerator().updateJavaDoc(completionList.getSelectedValue()); - textarea.requestFocusInWindow(); + popupMenu.setFocusable(false); + ASTGenerator astGenerator = editor.getErrorChecker().getASTGenerator(); + synchronized (astGenerator) { + astGenerator.updateJavaDoc(completionList.getSelectedValue()); + } popupMenu.show(textarea, location.x, textarea.getBaseline(0, 0) + location.y); //log("Suggestion shown: " + System.currentTimeMillis()); } @@ -395,13 +398,7 @@ public class CompletionPanel { if(mouseClickOnOverloadedMethods) { // See #2755 - SwingWorker worker = new SwingWorker() { - protected Object doInBackground() throws Exception { - ((JavaTextArea) editor.getTextArea()).fetchPhrase(null); - return null; - } - }; - worker.execute(); + ((JavaTextArea) editor.getTextArea()).fetchPhrase(); } return true; @@ -506,7 +503,10 @@ public class CompletionPanel { .getVerticalScrollBar() .getValue() - step); - editor.getErrorChecker().getASTGenerator().updateJavaDoc(completionList.getSelectedValue()); + ASTGenerator astGenerator = editor.getErrorChecker().getASTGenerator(); + synchronized (astGenerator) { + astGenerator.updateJavaDoc(completionList.getSelectedValue()); + } } } @@ -523,7 +523,10 @@ public class CompletionPanel { int index = Math.min(completionList.getSelectedIndex() + 1, completionList.getModel().getSize() - 1); selectIndex(index); - editor.getErrorChecker().getASTGenerator().updateJavaDoc(completionList.getSelectedValue()); + ASTGenerator astGenerator = editor.getErrorChecker().getASTGenerator(); + synchronized (astGenerator) { + astGenerator.updateJavaDoc(completionList.getSelectedValue()); + } int step = scrollPane.getVerticalScrollBar().getMaximum() / completionList.getModel().getSize(); scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getValue() + step); } diff --git a/java/src/processing/mode/java/pdex/ErrorCheckerService.java b/java/src/processing/mode/java/pdex/ErrorCheckerService.java index 487583442..3e3745a3d 100644 --- a/java/src/processing/mode/java/pdex/ErrorCheckerService.java +++ b/java/src/processing/mode/java/pdex/ErrorCheckerService.java @@ -316,7 +316,9 @@ public class ErrorCheckerService implements Runnable { // This is when the loaded sketch already has syntax errors. // Completion wouldn't be complete, but it'd be still something // better than nothing - astGenerator.buildAST(cu); + synchronized (astGenerator) { + astGenerator.buildAST(cu); + } handleErrorCheckingToggle(); while (!stopThread.get()) { try { @@ -349,7 +351,9 @@ public class ErrorCheckerService implements Runnable { } } - astGenerator.disposeAllWindows(); + synchronized (astGenerator) { + astGenerator.disposeAllWindows(); + } compilationChecker = null; checkerClass = null; classLoader = null; @@ -503,7 +507,9 @@ public class ErrorCheckerService implements Runnable { // log(editor.getSketch().getName() + "2 MCO " + mainClassOffset); } - astGenerator.buildAST(cu); + synchronized (astGenerator) { + astGenerator.buildAST(cu); + } if (!JavaMode.errorCheckEnabled) { problemsList.clear(); Messages.log("Error Check disabled, so not updating UI."); @@ -897,7 +903,9 @@ public class ErrorCheckerService implements Runnable { } new Thread(new Runnable() { public void run() { - astGenerator.loadJars(); // update jar file for completion lookup + synchronized (astGenerator) { + astGenerator.loadJars(); // update jar file for completion lookup + } } }).start(); } @@ -912,6 +920,40 @@ public class ErrorCheckerService implements Runnable { } + protected boolean ignorableSuggestionImport(String impName) { + String impNameLc = impName.toLowerCase(); + + for (ImportStatement impS : programImports) { + if (impNameLc.startsWith(impS.getPackageName().toLowerCase())) { + return false; + } + } + + for (ImportStatement impS : codeFolderImports) { + if (impNameLc.startsWith(impS.getPackageName().toLowerCase())) { + return false; + } + } + + final String include = "include"; + final String exclude = "exclude"; + + if (impName.startsWith("processing")) { + if (JavaMode.suggestionsMap.get(include).contains(impName)) { + return false; + } else if (JavaMode.suggestionsMap.get(exclude).contains(impName)) { + return true; + } + } else if (impName.startsWith("java")) { + if (JavaMode.suggestionsMap.get(include).contains(impName)) { + return false; + } + } + + return true; + } + + /** Options for the JDT Compiler */ protected Map compilerSettings; @@ -956,7 +998,10 @@ public class ErrorCheckerService implements Runnable { String[] args = p.getIProblem().getArguments(); if (args.length > 0) { String missingClass = args[0]; - String[] si = astGenerator.getSuggestImports(missingClass); + String[] si; + synchronized (astGenerator) { + si = astGenerator.getSuggestImports(missingClass); + } if (si != null && si.length > 0) { p.setImportSuggestions(si); // errorData[index][0] = "" + p.getMessage() + @@ -1353,7 +1398,9 @@ public class ErrorCheckerService implements Runnable { // Footer if (mode != PdePreprocessor.Mode.JAVA) { if (mode == PdePreprocessor.Mode.STATIC) { - sb.append("\nnoLoop();\n}"); + // no noLoop() here so it does not tell you + // "can't invoke noLoop() on obj" when you type "obj." + sb.append("\n}"); } sb.append("\n}"); } diff --git a/java/src/processing/mode/java/pdex/JavaTextArea.java b/java/src/processing/mode/java/pdex/JavaTextArea.java index c4f5936b1..9da488f0f 100644 --- a/java/src/processing/mode/java/pdex/JavaTextArea.java +++ b/java/src/processing/mode/java/pdex/JavaTextArea.java @@ -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 worker = new SwingWorker() { - 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 worker = new SwingWorker() { - protected Object doInBackground() throws Exception { + //SwingWorker worker = new SwingWorker() { + // 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() { - 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(); + } } @@ -368,134 +377,309 @@ public class JavaTextArea extends JEditTextArea { return null; } Messages.log("Mouse click, word: " + word.trim()); - editor.getErrorChecker().getASTGenerator().setLastClickedWord(line, word, xLS); + ASTGenerator astGenerator = editor.getErrorChecker().getASTGenerator(); + synchronized (astGenerator) { + astGenerator.setLastClickedWord(line, word, xLS); + } return word.trim(); } } + SwingWorker 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() { - 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 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 candidates = null; - while (true) { - i++; - //TODO: currently works on single line only. "a. 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; } @@ -796,9 +980,6 @@ public class JavaTextArea extends JEditTextArea { return; } - if (subWord.length() < 2) { - return; - } suggestion = new CompletionPanel(this, position, subWord, listModel, location, editor); requestFocusInWindow(); @@ -811,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 } } diff --git a/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java b/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java index 752ff2969..8a66452b3 100644 --- a/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java +++ b/java/src/processing/mode/java/pdex/JavaTextAreaPainter.java @@ -207,7 +207,10 @@ public class JavaTextAreaPainter extends TextAreaPainter return; Messages.log(getJavaEditor().getErrorChecker().mainClassOffset + line + "|" + line + "| offset " + xLS + word + " <= \n"); - getJavaEditor().getErrorChecker().getASTGenerator().scrollToDeclaration(line, word, xLS); + ASTGenerator astGenerator = getJavaEditor().getErrorChecker().getASTGenerator(); + synchronized (astGenerator) { + astGenerator.scrollToDeclaration(line, word, xLS); + } } } @@ -515,12 +518,14 @@ public class JavaTextAreaPainter extends TextAreaPainter return super.getToolTipText(event); } ASTGenerator ast = getJavaEditor().getErrorChecker().getASTGenerator(); - String tooltipText = ast.getLabelForASTNode(line, word, xLS); + synchronized (ast) { + String tooltipText = ast.getLabelForASTNode(line, word, xLS); - // log(errorCheckerService.mainClassOffset + " MCO " - // + "|" + line + "| offset " + xLS + word + " <= offf: "+off+ "\n"); - if (tooltipText != null) { - return tooltipText; + // log(errorCheckerService.mainClassOffset + " MCO " + // + "|" + line + "| offset " + xLS + word + " <= offf: "+off+ "\n"); + if (tooltipText != null) { + return tooltipText; + } } } } diff --git a/java/src/processing/mode/java/pdex/SketchOutline.java b/java/src/processing/mode/java/pdex/SketchOutline.java index 4f2f1f4f0..d5432101c 100644 --- a/java/src/processing/mode/java/pdex/SketchOutline.java +++ b/java/src/processing/mode/java/pdex/SketchOutline.java @@ -157,7 +157,7 @@ public class SketchOutline { .getLastSelectedPathComponent(); if (tnode.getUserObject() instanceof ASTNodeWrapper) { ASTNodeWrapper awrap = (ASTNodeWrapper) tnode.getUserObject(); - awrap.highlightNode(errorCheckerService.astGenerator); + awrap.highlightNode(editor); //errorCheckerService.highlightNode(awrap); close(); } @@ -284,7 +284,7 @@ public class SketchOutline { .getLastSelectedPathComponent(); if (tnode.getUserObject() instanceof ASTNodeWrapper) { ASTNodeWrapper awrap = (ASTNodeWrapper) tnode.getUserObject(); - awrap.highlightNode(errorCheckerService.astGenerator); + awrap.highlightNode(editor); // log(awrap); //errorCheckerService.highlightNode(awrap); close();