From 228f77afd1e1eee99e2d3e131d486c0e92ac54e0 Mon Sep 17 00:00:00 2001 From: Christian Kandeler Date: Mon, 29 Jan 2024 18:13:17 +0100 Subject: [PATCH] QbsProjectManager: Support completion via qbs' LSP server Task-number: QBS-395 Change-Id: I2571dc46c9fb2867daeb3a5d00709337b12a750b Reviewed-by: Reviewed-by: David Schulz --- src/plugins/qbsprojectmanager/qbseditor.cpp | 179 +++++++++++++++++- .../codeassist/genericproposalmodel.h | 1 + 2 files changed, 172 insertions(+), 8 deletions(-) diff --git a/src/plugins/qbsprojectmanager/qbseditor.cpp b/src/plugins/qbsprojectmanager/qbseditor.cpp index 7eed6977a2c..08f6e0a8725 100644 --- a/src/plugins/qbsprojectmanager/qbseditor.cpp +++ b/src/plugins/qbsprojectmanager/qbseditor.cpp @@ -6,13 +6,23 @@ #include "qbslanguageclient.h" #include "qbsprojectmanagertr.h" +#include #include +#include +#include +#include +#include +#include #include #include +#include +#include + using namespace LanguageClient; using namespace QmlJSEditor; +using namespace TextEditor; using namespace Utils; namespace QbsProjectManager::Internal { @@ -26,11 +36,79 @@ private: bool inNextSplit = false) override; }; +class QbsCompletionAssistProcessor : public LanguageClientCompletionAssistProcessor +{ +public: + QbsCompletionAssistProcessor(Client *client); + +private: + QList generateCompletionItems( + const QList &items) const override; +}; + +class MergedCompletionAssistProcessor : public IAssistProcessor +{ +public: + MergedCompletionAssistProcessor(const AssistInterface *interface) : m_interface(interface) {} + ~MergedCompletionAssistProcessor(); + +private: + IAssistProposal *perform() override; + bool running() override { return m_started && (!m_qmlProposal || !m_qbsProposal); } + void checkFinished(); + + const AssistInterface * const m_interface; + std::unique_ptr m_qmlProcessor; + std::unique_ptr m_qbsProcessor; + std::optional m_qmlProposal; + std::optional m_qbsProposal; + bool m_started = false; +}; + +class QbsCompletionAssistProvider : public QmlJSCompletionAssistProvider +{ +private: + IAssistProcessor *createProcessor(const AssistInterface *interface) const override + { + return new MergedCompletionAssistProcessor(interface); + } +}; + +class QbsCompletionItem : public LanguageClientCompletionItem +{ +public: + using LanguageClientCompletionItem::LanguageClientCompletionItem; + +private: + QIcon icon() const override; +}; + +class MergedProposalModel : public GenericProposalModel +{ +public: + MergedProposalModel(const QList &sourceModels); +}; + +static Client *clientForDocument(const TextDocument *doc) +{ + if (!doc) + return nullptr; + const QList &candidates = LanguageClientManager::clientsSupportingDocument(doc); + for (Client * const candidate : candidates) { + if (const auto qbsClient = qobject_cast(candidate); + qbsClient && qbsClient->isActive() && qbsClient->documentOpen(doc)) { + return qbsClient; + } + } + return nullptr; +} + QbsEditorFactory::QbsEditorFactory() : QmlJSEditorFactory("QbsEditor.QbsEditor") { setDisplayName(Tr::tr("Qbs Editor")); setMimeTypes({Utils::Constants::QBS_MIMETYPE}); setEditorWidgetCreator([] { return new QbsEditorWidget; }); + setCompletionAssistProvider(new QbsCompletionAssistProvider); } void QbsEditorWidget::findLinkAt(const QTextCursor &cursor, const LinkHandler &processLinkCallback, @@ -43,18 +121,103 @@ void QbsEditorWidget::findLinkAt(const QTextCursor &cursor, const LinkHandler &p if (!self) return; const auto doc = self->textDocument(); - if (!doc) - return; - const QList &candidates = LanguageClientManager::clientsSupportingDocument(doc); - for (Client * const candidate : candidates) { - const auto qbsClient = qobject_cast(candidate); - if (!qbsClient || !qbsClient->isActive() || !qbsClient->documentOpen(doc)) - continue; - qbsClient->findLinkAt(doc, cursor, processLinkCallback, resolveTarget, + if (Client * const client = clientForDocument(doc)) { + client->findLinkAt(doc, cursor, processLinkCallback, resolveTarget, LinkTarget::SymbolDef); } }; QmlJSEditorWidget::findLinkAt(cursor, extendedCallback, resolveTarget, inNextSplit); } +MergedCompletionAssistProcessor::~MergedCompletionAssistProcessor() +{ + if (m_qmlProposal) + delete *m_qmlProposal; + if (m_qbsProposal) + delete *m_qbsProposal; +} + +IAssistProposal *MergedCompletionAssistProcessor::perform() +{ + m_started = true; + if (Client *const qbsClient = clientForDocument( + TextDocument::textDocumentForFilePath(m_interface->filePath()))) { + m_qbsProcessor.reset(new QbsCompletionAssistProcessor(qbsClient)); + m_qbsProcessor->setAsyncCompletionAvailableHandler([this](IAssistProposal *proposal) { + m_qbsProposal = proposal; + checkFinished(); + }); + m_qbsProcessor->start(std::make_unique(m_interface->cursor(), + m_interface->filePath(), + ExplicitlyInvoked)); + } else { + m_qbsProposal = nullptr; + } + m_qmlProcessor.reset(QmlJSCompletionAssistProvider().createProcessor(m_interface)); + m_qmlProcessor->setAsyncCompletionAvailableHandler([this](IAssistProposal *proposal) { + m_qmlProposal = proposal; + checkFinished(); + }); + const auto qmlJsIface = static_cast(m_interface); + return m_qmlProcessor->start( + std::make_unique(qmlJsIface->cursor(), + qmlJsIface->filePath(), + ExplicitlyInvoked, + qmlJsIface->semanticInfo())); +} + +void MergedCompletionAssistProcessor::checkFinished() +{ + if (running()) + return; + + QList sourceModels; + int basePosition = -1; + for (const IAssistProposal * const proposal : {*m_qmlProposal, *m_qbsProposal}) { + if (proposal) { + if (const auto model = proposal->model().dynamicCast()) + sourceModels << model; + if (basePosition == -1) + basePosition = proposal->basePosition(); + else + QTC_CHECK(basePosition == proposal->basePosition()); + } + } + setAsyncProposalAvailable( + new GenericProposal(basePosition >= 0 ? basePosition : m_interface->position(), + GenericProposalModelPtr(new MergedProposalModel(sourceModels)))); +} + +MergedProposalModel::MergedProposalModel(const QList &sourceModels) +{ + QList items; + for (const GenericProposalModelPtr &model : sourceModels) { + items << model->originalItems(); + model->loadContent({}); + } + loadContent(items); +} + +QbsCompletionAssistProcessor::QbsCompletionAssistProcessor(Client *client) + : LanguageClientCompletionAssistProcessor(client, nullptr, {}) +{} + +QList QbsCompletionAssistProcessor::generateCompletionItems( + const QList &items) const +{ + return Utils::transform>( + items, [](const LanguageServerProtocol::CompletionItem &item) { + return new QbsCompletionItem(item); + }); +} + +QIcon QbsCompletionItem::icon() const +{ + if (!item().detail()) { + return ProjectExplorer::DirectoryIcon( + ProjectExplorer::Constants::FILEOVERLAY_MODULES).icon(); + } + return CodeModelIcon::iconForType(CodeModelIcon::Property); +} + } // namespace QbsProjectManager::Internal diff --git a/src/plugins/texteditor/codeassist/genericproposalmodel.h b/src/plugins/texteditor/codeassist/genericproposalmodel.h index 610fab1a1fb..3107fae0d33 100644 --- a/src/plugins/texteditor/codeassist/genericproposalmodel.h +++ b/src/plugins/texteditor/codeassist/genericproposalmodel.h @@ -45,6 +45,7 @@ public: virtual int indexOf(const std::function &predicate) const; void loadContent(const QList &items); + const QList &originalItems() const { return m_originalItems; } bool isPerfectMatch(const QString &prefix) const; bool hasItemsToPropose(const QString &prefix, AssistReason reason) const;