diff --git a/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/text/contentassist2/CompletionTests.java b/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/text/contentassist2/CompletionTests.java index bbf6ab8a1b3..14fc6373c87 100644 --- a/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/text/contentassist2/CompletionTests.java +++ b/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/text/contentassist2/CompletionTests.java @@ -1227,6 +1227,46 @@ public class CompletionTests extends CompletionTestBase { assertCompletionResults(fCursorOffset, expected, ID); } + // struct Waldo { + // Waldo(int, int); + // }; + // + // int main() { + // Waldo waldo{/*cursor*/} + // } + public void testUniformInitializationInSimpleDeclaration_509185() throws Exception { + final String[] expected = { "Waldo(const Waldo &)", "Waldo(int, int)" }; + assertCompletionResults(fCursorOffset, expected, ID); + assertCompletionResults(new String[] { "", "" }); // No replacements, just context info + } + + // struct Waldo { + // Waldo(int, int); + // }; + // + // int main() { + // auto waldo = Waldo{/*cursor*/} + // } + public void testUniformInitializationInSimpleTypeConstructorExpression_509185() throws Exception { + final String[] expected = { "Waldo(const Waldo &)", "Waldo(int, int)" }; + assertCompletionResults(fCursorOffset, expected, ID); + assertCompletionResults(new String[] { "", "" }); // No replacements, just context info + } + + // struct Waldo { + // Waldo(int, int); + // }; + // + // struct Finder { + // Waldo waldo; + // Finder() : waldo{/*cursor*/ + // }; + public void testUniformInitializationInConstructorChainInitializer_509185() throws Exception { + final String[] expected = { "Waldo(const Waldo &)", "Waldo(int, int)" }; + assertCompletionResults(fCursorOffset, expected, ID); + assertCompletionResults(new String[] { "", "" }); // No replacements, just context info + } + // struct Waldo { // Waldo(int, int); // }; diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/text/contentassist/CContentAssistInvocationContext.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/text/contentassist/CContentAssistInvocationContext.java index 02590fa3572..10b99910877 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/text/contentassist/CContentAssistInvocationContext.java +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/text/contentassist/CContentAssistInvocationContext.java @@ -24,12 +24,25 @@ import org.eclipse.jface.text.ITextViewer; import org.eclipse.ui.IEditorPart; import org.eclipse.cdt.core.CCorePlugin; +import org.eclipse.cdt.core.dom.ast.ASTCompletionNode; +import org.eclipse.cdt.core.dom.ast.ExpansionOverlapsBoundaryException; import org.eclipse.cdt.core.dom.ast.IASTCompletionNode; +import org.eclipse.cdt.core.dom.ast.IASTDeclarator; +import org.eclipse.cdt.core.dom.ast.IASTFileLocation; +import org.eclipse.cdt.core.dom.ast.IASTIdExpression; +import org.eclipse.cdt.core.dom.ast.IASTInitializerList; +import org.eclipse.cdt.core.dom.ast.IASTName; +import org.eclipse.cdt.core.dom.ast.IASTNamedTypeSpecifier; +import org.eclipse.cdt.core.dom.ast.IASTNode; +import org.eclipse.cdt.core.dom.ast.IASTSimpleDeclaration; +import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTConstructorChainInitializer; +import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTSimpleTypeConstructorExpression; import org.eclipse.cdt.core.formatter.DefaultCodeFormatterConstants; import org.eclipse.cdt.core.index.IIndex; import org.eclipse.cdt.core.index.IIndexManager; import org.eclipse.cdt.core.model.ICProject; import org.eclipse.cdt.core.model.ITranslationUnit; +import org.eclipse.cdt.core.parser.IToken; import org.eclipse.cdt.ui.CUIPlugin; import org.eclipse.cdt.ui.PreferenceConstants; import org.eclipse.cdt.ui.text.contentassist.ContentAssistInvocationContext; @@ -54,6 +67,12 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont private final boolean fIsCompletion; private final boolean fIsAutoActivated; private IIndex fIndex; + // Whether we are doing completion (false) or just showing context information (true). + private boolean fIsContextInformationStyle; + // The parse offset is the end of the name we are doing completion on. + // Since this name can be adjusted by adjustCompletionNode(), the parse offset + // may need a corresponding adjustment, and this stores the adjusted offset. + private int fAdjustedParseOffset = -1; private Lazy fContextInfoPosition = new Lazy() { @Override @@ -67,6 +86,19 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont private final Lazy fParseOffset = new Lazy() { @Override protected Integer calculateValue() { + int result = doCalculate(); + if (result != getInvocationOffset()) { + // If guessCompletionPosition() chose a parse offset that's different + // from the invocation offset, we are proposing completions for a name + // that's not right under the cursor, and so we just want to show + // context information. + fIsContextInformationStyle= true; + } + fAdjustedParseOffset = result; + return result; + } + + private int doCalculate() { if (fIsCompletion) { return guessCompletionPosition(getInvocationOffset()); } @@ -78,6 +110,93 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont } }; + // Helper function for adjustCompletionName(). + private IASTName getAdjustedCompletionName(IASTName completionName) { + if (completionName.getSimpleID().length > 0) { + return null; + } + if (completionName.getParent() instanceof IASTIdExpression && + completionName.getParent().getParent() instanceof IASTInitializerList) { + IASTNode initList = completionName.getParent().getParent(); + if (initList.getParent() instanceof IASTDeclarator && + initList.getParent().getParent() instanceof IASTSimpleDeclaration) { + IASTSimpleDeclaration decl = (IASTSimpleDeclaration) completionName.getParent().getParent() + .getParent().getParent(); + if (decl.getDeclSpecifier() instanceof IASTNamedTypeSpecifier) { + return ((IASTNamedTypeSpecifier) decl.getDeclSpecifier()).getName(); + } + } else if (initList.getParent() instanceof ICPPASTSimpleTypeConstructorExpression) { + ICPPASTSimpleTypeConstructorExpression expr = + (ICPPASTSimpleTypeConstructorExpression) initList.getParent(); + if (expr.getDeclSpecifier() instanceof IASTNamedTypeSpecifier) { + return ((IASTNamedTypeSpecifier) expr.getDeclSpecifier()).getName(); + } + } else if (initList.getParent() instanceof ICPPASTConstructorChainInitializer) { + ICPPASTConstructorChainInitializer ctorInit = + (ICPPASTConstructorChainInitializer) initList.getParent(); + return ctorInit.getMemberInitializerId(); + } + } + return null; + } + + /** + * Choose a better completion node based on information in the AST. + * + * Currently, this makes an adjustment in one case: if the completion node is + * an empty name at the top level of one of the initializers of an initializer + * list that may be a constructor call, the completion name is adjusted to + * instead be the name preceding the initializer list (which will either name + * the type being constructed, or a variable of that type). This allows us to + * offer completions for the constructors of this type. + * + * Currently we handle initializer lists in three contexts: + * 1) simple declaration + * 2) simple type constructor expression + * 3) constructor chain initializer + * + * TODO: Handle the additional context of a return-expression: + * S foo() { + * return {...}; // can invoke S constructor with args inside {...} + * } + * + * Note that for constructor calls with () rather than {} syntax, we + * accomplish the same goal by different means: in getParseOffset(), we choose + * a parse offset that will give us the desired completion node to begin with. + * We can't do this with the {} syntax, because in getParseOffset() we don't + * have an AST yet (the choice is made using CHeuristicScanner), and we cannot + * distinguish other uses of {} from the desired ones. + * + * @param completionName the initial completion name, based on the parse offset + * chosen using CHeuristicScanner + * @return the adjusted completion name, if different from the initial completion + * name, or {@code null} + */ + private IASTCompletionNode adjustCompletionNode(IASTCompletionNode existing) { + IASTName[] names = existing.getNames(); + // If there are multiple completion names, there is a parser ambiguity + // near the completion location. Just bail in this case. + if (names.length != 1) { + return null; + } + IASTName completionName = names[0]; + IASTName newCompletionName = getAdjustedCompletionName(completionName); + if (newCompletionName != null) { + IToken newCompletionToken = null; + try { + newCompletionToken = newCompletionName.getSyntax(); + } catch (ExpansionOverlapsBoundaryException e) { + } + if (newCompletionToken != null) { + ASTCompletionNode newCompletionNode = new ASTCompletionNode(newCompletionToken, + existing.getTranslationUnit()); + newCompletionNode.addName(newCompletionName); + return newCompletionNode; + } + } + return null; + } + private final Lazy fCN = new Lazy() { @Override protected IASTCompletionNode calculateValue() { @@ -106,7 +225,26 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont int flags = parseNonIndexed ? ITranslationUnit.AST_SKIP_INDEXED_HEADERS : ITranslationUnit.AST_SKIP_ALL_HEADERS; flags |= ITranslationUnit.AST_CONFIGURE_USING_SOURCE_CONTEXT; - return fTU.value().getCompletionNode(fIndex, flags, offset); + IASTCompletionNode result = fTU.value().getCompletionNode(fIndex, flags, offset); + if (result != null) { + // The initial completion code is determined by the parse offset chosen + // in getParseOffset() using CHeuristicScanner. Now that we have an AST, + // we may want to use information in the AST to choose a better completion + // node. + IASTCompletionNode adjusted = adjustCompletionNode(result); + if (adjusted != null) { + // If we made an adjustment, we just want to show context information. + fIsContextInformationStyle = true; + result = adjusted; + if (adjusted.getNames().length > 0) { + // Make a corresponding adjustment to the parse offset. + IASTFileLocation adjustedLocation = adjusted.getNames()[0].getFileLocation(); + fAdjustedParseOffset = adjustedLocation.getNodeOffset() + + adjustedLocation.getNodeLength(); + } + } + } + return result; } catch (CoreException e) { CUIPlugin.log(e); } @@ -125,11 +263,11 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont } }; - private final Lazy afterOpeningParenthesis = new Lazy() { + private final Lazy afterOpeningParenthesisOrBrace = new Lazy() { @Override protected Boolean calculateValue() { final int invocationOffset = getInvocationOffset(); - final int parseOffset = getParseOffset(); + final int parseOffset = fAdjustedParseOffset; final int bound = Math.max(-1, parseOffset - 1); final IDocument document = getDocument(); final CHeuristicScanner scanner = new CHeuristicScanner(document); @@ -140,12 +278,19 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont // character being searched." // If we are completing in between two empty parentheses with no space between them, // this condition won't be satisfied, so we start the search one character earlier. - if (document.getChar(start) == ')') + if (document.getChar(start) == ')' || document.getChar(start) == '}') start -= 1; } catch (BadLocationException e) { } final int parenthesisOffset = scanner.findOpeningPeer(start, bound, '(', ')'); - return parenthesisOffset != CHeuristicScanner.NOT_FOUND; + if (parenthesisOffset != CHeuristicScanner.NOT_FOUND) { + return true; + } + final int braceOffset = scanner.findOpeningPeer(start, bound, '{', '}'); + if (braceOffset != CHeuristicScanner.NOT_FOUND) { + return true; + } + return false; } }; @@ -239,6 +384,7 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont Assert.isNotNull(editor); fEditor= editor; fIsCompletion= isCompletion; + fIsContextInformationStyle= !isCompletion; fIsAutoActivated= isAutoActivated; fTU = new Lazy() { @Override @@ -263,6 +409,7 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont }; fEditor= null; fIsCompletion= isCompletion; + fIsContextInformationStyle= !isCompletion; fIsAutoActivated= false; } @@ -320,6 +467,11 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont assertNotDisposed(); return fParseOffset.value(); } + + public int getAdjustedParseOffset() { + assertNotDisposed(); + return fAdjustedParseOffset; + } /** * @return the offset where context information (parameter hints) starts. @@ -427,7 +579,7 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont @Override public boolean isContextInformationStyle() { assertNotDisposed(); - return !fIsCompletion || (getParseOffset() != getInvocationOffset()); + return fIsContextInformationStyle; } public boolean isAutoActivated() { @@ -444,9 +596,9 @@ public class CContentAssistInvocationContext extends ContentAssistInvocationCont super.dispose(); } - public boolean isAfterOpeningParenthesis() { + public boolean isAfterOpeningParenthesisOrBrace() { assertNotDisposed(); - return afterOpeningParenthesis.value(); + return afterOpeningParenthesisOrBrace.value(); } public boolean isAfterOpeningAngleBracket() { diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/text/contentassist/DOMCompletionProposalComputer.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/text/contentassist/DOMCompletionProposalComputer.java index b84c1d992ce..66657d824af 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/text/contentassist/DOMCompletionProposalComputer.java +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/text/contentassist/DOMCompletionProposalComputer.java @@ -477,7 +477,7 @@ public class DOMCompletionProposalComputer extends ParsingBasedProposalComputer private void handleClass(ICPPClassType classType, IASTCompletionContext astContext, CContentAssistInvocationContext cContext, int baseRelevance, List proposals) { - if (cContext.isContextInformationStyle() && cContext.isAfterOpeningParenthesis()) { + if (cContext.isContextInformationStyle() && cContext.isAfterOpeningParenthesisOrBrace()) { addProposalsForConstructors(classType, astContext, cContext, baseRelevance, proposals); } else if (classType instanceof ICPPClassTemplate) { addProposalForClassTemplate((ICPPClassTemplate) classType, cContext, baseRelevance, proposals); @@ -759,10 +759,11 @@ public class DOMCompletionProposalComputer extends ParsingBasedProposalComputer // Invocation offset and parse offset are the same if content assist is invoked while in the function // name (i.e. before the '('). After that, the parse offset will indicate the end of the name part. // If there is no difference between them, then we're still inside the function name part. - int relativeOffset = context.getInvocationOffset() - context.getParseOffset(); + int parseOffset = context.getAdjustedParseOffset(); + int relativeOffset = context.getInvocationOffset() - parseOffset; if (relativeOffset == 0) return true; - int startOffset = context.getParseOffset(); + int startOffset = parseOffset; String completePrefix = context.getDocument().get().substring(startOffset, context.getInvocationOffset()); int lastChar = getLastNonWhitespaceChar(completePrefix);