From e0e8fda580c7ac5d55dbae746d5651a6ac9caf17 Mon Sep 17 00:00:00 2001 From: Christian Kandeler Date: Fri, 18 Jun 2021 16:30:03 +0200 Subject: [PATCH] ClangCodeModel: Use clangd for completion and function hint Change-Id: I80160f3a40da18ac178682afe6caba5e5af6e3eb Reviewed-by: David Schulz --- src/libs/languageserverprotocol/lsptypes.cpp | 2 +- src/libs/languageserverprotocol/lsptypes.h | 2 +- ...langactivationsequencecontextprocessor.cpp | 62 +- .../clangactivationsequencecontextprocessor.h | 11 +- .../clangassistproposalitem.cpp | 35 - .../clangcodemodel/clangcodemodelplugin.cpp | 1 + .../clangcompletioncontextanalyzer.cpp | 46 +- .../clangcompletioncontextanalyzer.h | 12 +- src/plugins/clangcodemodel/clangdclient.cpp | 289 +++++++- src/plugins/clangcodemodel/clangdclient.h | 8 +- .../clangmodelmanagersupport.cpp | 7 +- src/plugins/clangcodemodel/clangutils.cpp | 38 + src/plugins/clangcodemodel/clangutils.h | 12 +- .../test/clangcodecompletion_test.cpp | 4 +- .../clangcodemodel/test/clangdtests.cpp | 653 +++++++++++++++++- src/plugins/clangcodemodel/test/clangdtests.h | 52 ++ .../test/data/clangtestdata.qrc | 49 +- .../classAndConstructorCompletion.cpp | 0 .../test/data/completion/completion.pro | 30 + .../completionWithProject.cpp | 0 .../constructorCompletion.cpp | 0 .../{ => completion}/dotToArrowCorrection.cpp | 0 .../doxygenKeywordsCompletion.cpp | 0 .../{ => completion}/exampleIncludeDir/file.h | 0 .../exampleIncludeDir/mylib/mylib.h | 0 .../exampleIncludeDir/otherFile.h | 0 .../test/data/completion/functionAddress.cpp | 8 + .../{ => completion}/functionCompletion.cpp | 0 .../functionCompletionFiltered.cpp | 0 .../functionCompletionFiltered2.cpp | 0 .../{ => completion}/globalCompletion.cpp | 0 .../includeDirectiveCompletion.cpp | 0 .../test/data/completion/main.cpp | 36 + .../test/data/completion/mainwindow.cpp | 40 ++ .../test/data/completion/mainwindow.h | 44 ++ .../test/data/completion/mainwindow.ui | 20 + .../{ => completion}/memberCompletion.cpp | 0 .../membercompletion-friend.cpp | 0 .../membercompletion-inside.cpp | 0 .../membercompletion-outside.cpp | 0 .../noDotToArrowCorrectionForFloats.cpp | 0 .../preprocessorKeywordsCompletion.cpp | 0 .../preprocessorKeywordsCompletion2.cpp | 3 + .../preprocessorKeywordsCompletion3.cpp | 3 + .../test/data/completion/signalCompletion.cpp | 23 + src/plugins/cppeditor/cppeditordocument.cpp | 22 +- src/plugins/cppeditor/cppeditordocument.h | 6 +- src/plugins/cppeditor/cppeditorwidget.cpp | 6 +- src/plugins/languageclient/client.cpp | 47 ++ src/plugins/languageclient/client.h | 9 + .../languageclientcompletionassist.cpp | 89 ++- .../languageclientcompletionassist.h | 25 + .../languageclientfunctionhint.cpp | 21 +- .../languageclientfunctionhint.h | 8 + .../languageclient/languageclientmanager.cpp | 6 +- .../languageclient/languageclientmanager.h | 1 + .../semantichighlightsupport.cpp | 5 +- .../languageclient/semantichighlightsupport.h | 2 + src/plugins/texteditor/textdocument.h | 4 +- src/plugins/texteditor/texteditor.cpp | 9 + src/plugins/texteditor/texteditor.h | 4 + ...ctivationsequencecontextprocessor-test.cpp | 2 +- 62 files changed, 1569 insertions(+), 187 deletions(-) rename src/plugins/clangcodemodel/test/data/{ => completion}/classAndConstructorCompletion.cpp (100%) create mode 100644 src/plugins/clangcodemodel/test/data/completion/completion.pro rename src/plugins/clangcodemodel/test/data/{ => completion}/completionWithProject.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/constructorCompletion.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/dotToArrowCorrection.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/doxygenKeywordsCompletion.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/exampleIncludeDir/file.h (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/exampleIncludeDir/mylib/mylib.h (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/exampleIncludeDir/otherFile.h (100%) create mode 100644 src/plugins/clangcodemodel/test/data/completion/functionAddress.cpp rename src/plugins/clangcodemodel/test/data/{ => completion}/functionCompletion.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/functionCompletionFiltered.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/functionCompletionFiltered2.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/globalCompletion.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/includeDirectiveCompletion.cpp (100%) create mode 100644 src/plugins/clangcodemodel/test/data/completion/main.cpp create mode 100644 src/plugins/clangcodemodel/test/data/completion/mainwindow.cpp create mode 100644 src/plugins/clangcodemodel/test/data/completion/mainwindow.h create mode 100644 src/plugins/clangcodemodel/test/data/completion/mainwindow.ui rename src/plugins/clangcodemodel/test/data/{ => completion}/memberCompletion.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/membercompletion-friend.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/membercompletion-inside.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/membercompletion-outside.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/noDotToArrowCorrectionForFloats.cpp (100%) rename src/plugins/clangcodemodel/test/data/{ => completion}/preprocessorKeywordsCompletion.cpp (100%) create mode 100644 src/plugins/clangcodemodel/test/data/completion/preprocessorKeywordsCompletion2.cpp create mode 100644 src/plugins/clangcodemodel/test/data/completion/preprocessorKeywordsCompletion3.cpp create mode 100644 src/plugins/clangcodemodel/test/data/completion/signalCompletion.cpp diff --git a/src/libs/languageserverprotocol/lsptypes.cpp b/src/libs/languageserverprotocol/lsptypes.cpp index 4faf420e3a0..820f4b6c4b4 100644 --- a/src/libs/languageserverprotocol/lsptypes.cpp +++ b/src/libs/languageserverprotocol/lsptypes.cpp @@ -276,7 +276,7 @@ Position::Position(const QTextCursor &cursor) : Position(cursor.blockNumber(), cursor.positionInBlock()) { } -int Position::toPositionInDocument(QTextDocument *doc) const +int Position::toPositionInDocument(const QTextDocument *doc) const { const QTextBlock block = doc->findBlockByNumber(line()); if (!block.isValid()) diff --git a/src/libs/languageserverprotocol/lsptypes.h b/src/libs/languageserverprotocol/lsptypes.h index 4348e4fd19d..7cbe0455637 100644 --- a/src/libs/languageserverprotocol/lsptypes.h +++ b/src/libs/languageserverprotocol/lsptypes.h @@ -88,7 +88,7 @@ public: bool isValid() const override { return contains(lineKey) && contains(characterKey); } - int toPositionInDocument(QTextDocument *doc) const; + int toPositionInDocument(const QTextDocument *doc) const; QTextCursor toTextCursor(QTextDocument *doc) const; }; diff --git a/src/plugins/clangcodemodel/clangactivationsequencecontextprocessor.cpp b/src/plugins/clangcodemodel/clangactivationsequencecontextprocessor.cpp index a23cbd026a7..0eaffa3f724 100644 --- a/src/plugins/clangcodemodel/clangactivationsequencecontextprocessor.cpp +++ b/src/plugins/clangcodemodel/clangactivationsequencecontextprocessor.cpp @@ -30,16 +30,19 @@ #include #include #include +#include #include namespace ClangCodeModel { namespace Internal { -ActivationSequenceContextProcessor::ActivationSequenceContextProcessor(const ClangCompletionAssistInterface *assistInterface) - : m_textCursor(assistInterface->textDocument()), - m_assistInterface(assistInterface), - m_positionInDocument(assistInterface->position()), +ActivationSequenceContextProcessor::ActivationSequenceContextProcessor( + QTextDocument *document, int position, CPlusPlus::LanguageFeatures languageFeatures) + : m_textCursor(document), + m_document(document), + m_languageFeatures(languageFeatures), + m_positionInDocument(position), m_startOfNamePosition(m_positionInDocument), m_operatorStartPosition(m_positionInDocument) @@ -49,6 +52,13 @@ ActivationSequenceContextProcessor::ActivationSequenceContextProcessor(const Cla process(); } +ActivationSequenceContextProcessor::ActivationSequenceContextProcessor( + const ClangCompletionAssistInterface *interface) + : ActivationSequenceContextProcessor(interface->textDocument(), interface->position(), + interface->languageFeatures()) +{ +} + CPlusPlus::Kind ActivationSequenceContextProcessor::completionKind() const { return m_completionKind; @@ -91,8 +101,9 @@ void ActivationSequenceContextProcessor::process() void ActivationSequenceContextProcessor::processActivationSequence() { - const int nonSpacePosition = skipPrecedingWhitespace(m_assistInterface, m_startOfNamePosition); - const auto activationSequence = m_assistInterface->textAt(nonSpacePosition - 3, 3); + const int nonSpacePosition = skipPrecedingWhitespace(m_document, m_startOfNamePosition); + const auto activationSequence = Utils::Text::textAt(QTextCursor(m_document), + nonSpacePosition - 3, 3); ActivationSequenceProcessor activationSequenceProcessor(activationSequence, nonSpacePosition, true); @@ -115,7 +126,7 @@ void ActivationSequenceContextProcessor::processStringLiteral() void ActivationSequenceContextProcessor::processComma() { if (m_completionKind == CPlusPlus::T_COMMA) { - CPlusPlus::ExpressionUnderCursor expressionUnderCursor(m_assistInterface->languageFeatures()); + CPlusPlus::ExpressionUnderCursor expressionUnderCursor(m_languageFeatures); if (expressionUnderCursor.startOfFunctionCall(m_textCursor) == -1) m_completionKind = CPlusPlus::T_EOF_SYMBOL; } @@ -124,7 +135,7 @@ void ActivationSequenceContextProcessor::processComma() void ActivationSequenceContextProcessor::generateTokens() { CPlusPlus::SimpleLexer tokenize; - tokenize.setLanguageFeatures(m_assistInterface->languageFeatures()); + tokenize.setLanguageFeatures(m_languageFeatures); tokenize.setSkipComments(false); auto state = CPlusPlus::BackwardsScanner::previousBlockState(m_textCursor.block()); m_tokens = tokenize(m_textCursor.block().text(), state); @@ -222,12 +233,11 @@ void ActivationSequenceContextProcessor::resetPositionsForEOFCompletionKind() m_operatorStartPosition = m_positionInDocument; } -int ActivationSequenceContextProcessor::skipPrecedingWhitespace( - const TextEditor::AssistInterface *assistInterface, - int startPosition) +int ActivationSequenceContextProcessor::skipPrecedingWhitespace(const QTextDocument *document, + int startPosition) { int position = startPosition; - while (assistInterface->characterAt(position - 1).isSpace()) + while (document->characterAt(position - 1).isSpace()) --position; return position; } @@ -241,7 +251,7 @@ static bool isValidIdentifierChar(const QChar &character) } int ActivationSequenceContextProcessor::findStartOfName( - const TextEditor::AssistInterface *assistInterface, + const QTextDocument *document, int startPosition, NameCategory category) { @@ -249,32 +259,32 @@ int ActivationSequenceContextProcessor::findStartOfName( QChar character; if (category == NameCategory::Function - && position > 2 && assistInterface->characterAt(position - 1) == '>' - && assistInterface->characterAt(position - 2) != '-') { + && position > 2 && document->characterAt(position - 1) == '>' + && document->characterAt(position - 2) != '-') { uint unbalancedLessGreater = 1; --position; while (unbalancedLessGreater > 0 && position > 2) { - character = assistInterface->characterAt(--position); + character = document->characterAt(--position); // Do not count -> usage inside temlate argument list if (character == '<') --unbalancedLessGreater; - else if (character == '>' && assistInterface->characterAt(position-1) != '-') + else if (character == '>' && document->characterAt(position-1) != '-') ++unbalancedLessGreater; } - position = skipPrecedingWhitespace(assistInterface, position) - 1; + position = skipPrecedingWhitespace(document, position) - 1; } do { - character = assistInterface->characterAt(--position); + character = document->characterAt(--position); } while (isValidIdentifierChar(character)); - int prevPosition = skipPrecedingWhitespace(assistInterface, position); + int prevPosition = skipPrecedingWhitespace(document, position); if (category == NameCategory::Function - && assistInterface->characterAt(prevPosition) == ':' - && assistInterface->characterAt(prevPosition - 1) == ':') { + && document->characterAt(prevPosition) == ':' + && document->characterAt(prevPosition - 1) == ':') { // Handle :: case - go recursive - prevPosition = skipPrecedingWhitespace(assistInterface, prevPosition - 2); - return findStartOfName(assistInterface, prevPosition + 1, category); + prevPosition = skipPrecedingWhitespace(document, prevPosition - 2); + return findStartOfName(document, prevPosition + 1, category); } return position + 1; @@ -283,7 +293,7 @@ int ActivationSequenceContextProcessor::findStartOfName( void ActivationSequenceContextProcessor::goBackToStartOfName() { CPlusPlus::SimpleLexer tokenize; - tokenize.setLanguageFeatures(m_assistInterface->languageFeatures()); + tokenize.setLanguageFeatures(m_languageFeatures); tokenize.setSkipComments(false); const int state = CPlusPlus::BackwardsScanner::previousBlockState(m_textCursor.block()); const CPlusPlus::Tokens tokens = tokenize(m_textCursor.block().text(), state); @@ -297,7 +307,7 @@ void ActivationSequenceContextProcessor::goBackToStartOfName() m_startOfNamePosition = m_textCursor.block().position() + std::max(slashIndex, tokenStart) + 1; } else { - m_startOfNamePosition = findStartOfName(m_assistInterface, m_positionInDocument); + m_startOfNamePosition = findStartOfName(m_document, m_positionInDocument); } if (m_startOfNamePosition != m_positionInDocument) diff --git a/src/plugins/clangcodemodel/clangactivationsequencecontextprocessor.h b/src/plugins/clangcodemodel/clangactivationsequencecontextprocessor.h index ca84f1663b9..c735ce419ef 100644 --- a/src/plugins/clangcodemodel/clangactivationsequencecontextprocessor.h +++ b/src/plugins/clangcodemodel/clangactivationsequencecontextprocessor.h @@ -41,7 +41,9 @@ namespace Internal { class ActivationSequenceContextProcessor { public: - ActivationSequenceContextProcessor(const ClangCompletionAssistInterface *assistInterface); + ActivationSequenceContextProcessor(QTextDocument *document, int position, + CPlusPlus::LanguageFeatures languageFeatures); + ActivationSequenceContextProcessor(const ClangCompletionAssistInterface *interface); CPlusPlus::Kind completionKind() const; int startOfNamePosition() const; // e.g. points to 'b' in "foo.bar" @@ -50,10 +52,10 @@ public: const QTextCursor &textCursor_forTestOnly() const; enum class NameCategory { Function, NonFunction }; - static int findStartOfName(const TextEditor::AssistInterface *assistInterface, + static int findStartOfName(const QTextDocument *document, int startPosition, NameCategory category = NameCategory::NonFunction); - static int skipPrecedingWhitespace(const TextEditor::AssistInterface *assistInterface, + static int skipPrecedingWhitespace(const QTextDocument *document, int startPosition); protected: @@ -78,7 +80,8 @@ private: QVector m_tokens; QTextCursor m_textCursor; CPlusPlus::Token m_token; - const ClangCompletionAssistInterface *m_assistInterface; + QTextDocument * const m_document; + const CPlusPlus::LanguageFeatures m_languageFeatures; int m_tokenIndex; const int m_positionInDocument; int m_startOfNamePosition; diff --git a/src/plugins/clangcodemodel/clangassistproposalitem.cpp b/src/plugins/clangcodemodel/clangassistproposalitem.cpp index 034504df4ba..068355ef911 100644 --- a/src/plugins/clangcodemodel/clangassistproposalitem.cpp +++ b/src/plugins/clangcodemodel/clangassistproposalitem.cpp @@ -79,41 +79,6 @@ bool ClangAssistProposalItem::implicitlyApplies() const return true; } -static QString textUntilPreviousStatement(TextDocumentManipulatorInterface &manipulator, - int startPosition) -{ - static const QString stopCharacters(";{}#"); - - int endPosition = 0; - for (int i = startPosition; i >= 0 ; --i) { - if (stopCharacters.contains(manipulator.characterAt(i))) { - endPosition = i + 1; - break; - } - } - - return manipulator.textAt(endPosition, startPosition - endPosition); -} - -// 7.3.3: using typename(opt) nested-name-specifier unqualified-id ; -static bool isAtUsingDeclaration(TextDocumentManipulatorInterface &manipulator, - int basePosition) -{ - SimpleLexer lexer; - lexer.setLanguageFeatures(LanguageFeatures::defaultFeatures()); - const QString textToLex = textUntilPreviousStatement(manipulator, basePosition); - const Tokens tokens = lexer(textToLex); - if (tokens.empty()) - return false; - - // The nested-name-specifier always ends with "::", so check for this first. - const Token lastToken = tokens[tokens.size() - 1]; - if (lastToken.kind() != T_COLON_COLON) - return false; - - return contains(tokens, [](const Token &token) { return token.kind() == T_USING; }); -} - static QString methodDefinitionParameters(const CodeCompletionChunks &chunks) { QString result; diff --git a/src/plugins/clangcodemodel/clangcodemodelplugin.cpp b/src/plugins/clangcodemodel/clangcodemodelplugin.cpp index bb055a1e922..3c7e055ca3c 100644 --- a/src/plugins/clangcodemodel/clangcodemodelplugin.cpp +++ b/src/plugins/clangcodemodel/clangcodemodelplugin.cpp @@ -210,6 +210,7 @@ QVector ClangCodeModelPlugin::createTestObjects() const { return { new Tests::ClangCodeCompletionTest, + new Tests::ClangdTestCompletion, new Tests::ClangdTestFindReferences, new Tests::ClangdTestFollowSymbol, new Tests::ClangdTestHighlighting, diff --git a/src/plugins/clangcodemodel/clangcompletioncontextanalyzer.cpp b/src/plugins/clangcodemodel/clangcompletioncontextanalyzer.cpp index 034b5298939..1cb2fcdf482 100644 --- a/src/plugins/clangcodemodel/clangcompletioncontextanalyzer.cpp +++ b/src/plugins/clangcodemodel/clangcompletioncontextanalyzer.cpp @@ -68,24 +68,33 @@ namespace Internal { ClangCompletionContextAnalyzer::ClangCompletionContextAnalyzer( const ClangCompletionAssistInterface *assistInterface, CPlusPlus::LanguageFeatures languageFeatures) - : m_interface(assistInterface) - , m_languageFeatures(languageFeatures) + : ClangCompletionContextAnalyzer(assistInterface->textDocument(), assistInterface->position(), + assistInterface->type() == CompletionType::FunctionHint, + languageFeatures) +{ +} + +ClangCompletionContextAnalyzer::ClangCompletionContextAnalyzer( + QTextDocument *document, int position, bool isFunctionHint, + CPlusPlus::LanguageFeatures languageFeatures) + : m_document(document), m_position(position), m_isFunctionHint(isFunctionHint), + m_languageFeatures(languageFeatures) { } void ClangCompletionContextAnalyzer::analyze() { - QTC_ASSERT(m_interface, return); + QTC_ASSERT(m_document, return); setActionAndClangPosition(PassThroughToLibClang, -1); - ActivationSequenceContextProcessor activationSequenceContextProcessor(m_interface); + ActivationSequenceContextProcessor activationSequenceContextProcessor( + m_document, m_position, m_languageFeatures); m_completionOperator = activationSequenceContextProcessor.completionKind(); int afterOperatorPosition = activationSequenceContextProcessor.startOfNamePosition(); m_positionEndOfExpression = activationSequenceContextProcessor.operatorStartPosition(); m_positionForProposal = activationSequenceContextProcessor.startOfNamePosition(); - const bool actionIsSet = m_interface->type() != CompletionType::FunctionHint - && handleNonFunctionCall(afterOperatorPosition); + const bool actionIsSet = !m_isFunctionHint && handleNonFunctionCall(afterOperatorPosition); if (!actionIsSet) { handleCommaInFunctionCall(); handleFunctionCall(afterOperatorPosition); @@ -94,20 +103,20 @@ void ClangCompletionContextAnalyzer::analyze() int ClangCompletionContextAnalyzer::startOfFunctionCall(int endOfOperator) const { - int index = ActivationSequenceContextProcessor::skipPrecedingWhitespace(m_interface, + int index = ActivationSequenceContextProcessor::skipPrecedingWhitespace(m_document, endOfOperator); - QTextCursor textCursor(m_interface->textDocument()); + QTextCursor textCursor(m_document); textCursor.setPosition(index); ExpressionUnderCursor euc(m_languageFeatures); index = euc.startOfFunctionCall(textCursor); - index = ActivationSequenceContextProcessor::skipPrecedingWhitespace(m_interface, index); + index = ActivationSequenceContextProcessor::skipPrecedingWhitespace(m_document, index); const int functionNameStart = ActivationSequenceContextProcessor::findStartOfName( - m_interface, index, ActivationSequenceContextProcessor::NameCategory::Function); + m_document, index, ActivationSequenceContextProcessor::NameCategory::Function); if (functionNameStart == -1) return -1; - QTextCursor functionNameSelector(m_interface->textDocument()); + QTextCursor functionNameSelector(m_document); functionNameSelector.setPosition(functionNameStart); functionNameSelector.setPosition(index, QTextCursor::KeepAnchor); const QString functionName = functionNameSelector.selectedText().trimmed(); @@ -137,12 +146,12 @@ void ClangCompletionContextAnalyzer::handleCommaInFunctionCall() { if (m_completionOperator == T_COMMA) { ExpressionUnderCursor expressionUnderCursor(m_languageFeatures); - QTextCursor textCursor(m_interface->textDocument()); + QTextCursor textCursor(m_document); textCursor.setPosition(m_positionEndOfExpression); const int start = expressionUnderCursor.startOfFunctionCall(textCursor); m_positionEndOfExpression = start; m_positionForProposal = start + 1; // After '(' of function call - if (m_interface->characterAt(start) == '(') + if (m_document->characterAt(start) == '(') m_completionOperator = T_LPAREN; else m_completionOperator = T_LBRACE; @@ -151,7 +160,7 @@ void ClangCompletionContextAnalyzer::handleCommaInFunctionCall() void ClangCompletionContextAnalyzer::handleFunctionCall(int afterOperatorPosition) { - if (m_interface->type() == CompletionType::FunctionHint) { + if (m_isFunctionHint) { const int functionNameStart = startOfFunctionCall(afterOperatorPosition); if (functionNameStart >= 0) { m_addSnippets = functionNameStart == afterOperatorPosition; @@ -166,15 +175,20 @@ void ClangCompletionContextAnalyzer::handleFunctionCall(int afterOperatorPositio if (m_completionOperator == T_LPAREN || m_completionOperator == T_LBRACE) { ExpressionUnderCursor expressionUnderCursor(m_languageFeatures); - QTextCursor textCursor(m_interface->textDocument()); + QTextCursor textCursor(m_document); textCursor.setPosition(m_positionEndOfExpression); const QString expression = expressionUnderCursor(textCursor); + const QString trimmedExpression = expression.trimmed(); + const QChar lastExprChar = trimmedExpression.isEmpty() + ? QChar() : trimmedExpression.at(trimmedExpression.length() - 1); + const bool mightBeConstructorCall = lastExprChar != ')'; if (expression.endsWith(QLatin1String("SIGNAL"))) { setActionAndClangPosition(CompleteSignal, afterOperatorPosition); } else if (expression.endsWith(QLatin1String("SLOT"))) { setActionAndClangPosition(CompleteSlot, afterOperatorPosition); - } else if (m_interface->position() != afterOperatorPosition) { + } else if (m_position != afterOperatorPosition + || (m_completionOperator == T_LBRACE && !mightBeConstructorCall)) { // No function completion if cursor is not after '(' or ',' m_addSnippets = true; m_positionForProposal = afterOperatorPosition; diff --git a/src/plugins/clangcodemodel/clangcompletioncontextanalyzer.h b/src/plugins/clangcodemodel/clangcompletioncontextanalyzer.h index 2f0e9423c07..18cebdb36f3 100644 --- a/src/plugins/clangcodemodel/clangcompletioncontextanalyzer.h +++ b/src/plugins/clangcodemodel/clangcompletioncontextanalyzer.h @@ -29,6 +29,10 @@ #include +QT_BEGIN_NAMESPACE +class QTextDocument; +QT_END_NAMESPACE + namespace TextEditor { class AssistInterface; } namespace ClangCodeModel { @@ -42,6 +46,8 @@ public: ClangCompletionContextAnalyzer() = delete; ClangCompletionContextAnalyzer(const ClangCompletionAssistInterface *assistInterface, CPlusPlus::LanguageFeatures languageFeatures); + ClangCompletionContextAnalyzer(QTextDocument *document, int position, bool isFunctionHint, + CPlusPlus::LanguageFeatures languageFeatures); void analyze(); enum CompletionAction { @@ -75,8 +81,10 @@ private: void handleFunctionCall(int endOfOperator); private: - const ClangCompletionAssistInterface *m_interface; // Not owned - const CPlusPlus::LanguageFeatures m_languageFeatures; // TODO: Get from assistInterface?! + QTextDocument * const m_document; + const int m_position; + const bool m_isFunctionHint; + const CPlusPlus::LanguageFeatures m_languageFeatures; // Results CompletionAction m_completionAction = PassThroughToLibClang; diff --git a/src/plugins/clangcodemodel/clangdclient.cpp b/src/plugins/clangcodemodel/clangdclient.cpp index 34bbd27bfd3..9a68fdc176b 100644 --- a/src/plugins/clangcodemodel/clangdclient.cpp +++ b/src/plugins/clangcodemodel/clangdclient.cpp @@ -25,7 +25,9 @@ #include "clangdclient.h" +#include "clangcompletioncontextanalyzer.h" #include "clangdiagnosticmanager.h" +#include "clangpreprocessorassistproposalitem.h" #include "clangtextmark.h" #include "clangutils.h" @@ -34,6 +36,11 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include @@ -50,6 +57,8 @@ #include #include #include +#include +#include #include #include #include @@ -676,6 +685,62 @@ public: { insert("publishDiagnostics", caps); } }; +class DoxygenAssistProcessor : public TextEditor::IAssistProcessor +{ +public: + DoxygenAssistProcessor(int position, unsigned completionOperator, + const ProposalHandler &handler) + : m_position(position), m_completionOperator(completionOperator), m_handler(handler) {} + +private: + TextEditor::IAssistProposal *perform(const TextEditor::AssistInterface *) override + { + QList completions; + for (int i = 1; i < CppTools::T_DOXY_LAST_TAG; ++i) { + const auto item = new ClangPreprocessorAssistProposalItem; + item->setText(QLatin1String(CppTools::doxygenTagSpell(i))); + item->setIcon(CPlusPlus::Icons::keywordIcon()); + item->setCompletionOperator(m_completionOperator); + completions.append(item); + } + TextEditor::GenericProposalModelPtr model(new TextEditor::GenericProposalModel); + model->loadContent(completions); + const auto proposal = new TextEditor::GenericProposal(m_position, model); + if (m_handler) { + m_handler(proposal); + return nullptr; + } + return proposal; + } + + const int m_position; + const unsigned m_completionOperator; + const ProposalHandler m_handler; +}; + +class DoxygenAssistProvider : public TextEditor::IAssistProvider +{ +public: + void setProposalHandler(const ProposalHandler &handler) { m_proposalHandler = handler; } + + void setParameters(int position, unsigned completionOperator) + { + m_position = position; + m_completionOperator = completionOperator; + } + +private: + RunType runType() const override { return Synchronous; } + TextEditor::IAssistProcessor *createProcessor() const override + { + return new DoxygenAssistProcessor(m_position, m_completionOperator, m_proposalHandler); + } + + ProposalHandler m_proposalHandler; + int m_position = 0; + unsigned m_completionOperator = 0; +}; + class ClangdClient::Private { public: @@ -711,8 +776,13 @@ public: void handleSemanticTokens(TextEditor::TextDocument *doc, const QList &tokens); + void applyCompletionItem(const CompletionItem &item, + TextEditor::TextDocumentManipulatorInterface &manipulator, + QChar typedChar); + ClangdClient * const q; const CppTools::ClangdSettings::Data settings; + DoxygenAssistProvider doxygenAssistProvider; QHash runningFindUsages; Utils::optional followSymbolData; Utils::optional switchDeclDefData; @@ -724,6 +794,16 @@ public: bool isTesting = false; }; +class ClangdCompletionCapabilities : public TextDocumentClientCapabilities::CompletionCapabilities +{ +public: + explicit ClangdCompletionCapabilities(const JsonObject &object) + : TextDocumentClientCapabilities::CompletionCapabilities(object) + { + insert("editsNearCursor", true); // For dot-to-arrow correction. + } +}; + ClangdClient::ClangdClient(Project *project, const Utils::FilePath &jsonDbDir) : Client(clientInterface(project, jsonDbDir)), d(new Private(this, project)) { @@ -745,12 +825,15 @@ ClangdClient::ClangdClient(Project *project, const Utils::FilePath &jsonDbDir) Utils::optional textCaps = caps.textDocument(); if (textCaps) { ClangdTextDocumentClientCapabilities clangdTextCaps(*textCaps); - clangdTextCaps.clearCompletion(); clangdTextCaps.clearDocumentHighlight(); DiagnosticsCapabilities diagnostics; diagnostics.enableCategorySupport(); diagnostics.enableCodeActionsInline(); clangdTextCaps.setPublishDiagnostics(diagnostics); + Utils::optional completionCaps + = textCaps->completion(); + if (completionCaps) + clangdTextCaps.setCompletion(ClangdCompletionCapabilities(*completionCaps)); caps.setTextDocument(clangdTextCaps); } caps.clearExperimental(); @@ -800,6 +883,34 @@ ClangdClient::ClangdClient(Project *project, const Utils::FilePath &jsonDbDir) const DocumentUri &uri) { gatherHelpItemForTooltip(response, uri); }); + setCompletionItemsTransformer([](const Utils::FilePath &filePath, const QString &content, + int pos, const QList &items) { + qCDebug(clangdLog) << "received" << items.count() << "completions"; + + // If there are signals among the candidates, we employ the built-in code model to find out + // whether the cursor was on the second argument of a (dis)connect() call. + // If so, we offer only signals, as nothing else makes sense in that context. + static const auto criterion = [](const CompletionItem &ci) { + const Utils::optional doc = ci.documentation(); + if (!doc) + return false; + QString docText; + if (Utils::holds_alternative(*doc)) + docText = Utils::get(*doc); + else if (Utils::holds_alternative(*doc)) + docText = Utils::get(*doc).content(); + return docText.contains("Annotation: qt_signal"); + }; + if (pos != -1 && Utils::anyOf(items, criterion) && CppTools::CppModelManager::instance() + ->positionRequiresSignal(filePath.toString(), content.toUtf8(), pos)) { + return Utils::filtered(items, criterion); + } + return items; + }); + setCompletionApplyHelper([this](const CompletionItem &item, + TextEditor::TextDocumentManipulatorInterface &manipulator, QChar typedChar) { + d->applyCompletionItem(item, manipulator, typedChar); + }); connect(this, &Client::workDone, this, [this, p = QPointer(project)](const ProgressToken &token) { @@ -900,8 +1011,6 @@ void ClangdClient::findUsages(TextEditor::TextDocument *document, const QTextCur sendContent(symReq); } -void ClangdClient::enableTesting() { d->isTesting = true; } - void ClangdClient::handleDiagnostics(const PublishDiagnosticsParams ¶ms) { const DocumentUri &uri = params.uri(); @@ -1003,6 +1112,55 @@ void ClangdClient::Private::findUsages(TextEditor::TextDocument *document, }); } +void ClangdClient::enableTesting() +{ + d->isTesting = true; + setCompletionProposalHandler([this](TextEditor::IAssistProposal *proposal) { + emit proposalReady(proposal); + }); + setFunctionHintProposalHandler([this](TextEditor::IAssistProposal *proposal) { + emit proposalReady(proposal); + }); + d->doxygenAssistProvider.setProposalHandler([this](TextEditor::IAssistProposal *proposal) { + QMetaObject::invokeMethod(this, [this, proposal] { emit proposalReady(proposal); }, + Qt::QueuedConnection); + }); +} + +void ClangdClient::openEditorDocument(TextEditor::BaseTextEditor *editor) +{ + if (!documentOpen(editor->textDocument())) + openDocument(editor->textDocument()); + const auto assistRequestHandler = [self = QPointer(this)]( + TextEditor::TextEditorWidget *editorWidget, TextEditor::AssistKind kind, + TextEditor::IAssistProvider *provider) { + if (!self) + return false; + if (kind != TextEditor::Completion || provider) + return false; + ClangCompletionContextAnalyzer contextAnalyzer(editorWidget->document(), + editorWidget->position(), false, {}); + contextAnalyzer.analyze(); + self->setSnippetsGroup(contextAnalyzer.addSnippets() + ? CppEditor::Constants::CPP_SNIPPETS_GROUP_ID : QString()); + switch (contextAnalyzer.completionAction()) { + case ClangCompletionContextAnalyzer::PassThroughToLibClangAfterLeftParen: + qCDebug(clangdLog) << "completion changed to function hint"; + editorWidget->invokeAssist(TextEditor::FunctionHint, provider); + return true; + case ClangCompletionContextAnalyzer::CompleteDoxygenKeyword: + self->d->doxygenAssistProvider.setParameters(contextAnalyzer.positionForProposal(), + contextAnalyzer.completionOperator()); + editorWidget->invokeAssist(kind, &self->d->doxygenAssistProvider); + return true; + default: + break; + } + return false; + }; + editor->editorWidget()->setAssistRequestHandler(assistRequestHandler); +} + void ClangdClient::Private::handleFindUsagesResult(quint64 key, const QList &locations) { const auto refData = runningFindUsages.find(key); @@ -2414,7 +2572,7 @@ static void semanticHighlighter(QFutureInterface void ClangdClient::Private::handleSemanticTokens(TextEditor::TextDocument *doc, const QList &tokens) { - qCDebug(clangdLog()) << "handling LSP tokens" << tokens.size(); + qCDebug(clangdLog()) << "handling LSP tokens" << doc->filePath() << tokens.size(); for (const ExpandedSemanticToken &t : tokens) qCDebug(clangdLogHighlight()) << '\t' << t.line << t.column << t.length << t.type << t.modifiers; @@ -2437,8 +2595,8 @@ void ClangdClient::Private::handleSemanticTokens(TextEditor::TextDocument *doc, if (isTesting) { const auto watcher = new QFutureWatcher(q); connect(watcher, &QFutureWatcher::finished, - q, [this, watcher] { - emit q->highlightingResultsReady(watcher->future().results()); + q, [this, watcher, fp = doc->filePath()] { + emit q->highlightingResultsReady(watcher->future().results(), fp); watcher->deleteLater(); }); watcher->setFuture(runner()); @@ -2457,6 +2615,125 @@ void ClangdClient::Private::handleSemanticTokens(TextEditor::TextDocument *doc, q->sendContent(astReq, SendDocUpdates::Ignore); } +void ClangdClient::Private::applyCompletionItem(const CompletionItem &item, + TextEditor::TextDocumentManipulatorInterface &manipulator, QChar typedChar) +{ + const auto edit = item.textEdit(); + if (!edit) + return; + + const auto kind = static_cast( + item.kind().value_or(CompletionItemKind::Text)); + if (kind != CompletionItemKind::Function && kind != CompletionItemKind::Method + && kind != CompletionItemKind::Constructor) { + applyTextEdit(manipulator, *edit, true); + return; + } + + const QString rawInsertText = edit->newText(); + const int firstParenOffset = rawInsertText.indexOf('('); + const int lastParenOffset = rawInsertText.lastIndexOf(')'); + if (firstParenOffset == -1 || lastParenOffset == -1) { + applyTextEdit(manipulator, *edit, true); + return; + } + + const QString detail = item.detail().value_or(QString()); + const TextEditor::CompletionSettings &completionSettings + = TextEditor::TextEditorSettings::completionSettings(); + QString textToBeInserted = rawInsertText.left(firstParenOffset); + QString extraCharacters; + int cursorOffset = 0; + bool setAutoCompleteSkipPos = false; + const QTextDocument * const doc = manipulator.textCursorAt( + manipulator.currentPosition()).document(); + const Range range = edit->range(); + const int rangeStart = range.start().toPositionInDocument(doc); + const int rangeLength = range.end().toPositionInDocument(doc) - rangeStart; + + if (completionSettings.m_autoInsertBrackets) { + // If the user typed the opening parenthesis, they'll likely also type the closing one, + // in which case it would be annoying if we put the cursor after the already automatically + // inserted closing parenthesis. + const bool skipClosingParenthesis = typedChar != '('; + QTextCursor cursor = manipulator.textCursorAt(rangeStart); + + bool abandonParen = false; + if (matchPreviousWord(manipulator, cursor, "&")) { + moveToPreviousWord(manipulator, cursor); + moveToPreviousChar(manipulator, cursor); + const QChar prevChar = manipulator.characterAt(cursor.position()); + cursor.setPosition(rangeStart); + abandonParen = QString("(;,{}=").contains(prevChar); + } + if (!abandonParen) + abandonParen = isAtUsingDeclaration(manipulator, rangeStart); + if (!abandonParen && matchPreviousWord(manipulator, cursor, detail)) // function definition? + abandonParen = true; + if (!abandonParen) { + if (completionSettings.m_spaceAfterFunctionName) + extraCharacters += ' '; + extraCharacters += '('; + if (typedChar == '(') + typedChar = {}; + + // If the function doesn't return anything, automatically place the semicolon, + // unless we're doing a scope completion (then it might be function definition). + const QChar characterAtCursor = manipulator.characterAt(manipulator.currentPosition()); + bool endWithSemicolon = typedChar == ';'; + const QChar semicolon = typedChar.isNull() ? QLatin1Char(';') : typedChar; + if (endWithSemicolon && characterAtCursor == semicolon) { + endWithSemicolon = false; + typedChar = {}; + } + + // If the function takes no arguments, automatically place the closing parenthesis + if (firstParenOffset + 1 == lastParenOffset && skipClosingParenthesis) { + extraCharacters += QLatin1Char(')'); + if (endWithSemicolon) { + extraCharacters += semicolon; + typedChar = {}; + } + } else { + const QChar lookAhead = manipulator.characterAt(manipulator.currentPosition() + 1); + if (MatchingText::shouldInsertMatchingText(lookAhead)) { + extraCharacters += ')'; + --cursorOffset; + setAutoCompleteSkipPos = true; + if (endWithSemicolon) { + extraCharacters += semicolon; + --cursorOffset; + typedChar = {}; + } + } + } + } + } + + // Append an unhandled typed character, adjusting cursor offset when it had been adjusted before + if (!typedChar.isNull()) { + extraCharacters += typedChar; + if (cursorOffset != 0) + --cursorOffset; + } + + textToBeInserted += extraCharacters; + + const bool isReplaced = manipulator.replace(rangeStart, rangeLength, textToBeInserted); + manipulator.setCursorPosition(rangeStart + textToBeInserted.length()); + if (isReplaced) { + if (cursorOffset) + manipulator.setCursorPosition(manipulator.currentPosition() + cursorOffset); + if (setAutoCompleteSkipPos) + manipulator.setAutoCompleteSkipPosition(manipulator.currentPosition()); + } + + if (auto additionalEdits = item.additionalTextEdits()) { + for (const auto &edit : *additionalEdits) + applyTextEdit(manipulator, edit); + } +} + void ClangdClient::VirtualFunctionAssistProcessor::cancel() { resetData(); diff --git a/src/plugins/clangcodemodel/clangdclient.h b/src/plugins/clangcodemodel/clangdclient.h index af1870ad550..8a7b3975520 100644 --- a/src/plugins/clangcodemodel/clangdclient.h +++ b/src/plugins/clangcodemodel/clangdclient.h @@ -36,7 +36,7 @@ namespace Core { class SearchResultItem; } namespace CppTools { class CppEditorWidgetInterface; } namespace ProjectExplorer { class Project; } -namespace TextEditor { class TextDocument; } +namespace TextEditor { class BaseTextEditor; } namespace ClangCodeModel { namespace Internal { @@ -52,6 +52,8 @@ public: QVersionNumber versionNumber() const; CppTools::ClangdSettings::Data settingsData() const; + void openEditorDocument(TextEditor::BaseTextEditor *editor); + void openExtraFile(const Utils::FilePath &filePath, const QString &content = {}); void closeExtraFile(const Utils::FilePath &filePath); @@ -83,7 +85,9 @@ signals: void foundReferences(const QList &items); void findUsagesDone(); void helpItemGathered(const Core::HelpItem &helpItem); - void highlightingResultsReady(const TextEditor::HighlightingResults &results); + void highlightingResultsReady(const TextEditor::HighlightingResults &results, + const Utils::FilePath &file); + void proposalReady(TextEditor::IAssistProposal *proposal); private: void handleDiagnostics(const LanguageServerProtocol::PublishDiagnosticsParams ¶ms) override; diff --git a/src/plugins/clangcodemodel/clangmodelmanagersupport.cpp b/src/plugins/clangcodemodel/clangmodelmanagersupport.cpp index ce546b7987a..37bd4bf7b90 100644 --- a/src/plugins/clangcodemodel/clangmodelmanagersupport.cpp +++ b/src/plugins/clangcodemodel/clangmodelmanagersupport.cpp @@ -334,8 +334,7 @@ void ClangModelManagerSupport::updateLanguageClient(ProjectExplorer::Project *pr continue; if (fallbackClient && fallbackClient->documentOpen(editor->textDocument())) fallbackClient->closeDocument(editor->textDocument()); - if (!client->documentOpen(editor->textDocument())) - client->openDocument(editor->textDocument()); + client->openEditorDocument(editor); ClangEditorDocumentProcessor::clearTextMarks(editor->textDocument()->filePath()); hasDocuments = true; } @@ -429,8 +428,8 @@ void ClangModelManagerSupport::onEditorOpened(Core::IEditor *editor) // instead. Is this feasible? ProjectExplorer::Project * const project = ProjectExplorer::SessionManager::projectForFile(document->filePath()); - if (Client * const client = clientForProject(project)) - client->openDocument(textDocument); + if (ClangdClient * const client = clientForProject(project)) + client->openEditorDocument(qobject_cast(editor)); } } diff --git a/src/plugins/clangcodemodel/clangutils.cpp b/src/plugins/clangcodemodel/clangutils.cpp index 57f3a522b8b..1d3fbb74516 100644 --- a/src/plugins/clangcodemodel/clangutils.cpp +++ b/src/plugins/clangcodemodel/clangutils.cpp @@ -44,7 +44,9 @@ #include #include #include +#include +#include #include #include #include @@ -608,5 +610,41 @@ const QStringList optionsForProject(ProjectExplorer::Project *project) return ClangProjectSettings::globalCommandLineOptions(); } +// 7.3.3: using typename(opt) nested-name-specifier unqualified-id ; +bool isAtUsingDeclaration(TextEditor::TextDocumentManipulatorInterface &manipulator, + int basePosition) +{ + using namespace CPlusPlus; + SimpleLexer lexer; + lexer.setLanguageFeatures(LanguageFeatures::defaultFeatures()); + const QString textToLex = textUntilPreviousStatement(manipulator, basePosition); + const Tokens tokens = lexer(textToLex); + if (tokens.empty()) + return false; + + // The nested-name-specifier always ends with "::", so check for this first. + const Token lastToken = tokens[tokens.size() - 1]; + if (lastToken.kind() != T_COLON_COLON) + return false; + + return contains(tokens, [](const Token &token) { return token.kind() == T_USING; }); +} + +QString textUntilPreviousStatement(TextEditor::TextDocumentManipulatorInterface &manipulator, + int startPosition) +{ + static const QString stopCharacters(";{}#"); + + int endPosition = 0; + for (int i = startPosition; i >= 0 ; --i) { + if (stopCharacters.contains(manipulator.characterAt(i))) { + endPosition = i + 1; + break; + } + } + + return manipulator.textAt(endPosition, startPosition - endPosition); +} + } // namespace Internal } // namespace Clang diff --git a/src/plugins/clangcodemodel/clangutils.h b/src/plugins/clangcodemodel/clangutils.h index e381654d083..82c35f189ae 100644 --- a/src/plugins/clangcodemodel/clangutils.h +++ b/src/plugins/clangcodemodel/clangutils.h @@ -42,6 +42,8 @@ class ClangDiagnosticConfig; class CppEditorDocumentHandle; } +namespace TextEditor { class TextDocumentManipulatorInterface; } + namespace ClangBackEnd { class TokenInfoContainer; } namespace ProjectExplorer { class Project; } @@ -107,7 +109,7 @@ private: }; template -void moveToPreviousChar(CharacterProvider &provider, QTextCursor &cursor) +void moveToPreviousChar(const CharacterProvider &provider, QTextCursor &cursor) { cursor.movePosition(QTextCursor::PreviousCharacter); while (provider.characterAt(cursor.position()).isSpace()) @@ -123,7 +125,7 @@ void moveToPreviousWord(CharacterProvider &provider, QTextCursor &cursor) } template -bool matchPreviousWord(CharacterProvider &provider, QTextCursor cursor, QString pattern) +bool matchPreviousWord(const CharacterProvider &provider, QTextCursor cursor, QString pattern) { cursor.movePosition(QTextCursor::PreviousWord); while (provider.characterAt(cursor.position()) == ':') @@ -151,5 +153,11 @@ bool matchPreviousWord(CharacterProvider &provider, QTextCursor cursor, QString return pattern.isEmpty(); } +QString textUntilPreviousStatement(TextEditor::TextDocumentManipulatorInterface &manipulator, + int startPosition); + +bool isAtUsingDeclaration(TextEditor::TextDocumentManipulatorInterface &manipulator, + int basePosition); + } // namespace Internal } // namespace Clang diff --git a/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp b/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp index fc8c995214c..097d74441ff 100644 --- a/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp +++ b/src/plugins/clangcodemodel/test/clangcodecompletion_test.cpp @@ -105,7 +105,7 @@ public: TestDocument(const QByteArray &fileName, CppTools::Tests::TemporaryDir *temporaryDir = nullptr) { QTC_ASSERT(!fileName.isEmpty(), return); - const QResource resource(qrcPath(fileName)); + const QResource resource(qrcPath("completion/" + fileName)); QTC_ASSERT(resource.isValid(), return); const QByteArray contents = QByteArray(reinterpret_cast(resource.data()), resource.size()); @@ -534,7 +534,7 @@ void ClangCodeCompletionTest::testCompletePreprocessorKeywords() void ClangCodeCompletionTest::testCompleteIncludeDirective() { - CppTools::Tests::TemporaryCopiedDir testDir(qrcPath("exampleIncludeDir")); + CppTools::Tests::TemporaryCopiedDir testDir(qrcPath("completion/exampleIncludeDir")); ProjectLessCompletionTest t("includeDirectiveCompletion.cpp", QString(), QStringList(testDir.path())); diff --git a/src/plugins/clangcodemodel/test/clangdtests.cpp b/src/plugins/clangcodemodel/test/clangdtests.cpp index caa48d11004..15c41b462d4 100644 --- a/src/plugins/clangcodemodel/test/clangdtests.cpp +++ b/src/plugins/clangcodemodel/test/clangdtests.cpp @@ -37,17 +37,22 @@ #include #include #include +#include #include #include #include +#include #include - +#include +#include #include #include #include +#include #include #include +#include #include #include #include @@ -78,6 +83,7 @@ namespace Tests { ClangdTest::~ClangdTest() { + EditorManager::closeAllEditors(false); if (m_project) ProjectExplorerPlugin::unloadProject(m_project); delete m_projectDir; @@ -88,6 +94,40 @@ Utils::FilePath ClangdTest::filePath(const QString &fileName) const return Utils::FilePath::fromString(m_projectDir->absolutePath(fileName.toLocal8Bit())); } +void ClangdTest::waitForNewClient(bool withIndex) +{ + // Setting up the project should result in a clangd client being created. + // Wait until that has happened. + m_client = nullptr; + const auto modelManagerSupport = ClangModelManagerSupport::instance(); + m_client = modelManagerSupport->clientForProject(project()); + if (!m_client) { + QVERIFY(waitForSignalOrTimeout(modelManagerSupport, + &ClangModelManagerSupport::createdClient, timeOutInMs())); + m_client = modelManagerSupport->clientForProject(m_project); + } + QVERIFY(m_client); + m_client->enableTesting(); + + // Wait until the client is fully initialized, i.e. it's completed the handshake + // with the server. + if (!m_client->reachable()) + QVERIFY(waitForSignalOrTimeout(m_client, &ClangdClient::initialized, timeOutInMs())); + QVERIFY(m_client->reachable()); + + if (m_minVersion != -1 && m_client->versionNumber() < QVersionNumber(m_minVersion)) + QSKIP("clangd is too old"); + + // Wait for index to build. + if (withIndex) { + if (!m_client->isFullyIndexed()) { + QVERIFY(waitForSignalOrTimeout(m_client, &ClangdClient::indexingFinished, + clangdIndexingTimeout())); + } + QVERIFY(m_client->isFullyIndexed()); + } +} + void ClangdTest::initTestCase() { const QString clangdFromEnv = qEnvironmentVariable("QTC_CLANGD"); @@ -115,34 +155,8 @@ void ClangdTest::initTestCase() m_project = openProjectResult.project(); m_project->configureAsExampleProject(m_kit); - // Setting up the project should result in a clangd client being created. - // Wait until that has happened. - const auto modelManagerSupport = ClangModelManagerSupport::instance(); - m_client = modelManagerSupport->clientForProject(openProjectResult.project()); - if (!m_client) { - QVERIFY(waitForSignalOrTimeout(modelManagerSupport, - &ClangModelManagerSupport::createdClient, timeOutInMs())); - m_client = modelManagerSupport->clientForProject(m_project); - } + waitForNewClient(); QVERIFY(m_client); - m_client->enableTesting(); - - // Wait until the client is fully initialized, i.e. it's completed the handshake - // with the server. - if (!m_client->reachable()) - QVERIFY(waitForSignalOrTimeout(m_client, &ClangdClient::initialized, timeOutInMs())); - QVERIFY(m_client->reachable()); - - // The kind of AST support we need was introduced in LLVM 13. - if (m_minVersion != -1 && m_client->versionNumber() < QVersionNumber(m_minVersion)) - QSKIP("clangd is too old"); - - // Wait for index to build. - if (!m_client->isFullyIndexed()) { - QVERIFY(waitForSignalOrTimeout(m_client, &ClangdClient::indexingFinished, - clangdIndexingTimeout())); - } - QVERIFY(m_client->isFullyIndexed()); // Open cpp documents. for (const QString &sourceFileName : qAsConst(m_sourceFileNames)) { @@ -153,7 +167,8 @@ void ClangdTest::initTestCase() QVERIFY(editor); const auto doc = qobject_cast(editor->document()); QVERIFY(doc); - QVERIFY(m_client->documentForFilePath(sourceFilePath) == doc); + QVERIFY2(m_client->documentForFilePath(sourceFilePath) == doc, + qPrintable(sourceFilePath.toUserOutput())); m_sourceDocuments.insert(sourceFileName, doc); } } @@ -1328,6 +1343,586 @@ void ClangdTestHighlighting::test() QCOMPARE(result.kind, expectedKind); } + +class Manipulator : public TextDocumentManipulatorInterface +{ +public: + Manipulator() + { + const auto textEditor = static_cast(EditorManager::currentEditor()); + QVERIFY(textEditor); + m_doc = textEditor->textDocument()->document(); + m_cursor = textEditor->editorWidget()->textCursor(); + } + + int currentPosition() const override { return m_cursor.position(); } + int positionAt(TextPositionOperation) const override { return 0; } + QChar characterAt(int position) const override { return m_doc->characterAt(position); } + + QString textAt(int position, int length) const override + { + return m_doc->toPlainText().mid(position, length); + } + + QTextCursor textCursorAt(int position) const override + { + QTextCursor cursor(m_doc); + cursor.setPosition(position); + return cursor; + } + + void setCursorPosition(int position) override { m_cursor.setPosition(position); } + void setAutoCompleteSkipPosition(int position) override { m_skipPos = position; } + + bool replace(int position, int length, const QString &text) override + { + QTextCursor cursor = textCursorAt(position); + cursor.setPosition(position + length, QTextCursor::KeepAnchor); + cursor.insertText(text); + return true; + } + + void insertCodeSnippet(int pos, const QString &text, const SnippetParser &parser) override + { + const auto parseResult = parser(text); + if (const auto snippet = Utils::get_if(&parseResult)) { + if (!snippet->parts.isEmpty()) + textCursorAt(pos).insertText(snippet->parts.first().text); + } + } + + void paste() override {} + void encourageApply() override {} + void autoIndent(int, int) override {} + + QString getLine(int line) const { return m_doc->findBlockByNumber(line - 1).text(); } + + QPair cursorPos() const + { + const int pos = currentPosition(); + QPair lineAndColumn; + Utils::Text::convertPosition(m_doc, pos, &lineAndColumn.first, &lineAndColumn.second); + return lineAndColumn; + } + + int skipPos() const { return m_skipPos; } + +private: + QTextDocument *m_doc; + QTextCursor m_cursor; + int m_skipPos = -1; +}; + +ClangdTestCompletion::ClangdTestCompletion() +{ + setProjectFileName("completion.pro"); + setSourceFileNames({"completionWithProject.cpp", "constructorCompletion.cpp", + "classAndConstructorCompletion.cpp", "dotToArrowCorrection.cpp", + "doxygenKeywordsCompletion.cpp", "functionAddress.cpp", + "functionCompletion.cpp", "functionCompletionFiltered2.cpp", + "functionCompletionFiltered.cpp", "globalCompletion.cpp", + "includeDirectiveCompletion.cpp", "mainwindow.cpp", + "memberCompletion.cpp", "membercompletion-friend.cpp", + "membercompletion-inside.cpp", "membercompletion-outside.cpp", + "noDotToArrowCorrectionForFloats.cpp", + "preprocessorKeywordsCompletion.cpp", "preprocessorKeywordsCompletion2.cpp", + "preprocessorKeywordsCompletion3.cpp", "signalCompletion.cpp"}); +} + +void ClangdTestCompletion::initTestCase() +{ + ClangdTest::initTestCase(); + client()->forceHighlightingOnEmptyDelta(); + startCollectingHighlightingInfo(); +} + +void ClangdTestCompletion::testCompleteDoxygenKeywords() +{ + ProposalModelPtr proposal; + getProposal("doxygenKeywordsCompletion.cpp", proposal); + + QVERIFY(proposal); + QVERIFY(hasItem(proposal, "brief")); + QVERIFY(hasItem(proposal, "param")); + QVERIFY(hasItem(proposal, "return")); + QVERIFY(!hasSnippet(proposal, "class ")); +} + +void ClangdTestCompletion::testCompletePreprocessorKeywords() +{ + ProposalModelPtr proposal; + getProposal("preprocessorKeywordsCompletion.cpp", proposal); + QVERIFY(proposal); + QVERIFY(hasItem(proposal, " ifdef macro")); + QVERIFY(!hasSnippet(proposal, "class ")); + + proposal.clear(); + getProposal("preprocessorKeywordsCompletion2.cpp", proposal); + QVERIFY(proposal); + QVERIFY(hasItem(proposal, " endif")); + QVERIFY(!hasSnippet(proposal, "class ")); + + proposal.clear(); + getProposal("preprocessorKeywordsCompletion3.cpp", proposal); + QVERIFY(proposal); + QEXPECT_FAIL("", "TODO: Fix in clangd", Continue); + QVERIFY(hasItem(proposal, " endif")); + QVERIFY(!hasSnippet(proposal, "class ")); +} + +void ClangdTestCompletion::testCompleteIncludeDirective() +{ + ProposalModelPtr proposal; + getProposal("includeDirectiveCompletion.cpp", proposal); + + QVERIFY(proposal); + QVERIFY(hasItem(proposal, " file.h>")); + QVERIFY(hasItem(proposal, " otherFile.h>")); + QVERIFY(hasItem(proposal, " mylib/")); + QVERIFY(!hasSnippet(proposal, "class ")); +} + +void ClangdTestCompletion::testCompleteGlobals() +{ + ProposalModelPtr proposal; + int cursorPos = -1; + getProposal("globalCompletion.cpp", proposal, {}, &cursorPos); + + QVERIFY(proposal); + QVERIFY(hasItem(proposal, " globalVariable", "int")); + QVERIFY(hasItem(proposal, " GlobalClass")); + QVERIFY(hasItem(proposal, " class")); // Keyword + QVERIFY(hasSnippet(proposal, "class ")); // Snippet + + const AssistProposalItemInterface * const item = getItem(proposal, " globalFunction()", "void"); + QVERIFY(item); + Manipulator manipulator; + item->apply(manipulator, cursorPos); + QCOMPARE(manipulator.getLine(7), " globalFunction() /* COMPLETE HERE */"); + QCOMPARE(manipulator.cursorPos(), qMakePair(7, 20)); + QCOMPARE(manipulator.skipPos(), -1); +} + +void ClangdTestCompletion::testCompleteMembers() +{ + ProposalModelPtr proposal; + int cursorPos = -1; + getProposal("memberCompletion.cpp", proposal, {}, &cursorPos); + + QVERIFY(proposal); + QVERIFY(!hasItem(proposal, " globalVariable")); + QVERIFY(!hasItem(proposal, " class")); // Keyword + QVERIFY(!hasSnippet(proposal, "class ")); // Snippet + + const AssistProposalItemInterface * const item = getItem(proposal, " member", "int"); + QVERIFY(item); + Manipulator manipulator; + item->apply(manipulator, cursorPos); + QCOMPARE(manipulator.getLine(7), " s.member /* COMPLETE HERE */"); + QCOMPARE(manipulator.cursorPos(), qMakePair(7, 13)); + QCOMPARE(manipulator.skipPos(), -1); +} + +void ClangdTestCompletion::testCompleteMembersFromInside() +{ + ProposalModelPtr proposal; + int cursorPos = -1; + getProposal("membercompletion-inside.cpp", proposal, {}, &cursorPos); + + QVERIFY(proposal); + QVERIFY(hasItem(proposal, " publicFunc()", "void")); + + const AssistProposalItemInterface * const item = getItem(proposal, " privateFunc()", "void"); + QVERIFY(item); + Manipulator manipulator; + item->apply(manipulator, cursorPos); + QCOMPARE(manipulator.getLine(4), " privateFunc() /* COMPLETE HERE */"); + QCOMPARE(manipulator.cursorPos(), qMakePair(4, 22)); + QCOMPARE(manipulator.skipPos(), -1); +} + +void ClangdTestCompletion::testCompleteMembersFromOutside() +{ + ProposalModelPtr proposal; + int cursorPos = -1; + getProposal("membercompletion-outside.cpp", proposal, {}, &cursorPos); + + QVERIFY(proposal); + QVERIFY(!hasItem(proposal, " privateFunc", "void")); + + const AssistProposalItemInterface * const item = getItem(proposal, " publicFunc()", "void"); + QVERIFY(item); + Manipulator manipulator; + item->apply(manipulator, cursorPos); + QCOMPARE(manipulator.getLine(13), " c.publicFunc() /* COMPLETE HERE */"); + QCOMPARE(manipulator.cursorPos(), qMakePair(13, 19)); + QCOMPARE(manipulator.skipPos(), -1); +} + +void ClangdTestCompletion::testCompleteMembersFromFriend() +{ + ProposalModelPtr proposal; + int cursorPos = -1; + getProposal("membercompletion-friend.cpp", proposal, {}, &cursorPos); + + QVERIFY(proposal); + QVERIFY(hasItem(proposal, " publicFunc()", "void")); + + const AssistProposalItemInterface * const item = getItem(proposal, " privateFunc()", "void"); + QVERIFY(item); + Manipulator manipulator; + item->apply(manipulator, cursorPos); + QCOMPARE(manipulator.getLine(14), " C().privateFunc() /* COMPLETE HERE */"); + QCOMPARE(manipulator.cursorPos(), qMakePair(14, 22)); + QCOMPARE(manipulator.skipPos(), -1); +} + +void ClangdTestCompletion::testFunctionAddress() +{ + ProposalModelPtr proposal; + int cursorPos = -1; + getProposal("functionAddress.cpp", proposal, {}, &cursorPos); + + QVERIFY(proposal); + + const AssistProposalItemInterface * const item = getItem(proposal, " memberFunc()", "void"); + QVERIFY(item); + Manipulator manipulator; + item->apply(manipulator, cursorPos); + QCOMPARE(manipulator.getLine(7), " const auto p = &S::memberFunc /* COMPLETE HERE */;"); + QCOMPARE(manipulator.cursorPos(), qMakePair(7, 34)); + QCOMPARE(manipulator.skipPos(), -1); +} + +void ClangdTestCompletion::testFunctionHints() +{ + ProposalModelPtr proposal; + getProposal("functionCompletion.cpp", proposal); + + QVERIFY(proposal); + QVERIFY(hasItem(proposal, "f() -> void")); + QVERIFY(hasItem(proposal, "f(int a) -> void")); + QVERIFY(hasItem(proposal, "f(const QString &s) -> void")); + QVERIFY(hasItem(proposal, "f(char c, int optional = 3) -> void")); + QVERIFY(hasItem(proposal, "f(char c, int optional1 = 3, int optional2 = 3) -> void")); + QVERIFY(hasItem(proposal, "f(const TType *t) -> void")); + QVERIFY(hasItem(proposal, "f(bool) -> TType")); +} + +void ClangdTestCompletion::testFunctionHintsFiltered() +{ + ProposalModelPtr proposal; + getProposal("functionCompletionFiltered.cpp", proposal); + + QVERIFY(proposal); + QCOMPARE(proposal->size(), 1); + QVERIFY(hasItem(proposal, "func(int i, int j) -> void")); + + proposal.clear(); + getProposal("functionCompletionFiltered2.cpp", proposal); + + QVERIFY(proposal); + QCOMPARE(proposal->size(), 2); + QVERIFY(hasItem(proposal, "func(const S &s, int j) -> void")); + QEXPECT_FAIL("", "FIXME: LanguageClient handles active parameter only in active signature", Abort); + QVERIFY(hasItem(proposal, "func(const S &s, int j, int k) -> void")); +} + +void ClangdTestCompletion::testFunctionHintConstructor() +{ + ProposalModelPtr proposal; + getProposal("constructorCompletion.cpp", proposal); + + QVERIFY(proposal); + QVERIFY(!hasItem(proposal, "globalVariable")); + QVERIFY(!hasItem(proposal, " class")); + QVERIFY(hasItem(proposal, "Foo(int)")); + QEXPECT_FAIL("", "FIXME: LanguageClient handles active parameter only in active signature", Abort); + QVERIFY(hasItem(proposal, "Foo(int, double)")); +} + +void ClangdTestCompletion::testCompleteClassAndConstructor() +{ + ProposalModelPtr proposal; + int cursorPos = -1; + getProposal("classAndConstructorCompletion.cpp", proposal, {}, &cursorPos); + + QVERIFY(proposal); + QVERIFY(hasItem(proposal, " Foo")); + + const AssistProposalItemInterface * const item + = getItem(proposal, QString::fromUtf8(" Foo(…)"), "[2 overloads]"); + QVERIFY(item); + Manipulator manipulator; + item->apply(manipulator, cursorPos); + QCOMPARE(manipulator.getLine(7), " Foo( /* COMPLETE HERE */"); + QCOMPARE(manipulator.cursorPos(), qMakePair(7, 9)); + QCOMPARE(manipulator.skipPos(), -1); +} + +void ClangdTestCompletion::testCompleteWithDotToArrowCorrection() +{ + ProposalModelPtr proposal; + int cursorPos = -1; + getProposal("dotToArrowCorrection.cpp", proposal, ".", &cursorPos); + + QVERIFY(proposal); + const AssistProposalItemInterface * const item = getItem(proposal, " member", "int"); + QVERIFY(item); + Manipulator manipulator; + item->apply(manipulator, cursorPos); + QCOMPARE(manipulator.getLine(4), " bar->member /* COMPLETE HERE */"); + QCOMPARE(manipulator.cursorPos(), qMakePair(4, 16)); + QCOMPARE(manipulator.skipPos(), -1); +} + +void ClangdTestCompletion::testDontCompleteWithDotToArrowCorrectionForFloats() +{ + ProposalModelPtr proposal; + getProposal("noDotToArrowCorrectionForFloats.cpp", proposal, "."); + + QVERIFY(proposal); + for (int i = 0; i < proposal->size(); ++i) // Offer only snippets. + QVERIFY2(hasSnippet(proposal, proposal->text(i)), qPrintable(proposal->text(i))); +} + +void ClangdTestCompletion::testCompleteCodeInGeneratedUiFile() +{ + ProposalModelPtr proposal; + int cursorPos = -1; + getProposal("mainwindow.cpp", proposal, {}, &cursorPos); + + QVERIFY(proposal); + QVERIFY(hasItem(proposal, " menuBar")); + QVERIFY(hasItem(proposal, " statusBar")); + QVERIFY(hasItem(proposal, " centralWidget")); + + const AssistProposalItemInterface * const item = getItem( + proposal, " setupUi(QMainWindow *MainWindow)", "void"); + QVERIFY(item); + Manipulator manipulator; + item->apply(manipulator, cursorPos); + QCOMPARE(manipulator.getLine(34), " ui->setupUi( /* COMPLETE HERE */"); + QCOMPARE(manipulator.cursorPos(), qMakePair(34, 17)); + QCOMPARE(manipulator.skipPos(), -1); +} + +void ClangdTestCompletion::testSignalCompletion_data() +{ + QTest::addColumn("customCode"); + QTest::addColumn("expectedSuggestions"); + + QTest::addRow("positive: connect() on QObject class") + << QString("QObject::connect(dummy, &QObject::") + << QStringList{"aSignal()", "anotherSignal()"}; + QTest::addRow("positive: connect() on QObject object") + << QString("QObject o; o.connect(dummy, &QObject::") + << QStringList{"aSignal()", "anotherSignal()"}; + QTest::addRow("positive: connect() on QObject pointer") + << QString("QObject *o; o->connect(dummy, &QObject::") + << QStringList{"aSignal()", "anotherSignal()"}; + QTest::addRow("positive: connect() on QObject rvalue") + << QString("QObject().connect(dummy, &QObject::") + << QStringList{"aSignal()", "anotherSignal()"}; + QTest::addRow("positive: connect() on QObject pointer rvalue") + << QString("(new QObject)->connect(dummy, &QObject::") + << QStringList{"aSignal()", "anotherSignal()"}; + QTest::addRow("positive: disconnect() on QObject") + << QString("QObject::disconnect(dummy, &QObject::") + << QStringList{"aSignal()", "anotherSignal()"}; + QTest::addRow("positive: connect() in member function of derived class") + << QString("}void DerivedFromQObject::alsoNotASignal() { connect(this, &DerivedFromQObject::") + << QStringList{"aSignal()", "anotherSignal()", "myOwnSignal()"}; + + const QStringList allQObjectFunctions{"aSignal()", "anotherSignal()", "notASignal()", + "connect()", "disconnect()", "QObject", "~QObject()", + "operator=(…)"}; + QTest::addRow("negative: different function name") + << QString("QObject::notASignal(dummy, &QObject::") + << allQObjectFunctions; + QTest::addRow("negative: connect function from other class") + << QString("NotAQObject::connect(dummy, &QObject::") + << allQObjectFunctions; + QTest::addRow("negative: first argument") + << QString("QObject::connect(QObject::") + << allQObjectFunctions; + QTest::addRow("negative: third argument") + << QString("QObject::connect(dummy1, dummy2, QObject::") + << allQObjectFunctions; + + QTest::addRow("negative: not a QObject") + << QString("QObject::connect(dummy, &NotAQObject::") + << QStringList{"notASignal()", "alsoNotASignal()", "connect()", "NotAQObject", + "~NotAQObject()", "operator=(…)"}; +} + +void ClangdTestCompletion::testSignalCompletion() +{ + QFETCH(QString, customCode); + QFETCH(QStringList, expectedSuggestions); + + ProposalModelPtr proposal; + getProposal("signalCompletion.cpp", proposal, customCode); + + QVERIFY(proposal); + if (client()->versionNumber() < QVersionNumber(14) + && QString::fromLatin1(QTest::currentDataTag()).startsWith("positive:")) { + QEXPECT_FAIL("", "Signal info in completions requires clangd >= 14", Abort); + } + QCOMPARE(proposal->size(), expectedSuggestions.size()); + for (const QString &expectedSuggestion : qAsConst(expectedSuggestions)) + QVERIFY2(hasItem(proposal, ' ' + expectedSuggestion), qPrintable(expectedSuggestion)); +} + +void ClangdTestCompletion::testCompleteAfterProjectChange() +{ + // No defines set, completion must come from #else branch. + ProposalModelPtr proposal; + getProposal("completionWithProject.cpp", proposal); + + QVERIFY(proposal); + QVERIFY(!hasItem(proposal, " projectConfiguration1")); + QVERIFY(!hasItem(proposal, " projectConfiguration2")); + QVERIFY(hasItem(proposal, " noProjectConfigurationDetected")); + + // Set define in project file, completion must come from #if branch. + const auto proFileEditor = qobject_cast( + EditorManager::openEditor(project()->projectFilePath())); + QVERIFY(proFileEditor); + proFileEditor->insert("DEFINES += PROJECT_CONFIGURATION_1\n"); + QString saveError; + QVERIFY2(proFileEditor->document()->save(&saveError), qPrintable(saveError)); + QVERIFY(waitForSignalOrTimeout(project(), &Project::anyParsingFinished, timeOutInMs())); + QVERIFY(waitForSignalOrTimeout(LanguageClient::LanguageClientManager::instance(), + &LanguageClient::LanguageClientManager::clientRemoved, + timeOutInMs())); + + // Waiting for the index will cause highlighting info collection to start too late, + // so skip it. + waitForNewClient(false); + QVERIFY(client()); + + startCollectingHighlightingInfo(); + proposal.clear(); + getProposal("completionWithProject.cpp", proposal); + + QVERIFY(proposal); + QVERIFY(hasItem(proposal, " projectConfiguration1")); + QVERIFY(!hasItem(proposal, " projectConfiguration2")); + QVERIFY(!hasItem(proposal, " noProjectConfigurationDetected")); +} + +void ClangdTestCompletion::startCollectingHighlightingInfo() +{ + m_documentsWithHighlighting.clear(); + connect(client(), &ClangdClient::highlightingResultsReady, this, + [this](const HighlightingResults &, const Utils::FilePath &file) { + m_documentsWithHighlighting.insert(file); + }); +} + +void ClangdTestCompletion::getProposal(const QString &fileName, + TextEditor::ProposalModelPtr &proposalModel, + const QString &insertString, + int *cursorPos) +{ + const TextDocument * const doc = document(fileName); + QVERIFY(doc); + const int pos = doc->document()->toPlainText().indexOf(" /* COMPLETE HERE */"); + QVERIFY(pos != -1); + if (cursorPos) + *cursorPos = pos; + int line, column; + Utils::Text::convertPosition(doc->document(), pos, &line, &column); + const auto editor = qobject_cast( + EditorManager::openEditorAt(doc->filePath().toString(), line, column - 1)); + QVERIFY(editor); + QCOMPARE(EditorManager::currentEditor(), editor); + QCOMPARE(editor->textDocument(), doc); + + if (!insertString.isEmpty()) { + m_documentsWithHighlighting.remove(doc->filePath()); + editor->insert(insertString); + if (cursorPos) + *cursorPos += insertString.length(); + } + + // Once clangd has sent highlighting information for a file, we know it is also + // ready for completion. + QElapsedTimer highlightingTimer; + highlightingTimer.start(); + while (!m_documentsWithHighlighting.contains(doc->filePath()) + && highlightingTimer.elapsed() < timeOutInMs()) { + waitForSignalOrTimeout(client(), &ClangdClient::highlightingResultsReady, 1000); + }; + QVERIFY2(m_documentsWithHighlighting.contains(doc->filePath()), qPrintable(fileName)); + + QTimer timer; + timer.setSingleShot(true); + QEventLoop loop; + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + IAssistProposal *proposal = nullptr; + connect(client(), &ClangdClient::proposalReady, &loop, [&proposal, &loop](IAssistProposal *p) { + proposal = p; + loop.quit(); + }); + editor->editorWidget()->invokeAssist(Completion, nullptr); + timer.start(5000); + loop.exec(); + QVERIFY(timer.isActive()); + QVERIFY(proposal); + proposalModel = proposal->model(); + delete proposal; + + // The "dot" test files are only used once. + if (!insertString.isEmpty() && insertString != ".") { + m_documentsWithHighlighting.remove(doc->filePath()); + editor->editorWidget()->undo(); + } +} + +bool ClangdTestCompletion::hasItem(TextEditor::ProposalModelPtr model, const QString &text, + const QString &detail) +{ + for (int i = 0; i < model->size(); ++i) { + if (model->text(i) == text) { + if (detail.isEmpty()) + return true; + const auto genericModel = model.dynamicCast(); + return genericModel && genericModel->detail(i) == detail; + } + } + return false; +} + +bool ClangdTestCompletion::hasSnippet(TextEditor::ProposalModelPtr model, const QString &text) +{ + const auto genericModel = model.dynamicCast(); + if (!genericModel) + return false; + for (int i = 0, size = genericModel->size(); i < size; ++i) { + const TextEditor::AssistProposalItemInterface * const item = genericModel->proposalItem(i); + if (item->text() == text) + return item->isSnippet(); + } + return false; +} + +AssistProposalItemInterface *ClangdTestCompletion::getItem( + TextEditor::ProposalModelPtr model, const QString &text, const QString &detail) +{ + const auto genericModel = model.dynamicCast(); + if (!genericModel) + return nullptr; + for (int i = 0; i < genericModel->size(); ++i) { + if (genericModel->text(i) == text && + (detail.isEmpty() || detail == genericModel->detail(i))) { + return genericModel->proposalItem(i); + } + } + return nullptr; +} + } // namespace Tests } // namespace Internal } // namespace ClangCodeModel diff --git a/src/plugins/clangcodemodel/test/clangdtests.h b/src/plugins/clangcodemodel/test/clangdtests.h index 87553469124..0e7305bbba5 100644 --- a/src/plugins/clangcodemodel/test/clangdtests.h +++ b/src/plugins/clangcodemodel/test/clangdtests.h @@ -27,11 +27,13 @@ #include #include +#include #include #include #include #include +#include #include namespace ProjectExplorer { @@ -63,6 +65,8 @@ protected: TextEditor::TextDocument *document(const QString &fileName) const { return m_sourceDocuments.value(fileName); } + ProjectExplorer::Project *project() const { return m_project; } + void waitForNewClient(bool withIndex = true); protected slots: virtual void initTestCase(); @@ -142,6 +146,54 @@ private: TextEditor::HighlightingResults m_results; }; +class ClangdTestCompletion : public ClangdTest +{ + Q_OBJECT +public: + ClangdTestCompletion(); + +private slots: + void initTestCase() override; + + void testCompleteDoxygenKeywords(); + void testCompletePreprocessorKeywords(); + void testCompleteIncludeDirective(); + + void testCompleteGlobals(); + void testCompleteMembers(); + void testCompleteMembersFromInside(); + void testCompleteMembersFromOutside(); + void testCompleteMembersFromFriend(); + void testFunctionAddress(); + void testFunctionHints(); + void testFunctionHintsFiltered(); + void testFunctionHintConstructor(); + void testCompleteClassAndConstructor(); + + void testCompleteWithDotToArrowCorrection(); + void testDontCompleteWithDotToArrowCorrectionForFloats(); + + void testCompleteCodeInGeneratedUiFile(); + + void testSignalCompletion_data(); + void testSignalCompletion(); + + void testCompleteAfterProjectChange(); + +private: + void startCollectingHighlightingInfo(); + void getProposal(const QString &fileName, TextEditor::ProposalModelPtr &proposalModel, + const QString &insertString = {}, int *cursorPos = nullptr); + static bool hasItem(TextEditor::ProposalModelPtr model, const QString &text, + const QString &detail = {}); + static bool hasSnippet(TextEditor::ProposalModelPtr model, const QString &text); + static int itemsWithText(TextEditor::ProposalModelPtr model, const QString &text); + static TextEditor::AssistProposalItemInterface *getItem( + TextEditor::ProposalModelPtr model, const QString &text, const QString &detail = {}); + + QSet m_documentsWithHighlighting; +}; + } // namespace Tests } // namespace Internal } // namespace ClangCodeModel diff --git a/src/plugins/clangcodemodel/test/data/clangtestdata.qrc b/src/plugins/clangcodemodel/test/data/clangtestdata.qrc index 6655568da40..26b73373fcb 100644 --- a/src/plugins/clangcodemodel/test/data/clangtestdata.qrc +++ b/src/plugins/clangcodemodel/test/data/clangtestdata.qrc @@ -5,30 +5,11 @@ qt-widgets-app/mainwindow.h qt-widgets-app/mainwindow.ui qt-widgets-app/qt-widgets-app.pro - exampleIncludeDir/file.h - exampleIncludeDir/mylib/mylib.h - exampleIncludeDir/otherFile.h - completionWithProject.cpp - constructorCompletion.cpp - doxygenKeywordsCompletion.cpp - functionCompletion.cpp - globalCompletion.cpp - includeDirectiveCompletion.cpp - memberCompletion.cpp myheader.h mysource.cpp objc_messages_1.mm objc_messages_2.mm objc_messages_3.mm - preprocessorKeywordsCompletion.cpp - dotToArrowCorrection.cpp - noDotToArrowCorrectionForFloats.cpp - classAndConstructorCompletion.cpp - membercompletion-outside.cpp - membercompletion-inside.cpp - membercompletion-friend.cpp - functionCompletionFiltered.cpp - functionCompletionFiltered2.cpp find-usages/defs.h find-usages/main.cpp find-usages/find-usages.pro @@ -39,10 +20,38 @@ follow-symbol/main.cpp local-references/local-references.pro local-references/references.cpp + tooltips/subdir/tooltipinfo.h tooltips/tooltips.cpp tooltips/tooltips.pro highlighting/highlighting.cpp highlighting/highlighting.pro - tooltips/subdir/tooltipinfo.h + completion/completion.pro + completion/classAndConstructorCompletion.cpp + completion/completionWithProject.cpp + completion/constructorCompletion.cpp + completion/doxygenKeywordsCompletion.cpp + completion/functionCompletion.cpp + completion/functionCompletionFiltered.cpp + completion/functionCompletionFiltered2.cpp + completion/globalCompletion.cpp + completion/includeDirectiveCompletion.cpp + completion/membercompletion-friend.cpp + completion/membercompletion-inside.cpp + completion/membercompletion-outside.cpp + completion/memberCompletion.cpp + completion/preprocessorKeywordsCompletion.cpp + completion/exampleIncludeDir/file.h + completion/exampleIncludeDir/otherFile.h + completion/exampleIncludeDir/mylib/mylib.h + completion/dotToArrowCorrection.cpp + completion/noDotToArrowCorrectionForFloats.cpp + completion/main.cpp + completion/mainwindow.cpp + completion/mainwindow.h + completion/mainwindow.ui + completion/signalCompletion.cpp + completion/functionAddress.cpp + completion/preprocessorKeywordsCompletion2.cpp + completion/preprocessorKeywordsCompletion3.cpp diff --git a/src/plugins/clangcodemodel/test/data/classAndConstructorCompletion.cpp b/src/plugins/clangcodemodel/test/data/completion/classAndConstructorCompletion.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/classAndConstructorCompletion.cpp rename to src/plugins/clangcodemodel/test/data/completion/classAndConstructorCompletion.cpp diff --git a/src/plugins/clangcodemodel/test/data/completion/completion.pro b/src/plugins/clangcodemodel/test/data/completion/completion.pro new file mode 100644 index 00000000000..03eff0bd309 --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/completion/completion.pro @@ -0,0 +1,30 @@ +QT += widgets + +INCLUDEPATH += $$PWD/exampleIncludeDir + +SOURCES = \ + classAndConstructorCompletion.cpp \ + completionWithProject.cpp \ + constructorCompletion.cpp \ + dotToArrowCorrection.cpp \ + doxygenKeywordsCompletion.cpp \ + functionAddress.cpp \ + functionCompletion.cpp \ + functionCompletionFiltered2.cpp \ + functionCompletionFiltered.cpp \ + globalCompletion.cpp \ + includeDirectiveCompletion.cpp \ + main.cpp \ + mainwindow.cpp \ + memberCompletion.cpp \ + membercompletion-friend.cpp \ + membercompletion-inside.cpp \ + membercompletion-outside.cpp \ + noDotToArrowCorrectionForFloats.cpp \ + preprocessorKeywordsCompletion.cpp \ + preprocessorKeywordsCompletion2.cpp \ + preprocessorKeywordsCompletion3.cpp \ + signalCompletion.cpp + +HEADERS = mainwindow.h +FORMS = mainwindow.ui diff --git a/src/plugins/clangcodemodel/test/data/completionWithProject.cpp b/src/plugins/clangcodemodel/test/data/completion/completionWithProject.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/completionWithProject.cpp rename to src/plugins/clangcodemodel/test/data/completion/completionWithProject.cpp diff --git a/src/plugins/clangcodemodel/test/data/constructorCompletion.cpp b/src/plugins/clangcodemodel/test/data/completion/constructorCompletion.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/constructorCompletion.cpp rename to src/plugins/clangcodemodel/test/data/completion/constructorCompletion.cpp diff --git a/src/plugins/clangcodemodel/test/data/dotToArrowCorrection.cpp b/src/plugins/clangcodemodel/test/data/completion/dotToArrowCorrection.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/dotToArrowCorrection.cpp rename to src/plugins/clangcodemodel/test/data/completion/dotToArrowCorrection.cpp diff --git a/src/plugins/clangcodemodel/test/data/doxygenKeywordsCompletion.cpp b/src/plugins/clangcodemodel/test/data/completion/doxygenKeywordsCompletion.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/doxygenKeywordsCompletion.cpp rename to src/plugins/clangcodemodel/test/data/completion/doxygenKeywordsCompletion.cpp diff --git a/src/plugins/clangcodemodel/test/data/exampleIncludeDir/file.h b/src/plugins/clangcodemodel/test/data/completion/exampleIncludeDir/file.h similarity index 100% rename from src/plugins/clangcodemodel/test/data/exampleIncludeDir/file.h rename to src/plugins/clangcodemodel/test/data/completion/exampleIncludeDir/file.h diff --git a/src/plugins/clangcodemodel/test/data/exampleIncludeDir/mylib/mylib.h b/src/plugins/clangcodemodel/test/data/completion/exampleIncludeDir/mylib/mylib.h similarity index 100% rename from src/plugins/clangcodemodel/test/data/exampleIncludeDir/mylib/mylib.h rename to src/plugins/clangcodemodel/test/data/completion/exampleIncludeDir/mylib/mylib.h diff --git a/src/plugins/clangcodemodel/test/data/exampleIncludeDir/otherFile.h b/src/plugins/clangcodemodel/test/data/completion/exampleIncludeDir/otherFile.h similarity index 100% rename from src/plugins/clangcodemodel/test/data/exampleIncludeDir/otherFile.h rename to src/plugins/clangcodemodel/test/data/completion/exampleIncludeDir/otherFile.h diff --git a/src/plugins/clangcodemodel/test/data/completion/functionAddress.cpp b/src/plugins/clangcodemodel/test/data/completion/functionAddress.cpp new file mode 100644 index 00000000000..65c0548be6f --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/completion/functionAddress.cpp @@ -0,0 +1,8 @@ +struct S { + void memberFunc(); +}; + +void func() +{ + const auto p = &S::mem /* COMPLETE HERE */; +} diff --git a/src/plugins/clangcodemodel/test/data/functionCompletion.cpp b/src/plugins/clangcodemodel/test/data/completion/functionCompletion.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/functionCompletion.cpp rename to src/plugins/clangcodemodel/test/data/completion/functionCompletion.cpp diff --git a/src/plugins/clangcodemodel/test/data/functionCompletionFiltered.cpp b/src/plugins/clangcodemodel/test/data/completion/functionCompletionFiltered.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/functionCompletionFiltered.cpp rename to src/plugins/clangcodemodel/test/data/completion/functionCompletionFiltered.cpp diff --git a/src/plugins/clangcodemodel/test/data/functionCompletionFiltered2.cpp b/src/plugins/clangcodemodel/test/data/completion/functionCompletionFiltered2.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/functionCompletionFiltered2.cpp rename to src/plugins/clangcodemodel/test/data/completion/functionCompletionFiltered2.cpp diff --git a/src/plugins/clangcodemodel/test/data/globalCompletion.cpp b/src/plugins/clangcodemodel/test/data/completion/globalCompletion.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/globalCompletion.cpp rename to src/plugins/clangcodemodel/test/data/completion/globalCompletion.cpp diff --git a/src/plugins/clangcodemodel/test/data/includeDirectiveCompletion.cpp b/src/plugins/clangcodemodel/test/data/completion/includeDirectiveCompletion.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/includeDirectiveCompletion.cpp rename to src/plugins/clangcodemodel/test/data/completion/includeDirectiveCompletion.cpp diff --git a/src/plugins/clangcodemodel/test/data/completion/main.cpp b/src/plugins/clangcodemodel/test/data/completion/main.cpp new file mode 100644 index 00000000000..c5b6aa1432c --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/completion/main.cpp @@ -0,0 +1,36 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "mainwindow.h" +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/src/plugins/clangcodemodel/test/data/completion/mainwindow.cpp b/src/plugins/clangcodemodel/test/data/completion/mainwindow.cpp new file mode 100644 index 00000000000..31164d3797f --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/completion/mainwindow.cpp @@ -0,0 +1,40 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "mainwindow.h" +#include "ui_mainwindow.h" + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow) +{ + ui->setupUi(this); + ui-> /* COMPLETE HERE */ +} + +MainWindow::~MainWindow() +{ + delete ui; +} diff --git a/src/plugins/clangcodemodel/test/data/completion/mainwindow.h b/src/plugins/clangcodemodel/test/data/completion/mainwindow.h new file mode 100644 index 00000000000..596295b6430 --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/completion/mainwindow.h @@ -0,0 +1,44 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include + +namespace Ui { +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +private: + Ui::MainWindow *ui; +}; diff --git a/src/plugins/clangcodemodel/test/data/completion/mainwindow.ui b/src/plugins/clangcodemodel/test/data/completion/mainwindow.ui new file mode 100644 index 00000000000..f6ca1871f3f --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/completion/mainwindow.ui @@ -0,0 +1,20 @@ + + MainWindow + + + + 0 + 0 + 400 + 300 + + + + + + + + + + + diff --git a/src/plugins/clangcodemodel/test/data/memberCompletion.cpp b/src/plugins/clangcodemodel/test/data/completion/memberCompletion.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/memberCompletion.cpp rename to src/plugins/clangcodemodel/test/data/completion/memberCompletion.cpp diff --git a/src/plugins/clangcodemodel/test/data/membercompletion-friend.cpp b/src/plugins/clangcodemodel/test/data/completion/membercompletion-friend.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/membercompletion-friend.cpp rename to src/plugins/clangcodemodel/test/data/completion/membercompletion-friend.cpp diff --git a/src/plugins/clangcodemodel/test/data/membercompletion-inside.cpp b/src/plugins/clangcodemodel/test/data/completion/membercompletion-inside.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/membercompletion-inside.cpp rename to src/plugins/clangcodemodel/test/data/completion/membercompletion-inside.cpp diff --git a/src/plugins/clangcodemodel/test/data/membercompletion-outside.cpp b/src/plugins/clangcodemodel/test/data/completion/membercompletion-outside.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/membercompletion-outside.cpp rename to src/plugins/clangcodemodel/test/data/completion/membercompletion-outside.cpp diff --git a/src/plugins/clangcodemodel/test/data/noDotToArrowCorrectionForFloats.cpp b/src/plugins/clangcodemodel/test/data/completion/noDotToArrowCorrectionForFloats.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/noDotToArrowCorrectionForFloats.cpp rename to src/plugins/clangcodemodel/test/data/completion/noDotToArrowCorrectionForFloats.cpp diff --git a/src/plugins/clangcodemodel/test/data/preprocessorKeywordsCompletion.cpp b/src/plugins/clangcodemodel/test/data/completion/preprocessorKeywordsCompletion.cpp similarity index 100% rename from src/plugins/clangcodemodel/test/data/preprocessorKeywordsCompletion.cpp rename to src/plugins/clangcodemodel/test/data/completion/preprocessorKeywordsCompletion.cpp diff --git a/src/plugins/clangcodemodel/test/data/completion/preprocessorKeywordsCompletion2.cpp b/src/plugins/clangcodemodel/test/data/completion/preprocessorKeywordsCompletion2.cpp new file mode 100644 index 00000000000..fc59b8f0ed5 --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/completion/preprocessorKeywordsCompletion2.cpp @@ -0,0 +1,3 @@ +#if 1 +int x; +#en /* COMPLETE HERE */ diff --git a/src/plugins/clangcodemodel/test/data/completion/preprocessorKeywordsCompletion3.cpp b/src/plugins/clangcodemodel/test/data/completion/preprocessorKeywordsCompletion3.cpp new file mode 100644 index 00000000000..61cbdaef3a5 --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/completion/preprocessorKeywordsCompletion3.cpp @@ -0,0 +1,3 @@ +#if 0 +int x; +#en /* COMPLETE HERE */ diff --git a/src/plugins/clangcodemodel/test/data/completion/signalCompletion.cpp b/src/plugins/clangcodemodel/test/data/completion/signalCompletion.cpp new file mode 100644 index 00000000000..2c629915841 --- /dev/null +++ b/src/plugins/clangcodemodel/test/data/completion/signalCompletion.cpp @@ -0,0 +1,23 @@ +class QObject { +public: + void aSignal() __attribute__((annotate("qt_signal"))); + void anotherSignal() __attribute__((annotate("qt_signal"))); + void notASignal(); + static void connect(); + static void disconnect(); +}; +class DerivedFromQObject : public QObject { +public: + void myOwnSignal() __attribute__((annotate("qt_signal"))); + void alsoNotASignal(); +}; +class NotAQObject { +public: + void notASignal(); + void alsoNotASignal(); + static void connect(); +}; + +void someFunction() +{ + /* COMPLETE HERE */ diff --git a/src/plugins/cppeditor/cppeditordocument.cpp b/src/plugins/cppeditor/cppeditordocument.cpp index 7fef7b5eff7..4e7cba1852d 100644 --- a/src/plugins/cppeditor/cppeditordocument.cpp +++ b/src/plugins/cppeditor/cppeditordocument.cpp @@ -137,14 +137,28 @@ bool CppEditorDocument::isObjCEnabled() const return m_isObjCEnabled; } -CppTools::CppCompletionAssistProvider *CppEditorDocument::completionAssistProvider() const +void CppEditorDocument::setCompletionAssistProvider(TextEditor::CompletionAssistProvider *provider) { - return m_completionAssistProvider; + TextDocument::setCompletionAssistProvider(provider); + m_completionAssistProvider = nullptr; } -CppTools::CppCompletionAssistProvider *CppEditorDocument::functionHintAssistProvider() const +void CppEditorDocument::setFunctionHintAssistProvider(TextEditor::CompletionAssistProvider *provider) { - return m_functionHintAssistProvider; + TextDocument::setFunctionHintAssistProvider(provider); + m_functionHintAssistProvider = nullptr; +} + +CompletionAssistProvider *CppEditorDocument::completionAssistProvider() const +{ + return m_completionAssistProvider + ? m_completionAssistProvider : TextDocument::completionAssistProvider(); +} + +CompletionAssistProvider *CppEditorDocument::functionHintAssistProvider() const +{ + return m_functionHintAssistProvider + ? m_functionHintAssistProvider : TextDocument::functionHintAssistProvider(); } TextEditor::IAssistProvider *CppEditorDocument::quickFixAssistProvider() const diff --git a/src/plugins/cppeditor/cppeditordocument.h b/src/plugins/cppeditor/cppeditordocument.h index ebb3e498b8f..ffcc8a20880 100644 --- a/src/plugins/cppeditor/cppeditordocument.h +++ b/src/plugins/cppeditor/cppeditordocument.h @@ -52,8 +52,10 @@ public: explicit CppEditorDocument(); bool isObjCEnabled() const; - CppTools::CppCompletionAssistProvider *completionAssistProvider() const override; - CppTools::CppCompletionAssistProvider *functionHintAssistProvider() const override; + void setCompletionAssistProvider(TextEditor::CompletionAssistProvider *provider) override; + void setFunctionHintAssistProvider(TextEditor::CompletionAssistProvider *provider) override; + TextEditor::CompletionAssistProvider *completionAssistProvider() const override; + TextEditor::CompletionAssistProvider *functionHintAssistProvider() const override; TextEditor::IAssistProvider *quickFixAssistProvider() const override; void recalculateSemanticInfoDetached(); diff --git a/src/plugins/cppeditor/cppeditorwidget.cpp b/src/plugins/cppeditor/cppeditorwidget.cpp index 27e8b810dd5..ae8fd52db73 100644 --- a/src/plugins/cppeditor/cppeditorwidget.cpp +++ b/src/plugins/cppeditor/cppeditorwidget.cpp @@ -1007,8 +1007,8 @@ AssistInterface *CppEditorWidget::createAssistInterface(AssistKind kind, AssistR { if (kind == Completion || kind == FunctionHint) { CppCompletionAssistProvider * const cap = kind == Completion - ? cppEditorDocument()->completionAssistProvider() - : cppEditorDocument()->functionHintAssistProvider(); + ? qobject_cast(cppEditorDocument()->completionAssistProvider()) + : qobject_cast(cppEditorDocument()->functionHintAssistProvider()); if (cap) { LanguageFeatures features = LanguageFeatures::defaultFeatures(); if (Document::Ptr doc = d->m_lastSemanticInfo.doc) @@ -1019,6 +1019,8 @@ AssistInterface *CppEditorWidget::createAssistInterface(AssistKind kind, AssistR features, position(), reason); + } else { + return TextEditorWidget::createAssistInterface(kind, reason); } } else if (kind == QuickFix) { if (isSemanticInfoValid()) diff --git a/src/plugins/languageclient/client.cpp b/src/plugins/languageclient/client.cpp index a3eb48ac811..404767b004b 100644 --- a/src/plugins/languageclient/client.cpp +++ b/src/plugins/languageclient/client.cpp @@ -1017,6 +1017,13 @@ void Client::setSemanticTokensHandler(const SemanticTokensHandler &handler) m_tokenSupport.setTokensHandler(handler); } +#ifdef WITH_TESTS +void Client::forceHighlightingOnEmptyDelta() +{ + m_tokenSupport.forceHighlightingOnEmptyDelta(); +} +#endif + void Client::setSymbolStringifier(const LanguageServerProtocol::SymbolStringifier &stringifier) { m_symbolStringifier = stringifier; @@ -1027,6 +1034,46 @@ SymbolStringifier Client::symbolStringifier() const return m_symbolStringifier; } +void Client::setCompletionItemsTransformer(const CompletionItemsTransformer &transformer) +{ + if (const auto provider = qobject_cast( + m_clientProviders.completionAssistProvider)) { + provider->setItemsTransformer(transformer); + } +} + +void Client::setCompletionApplyHelper(const CompletionApplyHelper &applyHelper) +{ + if (const auto provider = qobject_cast( + m_clientProviders.completionAssistProvider)) { + provider->setApplyHelper(applyHelper); + } +} + +void Client::setCompletionProposalHandler(const ProposalHandler &handler) +{ + if (const auto provider = qobject_cast( + m_clientProviders.completionAssistProvider)) { + provider->setProposalHandler(handler); + } +} + +void Client::setFunctionHintProposalHandler(const ProposalHandler &handler) +{ + if (const auto provider = qobject_cast( + m_clientProviders.functionHintProvider)) { + provider->setProposalHandler(handler); + } +} + +void Client::setSnippetsGroup(const QString &group) +{ + if (const auto provider = qobject_cast( + m_clientProviders.completionAssistProvider)) { + provider->setSnippetsGroup(group); + } +} + void Client::start() { if (m_clientInterface->start()) diff --git a/src/plugins/languageclient/client.h b/src/plugins/languageclient/client.h index c4ca395465d..a5821732a92 100644 --- a/src/plugins/languageclient/client.h +++ b/src/plugins/languageclient/client.h @@ -183,6 +183,11 @@ public: void setSemanticTokensHandler(const SemanticTokensHandler &handler); void setSymbolStringifier(const LanguageServerProtocol::SymbolStringifier &stringifier); LanguageServerProtocol::SymbolStringifier symbolStringifier() const; + void setCompletionItemsTransformer(const CompletionItemsTransformer &transformer); + void setCompletionApplyHelper(const CompletionApplyHelper &applyHelper); + void setCompletionProposalHandler(const ProposalHandler &handler); + void setFunctionHintProposalHandler(const ProposalHandler &handler); + void setSnippetsGroup(const QString &group); // logging void log(const QString &message) const; @@ -190,6 +195,10 @@ public: void log(const LanguageServerProtocol::ResponseError &responseError) const { log(responseError.toString()); } +#ifdef WITH_TESTS + void forceHighlightingOnEmptyDelta(); +#endif + signals: void initialized(const LanguageServerProtocol::ServerCapabilities &capabilities); void capabilitiesChanged(const DynamicCapabilities &capabilities); diff --git a/src/plugins/languageclient/languageclientcompletionassist.cpp b/src/plugins/languageclient/languageclientcompletionassist.cpp index 3826f151cc0..7daedb9911b 100644 --- a/src/plugins/languageclient/languageclientcompletionassist.cpp +++ b/src/plugins/languageclient/languageclientcompletionassist.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -58,7 +59,7 @@ namespace LanguageClient { class LanguageClientCompletionItem : public AssistProposalItemInterface { public: - LanguageClientCompletionItem(CompletionItem item); + LanguageClientCompletionItem(CompletionItem item, const CompletionApplyHelper &applyHelper); // AssistProposalItemInterface interface QString text() const override; @@ -80,13 +81,15 @@ public: private: CompletionItem m_item; + const CompletionApplyHelper m_applyHelper; mutable QChar m_triggeredCommitCharacter; mutable QString m_sortText; mutable QString m_filterText; }; -LanguageClientCompletionItem::LanguageClientCompletionItem(CompletionItem item) - : m_item(std::move(item)) +LanguageClientCompletionItem::LanguageClientCompletionItem(CompletionItem item, + const CompletionApplyHelper &applyHelper) + : m_item(std::move(item)), m_applyHelper(applyHelper) { } QString LanguageClientCompletionItem::text() const @@ -107,10 +110,15 @@ bool LanguageClientCompletionItem::prematurelyApplies(const QChar &typedCharacte void LanguageClientCompletionItem::apply(TextDocumentManipulatorInterface &manipulator, int /*basePosition*/) const { - const int pos = manipulator.currentPosition(); + if (m_applyHelper) { + m_applyHelper(m_item, manipulator, m_triggeredCommitCharacter); + return; + } + if (auto edit = m_item.textEdit()) { applyTextEdit(manipulator, *edit, isSnippet()); } else { + const int pos = manipulator.currentPosition(); const QString textToInsert(m_item.insertText().value_or(text())); int length = 0; for (auto it = textToInsert.crbegin(), end = textToInsert.crend(); it != end; ++it) { @@ -253,16 +261,22 @@ public: void sort(const QString &/*prefix*/) override; bool supportsPrefixExpansion() const override { return false; } - QList items() const - { return Utils::static_container_cast(m_currentItems); } + QList items() const { return m_currentItems; } }; void LanguageClientCompletionModel::sort(const QString &/*prefix*/) { std::sort(m_currentItems.begin(), m_currentItems.end(), [] (AssistProposalItemInterface *a, AssistProposalItemInterface *b){ - return *(dynamic_cast(a)) < *( - dynamic_cast(b)); + const auto lca = dynamic_cast(a); + const auto lcb = dynamic_cast(b); + if (!lca && !lcb) + return a->text() < b->text(); + if (lca && lcb) + return *lca < *lcb; + if (lca && !lcb) + return true; + return false; }); } @@ -281,8 +295,10 @@ public: return false; return m_model->keepPerfectMatch(reason) - || !Utils::anyOf(m_model->items(), [this](LanguageClientCompletionItem *item){ - return item->isPerfectMatch(m_pos, m_document); + || !Utils::anyOf(m_model->items(), [this](AssistProposalItemInterface *item){ + if (const auto lcItem = dynamic_cast(item)) + return lcItem->isPerfectMatch(m_pos, m_document); + return false; }); } @@ -295,7 +311,11 @@ public: class LanguageClientCompletionAssistProcessor final : public IAssistProcessor { public: - LanguageClientCompletionAssistProcessor(Client *client); + LanguageClientCompletionAssistProcessor(Client *client, + const CompletionItemsTransformer &itemsTransformer, + const CompletionApplyHelper &applyHelper, + const ProposalHandler &proposalHandler, + const QString &snippetsGroup); ~LanguageClientCompletionAssistProcessor() override; IAssistProposal *perform(const AssistInterface *interface) override; bool running() override; @@ -306,15 +326,23 @@ private: void handleCompletionResponse(const CompletionRequest::Response &response); QPointer m_document; + Utils::FilePath m_filePath; QPointer m_client; Utils::optional m_currentRequest; QMetaObject::Connection m_postponedUpdateConnection; + const CompletionItemsTransformer m_itemsTransformer; + const CompletionApplyHelper m_applyHelper; + const ProposalHandler m_proposalHandler; + const QString m_snippetsGroup; int m_pos = -1; int m_basePos = -1; }; -LanguageClientCompletionAssistProcessor::LanguageClientCompletionAssistProcessor(Client *client) - : m_client(client) +LanguageClientCompletionAssistProcessor::LanguageClientCompletionAssistProcessor(Client *client, + const CompletionItemsTransformer &itemsTransformer, const CompletionApplyHelper &applyHelper, + const ProposalHandler &proposalHandler, const QString &snippetsGroup) + : m_client(client), m_itemsTransformer(itemsTransformer), m_applyHelper(applyHelper), + m_proposalHandler(proposalHandler), m_snippetsGroup(snippetsGroup) { } LanguageClientCompletionAssistProcessor::~LanguageClientCompletionAssistProcessor() @@ -384,6 +412,7 @@ IAssistProposal *LanguageClientCompletionAssistProcessor::perform(const AssistIn m_client->addAssistProcessor(this); m_currentRequest = completionRequest.id(); m_document = interface->textDocument(); + m_filePath = interface->filePath(); qCDebug(LOGLSPCOMPLETION) << QTime::currentTime() << " : request completions at " << m_pos << " by " << assistReasonString(interface->reason()); @@ -430,17 +459,28 @@ void LanguageClientCompletionAssistProcessor::handleCompletionResponse( } else if (Utils::holds_alternative>(*result)) { items = Utils::get>(*result); } + if (m_itemsTransformer && m_document) + items = m_itemsTransformer(m_filePath, m_document->toPlainText(), m_basePos, items); auto model = new LanguageClientCompletionModel(); - model->loadContent(Utils::transform(items, [](const CompletionItem &item){ - return static_cast(new LanguageClientCompletionItem(item)); - })); + auto proposalItems = Utils::transform>(items, + [this](const CompletionItem &item) { + return new LanguageClientCompletionItem(item, m_applyHelper); + }); + if (!m_snippetsGroup.isEmpty()) { + proposalItems << TextEditor::SnippetAssistCollector( + m_snippetsGroup, QIcon(":/texteditor/images/snippet.png")).collect(); + } + model->loadContent(proposalItems); LanguageClientCompletionProposal *proposal = new LanguageClientCompletionProposal(m_basePos, model); proposal->m_document = m_document; proposal->m_pos = m_pos; proposal->setFragile(true); proposal->setSupportsPrefix(false); - setAsyncProposalAvailable(proposal); + if (m_proposalHandler) + m_proposalHandler(proposal); + else + setAsyncProposalAvailable(proposal); m_client->removeAssistProcessor(this); qCDebug(LOGLSPCOMPLETION) << QTime::currentTime() << " : " << items.count() << " completions handled"; @@ -453,7 +493,8 @@ LanguageClientCompletionAssistProvider::LanguageClientCompletionAssistProvider(C IAssistProcessor *LanguageClientCompletionAssistProvider::createProcessor() const { - return new LanguageClientCompletionAssistProcessor(m_client); + return new LanguageClientCompletionAssistProcessor(m_client, m_itemsTransformer, m_applyHelper, + m_proposalHandler, m_snippetsGroup); } IAssistProvider::RunType LanguageClientCompletionAssistProvider::runType() const @@ -484,4 +525,16 @@ void LanguageClientCompletionAssistProvider::setTriggerCharacters( } } +void LanguageClientCompletionAssistProvider::setItemsTransformer( + const CompletionItemsTransformer &transformer) +{ + m_itemsTransformer = transformer; +} + +void LanguageClientCompletionAssistProvider::setApplyHelper( + const CompletionApplyHelper &applyHelper) +{ + m_applyHelper = applyHelper; +} + } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientcompletionassist.h b/src/plugins/languageclient/languageclientcompletionassist.h index 34bf7b209ac..9d0869c2b00 100644 --- a/src/plugins/languageclient/languageclientcompletionassist.h +++ b/src/plugins/languageclient/languageclientcompletionassist.h @@ -25,14 +25,30 @@ #pragma once +#include #include #include +#include + +namespace TextEditor { +class IAssistProposal; +class TextDocumentManipulatorInterface; +} + namespace LanguageClient { class Client; +using CompletionItemsTransformer = std::function( + const Utils::FilePath &, const QString &, int, + const QList &)>; +using CompletionApplyHelper = std::function; +using ProposalHandler = std::function; + class LanguageClientCompletionAssistProvider : public TextEditor::CompletionAssistProvider { Q_OBJECT @@ -49,8 +65,17 @@ public: void setTriggerCharacters(const Utils::optional> triggerChars); + void setItemsTransformer(const CompletionItemsTransformer &transformer); + void setApplyHelper(const CompletionApplyHelper &applyHelper); + void setProposalHandler(const ProposalHandler &handler) { m_proposalHandler = handler; } + void setSnippetsGroup(const QString &group) { m_snippetsGroup = group; } + private: QList m_triggerChars; + CompletionItemsTransformer m_itemsTransformer; + CompletionApplyHelper m_applyHelper; + ProposalHandler m_proposalHandler; + QString m_snippetsGroup; int m_activationCharSequenceLength = 0; Client *m_client = nullptr; // not owned }; diff --git a/src/plugins/languageclient/languageclientfunctionhint.cpp b/src/plugins/languageclient/languageclientfunctionhint.cpp index c650c3cdbf0..216baabfb6a 100644 --- a/src/plugins/languageclient/languageclientfunctionhint.cpp +++ b/src/plugins/languageclient/languageclientfunctionhint.cpp @@ -84,7 +84,8 @@ QString FunctionHintProposalModel::text(int index) const class FunctionHintProcessor : public IAssistProcessor { public: - explicit FunctionHintProcessor(Client *client) : m_client(client) {} + explicit FunctionHintProcessor(Client *client, const ProposalHandler &proposalHandler) + : m_client(client), m_proposalHandler(proposalHandler) {} IAssistProposal *perform(const AssistInterface *interface) override; bool running() override { return m_currentRequest.has_value(); } bool needsRestart() const override { return true; } @@ -92,8 +93,10 @@ public: private: void handleSignatureResponse(const SignatureHelpRequest::Response &response); + void processProposal(TextEditor::IAssistProposal *proposal); QPointer m_client; + const ProposalHandler m_proposalHandler; Utils::optional m_currentRequest; int m_pos = -1; }; @@ -129,18 +132,26 @@ void FunctionHintProcessor::handleSignatureResponse(const SignatureHelpRequest:: m_client->removeAssistProcessor(this); auto result = response.result().value_or(LanguageClientValue()); if (result.isNull()) { - setAsyncProposalAvailable(nullptr); + processProposal(nullptr); return; } const SignatureHelp &signatureHelp = result.value(); if (signatureHelp.signatures().isEmpty()) { - setAsyncProposalAvailable(nullptr); + processProposal(nullptr); } else { FunctionHintProposalModelPtr model(new FunctionHintProposalModel(signatureHelp)); - setAsyncProposalAvailable(new FunctionHintProposal(m_pos, model)); + processProposal(new FunctionHintProposal(m_pos, model)); } } +void FunctionHintProcessor::processProposal(IAssistProposal *proposal) +{ + if (m_proposalHandler) + m_proposalHandler(proposal); + else + setAsyncProposalAvailable(proposal); +} + FunctionHintAssistProvider::FunctionHintAssistProvider(Client *client) : CompletionAssistProvider(client) , m_client(client) @@ -148,7 +159,7 @@ FunctionHintAssistProvider::FunctionHintAssistProvider(Client *client) TextEditor::IAssistProcessor *FunctionHintAssistProvider::createProcessor() const { - return new FunctionHintProcessor(m_client); + return new FunctionHintProcessor(m_client, m_proposalHandler); } IAssistProvider::RunType FunctionHintAssistProvider::runType() const diff --git a/src/plugins/languageclient/languageclientfunctionhint.h b/src/plugins/languageclient/languageclientfunctionhint.h index 6d6181f4503..c8ef3a76e17 100644 --- a/src/plugins/languageclient/languageclientfunctionhint.h +++ b/src/plugins/languageclient/languageclientfunctionhint.h @@ -28,10 +28,14 @@ #include #include +namespace TextEditor { class IAssistProposal; } + namespace LanguageClient { class Client; +using ProposalHandler = std::function; + class FunctionHintAssistProvider : public TextEditor::CompletionAssistProvider { Q_OBJECT @@ -47,8 +51,12 @@ public: bool isContinuationChar(const QChar &c) const override; void setTriggerCharacters(const Utils::optional> &triggerChars); + + void setProposalHandler(const ProposalHandler &handler) { m_proposalHandler = handler; } + private: QList m_triggerChars; + ProposalHandler m_proposalHandler; int m_activationCharSequenceLength = 0; Client *m_client = nullptr; // not owned }; diff --git a/src/plugins/languageclient/languageclientmanager.cpp b/src/plugins/languageclient/languageclientmanager.cpp index 025240755c4..cc7ff0d4f63 100644 --- a/src/plugins/languageclient/languageclientmanager.cpp +++ b/src/plugins/languageclient/languageclientmanager.cpp @@ -218,10 +218,12 @@ void LanguageClientManager::deleteClient(Client *client) managerInstance->m_clients.removeAll(client); for (QVector &clients : managerInstance->m_clientsForSetting) clients.removeAll(client); - if (managerInstance->m_shuttingDown) + if (managerInstance->m_shuttingDown) { delete client; - else + } else { client->deleteLater(); + emit instance()->clientRemoved(client); + } } void LanguageClientManager::shutdown() diff --git a/src/plugins/languageclient/languageclientmanager.h b/src/plugins/languageclient/languageclientmanager.h index 9800db8a130..3b5dd59cbeb 100644 --- a/src/plugins/languageclient/languageclientmanager.h +++ b/src/plugins/languageclient/languageclientmanager.h @@ -99,6 +99,7 @@ public: static void showInspector(); signals: + void clientRemoved(Client *client); void shutdownFinished(); private: diff --git a/src/plugins/languageclient/semantichighlightsupport.cpp b/src/plugins/languageclient/semantichighlightsupport.cpp index e27c3fbbd55..57707c3271a 100644 --- a/src/plugins/languageclient/semantichighlightsupport.cpp +++ b/src/plugins/languageclient/semantichighlightsupport.cpp @@ -386,8 +386,11 @@ void SemanticTokenSupport::handleSemanticTokensDelta( m_tokens[filePath] = *tokens; } else if (auto tokensDelta = Utils::get_if(&result)) { QList edits = tokensDelta->edits(); - if (edits.isEmpty()) + if (edits.isEmpty()) { + if (m_highlightOnEmptyDelta) + highlight(filePath); return; + } Utils::sort(edits, &SemanticTokensEdit::start); diff --git a/src/plugins/languageclient/semantichighlightsupport.h b/src/plugins/languageclient/semantichighlightsupport.h index 03fc2445fa1..b1144ec5a73 100644 --- a/src/plugins/languageclient/semantichighlightsupport.h +++ b/src/plugins/languageclient/semantichighlightsupport.h @@ -81,6 +81,7 @@ public: // void setAdditionalTokenModifierStyles(const QHash &modifierStyles); void setTokensHandler(const SemanticTokensHandler &handler) { m_tokensHandler = handler; } + void forceHighlightingOnEmptyDelta() { m_highlightOnEmptyDelta = true; } private: LanguageServerProtocol::SemanticRequestTypes supportedSemanticRequests( @@ -106,6 +107,7 @@ private: SemanticTokensHandler m_tokensHandler; QStringList m_tokenTypeStrings; QStringList m_tokenModifierStrings; + bool m_highlightOnEmptyDelta = false; }; } // namespace LanguageClient diff --git a/src/plugins/texteditor/textdocument.h b/src/plugins/texteditor/textdocument.h index 98e6546045a..35ff55f27fd 100644 --- a/src/plugins/texteditor/textdocument.h +++ b/src/plugins/texteditor/textdocument.h @@ -142,9 +142,9 @@ public: virtual void triggerPendingUpdates(); - void setCompletionAssistProvider(CompletionAssistProvider *provider); + virtual void setCompletionAssistProvider(CompletionAssistProvider *provider); virtual CompletionAssistProvider *completionAssistProvider() const; - void setFunctionHintAssistProvider(CompletionAssistProvider *provider); + virtual void setFunctionHintAssistProvider(CompletionAssistProvider *provider); virtual CompletionAssistProvider *functionHintAssistProvider() const; void setQuickFixAssistProvider(IAssistProvider *provider) const; virtual IAssistProvider *quickFixAssistProvider() const; diff --git a/src/plugins/texteditor/texteditor.cpp b/src/plugins/texteditor/texteditor.cpp index 9bf59d20f8d..b68086ac97f 100644 --- a/src/plugins/texteditor/texteditor.cpp +++ b/src/plugins/texteditor/texteditor.cpp @@ -781,6 +781,7 @@ public: TextMark* m_dragMark = nullptr; QScopedPointer m_clipboardAssistProvider; + TextEditorWidget::AssistRequestHandler m_assistRequestHandler; QScopedPointer m_autoCompleter; CommentDefinition m_commentDefinition; @@ -3531,6 +3532,11 @@ void TextEditorWidget::showTextMarksToolTip(const QPoint &pos, d->showTextMarksToolTip(pos, marks, mainTextMark); } +void TextEditorWidget::setAssistRequestHandler(const AssistRequestHandler &handler) +{ + d->m_assistRequestHandler = handler; +} + void TextEditorWidgetPrivate::processTooltipRequest(const QTextCursor &c) { const QPoint toolTipPoint = q->toolTipPosition(c); @@ -8526,6 +8532,9 @@ QTextBlock TextEditorWidget::blockForVerticalOffset(int offset) const void TextEditorWidget::invokeAssist(AssistKind kind, IAssistProvider *provider) { + if (d->m_assistRequestHandler && d->m_assistRequestHandler(this, kind, provider)) + return; + if (kind == QuickFix && d->m_snippetOverlay->isVisible()) d->m_snippetOverlay->accept(); diff --git a/src/plugins/texteditor/texteditor.h b/src/plugins/texteditor/texteditor.h index 70224a18ff9..36de5e24632 100644 --- a/src/plugins/texteditor/texteditor.h +++ b/src/plugins/texteditor/texteditor.h @@ -288,6 +288,10 @@ public: const TextMarks &marks, const TextMark *mainTextMark = nullptr) const; + using AssistRequestHandler = std::function; + void setAssistRequestHandler(const AssistRequestHandler &handler); + void invokeAssist(AssistKind assistKind, IAssistProvider *provider = nullptr); virtual TextEditor::AssistInterface *createAssistInterface(AssistKind assistKind, diff --git a/tests/unit/unittest/activationsequencecontextprocessor-test.cpp b/tests/unit/unittest/activationsequencecontextprocessor-test.cpp index 62a70d752cb..84c2e87c76b 100644 --- a/tests/unit/unittest/activationsequencecontextprocessor-test.cpp +++ b/tests/unit/unittest/activationsequencecontextprocessor-test.cpp @@ -147,7 +147,7 @@ TEST(ActivationSequenceContextProcessor, TemplateFunctionLeftParen) TEST(ActivationSequenceContextProcessor, TemplateFunctionSecondParameter) { ClangCompletionAssistInterface interface("foo(", 7); - int startOfname = ContextProcessor::findStartOfName(&interface, + int startOfname = ContextProcessor::findStartOfName(interface.textDocument(), 6, ContextProcessor::NameCategory::Function);