/* SPDX-License-Identifier: MIT SPDX-FileCopyrightText: 2019 Mark Nauwelaerts SPDX-License-Identifier: MIT */ #include "lspclientcompletion.h" #include "lspclientplugin.h" #include "lspclient_debug.h" #include #include #include #include #include #include #include // clang-format off #define RETURN_CACHED_ICON(name) \ { \ static QIcon icon(QIcon::fromTheme(QStringLiteral(name))); \ return icon; \ } // clang-format on static QIcon kind_icon(LSPCompletionItemKind kind) { switch (kind) { case LSPCompletionItemKind::Method: case LSPCompletionItemKind::Function: case LSPCompletionItemKind::Constructor: RETURN_CACHED_ICON("code-function") case LSPCompletionItemKind::Variable: RETURN_CACHED_ICON("code-variable") case LSPCompletionItemKind::Class: case LSPCompletionItemKind::Interface: case LSPCompletionItemKind::Struct: RETURN_CACHED_ICON("code-class"); case LSPCompletionItemKind::Module: RETURN_CACHED_ICON("code-block"); case LSPCompletionItemKind::Field: case LSPCompletionItemKind::Property: // align with symbolview RETURN_CACHED_ICON("code-variable"); case LSPCompletionItemKind::Enum: case LSPCompletionItemKind::EnumMember: RETURN_CACHED_ICON("enum"); default: break; } return QIcon(); } static KTextEditor::CodeCompletionModel::CompletionProperty kind_property(LSPCompletionItemKind kind) { using CompletionProperty = KTextEditor::CodeCompletionModel::CompletionProperty; auto p = CompletionProperty::NoProperty; switch (kind) { case LSPCompletionItemKind::Method: case LSPCompletionItemKind::Function: case LSPCompletionItemKind::Constructor: p = CompletionProperty::Function; break; case LSPCompletionItemKind::Variable: p = CompletionProperty::Variable; break; case LSPCompletionItemKind::Class: case LSPCompletionItemKind::Interface: p = CompletionProperty::Class; break; case LSPCompletionItemKind::Struct: p = CompletionProperty::Class; break; case LSPCompletionItemKind::Module: p = CompletionProperty::Namespace; break; case LSPCompletionItemKind::Enum: case LSPCompletionItemKind::EnumMember: p = CompletionProperty::Enum; break; default: break; } return p; } struct LSPClientCompletionItem : public LSPCompletionItem { int argumentHintDepth = 0; QString prefix; QString postfix; LSPClientCompletionItem(const LSPCompletionItem &item) : LSPCompletionItem(item) { // transform for later display // sigh, remove (leading) whitespace (looking at clangd here) // could skip the [] if empty detail, but it is a handy watermark anyway ;-) label = QString(label.simplified() + QLatin1String(" [") + detail.simplified() + QStringLiteral("]")); } LSPClientCompletionItem(const LSPSignatureInformation &sig, int activeParameter, const QString &_sortText) { argumentHintDepth = 1; documentation = sig.documentation; label = sig.label; sortText = _sortText; // transform into prefix, name, suffix if active if (activeParameter >= 0 && activeParameter < sig.parameters.length()) { const auto ¶m = sig.parameters.at(activeParameter); if (param.start >= 0 && param.start < label.length() && param.end >= 0 && param.end < label.length() && param.start < param.end) { prefix = label.mid(0, param.start); postfix = label.mid(param.end); label = label.mid(param.start, param.end - param.start); } } } }; static bool compare_match(const LSPCompletionItem &a, const LSPCompletionItem &b) { return a.sortText < b.sortText; } class LSPClientCompletionImpl : public LSPClientCompletion { Q_OBJECT typedef LSPClientCompletionImpl self_type; QSharedPointer m_manager; QSharedPointer m_server; bool m_selectedDocumentation = false; QVector m_triggersCompletion; QVector m_triggersSignature; bool m_triggerSignature = false; QList m_matches; LSPClientServer::RequestHandle m_handle, m_handleSig; public: LSPClientCompletionImpl(QSharedPointer manager) : LSPClientCompletion(nullptr) , m_manager(std::move(manager)) , m_server(nullptr) { } void setServer(QSharedPointer server) override { m_server = server; if (m_server) { const auto &caps = m_server->capabilities(); m_triggersCompletion = caps.completionProvider.triggerCharacters; m_triggersSignature = caps.signatureHelpProvider.triggerCharacters; } else { m_triggersCompletion.clear(); m_triggersSignature.clear(); } } void setSelectedDocumentation(bool s) override { m_selectedDocumentation = s; } QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid() || index.row() >= m_matches.size()) { return QVariant(); } const auto &match = m_matches.at(index.row()); if (role == Qt::DisplayRole) { if (index.column() == KTextEditor::CodeCompletionModel::Name) { return match.label; } else if (index.column() == KTextEditor::CodeCompletionModel::Prefix) { return match.prefix; } else if (index.column() == KTextEditor::CodeCompletionModel::Postfix) { return match.postfix; } } else if (role == Qt::DecorationRole && index.column() == KTextEditor::CodeCompletionModel::Icon) { return kind_icon(match.kind); } else if (role == KTextEditor::CodeCompletionModel::CompletionRole) { return kind_property(match.kind); } else if (role == KTextEditor::CodeCompletionModel::ArgumentHintDepth) { return match.argumentHintDepth; } else if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) { // (ab)use depth to indicate sort order return index.row(); } else if (role == KTextEditor::CodeCompletionModel::IsExpandable) { return !match.documentation.value.isEmpty(); } else if (role == KTextEditor::CodeCompletionModel::ExpandingWidget && !match.documentation.value.isEmpty()) { // probably plaintext, but let's show markdown as-is for now // FIXME better presentation of markdown return match.documentation.value; } else if (role == KTextEditor::CodeCompletionModel::ItemSelected && !match.argumentHintDepth && !match.documentation.value.isEmpty() && m_selectedDocumentation) { return match.documentation.value; } return QVariant(); } bool shouldStartCompletion(KTextEditor::View *view, const QString &insertedText, bool userInsertion, const KTextEditor::Cursor &position) override { qCInfo(LSPCLIENT) << "should start " << userInsertion << insertedText; if (!userInsertion || !m_server || insertedText.isEmpty()) { return false; } // covers most already ... bool complete = CodeCompletionModelControllerInterface::shouldStartCompletion(view, insertedText, userInsertion, position); QChar lastChar = insertedText.at(insertedText.count() - 1); m_triggerSignature = false; complete = complete || m_triggersCompletion.contains(lastChar); if (m_triggersSignature.contains(lastChar)) { complete = true; m_triggerSignature = true; } return complete; } void completionInvoked(KTextEditor::View *view, const KTextEditor::Range &range, InvocationType it) override { Q_UNUSED(it) qCInfo(LSPCLIENT) << "completion invoked" << m_server; // maybe use WaitForReset ?? // but more complex and already looks good anyway auto handler = [this](const QList & compl ) { beginResetModel(); qCInfo(LSPCLIENT) << "adding completions " << compl .size(); for (const auto &item : compl ) { m_matches.push_back(item); } std::stable_sort(m_matches.begin(), m_matches.end(), compare_match); setRowCount(m_matches.size()); endResetModel(); }; auto sigHandler = [this](const LSPSignatureHelp &sig) { beginResetModel(); qCInfo(LSPCLIENT) << "adding signatures " << sig.signatures.size(); int index = 0; for (const auto &item : sig.signatures) { int sortIndex = 10 + index; int active = -1; if (index == sig.activeSignature) { sortIndex = 0; active = sig.activeParameter; } // trick active first, others after that m_matches.push_back({item, active, QString(QStringLiteral("%1").arg(sortIndex, 3, 10))}); ++index; } std::stable_sort(m_matches.begin(), m_matches.end(), compare_match); setRowCount(m_matches.size()); endResetModel(); }; beginResetModel(); m_matches.clear(); auto document = view->document(); if (m_server && document) { // the default range is determined based on a reasonable identifier (word) // which is generally fine and nice, but let's pass actual cursor position // (which may be within this typical range) auto position = view->cursorPosition(); auto cursor = qMax(range.start(), qMin(range.end(), position)); m_manager->update(document, false); if (!m_triggerSignature) { m_handle = m_server->documentCompletion(document->url(), {cursor.line(), cursor.column()}, this, handler); } m_handleSig = m_server->signatureHelp(document->url(), {cursor.line(), cursor.column()}, this, sigHandler); } setRowCount(m_matches.size()); endResetModel(); } /** * @brief return next char *after* the range */ static QChar peekNextChar(KTextEditor::Document *doc, const KTextEditor::Range &range) { return doc->characterAt(KTextEditor::Cursor(range.end().line(), range.end().column())); } void executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const override { if (index.row() < m_matches.size()) { auto next = peekNextChar(view->document(), word); auto matching = m_matches.at(index.row()).insertText; // if there is already a '"' or >, remove it, this happens with #include "xx.h" if ((next == QLatin1Char('"') && matching.endsWith(QLatin1Char('"'))) || (next == QLatin1Char('>') && matching.endsWith(QLatin1Char('>')))) { matching.chop(1); } view->document()->replaceText(word, matching); } } void aborted(KTextEditor::View *view) override { Q_UNUSED(view); beginResetModel(); m_matches.clear(); m_handle.cancel(); m_handleSig.cancel(); m_triggerSignature = false; endResetModel(); } }; LSPClientCompletion *LSPClientCompletion::new_(QSharedPointer manager) { return new LSPClientCompletionImpl(std::move(manager)); } #include "lspclientcompletion.moc"