diff options
author | Christian Kandeler <christian.kandeler@qt.io> | 2022-07-07 14:53:36 +0200 |
---|---|---|
committer | Christian Kandeler <christian.kandeler@qt.io> | 2022-07-08 11:58:42 +0000 |
commit | bb3549a7210ce599ea353af52807144e455dc206 (patch) | |
tree | 93eda5f4f2b78e171a817d34b2340b099010b56f | |
parent | f0794797564041ba8bbe7553b07aef4a1f74a047 (diff) | |
download | qt-creator-bb3549a7210ce599ea353af52807144e455dc206.tar.gz |
ClangCodeModel: Move completion code to its own set of files
Change-Id: Iad856881ace80896f4aceab3fbf129d7635de642
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: David Schulz <david.schulz@qt.io>
-rw-r--r-- | src/plugins/clangcodemodel/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangcodemodel.qbs | 2 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangdclient.cpp | 594 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangdclient.h | 3 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangdcompletion.cpp | 643 | ||||
-rw-r--r-- | src/plugins/clangcodemodel/clangdcompletion.h | 62 |
6 files changed, 709 insertions, 596 deletions
diff --git a/src/plugins/clangcodemodel/CMakeLists.txt b/src/plugins/clangcodemodel/CMakeLists.txt index 435299a20d..ae8558b23e 100644 --- a/src/plugins/clangcodemodel/CMakeLists.txt +++ b/src/plugins/clangcodemodel/CMakeLists.txt @@ -17,6 +17,7 @@ add_qtc_plugin(ClangCodeModel clangconstants.h clangdast.cpp clangdast.h clangdclient.cpp clangdclient.h + clangdcompletion.cpp clangdcompletion.h clangdfollowsymbol.cpp clangdfollowsymbol.h clangdiagnostictooltipwidget.cpp clangdiagnostictooltipwidget.h clangdquickfixes.cpp clangdquickfixes.h diff --git a/src/plugins/clangcodemodel/clangcodemodel.qbs b/src/plugins/clangcodemodel/clangcodemodel.qbs index ae4ebeb487..8551f63777 100644 --- a/src/plugins/clangcodemodel/clangcodemodel.qbs +++ b/src/plugins/clangcodemodel/clangcodemodel.qbs @@ -34,6 +34,8 @@ QtcPlugin { "clangdast.h", "clangdclient.cpp", "clangdclient.h", + "clangdcompletion.cpp", + "clangdcompletion.h", "clangdfollowsymbol.cpp", "clangdfollowsymbol.h", "clangdiagnostictooltipwidget.cpp", diff --git a/src/plugins/clangcodemodel/clangdclient.cpp b/src/plugins/clangcodemodel/clangdclient.cpp index e6681a49e3..27d3f57b1b 100644 --- a/src/plugins/clangcodemodel/clangdclient.cpp +++ b/src/plugins/clangcodemodel/clangdclient.cpp @@ -25,14 +25,13 @@ #include "clangdclient.h" -#include "clangcompletioncontextanalyzer.h" #include "clangconstants.h" #include "clangdast.h" +#include "clangdcompletion.h" #include "clangdfollowsymbol.h" #include "clangdlocatorfilters.h" #include "clangdquickfixes.h" #include "clangdswitchdecldef.h" -#include "clangpreprocessorassistproposalitem.h" #include "clangtextmark.h" #include "clangutils.h" #include "clangdsemantichighlighting.h" @@ -45,12 +44,7 @@ #include <cplusplus/ASTPath.h> #include <cplusplus/FindUsages.h> #include <cplusplus/Icons.h> -#include <cplusplus/MatchingText.h> -#include <cppeditor/cppeditorconstants.h> #include <cppeditor/cppcodemodelsettings.h> -#include <cppeditor/cppcompletionassistprocessor.h> -#include <cppeditor/cppcompletionassistprovider.h> -#include <cppeditor/cppdoxygen.h> #include <cppeditor/cppeditorwidget.h> #include <cppeditor/cppfindreferences.h> #include <cppeditor/cppmodelmanager.h> @@ -61,8 +55,6 @@ #include <cppeditor/semantichighlighter.h> #include <cppeditor/cppsemanticinfo.h> #include <languageclient/diagnosticmanager.h> -#include <languageclient/languageclientcompletionassist.h> -#include <languageclient/languageclientfunctionhint.h> #include <languageclient/languageclienthoverhandler.h> #include <languageclient/languageclientinterface.h> #include <languageclient/languageclientmanager.h> @@ -79,7 +71,6 @@ #include <texteditor/codeassist/iassistprocessor.h> #include <texteditor/codeassist/iassistprovider.h> #include <texteditor/codeassist/textdocumentmanipulatorinterface.h> -#include <texteditor/texteditorsettings.h> #include <texteditor/texteditor.h> #include <utils/algorithm.h> #include <utils/fileutils.h> @@ -119,8 +110,6 @@ namespace Internal { Q_LOGGING_CATEGORY(clangdLog, "qtc.clangcodemodel.clangd", QtWarningMsg); Q_LOGGING_CATEGORY(clangdLogAst, "qtc.clangcodemodel.clangd.ast", QtWarningMsg); static Q_LOGGING_CATEGORY(clangdLogServer, "qtc.clangcodemodel.clangd.server", QtWarningMsg); -static Q_LOGGING_CATEGORY(clangdLogCompletion, "qtc.clangcodemodel.clangd.completion", - QtWarningMsg); static QString indexingToken() { return "backgroundIndexProgress"; } static Usage::Type getUsageType(const ClangdAstPath &path) @@ -350,178 +339,6 @@ public: { insert("publishDiagnostics", caps); } }; - -enum class CustomAssistMode { Doxygen, Preprocessor, IncludePath }; -class CustomAssistProcessor : public IAssistProcessor -{ -public: - CustomAssistProcessor(ClangdClient *client, int position, int endPos, - unsigned completionOperator, CustomAssistMode mode) - : m_client(client) - , m_position(position) - , m_endPos(endPos) - , m_completionOperator(completionOperator) - , m_mode(mode) - {} - -private: - IAssistProposal *perform(const AssistInterface *interface) override - { - QList<AssistProposalItemInterface *> completions; - switch (m_mode) { - case CustomAssistMode::Doxygen: - for (int i = 1; i < CppEditor::T_DOXY_LAST_TAG; ++i) { - completions << createItem(QLatin1String(CppEditor::doxygenTagSpell(i)), - CPlusPlus::Icons::keywordIcon()); - } - break; - case CustomAssistMode::Preprocessor: { - static QIcon macroIcon = Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::Macro); - for (const QString &completion - : CppEditor::CppCompletionAssistProcessor::preprocessorCompletions()) { - completions << createItem(completion, macroIcon); - } - if (CppEditor::ProjectFile::isObjC(interface->filePath().toString())) - completions << createItem("import", macroIcon); - break; - } - case ClangCodeModel::Internal::CustomAssistMode::IncludePath: { - HeaderPaths headerPaths; - const CppEditor::ProjectPart::ConstPtr projectPart - = projectPartForFile(interface->filePath().toString()); - if (projectPart) - headerPaths = projectPart->headerPaths; - completions = completeInclude(m_endPos, m_completionOperator, interface, headerPaths); - break; - } - } - GenericProposalModelPtr model(new GenericProposalModel); - model->loadContent(completions); - const auto proposal = new GenericProposal(m_position, model); - if (m_client->testingEnabled()) { - emit m_client->proposalReady(proposal); - return nullptr; - } - return proposal; - } - - AssistProposalItemInterface *createItem(const QString &text, const QIcon &icon) const - { - const auto item = new ClangPreprocessorAssistProposalItem; - item->setText(text); - item->setIcon(icon); - item->setCompletionOperator(m_completionOperator); - return item; - } - - /** - * @brief Creates completion proposals for #include and given cursor - * @param position - cursor placed after opening bracked or quote - * @param completionOperator - the type of token - * @param interface - relevant document data - * @param headerPaths - the include paths - * @return the list of completion items - */ - static QList<AssistProposalItemInterface *> completeInclude( - int position, unsigned completionOperator, const TextEditor::AssistInterface *interface, - const ProjectExplorer::HeaderPaths &headerPaths) - { - QTextCursor cursor(interface->textDocument()); - cursor.setPosition(position); - QString directoryPrefix; - if (completionOperator == T_SLASH) { - QTextCursor c = cursor; - c.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor); - QString sel = c.selectedText(); - int startCharPos = sel.indexOf(QLatin1Char('"')); - if (startCharPos == -1) { - startCharPos = sel.indexOf(QLatin1Char('<')); - completionOperator = T_ANGLE_STRING_LITERAL; - } else { - completionOperator = T_STRING_LITERAL; - } - if (startCharPos != -1) - directoryPrefix = sel.mid(startCharPos + 1, sel.length() - 1); - } - - // Make completion for all relevant includes - ProjectExplorer::HeaderPaths allHeaderPaths = headerPaths; - const auto currentFilePath = ProjectExplorer::HeaderPath::makeUser( - interface->filePath().toFileInfo().path()); - if (!allHeaderPaths.contains(currentFilePath)) - allHeaderPaths.append(currentFilePath); - - const ::Utils::MimeType mimeType = ::Utils::mimeTypeForName("text/x-c++hdr"); - const QStringList suffixes = mimeType.suffixes(); - - QList<AssistProposalItemInterface *> completions; - for (const ProjectExplorer::HeaderPath &headerPath : qAsConst(allHeaderPaths)) { - QString realPath = headerPath.path; - if (!directoryPrefix.isEmpty()) { - realPath += QLatin1Char('/'); - realPath += directoryPrefix; - if (headerPath.type == ProjectExplorer::HeaderPathType::Framework) - realPath += QLatin1String(".framework/Headers"); - } - completions << completeIncludePath(realPath, suffixes, completionOperator); - } - - QList<QPair<AssistProposalItemInterface *, QString>> completionsForSorting; - for (AssistProposalItemInterface * const item : qAsConst(completions)) { - QString s = item->text(); - s.replace('/', QChar(0)); // The dir separator should compare less than anything else. - completionsForSorting << qMakePair(item, s); - } - Utils::sort(completionsForSorting, [](const auto &left, const auto &right) { - return left.second < right.second; - }); - for (int i = 0; i < completionsForSorting.count(); ++i) - completions[i] = completionsForSorting[i].first; - - return completions; - } - - /** - * @brief Finds #include completion proposals using given include path - * @param realPath - one of directories where compiler searches includes - * @param suffixes - file suffixes for C/C++ header files - * @return a list of matching completion items - */ - static QList<AssistProposalItemInterface *> completeIncludePath( - const QString &realPath, const QStringList &suffixes, unsigned completionOperator) - { - QList<AssistProposalItemInterface *> completions; - QDirIterator i(realPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); - //: Parent folder for proposed #include completion - const QString hint = ClangdClient::tr("Location: %1") - .arg(QDir::toNativeSeparators(QDir::cleanPath(realPath))); - while (i.hasNext()) { - const QString fileName = i.next(); - const QFileInfo fileInfo = i.fileInfo(); - const QString suffix = fileInfo.suffix(); - if (suffix.isEmpty() || suffixes.contains(suffix)) { - QString text = fileName.mid(realPath.length() + 1); - if (fileInfo.isDir()) - text += QLatin1Char('/'); - - auto *item = new ClangPreprocessorAssistProposalItem; - item->setText(text); - item->setDetail(hint); - item->setIcon(CPlusPlus::Icons::keywordIcon()); - item->setCompletionOperator(completionOperator); - completions.append(item); - } - } - return completions; - } - - ClangdClient * const m_client; - const int m_position; - const int m_endPos; - const unsigned m_completionOperator; - const CustomAssistMode m_mode; -}; - static qint64 getRevision(const TextDocument *doc) { return doc->document()->revision(); @@ -663,118 +480,6 @@ 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. - if (Utils::optional<CompletionItemCapbilities> completionItemCaps = completionItem()) { - completionItemCaps->setSnippetSupport(false); - setCompletionItem(*completionItemCaps); - } - } -}; - -class ClangdCompletionItem : public LanguageClientCompletionItem -{ -public: - using LanguageClientCompletionItem::LanguageClientCompletionItem; - void apply(TextDocumentManipulatorInterface &manipulator, - int basePosition) const override; - - enum class SpecialQtType { Signal, Slot, None }; - static SpecialQtType getQtType(const CompletionItem &item); - -private: - QIcon icon() const override; - QString text() const override; -}; - -class ClangdClient::ClangdCompletionAssistProcessor : public LanguageClientCompletionAssistProcessor -{ -public: - ClangdCompletionAssistProcessor(ClangdClient *client, const QString &snippetsGroup) - : LanguageClientCompletionAssistProcessor(client, snippetsGroup) - , m_client(client) - { - m_timer.start(); - } - - ~ClangdCompletionAssistProcessor() - { - qCDebug(clangdLogTiming).noquote().nospace() - << "ClangdCompletionAssistProcessor took: " << m_timer.elapsed() << " ms"; - } - -private: - IAssistProposal *perform(const AssistInterface *interface) override - { - if (m_client->d->isTesting) { - setAsyncCompletionAvailableHandler([this](IAssistProposal *proposal) { - emit m_client->proposalReady(proposal); - }); - } - return LanguageClientCompletionAssistProcessor::perform(interface); - } - - QList<AssistProposalItemInterface *> generateCompletionItems( - const QList<LanguageServerProtocol::CompletionItem> &items) const override; - - ClangdClient * const m_client; - QElapsedTimer m_timer; -}; - -QList<AssistProposalItemInterface *> -ClangdClient::ClangdCompletionAssistProcessor::generateCompletionItems( - const QList<LanguageServerProtocol::CompletionItem> &items) const -{ - qCDebug(clangdLog) << "received" << items.count() << "completions"; - - auto itemGenerator = [](const QList<LanguageServerProtocol::CompletionItem> &items) { - return Utils::transform<QList<AssistProposalItemInterface *>>( - items, [](const LanguageServerProtocol::CompletionItem &item) { - return new ClangdCompletionItem(item); - }); - }; - - // 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) { - return ClangdCompletionItem::getQtType(ci) == ClangdCompletionItem::SpecialQtType::Signal; - }; - const QTextDocument *doc = document(); - const int pos = basePos(); - if (!doc || pos < 0 || !Utils::anyOf(items, criterion)) - return itemGenerator(items); - const QString content = doc->toPlainText(); - const bool requiresSignal = CppEditor::CppModelManager::instance() - ->getSignalSlotType(filePath().toString(), content.toUtf8(), pos) - == CppEditor::SignalSlotType::NewStyleSignal; - if (requiresSignal) - return itemGenerator(Utils::filtered(items, criterion)); - return itemGenerator(items); -} - -class ClangdClient::ClangdCompletionAssistProvider : public LanguageClientCompletionAssistProvider -{ -public: - ClangdCompletionAssistProvider(ClangdClient *client); - -private: - IAssistProcessor *createProcessor(const AssistInterface *interface) const override; - - int activationCharSequenceLength() const override { return 3; } - bool isActivationCharSequence(const QString &sequence) const override; - bool isContinuationChar(const QChar &c) const override; - - bool isInCommentOrString(const AssistInterface *interface) const; - - ClangdClient * const m_client; -}; - static void addToCompilationDb(QJsonObject &cdb, const CppEditor::ProjectPart &projectPart, CppEditor::UsePrecompiledHeaders usePch, @@ -2118,303 +1823,6 @@ QString ClangdDiagnostic::category() const return typedValue<QString>("category"); } -class ClangdClient::ClangdFunctionHintProcessor : public FunctionHintProcessor -{ -public: - ClangdFunctionHintProcessor(ClangdClient *client) - : FunctionHintProcessor(client) - , m_client(client) - {} - -private: - IAssistProposal *perform(const AssistInterface *interface) override - { - if (m_client->d->isTesting) { - setAsyncCompletionAvailableHandler([this](IAssistProposal *proposal) { - emit m_client->proposalReady(proposal); - }); - } - return FunctionHintProcessor::perform(interface); - } - - ClangdClient * const m_client; -}; - -ClangdClient::ClangdCompletionAssistProvider::ClangdCompletionAssistProvider(ClangdClient *client) - : LanguageClientCompletionAssistProvider(client) - , m_client(client) -{} - -IAssistProcessor *ClangdClient::ClangdCompletionAssistProvider::createProcessor( - const AssistInterface *interface) const -{ - qCDebug(clangdLogCompletion) << "completion processor requested for" << interface->filePath(); - qCDebug(clangdLogCompletion) << "text before cursor is" - << interface->textAt(interface->position(), -10); - qCDebug(clangdLogCompletion) << "text after cursor is" - << interface->textAt(interface->position(), 10); - ClangCompletionContextAnalyzer contextAnalyzer(interface->textDocument(), - interface->position(), false, {}); - contextAnalyzer.analyze(); - switch (contextAnalyzer.completionAction()) { - case ClangCompletionContextAnalyzer::PassThroughToLibClangAfterLeftParen: - qCDebug(clangdLogCompletion) << "creating function hint processor"; - return new ClangdFunctionHintProcessor(m_client); - case ClangCompletionContextAnalyzer::CompleteDoxygenKeyword: - qCDebug(clangdLogCompletion) << "creating doxygen processor"; - return new CustomAssistProcessor(m_client, - contextAnalyzer.positionForProposal(), - contextAnalyzer.positionEndOfExpression(), - contextAnalyzer.completionOperator(), - CustomAssistMode::Doxygen); - case ClangCompletionContextAnalyzer::CompletePreprocessorDirective: - qCDebug(clangdLogCompletion) << "creating macro processor"; - return new CustomAssistProcessor(m_client, - contextAnalyzer.positionForProposal(), - contextAnalyzer.positionEndOfExpression(), - contextAnalyzer.completionOperator(), - CustomAssistMode::Preprocessor); - case ClangCompletionContextAnalyzer::CompleteSignal: - case ClangCompletionContextAnalyzer::CompleteSlot: - if (!interface->isBaseObject()) - return CppEditor::getCppCompletionAssistProcessor(); - default: - break; - } - const QString snippetsGroup = contextAnalyzer.addSnippets() && !isInCommentOrString(interface) - ? CppEditor::Constants::CPP_SNIPPETS_GROUP_ID - : QString(); - qCDebug(clangdLogCompletion) << "creating proper completion processor" - << (snippetsGroup.isEmpty() ? "without" : "with") << "snippets"; - return new ClangdCompletionAssistProcessor(m_client, snippetsGroup); -} - -bool ClangdClient::ClangdCompletionAssistProvider::isActivationCharSequence(const QString &sequence) const -{ - const QChar &ch = sequence.at(2); - const QChar &ch2 = sequence.at(1); - const QChar &ch3 = sequence.at(0); - unsigned kind = T_EOF_SYMBOL; - const int pos = CppEditor::CppCompletionAssistProvider::activationSequenceChar( - ch, ch2, ch3, &kind, false, false); - if (pos == 0) - return false; - - // We want to minimize unneeded completion requests, as those trigger document updates, - // which trigger re-highlighting and diagnostics, which we try to delay. - // Therefore, we do not trigger on syntax elements that often occur in non-applicable - // contexts, such as '(', '<' or '/'. - switch (kind) { - case T_DOT: case T_COLON_COLON: case T_ARROW: case T_DOT_STAR: case T_ARROW_STAR: case T_POUND: - qCDebug(clangdLogCompletion) << "detected" << sequence << "as activation char sequence"; - return true; - } - return false; -} - -bool ClangdClient::ClangdCompletionAssistProvider::isContinuationChar(const QChar &c) const -{ - return CppEditor::isValidIdentifierChar(c); -} - -bool ClangdClient::ClangdCompletionAssistProvider::isInCommentOrString( - const AssistInterface *interface) const -{ - LanguageFeatures features = LanguageFeatures::defaultFeatures(); - features.objCEnabled = CppEditor::ProjectFile::isObjC(interface->filePath().toString()); - return CppEditor::isInCommentOrString(interface, features); -} - -void ClangdCompletionItem::apply(TextDocumentManipulatorInterface &manipulator, - int /*basePosition*/) const -{ - const LanguageServerProtocol::CompletionItem item = this->item(); - QChar typedChar = triggeredCommitCharacter(); - const auto edit = item.textEdit(); - if (!edit) - return; - - const int labelOpenParenOffset = item.label().indexOf('('); - const int labelClosingParenOffset = item.label().indexOf(')'); - const auto kind = static_cast<CompletionItemKind::Kind>( - item.kind().value_or(CompletionItemKind::Text)); - const bool isMacroCall = kind == CompletionItemKind::Text && labelOpenParenOffset != -1 - && labelClosingParenOffset > labelOpenParenOffset; // Heuristic - const bool isFunctionLike = kind == CompletionItemKind::Function - || kind == CompletionItemKind::Method || kind == CompletionItemKind::Constructor - || isMacroCall; - - QString rawInsertText = edit->newText(); - - // Some preparation for our magic involving (non-)insertion of parentheses and - // cursor placement. - if (isFunctionLike && !rawInsertText.contains('(')) { - if (labelOpenParenOffset != -1) { - if (labelClosingParenOffset == labelOpenParenOffset + 1) // function takes no arguments - rawInsertText += "()"; - else // function takes arguments - rawInsertText += "( )"; - } - } - - const int firstParenOffset = rawInsertText.indexOf('('); - const int lastParenOffset = rawInsertText.lastIndexOf(')'); - const QString detail = item.detail().value_or(QString()); - const CompletionSettings &completionSettings = TextEditorSettings::completionSettings(); - QString textToBeInserted = rawInsertText.left(firstParenOffset); - QString extraCharacters; - int extraLength = 0; - int cursorOffset = 0; - bool setAutoCompleteSkipPos = false; - int currentPos = manipulator.currentPosition(); - const QTextDocument * const doc = manipulator.textCursorAt(currentPos).document(); - const Range range = edit->range(); - const int rangeStart = range.start().toPositionInDocument(doc); - if (isFunctionLike && 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 && !isMacroCall && matchPreviousWord(manipulator, cursor, detail)) - abandonParen = true; // function definition - 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(currentPos); - 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(currentPos + 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; - } - - // Avoid inserting characters that are already there - QTextCursor cursor = manipulator.textCursorAt(rangeStart); - cursor.movePosition(QTextCursor::EndOfWord); - const QString textAfterCursor = manipulator.textAt(currentPos, cursor.position() - currentPos); - if (currentPos < cursor.position() - && textToBeInserted != textAfterCursor - && textToBeInserted.indexOf(textAfterCursor, currentPos - rangeStart) >= 0) { - currentPos = cursor.position(); - } - for (int i = 0; i < extraCharacters.length(); ++i) { - const QChar a = extraCharacters.at(i); - const QChar b = manipulator.characterAt(currentPos + i); - if (a == b) - ++extraLength; - else - break; - } - - textToBeInserted += extraCharacters; - const int length = currentPos - rangeStart + extraLength; - const bool isReplaced = manipulator.replace(rangeStart, length, 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); - } -} - -ClangdCompletionItem::SpecialQtType ClangdCompletionItem::getQtType(const CompletionItem &item) -{ - const Utils::optional<MarkupOrString> doc = item.documentation(); - if (!doc) - return SpecialQtType::None; - QString docText; - if (Utils::holds_alternative<QString>(*doc)) - docText = Utils::get<QString>(*doc); - else if (Utils::holds_alternative<MarkupContent>(*doc)) - docText = Utils::get<MarkupContent>(*doc).content(); - if (docText.contains("Annotation: qt_signal")) - return SpecialQtType::Signal; - if (docText.contains("Annotation: qt_slot")) - return SpecialQtType::Slot; - return SpecialQtType::None; -} - -QIcon ClangdCompletionItem::icon() const -{ - if (isDeprecated()) - return Utils::Icons::WARNING.icon(); - const SpecialQtType qtType = getQtType(item()); - switch (qtType) { - case SpecialQtType::Signal: - return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::Signal); - case SpecialQtType::Slot: - // FIXME: Add visibility info to completion item tags in clangd? - return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::SlotPublic); - case SpecialQtType::None: - break; - } - if (item().kind().value_or(CompletionItemKind::Text) == CompletionItemKind::Property) - return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::VarPublicStatic); - return LanguageClientCompletionItem::icon(); -} - -QString ClangdCompletionItem::text() const -{ - const QString clangdValue = LanguageClientCompletionItem::text(); - if (isDeprecated()) - return "[[deprecated]]" + clangdValue; - return clangdValue; -} - MessageId ClangdClient::Private::getAndHandleAst(const TextDocOrFile &doc, const AstHandler &astHandler, AstCallbackMode callbackMode, const Range &range) diff --git a/src/plugins/clangcodemodel/clangdclient.h b/src/plugins/clangcodemodel/clangdclient.h index 2aec6f8b57..5f2329b594 100644 --- a/src/plugins/clangcodemodel/clangdclient.h +++ b/src/plugins/clangcodemodel/clangdclient.h @@ -149,9 +149,6 @@ private: class Private; class VirtualFunctionAssistProcessor; class VirtualFunctionAssistProvider; - class ClangdFunctionHintProcessor; - class ClangdCompletionAssistProcessor; - class ClangdCompletionAssistProvider; Private * const d; }; diff --git a/src/plugins/clangcodemodel/clangdcompletion.cpp b/src/plugins/clangcodemodel/clangdcompletion.cpp new file mode 100644 index 0000000000..6109477270 --- /dev/null +++ b/src/plugins/clangcodemodel/clangdcompletion.cpp @@ -0,0 +1,643 @@ +/**************************************************************************** +** +** Copyright (C) 2022 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 "clangdcompletion.h" + +#include "clangcompletioncontextanalyzer.h" +#include "clangdclient.h" +#include "clangpreprocessorassistproposalitem.h" +#include "clangutils.h" +#include "tasktimers.h" + +#include <cppeditor/cppcompletionassistprocessor.h> +#include <cppeditor/cppcompletionassistprovider.h> +#include <cppeditor/cppdoxygen.h> +#include <cppeditor/cppeditorconstants.h> +#include <cppeditor/cppmodelmanager.h> +#include <cppeditor/cppprojectfile.h> +#include <cppeditor/projectpart.h> +#include <cplusplus/Icons.h> +#include <cplusplus/MatchingText.h> +#include <languageclient/languageclientfunctionhint.h> +#include <projectexplorer/headerpath.h> +#include <texteditor/codeassist/assistinterface.h> +#include <texteditor/codeassist/genericproposal.h> +#include <texteditor/codeassist/genericproposalmodel.h> +#include <texteditor/texteditorsettings.h> +#include <utils/utilsicons.h> + +using namespace CppEditor; +using namespace CPlusPlus; +using namespace LanguageClient; +using namespace LanguageServerProtocol; +using namespace ProjectExplorer; +using namespace TextEditor; +using namespace Utils; + +namespace ClangCodeModel::Internal { +static Q_LOGGING_CATEGORY(clangdLogCompletion, "qtc.clangcodemodel.clangd.completion", + QtWarningMsg); + +enum class CustomAssistMode { Doxygen, Preprocessor, IncludePath }; +class CustomAssistProcessor : public IAssistProcessor +{ +public: + CustomAssistProcessor(ClangdClient *client, int position, int endPos, + unsigned completionOperator, CustomAssistMode mode); + +private: + IAssistProposal *perform(const AssistInterface *interface) override; + + AssistProposalItemInterface *createItem(const QString &text, const QIcon &icon) const; + + static QList<AssistProposalItemInterface *> completeInclude( + int position, unsigned completionOperator, const AssistInterface *interface, + const HeaderPaths &headerPaths); + static QList<AssistProposalItemInterface *> completeIncludePath( + const QString &realPath, const QStringList &suffixes, unsigned completionOperator); + + ClangdClient * const m_client; + const int m_position; + const int m_endPos; + const unsigned m_completionOperator; + const CustomAssistMode m_mode; +}; + +class ClangdCompletionItem : public LanguageClientCompletionItem +{ +public: + using LanguageClientCompletionItem::LanguageClientCompletionItem; + void apply(TextDocumentManipulatorInterface &manipulator, + int basePosition) const override; + + enum class SpecialQtType { Signal, Slot, None }; + static SpecialQtType getQtType(const CompletionItem &item); + +private: + QIcon icon() const override; + QString text() const override; +}; + +class ClangdCompletionAssistProcessor : public LanguageClientCompletionAssistProcessor +{ +public: + ClangdCompletionAssistProcessor(ClangdClient *client, const QString &snippetsGroup); + ~ClangdCompletionAssistProcessor(); + +private: + IAssistProposal *perform(const AssistInterface *interface) override; + QList<AssistProposalItemInterface *> generateCompletionItems( + const QList<LanguageServerProtocol::CompletionItem> &items) const override; + + ClangdClient * const m_client; + QElapsedTimer m_timer; +}; + +class ClangdFunctionHintProcessor : public FunctionHintProcessor +{ +public: + ClangdFunctionHintProcessor(ClangdClient *client); + +private: + IAssistProposal *perform(const AssistInterface *interface) override; + + ClangdClient * const m_client; +}; + +ClangdCompletionAssistProvider::ClangdCompletionAssistProvider(ClangdClient *client) + : LanguageClientCompletionAssistProvider(client) + , m_client(client) +{} + +IAssistProcessor *ClangdCompletionAssistProvider::createProcessor( + const AssistInterface *interface) const +{ + qCDebug(clangdLogCompletion) << "completion processor requested for" << interface->filePath(); + qCDebug(clangdLogCompletion) << "text before cursor is" + << interface->textAt(interface->position(), -10); + qCDebug(clangdLogCompletion) << "text after cursor is" + << interface->textAt(interface->position(), 10); + ClangCompletionContextAnalyzer contextAnalyzer(interface->textDocument(), + interface->position(), false, {}); + contextAnalyzer.analyze(); + switch (contextAnalyzer.completionAction()) { + case ClangCompletionContextAnalyzer::PassThroughToLibClangAfterLeftParen: + qCDebug(clangdLogCompletion) << "creating function hint processor"; + return new ClangdFunctionHintProcessor(m_client); + case ClangCompletionContextAnalyzer::CompleteDoxygenKeyword: + qCDebug(clangdLogCompletion) << "creating doxygen processor"; + return new CustomAssistProcessor(m_client, + contextAnalyzer.positionForProposal(), + contextAnalyzer.positionEndOfExpression(), + contextAnalyzer.completionOperator(), + CustomAssistMode::Doxygen); + case ClangCompletionContextAnalyzer::CompletePreprocessorDirective: + qCDebug(clangdLogCompletion) << "creating macro processor"; + return new CustomAssistProcessor(m_client, + contextAnalyzer.positionForProposal(), + contextAnalyzer.positionEndOfExpression(), + contextAnalyzer.completionOperator(), + CustomAssistMode::Preprocessor); + case ClangCompletionContextAnalyzer::CompleteSignal: + case ClangCompletionContextAnalyzer::CompleteSlot: + if (!interface->isBaseObject()) + return CppEditor::getCppCompletionAssistProcessor(); + default: + break; + } + const QString snippetsGroup = contextAnalyzer.addSnippets() && !isInCommentOrString(interface) + ? CppEditor::Constants::CPP_SNIPPETS_GROUP_ID + : QString(); + qCDebug(clangdLogCompletion) << "creating proper completion processor" + << (snippetsGroup.isEmpty() ? "without" : "with") << "snippets"; + return new ClangdCompletionAssistProcessor(m_client, snippetsGroup); +} + +bool ClangdCompletionAssistProvider::isActivationCharSequence(const QString &sequence) const +{ + const QChar &ch = sequence.at(2); + const QChar &ch2 = sequence.at(1); + const QChar &ch3 = sequence.at(0); + unsigned kind = T_EOF_SYMBOL; + const int pos = CppCompletionAssistProvider::activationSequenceChar( + ch, ch2, ch3, &kind, false, false); + if (pos == 0) + return false; + + // We want to minimize unneeded completion requests, as those trigger document updates, + // which trigger re-highlighting and diagnostics, which we try to delay. + // Therefore, we do not trigger on syntax elements that often occur in non-applicable + // contexts, such as '(', '<' or '/'. + switch (kind) { + case T_DOT: case T_COLON_COLON: case T_ARROW: case T_DOT_STAR: case T_ARROW_STAR: case T_POUND: + qCDebug(clangdLogCompletion) << "detected" << sequence << "as activation char sequence"; + return true; + } + return false; +} + +bool ClangdCompletionAssistProvider::isContinuationChar(const QChar &c) const +{ + return isValidIdentifierChar(c); +} + +bool ClangdCompletionAssistProvider::isInCommentOrString(const AssistInterface *interface) const +{ + LanguageFeatures features = LanguageFeatures::defaultFeatures(); + features.objCEnabled = ProjectFile::isObjC(interface->filePath().toString()); + return CppEditor::isInCommentOrString(interface, features); +} + +void ClangdCompletionItem::apply(TextDocumentManipulatorInterface &manipulator, + int /*basePosition*/) const +{ + const CompletionItem item = this->item(); + QChar typedChar = triggeredCommitCharacter(); + const auto edit = item.textEdit(); + if (!edit) + return; + + const int labelOpenParenOffset = item.label().indexOf('('); + const int labelClosingParenOffset = item.label().indexOf(')'); + const auto kind = static_cast<CompletionItemKind::Kind>( + item.kind().value_or(CompletionItemKind::Text)); + const bool isMacroCall = kind == CompletionItemKind::Text && labelOpenParenOffset != -1 + && labelClosingParenOffset > labelOpenParenOffset; // Heuristic + const bool isFunctionLike = kind == CompletionItemKind::Function + || kind == CompletionItemKind::Method || kind == CompletionItemKind::Constructor + || isMacroCall; + + QString rawInsertText = edit->newText(); + + // Some preparation for our magic involving (non-)insertion of parentheses and + // cursor placement. + if (isFunctionLike && !rawInsertText.contains('(')) { + if (labelOpenParenOffset != -1) { + if (labelClosingParenOffset == labelOpenParenOffset + 1) // function takes no arguments + rawInsertText += "()"; + else // function takes arguments + rawInsertText += "( )"; + } + } + + const int firstParenOffset = rawInsertText.indexOf('('); + const int lastParenOffset = rawInsertText.lastIndexOf(')'); + const QString detail = item.detail().value_or(QString()); + const CompletionSettings &completionSettings = TextEditorSettings::completionSettings(); + QString textToBeInserted = rawInsertText.left(firstParenOffset); + QString extraCharacters; + int extraLength = 0; + int cursorOffset = 0; + bool setAutoCompleteSkipPos = false; + int currentPos = manipulator.currentPosition(); + const QTextDocument * const doc = manipulator.textCursorAt(currentPos).document(); + const Range range = edit->range(); + const int rangeStart = range.start().toPositionInDocument(doc); + if (isFunctionLike && 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 && !isMacroCall && matchPreviousWord(manipulator, cursor, detail)) + abandonParen = true; // function definition + 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(currentPos); + 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(currentPos + 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; + } + + // Avoid inserting characters that are already there + QTextCursor cursor = manipulator.textCursorAt(rangeStart); + cursor.movePosition(QTextCursor::EndOfWord); + const QString textAfterCursor = manipulator.textAt(currentPos, cursor.position() - currentPos); + if (currentPos < cursor.position() + && textToBeInserted != textAfterCursor + && textToBeInserted.indexOf(textAfterCursor, currentPos - rangeStart) >= 0) { + currentPos = cursor.position(); + } + for (int i = 0; i < extraCharacters.length(); ++i) { + const QChar a = extraCharacters.at(i); + const QChar b = manipulator.characterAt(currentPos + i); + if (a == b) + ++extraLength; + else + break; + } + + textToBeInserted += extraCharacters; + const int length = currentPos - rangeStart + extraLength; + const bool isReplaced = manipulator.replace(rangeStart, length, 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); + } +} + +ClangdCompletionItem::SpecialQtType ClangdCompletionItem::getQtType(const CompletionItem &item) +{ + const Utils::optional<MarkupOrString> doc = item.documentation(); + if (!doc) + return SpecialQtType::None; + QString docText; + if (Utils::holds_alternative<QString>(*doc)) + docText = Utils::get<QString>(*doc); + else if (Utils::holds_alternative<MarkupContent>(*doc)) + docText = Utils::get<MarkupContent>(*doc).content(); + if (docText.contains("Annotation: qt_signal")) + return SpecialQtType::Signal; + if (docText.contains("Annotation: qt_slot")) + return SpecialQtType::Slot; + return SpecialQtType::None; +} + +QIcon ClangdCompletionItem::icon() const +{ + if (isDeprecated()) + return Utils::Icons::WARNING.icon(); + const SpecialQtType qtType = getQtType(item()); + switch (qtType) { + case SpecialQtType::Signal: + return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::Signal); + case SpecialQtType::Slot: + // FIXME: Add visibility info to completion item tags in clangd? + return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::SlotPublic); + case SpecialQtType::None: + break; + } + if (item().kind().value_or(CompletionItemKind::Text) == CompletionItemKind::Property) + return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::VarPublicStatic); + return LanguageClientCompletionItem::icon(); +} + +QString ClangdCompletionItem::text() const +{ + const QString clangdValue = LanguageClientCompletionItem::text(); + if (isDeprecated()) + return "[[deprecated]]" + clangdValue; + return clangdValue; +} + +CustomAssistProcessor::CustomAssistProcessor(ClangdClient *client, int position, int endPos, + unsigned completionOperator, CustomAssistMode mode) + : m_client(client) + , m_position(position) + , m_endPos(endPos) + , m_completionOperator(completionOperator) + , m_mode(mode) +{} + +IAssistProposal *CustomAssistProcessor::perform(const AssistInterface *interface) +{ + QList<AssistProposalItemInterface *> completions; + switch (m_mode) { + case CustomAssistMode::Doxygen: + for (int i = 1; i < T_DOXY_LAST_TAG; ++i) { + completions << createItem(QLatin1String(doxygenTagSpell(i)), + CPlusPlus::Icons::keywordIcon()); + } + break; + case CustomAssistMode::Preprocessor: { + static QIcon macroIcon = Utils::CodeModelIcon::iconForType(CodeModelIcon::Macro); + for (const QString &completion + : CppCompletionAssistProcessor::preprocessorCompletions()) { + completions << createItem(completion, macroIcon); + } + if (ProjectFile::isObjC(interface->filePath().toString())) + completions << createItem("import", macroIcon); + break; + } + case CustomAssistMode::IncludePath: { + HeaderPaths headerPaths; + const ProjectPart::ConstPtr projectPart + = projectPartForFile(interface->filePath().toString()); + if (projectPart) + headerPaths = projectPart->headerPaths; + completions = completeInclude(m_endPos, m_completionOperator, interface, headerPaths); + break; + } + } + GenericProposalModelPtr model(new GenericProposalModel); + model->loadContent(completions); + const auto proposal = new GenericProposal(m_position, model); + if (m_client->testingEnabled()) { + emit m_client->proposalReady(proposal); + return nullptr; + } + return proposal; +} + +AssistProposalItemInterface *CustomAssistProcessor::createItem(const QString &text, + const QIcon &icon) const +{ + const auto item = new ClangPreprocessorAssistProposalItem; + item->setText(text); + item->setIcon(icon); + item->setCompletionOperator(m_completionOperator); + return item; +} + +/** + * @brief Creates completion proposals for #include and given cursor + * @param position - cursor placed after opening bracked or quote + * @param completionOperator - the type of token + * @param interface - relevant document data + * @param headerPaths - the include paths + * @return the list of completion items + */ +QList<AssistProposalItemInterface *> CustomAssistProcessor::completeInclude( + int position, unsigned completionOperator, const AssistInterface *interface, + const HeaderPaths &headerPaths) +{ + QTextCursor cursor(interface->textDocument()); + cursor.setPosition(position); + QString directoryPrefix; + if (completionOperator == T_SLASH) { + QTextCursor c = cursor; + c.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor); + QString sel = c.selectedText(); + int startCharPos = sel.indexOf(QLatin1Char('"')); + if (startCharPos == -1) { + startCharPos = sel.indexOf(QLatin1Char('<')); + completionOperator = T_ANGLE_STRING_LITERAL; + } else { + completionOperator = T_STRING_LITERAL; + } + if (startCharPos != -1) + directoryPrefix = sel.mid(startCharPos + 1, sel.length() - 1); + } + + // Make completion for all relevant includes + HeaderPaths allHeaderPaths = headerPaths; + const auto currentFilePath = HeaderPath::makeUser( + interface->filePath().toFileInfo().path()); + if (!allHeaderPaths.contains(currentFilePath)) + allHeaderPaths.append(currentFilePath); + + const MimeType mimeType = mimeTypeForName("text/x-c++hdr"); + const QStringList suffixes = mimeType.suffixes(); + + QList<AssistProposalItemInterface *> completions; + for (const HeaderPath &headerPath : qAsConst(allHeaderPaths)) { + QString realPath = headerPath.path; + if (!directoryPrefix.isEmpty()) { + realPath += QLatin1Char('/'); + realPath += directoryPrefix; + if (headerPath.type == HeaderPathType::Framework) + realPath += QLatin1String(".framework/Headers"); + } + completions << completeIncludePath(realPath, suffixes, completionOperator); + } + + QList<QPair<AssistProposalItemInterface *, QString>> completionsForSorting; + for (AssistProposalItemInterface * const item : qAsConst(completions)) { + QString s = item->text(); + s.replace('/', QChar(0)); // The dir separator should compare less than anything else. + completionsForSorting << qMakePair(item, s); + } + Utils::sort(completionsForSorting, [](const auto &left, const auto &right) { + return left.second < right.second; + }); + for (int i = 0; i < completionsForSorting.count(); ++i) + completions[i] = completionsForSorting[i].first; + + return completions; +} + +/** + * @brief Finds #include completion proposals using given include path + * @param realPath - one of directories where compiler searches includes + * @param suffixes - file suffixes for C/C++ header files + * @return a list of matching completion items + */ +QList<AssistProposalItemInterface *> CustomAssistProcessor::completeIncludePath( + const QString &realPath, const QStringList &suffixes, unsigned completionOperator) +{ + QList<AssistProposalItemInterface *> completions; + QDirIterator i(realPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + //: Parent folder for proposed #include completion + const QString hint = ClangdClient::tr("Location: %1") + .arg(QDir::toNativeSeparators(QDir::cleanPath(realPath))); + while (i.hasNext()) { + const QString fileName = i.next(); + const QFileInfo fileInfo = i.fileInfo(); + const QString suffix = fileInfo.suffix(); + if (suffix.isEmpty() || suffixes.contains(suffix)) { + QString text = fileName.mid(realPath.length() + 1); + if (fileInfo.isDir()) + text += QLatin1Char('/'); + + auto *item = new ClangPreprocessorAssistProposalItem; + item->setText(text); + item->setDetail(hint); + item->setIcon(CPlusPlus::Icons::keywordIcon()); + item->setCompletionOperator(completionOperator); + completions.append(item); + } + } + return completions; +} + +ClangdCompletionAssistProcessor::ClangdCompletionAssistProcessor(ClangdClient *client, + const QString &snippetsGroup) + : LanguageClientCompletionAssistProcessor(client, snippetsGroup) + , m_client(client) +{ + m_timer.start(); +} + +ClangdCompletionAssistProcessor::~ClangdCompletionAssistProcessor() +{ + qCDebug(clangdLogTiming).noquote().nospace() + << "ClangdCompletionAssistProcessor took: " << m_timer.elapsed() << " ms"; +} + +IAssistProposal *ClangdCompletionAssistProcessor::perform(const AssistInterface *interface) +{ + if (m_client->testingEnabled()) { + setAsyncCompletionAvailableHandler([this](IAssistProposal *proposal) { + emit m_client->proposalReady(proposal); + }); + } + return LanguageClientCompletionAssistProcessor::perform(interface); +} + +QList<AssistProposalItemInterface *> ClangdCompletionAssistProcessor::generateCompletionItems( + const QList<CompletionItem> &items) const +{ + qCDebug(clangdLog) << "received" << items.count() << "completions"; + + auto itemGenerator = [](const QList<LanguageServerProtocol::CompletionItem> &items) { + return Utils::transform<QList<AssistProposalItemInterface *>>(items, + [](const LanguageServerProtocol::CompletionItem &item) { + return new ClangdCompletionItem(item); + }); + }; + + // 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) { + return ClangdCompletionItem::getQtType(ci) == ClangdCompletionItem::SpecialQtType::Signal; + }; + const QTextDocument *doc = document(); + const int pos = basePos(); + if (!doc || pos < 0 || !Utils::anyOf(items, criterion)) + return itemGenerator(items); + const QString content = doc->toPlainText(); + const bool requiresSignal = CppModelManager::instance()->getSignalSlotType( + filePath().toString(), content.toUtf8(), pos) + == SignalSlotType::NewStyleSignal; + if (requiresSignal) + return itemGenerator(Utils::filtered(items, criterion)); + return itemGenerator(items); +} + +ClangdFunctionHintProcessor::ClangdFunctionHintProcessor(ClangdClient *client) + : FunctionHintProcessor(client) + , m_client(client) +{} + +IAssistProposal *ClangdFunctionHintProcessor::perform(const AssistInterface *interface) +{ + if (m_client->testingEnabled()) { + setAsyncCompletionAvailableHandler([this](IAssistProposal *proposal) { + emit m_client->proposalReady(proposal); + }); + } + return FunctionHintProcessor::perform(interface); +} + +ClangdCompletionCapabilities::ClangdCompletionCapabilities(const JsonObject &object) + : TextDocumentClientCapabilities::CompletionCapabilities(object) +{ + insert("editsNearCursor", true); // For dot-to-arrow correction. + if (Utils::optional<CompletionItemCapbilities> completionItemCaps = completionItem()) { + completionItemCaps->setSnippetSupport(false); + setCompletionItem(*completionItemCaps); + } +} + +} // namespace ClangCodeModel::Internal diff --git a/src/plugins/clangcodemodel/clangdcompletion.h b/src/plugins/clangcodemodel/clangdcompletion.h new file mode 100644 index 0000000000..de3c1b39bd --- /dev/null +++ b/src/plugins/clangcodemodel/clangdcompletion.h @@ -0,0 +1,62 @@ + +#include <languageclient/languageclientcompletionassist.h> +/**************************************************************************** +** +** Copyright (C) 2022 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 <languageclient/languageclientcompletionassist.h> +#include <languageserverprotocol/clientcapabilities.h> + +namespace TextEditor { class IAssistProcessor; } +namespace ClangCodeModel::Internal { +class ClangdClient; + +class ClangdCompletionAssistProvider : public LanguageClient::LanguageClientCompletionAssistProvider +{ +public: + ClangdCompletionAssistProvider(ClangdClient *client); + +private: + TextEditor::IAssistProcessor *createProcessor( + const TextEditor::AssistInterface *interface) const override; + + int activationCharSequenceLength() const override { return 3; } + bool isActivationCharSequence(const QString &sequence) const override; + bool isContinuationChar(const QChar &c) const override; + + bool isInCommentOrString(const TextEditor::AssistInterface *interface) const; + + ClangdClient * const m_client; +}; + +class ClangdCompletionCapabilities + : public LanguageServerProtocol::TextDocumentClientCapabilities::CompletionCapabilities +{ +public: + explicit ClangdCompletionCapabilities(const JsonObject &object); +}; + +} // namespace ClangCodeModel::Internal |