QbsProjectManager: Support completion via qbs' LSP server

Task-number: QBS-395
Change-Id: I2571dc46c9fb2867daeb3a5d00709337b12a750b
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
Christian Kandeler
2024-01-29 18:13:17 +01:00
parent eb95794e67
commit 228f77afd1
2 changed files with 172 additions and 8 deletions

View File

@@ -6,13 +6,23 @@
#include "qbslanguageclient.h" #include "qbslanguageclient.h"
#include "qbsprojectmanagertr.h" #include "qbsprojectmanagertr.h"
#include <languageclient/languageclientcompletionassist.h>
#include <languageclient/languageclientmanager.h> #include <languageclient/languageclientmanager.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <projectexplorer/projectnodes.h>
#include <qmljseditor/qmljscompletionassist.h>
#include <texteditor/codeassist/genericproposal.h>
#include <utils/utilsicons.h>
#include <utils/mimeconstants.h> #include <utils/mimeconstants.h>
#include <QPointer> #include <QPointer>
#include <memory>
#include <optional>
using namespace LanguageClient; using namespace LanguageClient;
using namespace QmlJSEditor; using namespace QmlJSEditor;
using namespace TextEditor;
using namespace Utils; using namespace Utils;
namespace QbsProjectManager::Internal { namespace QbsProjectManager::Internal {
@@ -26,11 +36,79 @@ private:
bool inNextSplit = false) override; bool inNextSplit = false) override;
}; };
class QbsCompletionAssistProcessor : public LanguageClientCompletionAssistProcessor
{
public:
QbsCompletionAssistProcessor(Client *client);
private:
QList<AssistProposalItemInterface *> generateCompletionItems(
const QList<LanguageServerProtocol::CompletionItem> &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<IAssistProcessor> m_qmlProcessor;
std::unique_ptr<IAssistProcessor> m_qbsProcessor;
std::optional<IAssistProposal *> m_qmlProposal;
std::optional<IAssistProposal *> 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<GenericProposalModelPtr> &sourceModels);
};
static Client *clientForDocument(const TextDocument *doc)
{
if (!doc)
return nullptr;
const QList<Client *> &candidates = LanguageClientManager::clientsSupportingDocument(doc);
for (Client * const candidate : candidates) {
if (const auto qbsClient = qobject_cast<QbsLanguageClient *>(candidate);
qbsClient && qbsClient->isActive() && qbsClient->documentOpen(doc)) {
return qbsClient;
}
}
return nullptr;
}
QbsEditorFactory::QbsEditorFactory() : QmlJSEditorFactory("QbsEditor.QbsEditor") QbsEditorFactory::QbsEditorFactory() : QmlJSEditorFactory("QbsEditor.QbsEditor")
{ {
setDisplayName(Tr::tr("Qbs Editor")); setDisplayName(Tr::tr("Qbs Editor"));
setMimeTypes({Utils::Constants::QBS_MIMETYPE}); setMimeTypes({Utils::Constants::QBS_MIMETYPE});
setEditorWidgetCreator([] { return new QbsEditorWidget; }); setEditorWidgetCreator([] { return new QbsEditorWidget; });
setCompletionAssistProvider(new QbsCompletionAssistProvider);
} }
void QbsEditorWidget::findLinkAt(const QTextCursor &cursor, const LinkHandler &processLinkCallback, void QbsEditorWidget::findLinkAt(const QTextCursor &cursor, const LinkHandler &processLinkCallback,
@@ -43,18 +121,103 @@ void QbsEditorWidget::findLinkAt(const QTextCursor &cursor, const LinkHandler &p
if (!self) if (!self)
return; return;
const auto doc = self->textDocument(); const auto doc = self->textDocument();
if (!doc) if (Client * const client = clientForDocument(doc)) {
return; client->findLinkAt(doc, cursor, processLinkCallback, resolveTarget,
const QList<Client *> &candidates = LanguageClientManager::clientsSupportingDocument(doc);
for (Client * const candidate : candidates) {
const auto qbsClient = qobject_cast<QbsLanguageClient *>(candidate);
if (!qbsClient || !qbsClient->isActive() || !qbsClient->documentOpen(doc))
continue;
qbsClient->findLinkAt(doc, cursor, processLinkCallback, resolveTarget,
LinkTarget::SymbolDef); LinkTarget::SymbolDef);
} }
}; };
QmlJSEditorWidget::findLinkAt(cursor, extendedCallback, resolveTarget, inNextSplit); 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<AssistInterface>(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<const QmlJSCompletionAssistInterface *>(m_interface);
return m_qmlProcessor->start(
std::make_unique<QmlJSCompletionAssistInterface>(qmlJsIface->cursor(),
qmlJsIface->filePath(),
ExplicitlyInvoked,
qmlJsIface->semanticInfo()));
}
void MergedCompletionAssistProcessor::checkFinished()
{
if (running())
return;
QList<GenericProposalModelPtr> sourceModels;
int basePosition = -1;
for (const IAssistProposal * const proposal : {*m_qmlProposal, *m_qbsProposal}) {
if (proposal) {
if (const auto model = proposal->model().dynamicCast<GenericProposalModel>())
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<GenericProposalModelPtr> &sourceModels)
{
QList<AssistProposalItemInterface *> items;
for (const GenericProposalModelPtr &model : sourceModels) {
items << model->originalItems();
model->loadContent({});
}
loadContent(items);
}
QbsCompletionAssistProcessor::QbsCompletionAssistProcessor(Client *client)
: LanguageClientCompletionAssistProcessor(client, nullptr, {})
{}
QList<AssistProposalItemInterface *> QbsCompletionAssistProcessor::generateCompletionItems(
const QList<LanguageServerProtocol::CompletionItem> &items) const
{
return Utils::transform<QList<AssistProposalItemInterface *>>(
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 } // namespace QbsProjectManager::Internal

View File

@@ -45,6 +45,7 @@ public:
virtual int indexOf(const std::function<bool (AssistProposalItemInterface *)> &predicate) const; virtual int indexOf(const std::function<bool (AssistProposalItemInterface *)> &predicate) const;
void loadContent(const QList<AssistProposalItemInterface *> &items); void loadContent(const QList<AssistProposalItemInterface *> &items);
const QList<AssistProposalItemInterface *> &originalItems() const { return m_originalItems; }
bool isPerfectMatch(const QString &prefix) const; bool isPerfectMatch(const QString &prefix) const;
bool hasItemsToPropose(const QString &prefix, AssistReason reason) const; bool hasItemsToPropose(const QString &prefix, AssistReason reason) const;