diff options
Diffstat (limited to 'src/qmlls')
57 files changed, 11537 insertions, 0 deletions
diff --git a/src/qmlls/CMakeLists.txt b/src/qmlls/CMakeLists.txt new file mode 100644 index 0000000000..b15fb396f8 --- /dev/null +++ b/src/qmlls/CMakeLists.txt @@ -0,0 +1,46 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_module(QmlLSPrivate + STATIC + INTERNAL_MODULE + PLUGIN_TYPES qmlls + SOURCES + qlspcustomtypes_p.h + qlanguageserver_p.h qlanguageserver_p.h qlanguageserver.cpp + qqmllanguageserver_p.h qqmllanguageserver.cpp + qworkspace.cpp qworkspace_p.h + qtextblock_p.h qtextblock.cpp + qtextcursor_p.h qtextcursor.cpp + qtextdocument.cpp qtextdocument_p.h + qqmllintsuggestions_p.h qqmllintsuggestions.cpp + qtextsynchronization.cpp qtextsynchronization_p.h + qqmlcompletionsupport_p.h qqmlcompletionsupport.cpp + qqmlcodemodel_p.h qqmlcodemodel.cpp + qqmlbasemodule_p.h + qqmlgototypedefinitionsupport_p.h qqmlgototypedefinitionsupport.cpp + qqmlformatting_p.h qqmlformatting.cpp + qqmlrangeformatting_p.h qqmlrangeformatting.cpp + qqmllsutils_p.h qqmllsutils.cpp + qqmlfindusagessupport_p.h qqmlfindusagessupport.cpp + qqmlgotodefinitionsupport.cpp qqmlgotodefinitionsupport_p.h + qqmlrenamesymbolsupport_p.h qqmlrenamesymbolsupport.cpp + qqmlcompletioncontextstrings_p.h qqmlcompletioncontextstrings.cpp + qqmlhover_p.h qqmlhover.cpp + qqmllsplugin_p.h + qqmllscompletion.cpp qqmllscompletion_p.h + qqmllscompletionplugin.cpp qqmllscompletionplugin_p.h + qdochtmlparser_p.h qdochtmlparser.cpp + qqmlhighlightsupport_p.h qqmlhighlightsupport.cpp + qqmlsemantictokens_p.h qqmlsemantictokens.cpp + qqmllshelpplugininterface_p.h qqmllshelpplugininterface.cpp + qqmllshelputils_p.h qqmllshelputils.cpp + + PUBLIC_LIBRARIES + Qt::LanguageServerPrivate + Qt::CorePrivate + Qt::QmlDomPrivate + Qt::QmlCompilerPrivate + Qt::QmlToolingSettingsPrivate + Qt::LanguageServerPrivate + ) diff --git a/src/qmlls/qdochtmlparser.cpp b/src/qmlls/qdochtmlparser.cpp new file mode 100644 index 0000000000..bf885fb258 --- /dev/null +++ b/src/qmlls/qdochtmlparser.cpp @@ -0,0 +1,227 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <qdochtmlparser_p.h> +#include <QtCore/qregularexpression.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +// An emprical value to avoid too much content +static constexpr qsizetype firstIndexOfParagraphTag = 400; + +// A paragraph can start with <p><i>, or <p><tt> +// We need smallest value to use QString::indexOf +static constexpr auto lengthOfSmallestOpeningTag = qsizetype(std::char_traits<char>::length("<p><i>")); +static constexpr auto lengthOfStartParagraphTag = qsizetype(std::char_traits<char>::length("<p>")); +static constexpr auto lengthOfEndParagraphTag = qsizetype(std::char_traits<char>::length("</p>")); +static constexpr auto lengthOfPeriod = qsizetype(std::char_traits<char>::length(".")); + +static QString getContentsByMarks(const QString &html, QString startMark, QString endMark) +{ + startMark.prepend("$$$"_L1); + endMark.prepend("<!-- @@@"_L1); + + QString contents; + qsizetype start = html.indexOf(startMark); + if (start != -1) { + start = html.indexOf("-->"_L1, start); + if (start != -1) { + qsizetype end = html.indexOf(endMark, start); + if (end != -1) { + start += qsizetype(std::char_traits<char>::length("-->")); + contents = html.mid(start, end - start); + } + } + } + return contents; +} + + +static void stripAllHtml(QString *html) +{ + Q_ASSERT(html); + html->remove(QRegularExpression("<.*?>"_L1)); +} + +/*! \internal + \brief Process the string obtained from start mark to end mark. + This is duplicated from QtC's Utils::HtmlExtractor, modified on top of it. +*/ +static void processOutput(QString *html) +{ + Q_ASSERT(html); + if (html->isEmpty()) + return; + + // Do not write the first paragraph in case it has extra tags below. + // <p><i>This is only used on the Maemo platform.</i></p> + // or: <p><tt>This is used on Windows only.</tt></p> + // or: <p>[Conditional]</p> + const auto skipFirstParagraphIfNeeded = [html](qsizetype &index){ + const bool shouldSkipFirstParagraph = html->indexOf(QLatin1String("<p><i>")) == index || + html->indexOf(QLatin1String("<p><tt>")) == index || + html->indexOf(QLatin1String("<p>[Conditional]</p>")) == index; + + if (shouldSkipFirstParagraph) + index = html->indexOf(QLatin1String("<p>"), index + lengthOfSmallestOpeningTag); + }; + + // Try to get the entire first paragraph, but if one is not found or if its opening + // tag is not in the very beginning (using an empirical value as the limit) + // the html is cleared out to avoid too much content. + qsizetype index = html->indexOf(QLatin1String("<p>")); + if (index != -1 && index < firstIndexOfParagraphTag) { + skipFirstParagraphIfNeeded(index); + index = html->indexOf(QLatin1String("</p>"), index + lengthOfStartParagraphTag); + if (index != -1) { + // Most paragraphs end with a period, but there are cases without punctuation + // and cases like this: <p>This is a description. Example:</p> + const auto period = html->lastIndexOf(QLatin1Char('.'), index); + if (period != -1) { + html->truncate(period + lengthOfPeriod); + html->append(QLatin1String("</p>")); + } else { + html->truncate(index + lengthOfEndParagraphTag); + } + } else { + html->clear(); + } + } else { + html->clear(); + } +} + +class ExtractQmlType : public HtmlExtractor +{ +public: + QString extract(const QString &code, const QString &keyword, ExtractionMode mode) override; +}; + +class ExtractQmlProperty : public HtmlExtractor +{ +public: + QString extract(const QString &code, const QString &keyword, ExtractionMode mode) override; +}; + +class ExtractQmlMethodOrSignal : public HtmlExtractor +{ +public: + QString extract(const QString &code, const QString &keyword, ExtractionMode mode) override; +}; + +QString ExtractQmlType::extract(const QString &code, const QString &element, ExtractionMode mode) +{ + QString result; + // Get brief description + if (mode == ExtractionMode::Simplified) { + result = getContentsByMarks(code, element + "-brief"_L1 , element); + // Remove More... + if (!result.isEmpty()) { + const auto tailToRemove = "More..."_L1; + const auto lastIndex = result.lastIndexOf(tailToRemove); + if (lastIndex != -1) + result.remove(lastIndex, tailToRemove.length()); + } + } else { + result = getContentsByMarks(code, element + "-description"_L1, element); + // Remove header + if (!result.isEmpty()) { + const auto headerToRemove = "Detailed Description"_L1; + const auto firstIndex = result.indexOf(headerToRemove); + if (firstIndex != -1) + result.remove(firstIndex, headerToRemove.length()); + } + } + + stripAllHtml(&result); + return result.trimmed(); +} + +QString ExtractQmlProperty::extract(const QString &code, const QString &keyword, ExtractionMode mode) +{ + // Qt 5.15 way of finding properties in doc + QString startMark = QString::fromLatin1("<a name=\"%1-prop\">").arg(keyword); + qsizetype startIndex = code.indexOf(startMark); + if (startIndex == -1) { + // if not found, try Qt6 + startMark = QString::fromLatin1( + "<td class=\"tblQmlPropNode\"><p>\n<span class=\"name\">%1</span>") + .arg(keyword); + startIndex = code.indexOf(startMark); + if (startIndex == -1) + return {}; + } + + QString contents = code.mid(startIndex + startMark.size()); + startIndex = contents.indexOf(QLatin1String("<div class=\"qmldoc\"><p>")); + if (startIndex == -1) + return {}; + + contents = contents.mid(startIndex); + if (mode == ExtractionMode::Simplified) + processOutput(&contents); + stripAllHtml(&contents); + return contents.trimmed(); +} + +QString ExtractQmlMethodOrSignal::extract(const QString &code, const QString &keyword, ExtractionMode mode) +{ + // the case with <!-- $$$childAt[overload1]$$$childAtrealreal --> + QString mark = QString::fromLatin1("$$$%1[overload1]$$$%1").arg(keyword); + qsizetype startIndex = code.indexOf(mark); + if (startIndex != -1) { + startIndex = code.indexOf("-->"_L1, startIndex + mark.length()); + if (startIndex == -1) + return {}; + } else { + // it could be part of the method list + mark = QString::fromLatin1("<span class=\"name\">%1</span>") + .arg(keyword); + startIndex = code.indexOf(mark); + if (startIndex != -1) + startIndex += mark.length(); + else + return {}; + } + + startIndex = code.indexOf(QLatin1String("<div class=\"qmldoc\"><p>"), startIndex); + if (startIndex == -1) + return {}; + + QString endMark = QString::fromLatin1("<!-- @@@"); + qsizetype endIndex = code.indexOf(endMark, startIndex); + QString contents = code.mid(startIndex, endIndex); + if (mode == ExtractionMode::Simplified) + processOutput(&contents); + stripAllHtml(&contents); + return contents.trimmed(); +} + +ExtractDocumentation::ExtractDocumentation(QQmlJS::Dom::DomType domType) +{ + using namespace QQmlJS::Dom; + switch (domType) { + case DomType::QmlObject: + m_extractor = std::make_unique<ExtractQmlType>(); + break; + case DomType::Binding: + case DomType::PropertyDefinition: + m_extractor = std::make_unique<ExtractQmlProperty>(); + break; + case DomType::MethodInfo: + m_extractor = std::make_unique<ExtractQmlMethodOrSignal>(); + break; + default: + break; + } +} + +QString ExtractDocumentation::execute(const QString &code, const QString &keyword, HtmlExtractor::ExtractionMode mode) +{ + Q_ASSERT(m_extractor); + return m_extractor->extract(code, keyword, mode); +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qdochtmlparser_p.h b/src/qmlls/qdochtmlparser_p.h new file mode 100644 index 0000000000..d09f2f882e --- /dev/null +++ b/src/qmlls/qdochtmlparser_p.h @@ -0,0 +1,43 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QDOCHTMLEXTRACTOR_P_H +#define QDOCHTMLEXTRACTOR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQmlDom/private/qqmldomtop_p.h> +#include <QString> + +QT_BEGIN_NAMESPACE + +class HtmlExtractor +{ +public: + enum class ExtractionMode : char { Simplified, Extended }; + + virtual QString extract(const QString &code, const QString &keyword, ExtractionMode mode) = 0; + virtual ~HtmlExtractor() = default; +}; + +class ExtractDocumentation +{ +public: + ExtractDocumentation(QQmlJS::Dom::DomType domType); + QString execute(const QString &code, const QString &keyword, HtmlExtractor::ExtractionMode mode); +private: + std::unique_ptr<HtmlExtractor> m_extractor; +}; + +QT_END_NAMESPACE + +#endif // QDOCHTMLEXTRACTOR_P_H diff --git a/src/qmlls/qlanguageserver.cpp b/src/qmlls/qlanguageserver.cpp new file mode 100644 index 0000000000..4babb3ebba --- /dev/null +++ b/src/qmlls/qlanguageserver.cpp @@ -0,0 +1,384 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qlanguageserver_p_p.h" + +#include <QtLanguageServer/private/qlspnotifysignals_p.h> +#include <QtJsonRpc/private/qjsonrpcprotocol_p_p.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lspServerLog, "qt.languageserver.server") + +using namespace QLspSpecification; +using namespace Qt::StringLiterals; + +QLanguageServerPrivate::QLanguageServerPrivate(const QJsonRpcTransport::DataHandler &h) + : protocol(h) +{ +} + +/*! +\internal +\class QLanguageServer +\brief Implements a server for the language server protocol + +QLanguageServer is a class that uses the QLanguageServerProtocol to +provide a server implementation. +It handles the lifecycle management, and can be extended via +QLanguageServerModule subclasses. + +The language server keeps a strictly monotonically increasing runState that can be queried +from any thread (and is thus mutex gated), the normal run state is DidInitialize. + +The language server also keeps track of the task canceled by the client (or implicitly when +shutting down, and isRequestCanceled can be called from any thread. +*/ + +QLanguageServer::QLanguageServer(const QJsonRpcTransport::DataHandler &h, QObject *parent) + : QObject(*new QLanguageServerPrivate(h), parent) +{ + Q_D(QLanguageServer); + registerMethods(*d->protocol.typedRpc()); + d->notifySignals.registerHandlers(&d->protocol); +} + +QLanguageServerProtocol *QLanguageServer::protocol() +{ + Q_D(QLanguageServer); + return &d->protocol; +} + +QLanguageServer::RunStatus QLanguageServer::runStatus() const +{ + const Q_D(QLanguageServer); + QMutexLocker l(&d->mutex); + return d->runStatus; +} + +void QLanguageServer::finishSetup() +{ + Q_D(QLanguageServer); + RunStatus rStatus; + { + QMutexLocker l(&d->mutex); + rStatus = d->runStatus; + if (rStatus == RunStatus::NotSetup) + d->runStatus = RunStatus::SettingUp; + } + if (rStatus != RunStatus::NotSetup) { + emit lifecycleError(); + return; + } + emit runStatusChanged(RunStatus::SettingUp); + + registerHandlers(&d->protocol); + for (auto module : d->modules) + module->registerHandlers(this, &d->protocol); + + { + QMutexLocker l(&d->mutex); + rStatus = d->runStatus; + if (rStatus == RunStatus::SettingUp) + d->runStatus = RunStatus::DidSetup; + } + if (rStatus != RunStatus::SettingUp) { + emit lifecycleError(); + return; + } + emit runStatusChanged(RunStatus::DidSetup); +} + +void QLanguageServer::addServerModule(QLanguageServerModule *serverModule) +{ + Q_D(QLanguageServer); + Q_ASSERT(serverModule); + RunStatus rStatus; + { + QMutexLocker l(&d->mutex); + rStatus = d->runStatus; + if (rStatus == RunStatus::NotSetup) { + if (d->modules.contains(serverModule->name())) { + d->modules.insert(serverModule->name(), serverModule); + qCWarning(lspServerLog) << "Duplicate add of QLanguageServerModule named" + << serverModule->name() << ", overwriting."; + } else { + d->modules.insert(serverModule->name(), serverModule); + } + } + } + if (rStatus != RunStatus::NotSetup) { + qCWarning(lspServerLog) << "Called QLanguageServer::addServerModule after setup"; + emit lifecycleError(); + return; + } +} + +QLanguageServerModule *QLanguageServer::moduleByName(const QString &n) const +{ + const Q_D(QLanguageServer); + QMutexLocker l(&d->mutex); + return d->modules.value(n); +} + +QLspNotifySignals *QLanguageServer::notifySignals() +{ + Q_D(QLanguageServer); + return &d->notifySignals; +} + +void QLanguageServer::registerMethods(QJsonRpc::TypedRpc &typedRpc) +{ + typedRpc.installMessagePreprocessor( + [this](const QJsonDocument &doc, const QJsonParseError &err, + const QJsonRpcProtocol::Handler<QJsonRpcProtocol::Response> &responder) { + Q_D(QLanguageServer); + if (!doc.isObject()) { + qCWarning(lspServerLog) + << "non object jsonrpc message" << doc << err.errorString(); + return QJsonRpcProtocol::Processing::Stop; + } + bool sendErrorResponse = false; + RunStatus rState; + QJsonValue id = doc.object()[u"id"]; + { + QMutexLocker l(&d->mutex); + // the normal case is d->runStatus == RunStatus::DidInitialize + if (d->runStatus != RunStatus::DidInitialize) { + if (d->runStatus == RunStatus::DidSetup && !doc.isNull() + && doc.object()[u"method"].toString() + == QString::fromUtf8( + QLspSpecification::Requests::InitializeMethod)) { + return QJsonRpcProtocol::Processing::Continue; + } else if (!doc.isNull() + && doc.object()[u"method"].toString() + == QString::fromUtf8( + QLspSpecification::Notifications::ExitMethod)) { + return QJsonRpcProtocol::Processing::Continue; + } + if (id.isString() || id.isDouble()) { + sendErrorResponse = true; + rState = d->runStatus; + } else { + return QJsonRpcProtocol::Processing::Stop; + } + } + } + if (!sendErrorResponse) { + if (id.isString() || id.isDouble()) { + QMutexLocker l(&d->mutex); + d->requestsInProgress.insert(id, QRequestInProgress {}); + } + return QJsonRpcProtocol::Processing::Continue; + } + if (rState == RunStatus::NotSetup || rState == RunStatus::DidSetup) + responder(QJsonRpcProtocol::MessageHandler::error( + int(QLspSpecification::ErrorCodes::ServerNotInitialized), + u"Request on non initialized Language Server (runStatus %1): %2"_s + .arg(int(rState)) + .arg(QString::fromUtf8(doc.toJson())))); + else + responder(QJsonRpcProtocol::MessageHandler::error( + int(QLspSpecification::ErrorCodes::InvalidRequest), + u"Method called on stopping Language Server (runStatus %1)"_s.arg( + int(rState)))); + return QJsonRpcProtocol::Processing::Stop; + }); + typedRpc.installOnCloseAction([this](QJsonRpc::TypedResponse::Status, + const QJsonRpc::IdType &id, QJsonRpc::TypedRpc &) { + Q_D(QLanguageServer); + QJsonValue idValue = QTypedJson::toJsonValue(id); + bool lastReq; + { + QMutexLocker l(&d->mutex); + d->requestsInProgress.remove(idValue); + lastReq = d->runStatus == RunStatus::WaitPending && d->requestsInProgress.size() <= 1; + if (lastReq) + d->runStatus = RunStatus::Stopping; + } + if (lastReq) + executeShutdown(); + }); +} + +void QLanguageServer::setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &serverInfo) +{ + Q_D(QLanguageServer); + for (auto module : std::as_const(d->modules)) + module->setupCapabilities(clientInfo, serverInfo); +} + +const QLspSpecification::InitializeParams &QLanguageServer::clientInfo() const +{ + const Q_D(QLanguageServer); + + if (int(runStatus()) < int(RunStatus::DidInitialize)) + qCWarning(lspServerLog) << "asked for Language Server clientInfo before initialization"; + return d->clientInfo; +} + +const QLspSpecification::InitializeResult &QLanguageServer::serverInfo() const +{ + const Q_D(QLanguageServer); + if (int(runStatus()) < int(RunStatus::DidInitialize)) + qCWarning(lspServerLog) << "asked for Language Server serverInfo before initialization"; + return d->serverInfo; +} + +void QLanguageServer::receiveData(const QByteArray &data, bool isEndOfMessage) +{ + if (!data.isEmpty()) + protocol()->receiveData(data); + + const Q_D(QLanguageServer); + // read next message if not shutting down + if (isEndOfMessage && d->runStatus != RunStatus::Stopped) + emit readNextMessage(); +} + +void QLanguageServer::registerHandlers(QLanguageServerProtocol *protocol) +{ + QObject::connect(notifySignals(), &QLspNotifySignals::receivedCancelNotification, this, + [this](const QLspSpecification::Notifications::CancelParamsType ¶ms) { + Q_D(QLanguageServer); + QJsonValue id = QTypedJson::toJsonValue(params.id); + QMutexLocker l(&d->mutex); + if (d->requestsInProgress.contains(id)) + d->requestsInProgress[id].canceled = true; + else + qCWarning(lspServerLog) + << "Ignoring cancellation of non in progress request" << id; + }); + + protocol->registerInitializeRequestHandler( + [this](const QByteArray &, + const QLspSpecification::Requests::InitializeParamsType ¶ms, + QLspSpecification::Responses::InitializeResponseType &&response) { + qCDebug(lspServerLog) << "init"; + Q_D(QLanguageServer); + RunStatus rStatus; + { + QMutexLocker l(&d->mutex); + rStatus = d->runStatus; + if (rStatus == RunStatus::DidSetup) + d->runStatus = RunStatus::Initializing; + } + if (rStatus != RunStatus::DidSetup) { + if (rStatus == RunStatus::NotSetup || rStatus == RunStatus::SettingUp) + response.sendErrorResponse( + int(QLspSpecification::ErrorCodes::InvalidRequest), + u"Initialization request received on non setup language server"_s + .toUtf8()); + else + response.sendErrorResponse( + int(QLspSpecification::ErrorCodes::InvalidRequest), + u"Received multiple initialization requests"_s.toUtf8()); + emit lifecycleError(); + return; + } + emit runStatusChanged(RunStatus::Initializing); + d->clientInfo = params; + setupCapabilities(d->clientInfo, d->serverInfo); + { + QMutexLocker l(&d->mutex); + d->runStatus = RunStatus::DidInitialize; + } + emit runStatusChanged(RunStatus::DidInitialize); + response.sendResponse(d->serverInfo); + }); + + QObject::connect(notifySignals(), &QLspNotifySignals::receivedInitializedNotification, this, + [this](const QLspSpecification::Notifications::InitializedParamsType &) { + Q_D(QLanguageServer); + { + QMutexLocker l(&d->mutex); + d->clientInitialized = true; + } + emit clientInitialized(this); + }); + + protocol->registerShutdownRequestHandler( + [this](const QByteArray &, const QLspSpecification::Requests::ShutdownParamsType &, + QLspSpecification::Responses::ShutdownResponseType &&response) { + Q_D(QLanguageServer); + RunStatus rStatus; + bool shouldExecuteShutdown = false; + { + QMutexLocker l(&d->mutex); + rStatus = d->runStatus; + if (rStatus == RunStatus::DidInitialize) { + d->shutdownResponse = std::move(response); + if (d->requestsInProgress.size() <= 1) { + d->runStatus = RunStatus::Stopping; + shouldExecuteShutdown = true; + } else { + d->runStatus = RunStatus::WaitPending; + } + } + } + if (rStatus != RunStatus::DidInitialize) + emit lifecycleError(); + else if (shouldExecuteShutdown) + executeShutdown(); + }); + + QObject::connect(notifySignals(), &QLspNotifySignals::receivedExitNotification, this, + [this](const QLspSpecification::Notifications::ExitParamsType &) { + if (runStatus() != RunStatus::Stopped) + emit lifecycleError(); + else + emit exit(); + }); +} + +void QLanguageServer::executeShutdown() +{ + RunStatus rStatus = runStatus(); + if (rStatus != RunStatus::Stopping) { + emit lifecycleError(); + return; + } + emit shutdown(); + QLspSpecification::Responses::ShutdownResponseType shutdownResponse; + { + Q_D(QLanguageServer); + QMutexLocker l(&d->mutex); + rStatus = d->runStatus; + if (rStatus == RunStatus::Stopping) { + shutdownResponse = std::move(d->shutdownResponse); + d->runStatus = RunStatus::Stopped; + } + } + if (rStatus != RunStatus::Stopping) + emit lifecycleError(); + else + shutdownResponse.sendResponse(nullptr); +} + +bool QLanguageServer::isRequestCanceled(const QJsonRpc::IdType &id) const +{ + const Q_D(QLanguageServer); + QJsonValue idVal = QTypedJson::toJsonValue(id); + QMutexLocker l(&d->mutex); + return d->requestsInProgress.value(idVal).canceled || d->runStatus != RunStatus::DidInitialize; +} + +bool QLanguageServer::isInitialized() const +{ + switch (runStatus()) { + case RunStatus::NotSetup: + case RunStatus::SettingUp: + case RunStatus::DidSetup: + case RunStatus::Initializing: + return false; + case RunStatus::DidInitialize: + case RunStatus::WaitPending: + case RunStatus::Stopping: + case RunStatus::Stopped: + break; + } + return true; +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qlanguageserver_p.h b/src/qmlls/qlanguageserver_p.h new file mode 100644 index 0000000000..53118c00c8 --- /dev/null +++ b/src/qmlls/qlanguageserver_p.h @@ -0,0 +1,93 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QLANGUAGESERVER_P_H +#define QLANGUAGESERVER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtLanguageServer/private/qlanguageserverspec_p.h> +#include <QtLanguageServer/private/qlanguageserverprotocol_p.h> +#include <QtLanguageServer/private/qlspnotifysignals_p.h> +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +class QLanguageServer; +class QLanguageServerPrivate; +Q_DECLARE_LOGGING_CATEGORY(lspServerLog) + +class QLanguageServerModule : public QObject +{ + Q_OBJECT +public: + QLanguageServerModule(QObject *parent = nullptr) : QObject(parent) { } + virtual QString name() const = 0; + virtual void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) = 0; + virtual void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) = 0; +}; + +class QLanguageServer : public QObject +{ + Q_OBJECT + Q_PROPERTY(RunStatus runStatus READ runStatus NOTIFY runStatusChanged) + Q_PROPERTY(bool isInitialized READ isInitialized) +public: + QLanguageServer(const QJsonRpcTransport::DataHandler &h, QObject *parent = nullptr); + enum class RunStatus { + NotSetup, + SettingUp, + DidSetup, + Initializing, + DidInitialize, // normal state of execution + WaitPending, + Stopping, + Stopped + }; + Q_ENUM(RunStatus) + + QLanguageServerProtocol *protocol(); + void finishSetup(); + void registerHandlers(QLanguageServerProtocol *protocol); + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &serverInfo); + void addServerModule(QLanguageServerModule *serverModule); + QLanguageServerModule *moduleByName(const QString &n) const; + QLspNotifySignals *notifySignals(); + + // API + RunStatus runStatus() const; + bool isInitialized() const; + bool isRequestCanceled(const QJsonRpc::IdType &id) const; + const QLspSpecification::InitializeParams &clientInfo() const; + const QLspSpecification::InitializeResult &serverInfo() const; + +public Q_SLOTS: + void receiveData(const QByteArray &d, bool isEndOfMessage); +Q_SIGNALS: + void runStatusChanged(RunStatus); + void clientInitialized(QLanguageServer *server); + void shutdown(); + void exit(); + void lifecycleError(); + void readNextMessage(); + +private: + void registerMethods(QJsonRpc::TypedRpc &typedRpc); + void executeShutdown(); + Q_DECLARE_PRIVATE(QLanguageServer) +}; + +QT_END_NAMESPACE + +#endif // QLANGUAGESERVER_P_H diff --git a/src/qmlls/qlanguageserver_p_p.h b/src/qmlls/qlanguageserver_p_p.h new file mode 100644 index 0000000000..40e7a17432 --- /dev/null +++ b/src/qmlls/qlanguageserver_p_p.h @@ -0,0 +1,53 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QLANGUAGESERVER_P_P_H +#define QLANGUAGESERVER_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include <QtLanguageServer/private/qlanguageserverprotocol_p.h> +#include <QtCore/QMutex> +#include <QtCore/QHash> +#include <QtCore/private/qobject_p.h> +#include <QtLanguageServer/private/qlspnotifysignals_p.h> + +#include <memory> + +QT_BEGIN_NAMESPACE + +class QRequestInProgress +{ +public: + bool canceled = false; +}; + +class QLanguageServerPrivate : public QObjectPrivate +{ +public: + QLanguageServerPrivate(const QJsonRpcTransport::DataHandler &h); + mutable QMutex mutex; + // mutex gated, monotonically increasing + QLanguageServer::RunStatus runStatus = QLanguageServer::RunStatus::NotSetup; + QHash<QJsonValue, QRequestInProgress> requestsInProgress; // mutex gated + bool clientInitialized = false; // mutex gated + QLspSpecification::InitializeParams clientInfo; // immutable after runStatus > DidInitialize + QLspSpecification::InitializeResult serverInfo; // immutable after runStatus > DidInitialize + QLspSpecification::Responses::ShutdownResponseType shutdownResponse; + QHash<QString, QLanguageServerModule *> modules; + QLanguageServerProtocol protocol; + QLspNotifySignals notifySignals; +}; + +QT_END_NAMESPACE +#endif // QLANGUAGESERVER_P_P_H diff --git a/src/qmlls/qlspcustomtypes_p.h b/src/qmlls/qlspcustomtypes_p.h new file mode 100644 index 0000000000..e299388b79 --- /dev/null +++ b/src/qmlls/qlspcustomtypes_p.h @@ -0,0 +1,56 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +#ifndef QLSPCUSTOMTYPES_P_H +#define QLSPCUSTOMTYPES_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtLanguageServer/private/qlanguageserverspec_p.h> + +QT_BEGIN_NAMESPACE + +namespace QLspSpecification { + +class UriToBuildDirs +{ +public: + QByteArray baseUri = {}; + QList<QByteArray> buildDirs = {}; + + template<typename W> + void walk(W &w) + { + field(w, "baseUri", baseUri); + field(w, "buildDirs", buildDirs); + } +}; + +namespace Notifications { +constexpr auto AddBuildDirsMethod = "$/addBuildDirs"; + +class AddBuildDirsParams +{ +public: + QList<UriToBuildDirs> buildDirsToSet = {}; + + template<typename W> + void walk(W &w) + { + field(w, "buildDirsToSet", buildDirsToSet); + } +}; +} // namespace Notifications +} // namespace QLspSpecification + +QT_END_NAMESPACE + +#endif // QLSPCUSTOMTYPES_P_H diff --git a/src/qmlls/qqmlbasemodule_p.h b/src/qmlls/qqmlbasemodule_p.h new file mode 100644 index 0000000000..294b38fa40 --- /dev/null +++ b/src/qmlls/qqmlbasemodule_p.h @@ -0,0 +1,267 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLBASEMODULE_P_H +#define QQMLBASEMODULE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlcodemodel_p.h" +#include "qqmllsutils_p.h" +#include <QtQmlDom/private/qqmldom_utils_p.h> + +#include <QObject> +#include <type_traits> +#include <unordered_map> + +template<typename ParametersT, typename ResponseT> +struct BaseRequest +{ + // allow using Parameters and Response type aliases in the + // implementations of the different requests. + using Parameters = ParametersT; + using Response = ResponseT; + + // The version of the code on which the typedefinition request was made. + // Request is received: mark it with the current version of the textDocument. + // Then, wait for the codemodel to finish creating a snapshot version that is newer or equal to + // the textDocument version at request-received-time. + int m_minVersion; + Parameters m_parameters; + Response m_response; + + bool fillFrom(QmlLsp::OpenDocument doc, const Parameters ¶ms, Response &&response); +}; + +/*! +\internal +\brief This class sends a result or an error when going out of scope. + +It has a helper method \c setErrorFrom that sets an error from variant and optionals. +*/ + +template<typename Result, typename ResponseCallback> +struct ResponseScopeGuard +{ + Q_DISABLE_COPY_MOVE(ResponseScopeGuard) + + std::variant<Result *, QQmlLSUtils::ErrorMessage> m_response; + ResponseCallback &m_callback; + + ResponseScopeGuard(Result &results, ResponseCallback &callback) + : m_response(&results), m_callback(callback) + { + } + + // note: discards the current result or error message, if there is any + void setError(const QQmlLSUtils::ErrorMessage &error) { m_response = error; } + + template<typename... T> + bool setErrorFrom(const std::variant<T...> &variant) + { + static_assert(std::disjunction_v<std::is_same<T, QQmlLSUtils::ErrorMessage>...>, + "ResponseScopeGuard::setErrorFrom was passed a variant that never contains" + " an error message."); + if (auto x = std::get_if<QQmlLSUtils::ErrorMessage>(&variant)) { + setError(*x); + return true; + } + return false; + } + + /*! + \internal + Note: use it as follows: + \badcode + if (scopeGuard.setErrorFrom(xxx)) { + // do early exit + } + // xxx was not an error, continue + \endcode + */ + bool setErrorFrom(const std::optional<QQmlLSUtils::ErrorMessage> &error) + { + if (error) { + setError(*error); + return true; + } + return false; + } + + ~ResponseScopeGuard() + { + std::visit(qOverloadedVisitor{ [this](Result *result) { m_callback.sendResponse(*result); }, + [this](const QQmlLSUtils::ErrorMessage &error) { + m_callback.sendErrorResponse(error.code, + error.message.toUtf8()); + } }, + m_response); + } +}; + +template<typename RequestType> +struct QQmlBaseModule : public QLanguageServerModule +{ + using RequestParameters = typename RequestType::Parameters; + using RequestResponse = typename RequestType::Response; + using RequestPointer = std::unique_ptr<RequestType>; + using RequestPointerArgument = RequestPointer &&; + using BaseT = QQmlBaseModule<RequestType>; + + QQmlBaseModule(QmlLsp::QQmlCodeModel *codeModel); + ~QQmlBaseModule(); + + void requestHandler(const RequestParameters ¶meters, RequestResponse &&response); + decltype(auto) getRequestHandler(); + // processes a request in a different thread. + virtual void process(RequestPointerArgument toBeProcessed) = 0; + std::variant<QList<QQmlLSUtils::ItemLocation>, QQmlLSUtils::ErrorMessage> + itemsForRequest(const RequestPointer &request); + +public Q_SLOTS: + void updatedSnapshot(const QByteArray &uri); + +protected: + QMutex m_pending_mutex; + std::unordered_multimap<QString, RequestPointer> m_pending; + QmlLsp::QQmlCodeModel *m_codeModel; +}; + +template<typename Parameters, typename Response> +bool BaseRequest<Parameters, Response>::fillFrom(QmlLsp::OpenDocument doc, const Parameters ¶ms, + Response &&response) +{ + Q_UNUSED(doc); + m_parameters = params; + m_response = std::move(response); + + if (!doc.textDocument) { + qDebug() << "Cannot find document in qmlls's codemodel, did you open it before accessing " + "it?"; + return false; + } + + { + QMutexLocker l(doc.textDocument->mutex()); + m_minVersion = doc.textDocument->version().value_or(0); + } + return true; +} + +template<typename RequestType> +QQmlBaseModule<RequestType>::QQmlBaseModule(QmlLsp::QQmlCodeModel *codeModel) + : m_codeModel(codeModel) +{ + QObject::connect(m_codeModel, &QmlLsp::QQmlCodeModel::updatedSnapshot, this, + &QQmlBaseModule<RequestType>::updatedSnapshot); +} + +template<typename RequestType> +QQmlBaseModule<RequestType>::~QQmlBaseModule() +{ + QMutexLocker l(&m_pending_mutex); + m_pending.clear(); // empty the m_pending while the mutex is hold +} + +template<typename RequestType> +decltype(auto) QQmlBaseModule<RequestType>::getRequestHandler() +{ + auto handler = [this](const QByteArray &, const RequestParameters ¶meters, + RequestResponse &&response) { + requestHandler(parameters, std::move(response)); + }; + return handler; +} + +template<typename RequestType> +void QQmlBaseModule<RequestType>::requestHandler(const RequestParameters ¶meters, + RequestResponse &&response) +{ + auto req = std::make_unique<RequestType>(); + QmlLsp::OpenDocument doc = m_codeModel->openDocumentByUrl( + QQmlLSUtils::lspUriToQmlUrl(parameters.textDocument.uri)); + + if (!req->fillFrom(doc, parameters, std::move(response))) { + req->m_response.sendErrorResponse(0, "Received invalid request", parameters); + return; + } + const int minVersion = req->m_minVersion; + { + QMutexLocker l(&m_pending_mutex); + m_pending.insert({ QString::fromUtf8(req->m_parameters.textDocument.uri), std::move(req) }); + } + + if (doc.snapshot.docVersion && *doc.snapshot.docVersion >= minVersion) + updatedSnapshot(QQmlLSUtils::lspUriToQmlUrl(parameters.textDocument.uri)); +} + +template<typename RequestType> +void QQmlBaseModule<RequestType>::updatedSnapshot(const QByteArray &url) +{ + QmlLsp::OpenDocumentSnapshot doc = m_codeModel->snapshotByUrl(url); + std::vector<RequestPointer> toCompl; + { + QMutexLocker l(&m_pending_mutex); + for (auto [it, end] = m_pending.equal_range(QString::fromUtf8(url)); it != end;) { + if (auto &[key, value] = *it; + doc.docVersion && value->m_minVersion <= *doc.docVersion) { + toCompl.push_back(std::move(value)); + it = m_pending.erase(it); + } else { + ++it; + } + } + } + for (auto it = toCompl.rbegin(), end = toCompl.rend(); it != end; ++it) { + process(std::move(*it)); + } +} + +template<typename RequestType> +std::variant<QList<QQmlLSUtils::ItemLocation>, QQmlLSUtils::ErrorMessage> +QQmlBaseModule<RequestType>::itemsForRequest(const RequestPointer &request) +{ + + QmlLsp::OpenDocument doc = m_codeModel->openDocumentByUrl( + QQmlLSUtils::lspUriToQmlUrl(request->m_parameters.textDocument.uri)); + + if (!doc.snapshot.validDocVersion || doc.snapshot.validDocVersion != doc.snapshot.docVersion) { + return QQmlLSUtils::ErrorMessage{ 0, + u"Cannot proceed: current QML document is invalid! Fix" + u" all the errors in your QML code and try again."_s }; + } + + QQmlJS::Dom::DomItem file = doc.snapshot.validDoc.fileObject(QQmlJS::Dom::GoTo::MostLikely); + // clear reference cache to resolve latest versions (use a local env instead?) + if (auto envPtr = file.environment().ownerAs<QQmlJS::Dom::DomEnvironment>()) + envPtr->clearReferenceCache(); + if (!file) { + return QQmlLSUtils::ErrorMessage{ + 0, + u"Could not find file %1 in project."_s.arg(doc.snapshot.doc.toString()), + }; + } + + auto itemsFound = QQmlLSUtils::itemsFromTextLocation(file, request->m_parameters.position.line, + request->m_parameters.position.character); + + if (itemsFound.isEmpty()) { + return QQmlLSUtils::ErrorMessage{ + 0, + u"Could not find any items at given text location."_s, + }; + } + return itemsFound; +} + +#endif // QQMLBASEMODULE_P_H diff --git a/src/qmlls/qqmlcodemodel.cpp b/src/qmlls/qqmlcodemodel.cpp new file mode 100644 index 0000000000..fe6c220f3b --- /dev/null +++ b/src/qmlls/qqmlcodemodel.cpp @@ -0,0 +1,927 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmlcodemodel_p.h" +#include "qqmllsplugin_p.h" +#include "qtextdocument_p.h" +#include "qqmllsutils_p.h" + +#include <QtCore/qfileinfo.h> +#include <QtCore/qdir.h> +#include <QtCore/qthreadpool.h> +#include <QtCore/qlibraryinfo.h> +#include <QtCore/qprocess.h> +#include <QtCore/qdiriterator.h> +#include <QtQmlDom/private/qqmldomtop_p.h> + +#include <memory> +#include <algorithm> + +QT_BEGIN_NAMESPACE + +namespace QmlLsp { + +Q_STATIC_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel") + +using namespace QQmlJS::Dom; +using namespace Qt::StringLiterals; + +/*! +\internal +\class QQmlCodeModel + +The code model offers a view of the current state of the current files, and traks open files. +All methods are threadsafe, and generally return immutable or threadsafe objects that can be +worked on from any thread (unless otherwise noted). +The idea is the let all other operations be as lock free as possible, concentrating all tricky +synchronization here. + +\section2 Global views +\list +\li currentEnv() offers a view that contains the latest version of all the loaded files +\li validEnv() is just like current env but stores only the valid (meaning correctly parsed, + not necessarily without errors) version of a file, it is normally a better choice to load the + dependencies/symbol information from +\endlist + +\section2 OpenFiles +\list +\li snapshotByUrl() returns an OpenDocumentSnapshot of an open document. From it you can get the + document, its latest valid version, scope, all connected to a specific version of the document + and immutable. The signal updatedSnapshot() is called every time a snapshot changes (also for + every partial change: document change, validDocument change, scope change). +\li openDocumentByUrl() is a lower level and more intrusive access to OpenDocument objects. These + contains the current snapshot, and shared pointer to a Utils::TextDocument. This is *always* the + current version of the document, and has line by line support. + Working on it is more delicate and intrusive, because you have to explicitly acquire its mutex() + before *any* read or write/modification to it. + It has a version nuber which is supposed to always change and increase. + It is mainly used for highlighting/indenting, and is immediately updated when the user edits a + document. Its use should be avoided if possible, preferring the snapshots. +\endlist + +\section2 Parallelism/Theading +Most operations are not parallel and usually take place in the main thread (but are still thread +safe). +There are two main task that are executed in parallel: Indexing, and OpenDocumentUpdate. +Indexing is meant to keep the global view up to date. +OpenDocumentUpdate keeps the snapshots of the open documents up to date. + +There is always a tension between being responsive, using all threads available, and avoid to hog +too many resources. One can choose different parallelization strategies, we went with a flexiable +approach. +We have (private) functions that execute part of the work: indexSome() and openUpdateSome(). These +do all locking needed, get some work, do it without locks, and at the end update the state of the +code model. If there is more work, then they return true. Thus while (xxxSome()); works until there +is no work left. + +addDirectoriesToIndex(), the internal addDirectory() and addOpenToUpdate() add more work to do. + +indexNeedsUpdate() and openNeedUpdate(), check if there is work to do, and if yes ensure that a +worker thread (or more) that work on it exist. +*/ + +QQmlCodeModel::QQmlCodeModel(QObject *parent, QQmlToolingSettings *settings) + : QObject { parent }, + m_importPaths(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)), + m_currentEnv(std::make_shared<DomEnvironment>( + m_importPaths, DomEnvironment::Option::SingleThreaded, + DomCreationOptions{} | DomCreationOption::WithRecovery + | DomCreationOption::WithScriptExpressions + | DomCreationOption::WithSemanticAnalysis)), + m_validEnv(std::make_shared<DomEnvironment>( + m_importPaths, DomEnvironment::Option::SingleThreaded, + DomCreationOptions{} | DomCreationOption::WithRecovery + | DomCreationOption::WithScriptExpressions + | DomCreationOption::WithSemanticAnalysis)), + m_settings(settings), + m_pluginLoader(QmlLSPluginInterface_iid, u"/qmlls"_s) +{ +} + +/*! +\internal +Disable the functionality that uses CMake, and remove the already watched paths if there are some. +*/ +void QQmlCodeModel::disableCMakeCalls() +{ + m_cmakeStatus = DoesNotHaveCMake; + m_cppFileWatcher.removePaths(m_cppFileWatcher.files()); + QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, nullptr, nullptr); +} + +QQmlCodeModel::~QQmlCodeModel() +{ + QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, nullptr, nullptr); + while (true) { + bool shouldWait; + { + QMutexLocker l(&m_mutex); + m_state = State::Stopping; + m_openDocumentsToUpdate.clear(); + shouldWait = m_nIndexInProgress != 0 || m_nUpdateInProgress != 0; + } + if (!shouldWait) + break; + QThread::yieldCurrentThread(); + } +} + +OpenDocumentSnapshot QQmlCodeModel::snapshotByUrl(const QByteArray &url) +{ + return openDocumentByUrl(url).snapshot; +} + +int QQmlCodeModel::indexEvalProgress() const +{ + Q_ASSERT(!m_mutex.tryLock()); // should be called while locked + const int dirCost = 10; + int costToDo = 1; + for (const ToIndex &el : std::as_const(m_toIndex)) + costToDo += dirCost * el.leftDepth; + costToDo += m_indexInProgressCost; + return m_indexDoneCost * 100 / (costToDo + m_indexDoneCost); +} + +void QQmlCodeModel::indexStart() +{ + Q_ASSERT(!m_mutex.tryLock()); // should be called while locked + qCDebug(codeModelLog) << "indexStart"; +} + +void QQmlCodeModel::indexEnd() +{ + Q_ASSERT(!m_mutex.tryLock()); // should be called while locked + qCDebug(codeModelLog) << "indexEnd"; + m_lastIndexProgress = 0; + m_nIndexInProgress = 0; + m_toIndex.clear(); + m_indexInProgressCost = 0; + m_indexDoneCost = 0; +} + +void QQmlCodeModel::indexSendProgress(int progress) +{ + if (progress <= m_lastIndexProgress) + return; + m_lastIndexProgress = progress; + // ### actually send progress +} + +bool QQmlCodeModel::indexCancelled() +{ + QMutexLocker l(&m_mutex); + if (m_state == State::Stopping) + return true; + return false; +} + +void QQmlCodeModel::indexDirectory(const QString &path, int depthLeft) +{ + if (indexCancelled()) + return; + QDir dir(path); + if (depthLeft > 1) { + const QStringList dirs = + dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); + for (const QString &child : dirs) + addDirectory(dir.filePath(child), --depthLeft); + } + const QStringList qmljs = + dir.entryList(QStringList({ u"*.qml"_s, u"*.js"_s, u"*.mjs"_s }), QDir::Files); + int progress = 0; + { + QMutexLocker l(&m_mutex); + m_indexInProgressCost += qmljs.size(); + progress = indexEvalProgress(); + } + indexSendProgress(progress); + if (qmljs.isEmpty()) + return; + DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item(); + for (const QString &file : qmljs) { + if (indexCancelled()) + return; + QString fPath = dir.filePath(file); + auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>(); + FileToLoad fileToLoad = FileToLoad::fromFileSystem(newCurrentPtr, fPath); + if (!fileToLoad.canonicalPath().isEmpty()) { + newCurrentPtr->loadBuiltins(); + newCurrentPtr->loadFile(fileToLoad, [](Path, const DomItem &, const DomItem &) {}); + newCurrentPtr->loadPendingDependencies(); + newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>()); + } + { + QMutexLocker l(&m_mutex); + ++m_indexDoneCost; + --m_indexInProgressCost; + progress = indexEvalProgress(); + } + indexSendProgress(progress); + } +} + +void QQmlCodeModel::addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server) +{ + Q_UNUSED(server); + // ### create progress, &scan in a separate instance + const int maxDepth = 5; + for (const auto &path : paths) + addDirectory(path, maxDepth); + indexNeedsUpdate(); +} + +void QQmlCodeModel::addDirectory(const QString &path, int depthLeft) +{ + if (depthLeft < 1) + return; + { + QMutexLocker l(&m_mutex); + for (auto it = m_toIndex.begin(); it != m_toIndex.end();) { + if (it->path.startsWith(path)) { + if (it->path.size() == path.size()) + return; + if (it->path.at(path.size()) == u'/') { + it = m_toIndex.erase(it); + continue; + } + } else if (path.startsWith(it->path) && path.at(it->path.size()) == u'/') + return; + ++it; + } + m_toIndex.append({ path, depthLeft }); + } +} + +void QQmlCodeModel::removeDirectory(const QString &path) +{ + { + QMutexLocker l(&m_mutex); + auto toRemove = [path](const QString &p) { + return p.startsWith(path) && (p.size() == path.size() || p.at(path.size()) == u'/'); + }; + auto it = m_toIndex.begin(); + auto end = m_toIndex.end(); + while (it != end) { + if (toRemove(it->path)) + it = m_toIndex.erase(it); + else + ++it; + } + } + if (auto validEnvPtr = m_validEnv.ownerAs<DomEnvironment>()) + validEnvPtr->removePath(path); + if (auto currentEnvPtr = m_currentEnv.ownerAs<DomEnvironment>()) + currentEnvPtr->removePath(path); +} + +QString QQmlCodeModel::url2Path(const QByteArray &url, UrlLookup options) +{ + QString res; + { + QMutexLocker l(&m_mutex); + res = m_url2path.value(url); + } + if (!res.isEmpty() && options == UrlLookup::Caching) + return res; + QUrl qurl(QString::fromUtf8(url)); + QFileInfo f(qurl.toLocalFile()); + QString cPath = f.canonicalFilePath(); + if (cPath.isEmpty()) + cPath = f.filePath(); + { + QMutexLocker l(&m_mutex); + if (!res.isEmpty() && res != cPath) + m_path2url.remove(res); + m_url2path.insert(url, cPath); + m_path2url.insert(cPath, url); + } + return cPath; +} + +void QQmlCodeModel::newOpenFile(const QByteArray &url, int version, const QString &docText) +{ + { + QMutexLocker l(&m_mutex); + auto &openDoc = m_openDocuments[url]; + if (!openDoc.textDocument) + openDoc.textDocument = std::make_shared<Utils::TextDocument>(); + QMutexLocker l2(openDoc.textDocument->mutex()); + openDoc.textDocument->setVersion(version); + openDoc.textDocument->setPlainText(docText); + } + addOpenToUpdate(url); + openNeedUpdate(); +} + +OpenDocument QQmlCodeModel::openDocumentByUrl(const QByteArray &url) +{ + QMutexLocker l(&m_mutex); + return m_openDocuments.value(url); +} + +RegisteredSemanticTokens &QQmlCodeModel::registeredTokens() +{ + QMutexLocker l(&m_mutex); + return m_tokens; +} + +const RegisteredSemanticTokens &QQmlCodeModel::registeredTokens() const +{ + QMutexLocker l(&m_mutex); + return m_tokens; +} + +void QQmlCodeModel::indexNeedsUpdate() +{ + const int maxIndexThreads = 1; + { + QMutexLocker l(&m_mutex); + if (m_toIndex.isEmpty() || m_nIndexInProgress >= maxIndexThreads) + return; + if (++m_nIndexInProgress == 1) + indexStart(); + } + QThreadPool::globalInstance()->start([this]() { + while (indexSome()) { } + }); +} + +bool QQmlCodeModel::indexSome() +{ + qCDebug(codeModelLog) << "indexSome"; + ToIndex toIndex; + { + QMutexLocker l(&m_mutex); + if (m_toIndex.isEmpty()) { + if (--m_nIndexInProgress == 0) + indexEnd(); + return false; + } + toIndex = m_toIndex.last(); + m_toIndex.removeLast(); + } + bool hasMore = false; + { + auto guard = qScopeGuard([this, &hasMore]() { + QMutexLocker l(&m_mutex); + if (m_toIndex.isEmpty()) { + if (--m_nIndexInProgress == 0) + indexEnd(); + hasMore = false; + } else { + hasMore = true; + } + }); + indexDirectory(toIndex.path, toIndex.leftDepth); + } + return hasMore; +} + +void QQmlCodeModel::openNeedUpdate() +{ + qCDebug(codeModelLog) << "openNeedUpdate"; + const int maxIndexThreads = 1; + { + QMutexLocker l(&m_mutex); + if (m_openDocumentsToUpdate.isEmpty() || m_nUpdateInProgress >= maxIndexThreads) + return; + if (++m_nUpdateInProgress == 1) + openUpdateStart(); + } + QThreadPool::globalInstance()->start([this]() { + while (openUpdateSome()) { } + }); +} + +bool QQmlCodeModel::openUpdateSome() +{ + qCDebug(codeModelLog) << "openUpdateSome start"; + QByteArray toUpdate; + { + QMutexLocker l(&m_mutex); + if (m_openDocumentsToUpdate.isEmpty()) { + if (--m_nUpdateInProgress == 0) + openUpdateEnd(); + return false; + } + auto it = m_openDocumentsToUpdate.find(m_lastOpenDocumentUpdated); + auto end = m_openDocumentsToUpdate.end(); + if (it == end) + it = m_openDocumentsToUpdate.begin(); + else if (++it == end) + it = m_openDocumentsToUpdate.begin(); + toUpdate = *it; + m_openDocumentsToUpdate.erase(it); + } + bool hasMore = false; + { + auto guard = qScopeGuard([this, &hasMore]() { + QMutexLocker l(&m_mutex); + if (m_openDocumentsToUpdate.isEmpty()) { + if (--m_nUpdateInProgress == 0) + openUpdateEnd(); + hasMore = false; + } else { + hasMore = true; + } + }); + openUpdate(toUpdate); + } + return hasMore; +} + +void QQmlCodeModel::openUpdateStart() +{ + qCDebug(codeModelLog) << "openUpdateStart"; +} + +void QQmlCodeModel::openUpdateEnd() +{ + qCDebug(codeModelLog) << "openUpdateEnd"; +} + +/*! +\internal +Performs initialization for m_cmakeStatus, including testing for CMake on the current system. +*/ +void QQmlCodeModel::initializeCMakeStatus(const QString &pathForSettings) +{ + if (m_settings) { + const QString cmakeCalls = u"no-cmake-calls"_s; + m_settings->search(pathForSettings); + if (m_settings->isSet(cmakeCalls) && m_settings->value(cmakeCalls).toBool()) { + qWarning() << "Disabling CMake calls via .qmlls.ini setting."; + m_cmakeStatus = DoesNotHaveCMake; + return; + } + } + + QProcess process; + process.setProgram(u"cmake"_s); + process.setArguments({ u"--version"_s }); + process.start(); + process.waitForFinished(); + m_cmakeStatus = process.exitCode() == 0 ? HasCMake : DoesNotHaveCMake; + + if (m_cmakeStatus == DoesNotHaveCMake) { + qWarning() << "Disabling CMake calls because CMake was not found."; + return; + } + + QObject::connect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, this, + &QQmlCodeModel::onCppFileChanged); +} + +/*! +\internal +For each build path that is a also a CMake build path, call CMake with \l cmakeBuildCommand to +generate/update the .qmltypes, qmldir and .qrc files. +It is assumed here that the number of build folders is usually no more than one, so execute the +CMake builds one at a time. + +If CMake cannot be executed, false is returned. This may happen when CMake does not exist on the +current system, when the target executed by CMake does not exist (for example when something else +than qt_add_qml_module is used to setup the module in CMake), or the when the CMake build itself +fails. +*/ +bool QQmlCodeModel::callCMakeBuild(const QStringList &buildPaths) +{ + bool success = true; + for (const auto &path : buildPaths) { + if (!QFileInfo::exists(path + u"/.cmake"_s)) + continue; + + QProcess process; + const auto command = QQmlLSUtils::cmakeBuildCommand(path); + process.setProgram(command.first); + process.setArguments(command.second); + qCDebug(codeModelLog) << "Running" << process.program() << process.arguments(); + process.start(); + + // TODO: run process concurrently instead of blocking qmlls + success &= process.waitForFinished(); + success &= (process.exitCode() == 0); + qCDebug(codeModelLog) << process.program() << process.arguments() << "terminated with" + << process.exitCode(); + } + return success; +} + +/*! +\internal +Iterate the entire source directory to find all C++ files that have their names in fileNames, and +return all the found file paths. + +This is an overapproximation and might find unrelated files with the same name. +*/ +QStringList QQmlCodeModel::findFilePathsFromFileNames(const QStringList &fileNames) const +{ + QStringList result; + for (const auto &rootUrl : m_rootUrls) { + const QString rootDir = QUrl(QString::fromUtf8(rootUrl)).toLocalFile(); + + if (rootDir.isEmpty()) + continue; + + qCDebug(codeModelLog) << "Searching for files to watch in workspace folder" << rootDir; + QDirIterator it(rootDir, fileNames, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + QFileInfo info = it.nextFileInfo(); + result << info.absoluteFilePath(); + } + } + return result; +} + +/*! +\internal +Find all C++ file names (not path, for file paths call \l findFilePathsFromFileNames on the result +of this method) that this qmlFile relies on. +*/ +QStringList QQmlCodeModel::fileNamesToWatch(const DomItem &qmlFile) +{ + const QmlFile *file = qmlFile.as<QmlFile>(); + if (!file) + return {}; + + auto resolver = file->typeResolver(); + if (!resolver) + return {}; + + auto types = resolver->importedTypes(); + + QStringList result; + for (const auto &type : types) { + if (!type.scope) + continue; + // note: the factory only loads composite types + const bool isComposite = type.scope.factory() || type.scope->isComposite(); + if (isComposite) + continue; + + const QString filePath = QFileInfo(type.scope->filePath()).fileName(); + result << filePath; + } + + return result; +} + +/*! +\internal +Add watches for all C++ files that this qmlFile relies on, so a rebuild can be triggered when they +are modified. +*/ +void QQmlCodeModel::addFileWatches(const DomItem &qmlFile) +{ + const auto filesToWatch = fileNamesToWatch(qmlFile); + const QStringList filepathsToWatch = findFilePathsFromFileNames(filesToWatch); + const auto unwatchedPaths = m_cppFileWatcher.addPaths(filepathsToWatch); + if (!unwatchedPaths.isEmpty()) { + qCDebug(codeModelLog) << "Cannot watch paths" << unwatchedPaths << "from requested" + << filepathsToWatch; + } +} + +void QQmlCodeModel::onCppFileChanged(const QString &) +{ + m_rebuildRequired = true; +} + +void QQmlCodeModel::newDocForOpenFile(const QByteArray &url, int version, const QString &docText) +{ + qCDebug(codeModelLog) << "updating doc" << url << "to version" << version << "(" + << docText.size() << "chars)"; + + const QString fPath = url2Path(url, UrlLookup::ForceLookup); + if (m_cmakeStatus == RequiresInitialization) + initializeCMakeStatus(fPath); + + DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item(); + QStringList loadPaths = buildPathsForFileUrl(url); + + if (m_cmakeStatus == HasCMake && !loadPaths.isEmpty() && m_rebuildRequired) { + callCMakeBuild(loadPaths); + m_rebuildRequired = false; + } + + loadPaths.append(m_importPaths); + if (std::shared_ptr<DomEnvironment> newCurrentPtr = newCurrent.ownerAs<DomEnvironment>()) { + newCurrentPtr->setLoadPaths(loadPaths); + } + + // if the documentation root path is not set through the commandline, + // try to set it from the settings file (.qmlls.ini file) + if (m_documentationRootPath.isEmpty()) { + const QString path = url2Path(url); + if (m_settings && m_settings->search(path)) { + const QString docDir = QStringLiteral(u"docDir"); + if (m_settings->isSet(docDir)) + setDocumentationRootPath(m_settings->value(docDir).toString()); + } + } + + Path p; + auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>(); + newCurrentPtr->loadFile(FileToLoad::fromMemory(newCurrentPtr, fPath, docText), + [&p, this](Path, const DomItem &, const DomItem &newValue) { + const DomItem file = newValue.fileObject(); + p = file.canonicalPath(); + if (m_cmakeStatus == HasCMake) + addFileWatches(file); + }); + newCurrentPtr->loadPendingDependencies(); + if (p) { + newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>()); + DomItem item = m_currentEnv.path(p); + { + QMutexLocker l(&m_mutex); + OpenDocument &doc = m_openDocuments[url]; + if (!doc.textDocument) { + qCWarning(lspServerLog) + << "ignoring update to closed document" << QString::fromUtf8(url); + return; + } else { + QMutexLocker l(doc.textDocument->mutex()); + if (doc.textDocument->version() && *doc.textDocument->version() > version) { + qCWarning(lspServerLog) + << "docUpdate: version" << version << "of document" + << QString::fromUtf8(url) << "is not the latest anymore"; + return; + } + } + if (!doc.snapshot.docVersion || *doc.snapshot.docVersion < version) { + doc.snapshot.docVersion = version; + doc.snapshot.doc = item; + } else { + qCWarning(lspServerLog) << "skipping update of current doc to obsolete version" + << version << "of document" << QString::fromUtf8(url); + } + if (item.field(Fields::isValid).value().toBool(false)) { + if (!doc.snapshot.validDocVersion || *doc.snapshot.validDocVersion < version) { + DomItem vDoc = m_validEnv.path(p); + doc.snapshot.validDocVersion = version; + doc.snapshot.validDoc = vDoc; + } else { + qCWarning(lspServerLog) << "skippig update of valid doc to obsolete version" + << version << "of document" << QString::fromUtf8(url); + } + } else { + qCWarning(lspServerLog) + << "avoid update of validDoc to " << version << "of document" + << QString::fromUtf8(url) << "as it is invalid"; + } + } + } + if (codeModelLog().isDebugEnabled()) { + qCDebug(codeModelLog) << "finished update doc of " << url << "to version" << version; + snapshotByUrl(url).dump(qDebug() << "postSnapshot", + OpenDocumentSnapshot::DumpOption::AllCode); + } + // we should update the scope in the future thus call addOpen(url) + emit updatedSnapshot(url); +} + +void QQmlCodeModel::closeOpenFile(const QByteArray &url) +{ + QMutexLocker l(&m_mutex); + m_openDocuments.remove(url); +} + +void QQmlCodeModel::setRootUrls(const QList<QByteArray> &urls) +{ + QMutexLocker l(&m_mutex); + m_rootUrls = urls; +} + +void QQmlCodeModel::addRootUrls(const QList<QByteArray> &urls) +{ + QMutexLocker l(&m_mutex); + for (const QByteArray &url : urls) { + if (!m_rootUrls.contains(url)) + m_rootUrls.append(url); + } +} + +void QQmlCodeModel::removeRootUrls(const QList<QByteArray> &urls) +{ + QMutexLocker l(&m_mutex); + for (const QByteArray &url : urls) + m_rootUrls.removeOne(url); +} + +QList<QByteArray> QQmlCodeModel::rootUrls() const +{ + QMutexLocker l(&m_mutex); + return m_rootUrls; +} + +QStringList QQmlCodeModel::buildPathsForRootUrl(const QByteArray &url) +{ + QMutexLocker l(&m_mutex); + return m_buildPathsForRootUrl.value(url); +} + +static bool isNotSeparator(char c) +{ + return c != '/'; +} + +QStringList QQmlCodeModel::buildPathsForFileUrl(const QByteArray &url) +{ + QList<QByteArray> roots; + { + QMutexLocker l(&m_mutex); + roots = m_buildPathsForRootUrl.keys(); + } + // we want to longest match to be first, as it should override shorter matches + std::sort(roots.begin(), roots.end(), [](const QByteArray &el1, const QByteArray &el2) { + if (el1.size() > el2.size()) + return true; + if (el1.size() < el2.size()) + return false; + return el1 < el2; + }); + QStringList buildPaths; + QStringList defaultValues; + if (!roots.isEmpty() && roots.last().isEmpty()) + roots.removeLast(); + QByteArray urlSlash(url); + if (!urlSlash.isEmpty() && isNotSeparator(urlSlash.at(urlSlash.size() - 1))) + urlSlash.append('/'); + // look if the file has a know prefix path + for (const QByteArray &root : roots) { + if (urlSlash.startsWith(root)) { + buildPaths += buildPathsForRootUrl(root); + break; + } + } + QString path = url2Path(url); + + // fallback to the empty root, if is has an entry. + // This is the buildPath that is passed to qmlls via --build-dir. + if (buildPaths.isEmpty()) { + buildPaths += buildPathsForRootUrl(QByteArray()); + } + + // look in the QMLLS_BUILD_DIRS environment variable + if (buildPaths.isEmpty()) { + QStringList envPaths = qEnvironmentVariable("QMLLS_BUILD_DIRS") + .split(QDir::listSeparator(), Qt::SkipEmptyParts); + buildPaths += envPaths; + } + + // look in the settings. + // This is the one that is passed via the .qmlls.ini file. + if (buildPaths.isEmpty() && m_settings) { + m_settings->search(path); + QString buildDir = QStringLiteral(u"buildDir"); + if (m_settings->isSet(buildDir)) + buildPaths += m_settings->value(buildDir).toString().split(QDir::listSeparator(), + Qt::SkipEmptyParts); + } + + // heuristic to find build directory + if (buildPaths.isEmpty()) { + QDir d(path); + d.setNameFilters(QStringList({ u"build*"_s })); + const int maxDirDepth = 8; + int iDir = maxDirDepth; + QString dirName = d.dirName(); + QDateTime lastModified; + while (d.cdUp() && --iDir > 0) { + for (const QFileInfo &fInfo : d.entryInfoList(QDir::Dirs)) { + if (fInfo.completeBaseName() == u"build" + || fInfo.completeBaseName().startsWith(u"build-%1"_s.arg(dirName))) { + if (iDir > 1) + iDir = 1; + if (!lastModified.isValid() || lastModified < fInfo.lastModified()) { + buildPaths.clear(); + buildPaths.append(fInfo.absoluteFilePath()); + } + } + } + } + } + // add dependent build directories + QStringList res; + std::reverse(buildPaths.begin(), buildPaths.end()); + const int maxDeps = 4; + while (!buildPaths.isEmpty()) { + QString bPath = buildPaths.last(); + buildPaths.removeLast(); + res += bPath; + if (QFile::exists(bPath + u"/_deps") && bPath.split(u"/_deps/"_s).size() < maxDeps) { + QDir d(bPath + u"/_deps"); + for (const QFileInfo &fInfo : d.entryInfoList(QDir::Dirs)) + buildPaths.append(fInfo.absoluteFilePath()); + } + } + return res; +} + +void QQmlCodeModel::setDocumentationRootPath(const QString &path) +{ + QMutexLocker l(&m_mutex); + if (m_documentationRootPath != path) { + m_documentationRootPath = path; + emit documentationRootPathChanged(path); + } +} + +void QQmlCodeModel::setBuildPathsForRootUrl(QByteArray url, const QStringList &paths) +{ + QMutexLocker l(&m_mutex); + if (!url.isEmpty() && isNotSeparator(url.at(url.size() - 1))) + url.append('/'); + if (paths.isEmpty()) + m_buildPathsForRootUrl.remove(url); + else + m_buildPathsForRootUrl.insert(url, paths); +} + +void QQmlCodeModel::openUpdate(const QByteArray &url) +{ + bool updateDoc = false; + bool updateScope = false; + std::optional<int> rNow = 0; + QString docText; + DomItem validDoc; + std::shared_ptr<Utils::TextDocument> document; + { + QMutexLocker l(&m_mutex); + OpenDocument &doc = m_openDocuments[url]; + document = doc.textDocument; + if (!document) + return; + { + QMutexLocker l2(document->mutex()); + rNow = document->version(); + } + if (rNow && (!doc.snapshot.docVersion || *doc.snapshot.docVersion != *rNow)) + updateDoc = true; + else if (doc.snapshot.validDocVersion + && (!doc.snapshot.scopeVersion + || *doc.snapshot.scopeVersion != *doc.snapshot.validDocVersion)) + updateScope = true; + else + return; + if (updateDoc) { + QMutexLocker l2(doc.textDocument->mutex()); + rNow = doc.textDocument->version(); + docText = doc.textDocument->toPlainText(); + } else { + validDoc = doc.snapshot.validDoc; + rNow = doc.snapshot.validDocVersion; + } + } + if (updateDoc) { + newDocForOpenFile(url, *rNow, docText); + } + if (updateScope) { + // to do + } +} + +void QQmlCodeModel::addOpenToUpdate(const QByteArray &url) +{ + QMutexLocker l(&m_mutex); + m_openDocumentsToUpdate.insert(url); +} + +QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options) +{ + dbg.noquote().nospace() << "{"; + dbg << " url:" << QString::fromUtf8(url) << "\n"; + dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s) << "\n"; + if (options & DumpOption::LatestCode) { + dbg << " doc: ------------\n" + << doc.field(Fields::code).value().toString() << "\n==========\n"; + } else { + dbg << u" doc:" + << (doc ? u"%1chars"_s.arg(doc.field(Fields::code).value().toString().size()) + : u"*none*"_s) + << "\n"; + } + dbg << " validDocVersion:" + << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s) << "\n"; + if (options & DumpOption::ValidCode) { + dbg << " validDoc: ------------\n" + << validDoc.field(Fields::code).value().toString() << "\n==========\n"; + } else { + dbg << u" validDoc:" + << (validDoc ? u"%1chars"_s.arg(validDoc.field(Fields::code).value().toString().size()) + : u"*none*"_s) + << "\n"; + } + dbg << " scopeVersion:" << (scopeVersion ? QString::number(*scopeVersion) : u"*none*"_s) + << "\n"; + dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n"; + dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n"; + dbg << "}"; + return dbg; +} + +} // namespace QmlLsp + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmlcodemodel_p.h b/src/qmlls/qqmlcodemodel_p.h new file mode 100644 index 0000000000..38aec2c244 --- /dev/null +++ b/src/qmlls/qqmlcodemodel_p.h @@ -0,0 +1,178 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLCODEMODEL_P_H +#define QQMLCODEMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qtextdocument_p.h" + +#include <QObject> +#include <QHash> +#include <QtCore/qfilesystemwatcher.h> +#include <QtCore/private/qfactoryloader_p.h> +#include <QtQmlDom/private/qqmldomitem_p.h> +#include <QtQmlCompiler/private/qqmljsscope_p.h> +#include <QtQmlToolingSettings/private/qqmltoolingsettings_p.h> + +#include <functional> +#include <memory> + +QT_BEGIN_NAMESPACE +class TextSynchronization; +namespace QmlLsp { + +class OpenDocumentSnapshot +{ +public: + enum class DumpOption { + NoCode = 0, + LatestCode = 0x1, + ValidCode = 0x2, + AllCode = LatestCode | ValidCode + }; + Q_DECLARE_FLAGS(DumpOptions, DumpOption) + QStringList searchPath; + QByteArray url; + std::optional<int> docVersion; + QQmlJS::Dom::DomItem doc; + std::optional<int> validDocVersion; + QQmlJS::Dom::DomItem validDoc; + std::optional<int> scopeVersion; + QDateTime scopeDependenciesLoadTime; + bool scopeDependenciesChanged = false; + QQmlJSScope::ConstPtr scope; + QDebug dump(QDebug dbg, DumpOptions dump = DumpOption::NoCode); +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(OpenDocumentSnapshot::DumpOptions) + +class OpenDocument +{ +public: + OpenDocumentSnapshot snapshot; + std::shared_ptr<Utils::TextDocument> textDocument; +}; + +struct ToIndex +{ + QString path; + int leftDepth; +}; + +struct RegisteredSemanticTokens +{ + QByteArray resultId = "0"; + QList<int> lastTokens; +}; + +class QQmlCodeModel : public QObject +{ + Q_OBJECT +public: + enum class UrlLookup { Caching, ForceLookup }; + enum class State { Running, Stopping }; + + explicit QQmlCodeModel(QObject *parent = nullptr, QQmlToolingSettings *settings = nullptr); + ~QQmlCodeModel(); + QQmlJS::Dom::DomItem currentEnv() const { return m_currentEnv; }; + QQmlJS::Dom::DomItem validEnv() const { return m_validEnv; }; + OpenDocumentSnapshot snapshotByUrl(const QByteArray &url); + OpenDocument openDocumentByUrl(const QByteArray &url); + + void openNeedUpdate(); + void indexNeedsUpdate(); + void addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server); + void addOpenToUpdate(const QByteArray &); + void removeDirectory(const QString &path); + // void updateDocument(const OpenDocument &doc); + QString url2Path(const QByteArray &url, UrlLookup options = UrlLookup::Caching); + void newOpenFile(const QByteArray &url, int version, const QString &docText); + void newDocForOpenFile(const QByteArray &url, int version, const QString &docText); + void closeOpenFile(const QByteArray &url); + void setRootUrls(const QList<QByteArray> &urls); + QList<QByteArray> rootUrls() const; + void addRootUrls(const QList<QByteArray> &urls); + QStringList buildPathsForRootUrl(const QByteArray &url); + QStringList buildPathsForFileUrl(const QByteArray &url); + void setBuildPathsForRootUrl(QByteArray url, const QStringList &paths); + QStringList importPaths() const { return m_importPaths; }; + void setImportPaths(const QStringList &paths) { m_importPaths = paths; }; + void removeRootUrls(const QList<QByteArray> &urls); + QQmlToolingSettings *settings() const { return m_settings; } + QStringList findFilePathsFromFileNames(const QStringList &fileNames) const; + static QStringList fileNamesToWatch(const QQmlJS::Dom::DomItem &qmlFile); + void disableCMakeCalls(); + const QFactoryLoader &pluginLoader() const { return m_pluginLoader; } + + RegisteredSemanticTokens ®isteredTokens(); + const RegisteredSemanticTokens ®isteredTokens() const; + QString documentationRootPath() const { return m_documentationRootPath; } + void setDocumentationRootPath(const QString &path); + +Q_SIGNALS: + void updatedSnapshot(const QByteArray &url); + void documentationRootPathChanged(const QString &path); + +private: + void indexDirectory(const QString &path, int depthLeft); + int indexEvalProgress() const; // to be called in the mutex + void indexStart(); // to be called in the mutex + void indexEnd(); // to be called in the mutex + void indexSendProgress(int progress); + bool indexCancelled(); + bool indexSome(); + void addDirectory(const QString &path, int leftDepth); + bool openUpdateSome(); + void openUpdateStart(); + void openUpdateEnd(); + void openUpdate(const QByteArray &); + + static bool callCMakeBuild(const QStringList &buildPaths); + void addFileWatches(const QQmlJS::Dom::DomItem &qmlFile); + enum CMakeStatus { RequiresInitialization, HasCMake, DoesNotHaveCMake }; + void initializeCMakeStatus(const QString &); + + mutable QMutex m_mutex; + State m_state = State::Running; + int m_lastIndexProgress = 0; + int m_nIndexInProgress = 0; + QList<ToIndex> m_toIndex; + int m_indexInProgressCost = 0; + int m_indexDoneCost = 0; + int m_nUpdateInProgress = 0; + QStringList m_importPaths; + QQmlJS::Dom::DomItem m_currentEnv; + QQmlJS::Dom::DomItem m_validEnv; + QByteArray m_lastOpenDocumentUpdated; + QSet<QByteArray> m_openDocumentsToUpdate; + QHash<QByteArray, QStringList> m_buildPathsForRootUrl; + QList<QByteArray> m_rootUrls; + QHash<QByteArray, QString> m_url2path; + QHash<QString, QByteArray> m_path2url; + QHash<QByteArray, OpenDocument> m_openDocuments; + QQmlToolingSettings *m_settings; + QFileSystemWatcher m_cppFileWatcher; + QFactoryLoader m_pluginLoader; + bool m_rebuildRequired = true; // always trigger a rebuild on start + CMakeStatus m_cmakeStatus = RequiresInitialization; + RegisteredSemanticTokens m_tokens; + QString m_documentationRootPath; +private slots: + void onCppFileChanged(const QString &); +}; + +} // namespace QmlLsp +QT_END_NAMESPACE +#endif // QQMLCODEMODEL_P_H diff --git a/src/qmlls/qqmlcompletioncontextstrings.cpp b/src/qmlls/qqmlcompletioncontextstrings.cpp new file mode 100644 index 0000000000..5fc2006661 --- /dev/null +++ b/src/qmlls/qqmlcompletioncontextstrings.cpp @@ -0,0 +1,50 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmlcompletioncontextstrings_p.h" + +CompletionContextStrings::CompletionContextStrings(QString code, qsizetype pos) + : m_code(code), m_pos(pos) +{ + // computes the context just before pos in code. + // After this code all the values of all the attributes should be correct (see above) + // handle also letter or numbers represented a surrogate pairs? + m_filterStart = m_pos; + while (m_filterStart != 0) { + QChar c = code.at(m_filterStart - 1); + if (!c.isLetterOrNumber() && c != u'_') + break; + else + --m_filterStart; + } + // handle spaces? + m_baseStart = m_filterStart; + while (m_baseStart != 0) { + QChar c = code.at(m_baseStart - 1); + if (c != u'.' || m_baseStart == 1) + break; + c = code.at(m_baseStart - 2); + if (!c.isLetterOrNumber() && c != u'_') + break; + qsizetype baseEnd = --m_baseStart; + while (m_baseStart != 0) { + QChar c = code.at(m_baseStart - 1); + if (!c.isLetterOrNumber() && c != u'_') + break; + else + --m_baseStart; + } + if (m_baseStart == baseEnd) + break; + } + m_atLineStart = true; + m_lineStart = m_baseStart; + while (m_lineStart != 0) { + QChar c = code.at(m_lineStart - 1); + if (c == u'\n' || c == u'\r') + break; + if (!c.isSpace()) + m_atLineStart = false; + --m_lineStart; + } +} diff --git a/src/qmlls/qqmlcompletioncontextstrings_p.h b/src/qmlls/qqmlcompletioncontextstrings_p.h new file mode 100644 index 0000000000..78cf2b1553 --- /dev/null +++ b/src/qmlls/qqmlcompletioncontextstrings_p.h @@ -0,0 +1,62 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLLSCOMPLETIONCONTEXTSTRINGS_H +#define QQMLLSCOMPLETIONCONTEXTSTRINGS_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qtconfigmacros.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringview.h> + +QT_BEGIN_NAMESPACE + +// finds the filter string, the base (for fully qualified accesses) and the whole string +// just before pos in code +struct CompletionContextStrings +{ + CompletionContextStrings(QString code, qsizetype pos); + +public: + // line up until pos + QStringView preLine() const + { + return QStringView(m_code).mid(m_lineStart, m_pos - m_lineStart); + } + // the part used to filter the completion (normally actual filtering is left to the client) + QStringView filterChars() const + { + return QStringView(m_code).mid(m_filterStart, m_pos - m_filterStart); + } + // the base part (qualified access) + QStringView base() const + { + return QStringView(m_code).mid(m_baseStart, m_filterStart - m_baseStart); + } + // if we are at line start + bool atLineStart() const { return m_atLineStart; } + + qsizetype offset() const { return m_pos; } + +private: + QString m_code; // the current code + qsizetype m_pos = {}; // current position of the cursor + qsizetype m_filterStart = {}; // start of the characters that are used to filter the suggestions + qsizetype m_lineStart = {}; // start of the current line + qsizetype m_baseStart = {}; // start of the dotted expression that ends at the cursor position + bool m_atLineStart = {}; // if there are only spaces before base +}; + +QT_END_NAMESPACE + +#endif // QQMLLSCOMPLETIONCONTEXTSTRINGS_H diff --git a/src/qmlls/qqmlcompletionsupport.cpp b/src/qmlls/qqmlcompletionsupport.cpp new file mode 100644 index 0000000000..632a5e902a --- /dev/null +++ b/src/qmlls/qqmlcompletionsupport.cpp @@ -0,0 +1,196 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmlcompletionsupport_p.h" +#include "qqmllsutils_p.h" + +#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> +#include <QtCore/qthreadpool.h> +#include <QtCore/private/qduplicatetracker_p.h> +#include <QtCore/QRegularExpression> +#include <QtQmlDom/private/qqmldomexternalitems_p.h> +#include <QtQmlDom/private/qqmldomtop_p.h> +#include <QtQml/private/qqmlsignalnames_p.h> + +QT_BEGIN_NAMESPACE +using namespace QLspSpecification; +using namespace QQmlJS::Dom; +using namespace Qt::StringLiterals; + +bool CompletionRequest::fillFrom(QmlLsp::OpenDocument doc, const Parameters ¶ms, + Response &&response) +{ + // do not call BaseRequest::fillFrom() to avoid taking the Mutex twice and getting an + // inconsistent state. + m_parameters = params; + m_response = std::move(response); + + if (!doc.textDocument) + return false; + + std::optional<int> targetVersion; + { + QMutexLocker l(doc.textDocument->mutex()); + targetVersion = doc.textDocument->version(); + code = doc.textDocument->toPlainText(); + } + m_minVersion = (targetVersion ? *targetVersion : 0); + + return true; +} + +QmlCompletionSupport::QmlCompletionSupport(QmlLsp::QQmlCodeModel *codeModel) + : BaseT(codeModel), m_completionEngine(codeModel->pluginLoader()) +{ +} + +void QmlCompletionSupport::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) +{ + protocol->registerCompletionRequestHandler(getRequestHandler()); + protocol->registerCompletionItemResolveRequestHandler( + [](const QByteArray &, const CompletionItem &cParams, + LSPResponse<CompletionItem> &&response) { response.sendResponse(cParams); }); +} + +QString QmlCompletionSupport::name() const +{ + return u"QmlCompletionSupport"_s; +} + +void QmlCompletionSupport::setupCapabilities( + const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverCapabilities) +{ + QLspSpecification::CompletionOptions cOptions; + if (serverCapabilities.capabilities.completionProvider) + cOptions = *serverCapabilities.capabilities.completionProvider; + cOptions.resolveProvider = false; + cOptions.triggerCharacters = QList<QByteArray>({ QByteArray(".") }); + serverCapabilities.capabilities.completionProvider = cOptions; +} + +void QmlCompletionSupport::process(RequestPointerArgument req) +{ + QmlLsp::OpenDocumentSnapshot doc = + m_codeModel->snapshotByUrl(req->m_parameters.textDocument.uri); + req->sendCompletions(req->completions(doc, m_completionEngine)); +} + +QString CompletionRequest::urlAndPos() const +{ + return QString::fromUtf8(m_parameters.textDocument.uri) + u":" + + QString::number(m_parameters.position.line) + u":" + + QString::number(m_parameters.position.character); +} + +void CompletionRequest::sendCompletions(const QList<CompletionItem> &completions) +{ + m_response.sendResponse(completions); +} + +static bool positionIsFollowedBySpaces(qsizetype position, const QString &code) +{ + if (position >= code.size()) + return false; + + auto newline = + std::find_if(std::next(code.cbegin(), position), code.cend(), + [](const QChar &c) { return c == u'\n' || c == u'\r' || !c.isSpace(); }); + + return newline == code.cend() || newline->isSpace(); +} + +/*! +\internal + +\note Remove this method and all its usages once the new fault-tolerant parser from QTBUG-118053 is +introduced!!! + +Tries to make the document valid for the parser, to be able to provide completions after dots. +The created DomItem is not in the qqmlcodemodel which mean it cannot be seen and cannot bother +other modules: it would be bad to have the linting module complain about code that was modified +here, but cannot be seen by the user. +*/ +DomItem CompletionRequest::patchInvalidFileForParser(const DomItem &file, qsizetype position) const +{ + // automatic semicolon insertion after dots, if there is nothing behind the dot! + if (position > 0 && code[position - 1] == u'.' && positionIsFollowedBySpaces(position, code)) { + qCWarning(QQmlLSCompletionLog) + << "Patching invalid document: adding a semicolon after '.' for " + << QString::fromUtf8(m_parameters.textDocument.uri); + + const QString patchedCode = + code.first(position).append(u"_dummyIdentifier;").append(code.sliced(position)); + + // create a new (local) Dom only for the completions. + // This avoids weird behaviors, like the linting module complaining about the inserted + // semicolon that the user cannot see, for example. + DomItem newCurrent = file.environment().makeCopy(DomItem::CopyOption::EnvConnected).item(); + + DomItem result; + auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>(); + newCurrentPtr->loadFile( + FileToLoad::fromMemory(newCurrentPtr, file.canonicalFilePath(), patchedCode), + [&result](Path, const DomItem &, const DomItem &newValue) { + result = newValue.fileObject(); + }); + newCurrentPtr->loadPendingDependencies(); + return result; + } + + qCWarning(QQmlLSCompletionLog) << "No valid document for completions for " + << QString::fromUtf8(m_parameters.textDocument.uri); + + return file; +} + +QList<CompletionItem> CompletionRequest::completions(QmlLsp::OpenDocumentSnapshot &doc, + const QQmlLSCompletion &completionEngine) const +{ + QList<CompletionItem> res; + + + const qsizetype pos = QQmlLSUtils::textOffsetFrom(code, m_parameters.position.line, + m_parameters.position.character); + + const bool useValidDoc = + doc.validDoc && doc.validDocVersion && *doc.validDocVersion >= m_minVersion; + + const DomItem file = useValidDoc + ? doc.validDoc.fileObject(QQmlJS::Dom::GoTo::MostLikely) + : patchInvalidFileForParser(doc.doc.fileObject(QQmlJS::Dom::GoTo::MostLikely), pos); + + // clear reference cache to resolve latest versions (use a local env instead?) + if (std::shared_ptr<DomEnvironment> envPtr = file.environment().ownerAs<DomEnvironment>()) + envPtr->clearReferenceCache(); + + + CompletionContextStrings ctx(code, pos); + auto itemsFound = QQmlLSUtils::itemsFromTextLocation(file, m_parameters.position.line, + m_parameters.position.character + - ctx.filterChars().size()); + if (itemsFound.isEmpty()) { + qCDebug(QQmlLSCompletionLog) << "No items found for completions at" << urlAndPos(); + return {}; + } + + if (itemsFound.size() > 1) { + QStringList paths; + for (auto &it : itemsFound) + paths.append(it.domItem.canonicalPath().toString()); + qCWarning(QQmlLSCompletionLog) << "Multiple elements of " << urlAndPos() + << " at the same depth:" << paths << "(using first)"; + } + const DomItem currentItem = itemsFound.first().domItem; + qCDebug(QQmlLSCompletionLog) << "Completion at " << urlAndPos() << " " + << m_parameters.position.line << ":" + << m_parameters.position.character << "offset:" << pos + << "base:" << ctx.base() << "filter:" << ctx.filterChars() + << "lastVersion:" << (doc.docVersion ? (*doc.docVersion) : -1) + << "validVersion:" + << (doc.validDocVersion ? (*doc.validDocVersion) : -1) << "in" + << currentItem.internalKindStr() << currentItem.canonicalPath(); + auto result = completionEngine.completions(currentItem, ctx); + return result; +} +QT_END_NAMESPACE diff --git a/src/qmlls/qqmlcompletionsupport_p.h b/src/qmlls/qqmlcompletionsupport_p.h new file mode 100644 index 0000000000..3d7fa03d94 --- /dev/null +++ b/src/qmlls/qqmlcompletionsupport_p.h @@ -0,0 +1,61 @@ +// Copyright (C) 2018 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLCOMPLETIONSUPPORT_P_H +#define QQMLCOMPLETIONSUPPORT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlbasemodule_p.h" +#include "qqmlcodemodel_p.h" + +#include <QtCore/qmutex.h> +#include <QtCore/qhash.h> +#include <QtQmlLS/private/qqmllscompletion_p.h> + +QT_BEGIN_NAMESPACE +struct CompletionRequest + : BaseRequest<QLspSpecification::CompletionParams, + QLspSpecification::LSPPartialResponse< + std::variant<QList<QLspSpecification::CompletionItem>, + QLspSpecification::CompletionList, std::nullptr_t>, + std::variant<QLspSpecification::CompletionList, + QList<QLspSpecification::CompletionItem>>>> +{ + QString code; + + bool fillFrom(QmlLsp::OpenDocument doc, const Parameters ¶ms, Response &&response); + void sendCompletions(const QList<QLspSpecification::CompletionItem> &completions); + QString urlAndPos() const; + QList<QLspSpecification::CompletionItem> + completions(QmlLsp::OpenDocumentSnapshot &doc, const QQmlLSCompletion &completionEngine) const; + QQmlJS::Dom::DomItem patchInvalidFileForParser(const QQmlJS::Dom::DomItem &file, + qsizetype position) const; +}; + +class QmlCompletionSupport : public QQmlBaseModule<CompletionRequest> +{ + Q_OBJECT +public: + QmlCompletionSupport(QmlLsp::QQmlCodeModel *codeModel); + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; + void process(RequestPointerArgument req) override; + + QQmlLSCompletion m_completionEngine; +}; +QT_END_NAMESPACE + +#endif // QMLCOMPLETIONSUPPORT_P_H diff --git a/src/qmlls/qqmlfindusagessupport.cpp b/src/qmlls/qqmlfindusagessupport.cpp new file mode 100644 index 0000000000..dc407f71bc --- /dev/null +++ b/src/qmlls/qqmlfindusagessupport.cpp @@ -0,0 +1,81 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmlfindusagessupport_p.h" +#include "qqmllsutils_p.h" +#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> +#include <QtQmlDom/private/qqmldomexternalitems_p.h> +#include <QtQmlDom/private/qqmldomtop_p.h> +#include <variant> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +QQmlFindUsagesSupport::QQmlFindUsagesSupport(QmlLsp::QQmlCodeModel *codeModel) + : BaseT(codeModel) { } + +QString QQmlFindUsagesSupport::name() const +{ + return u"QmlFindUsagesSupport"_s; +} + +void QQmlFindUsagesSupport::setupCapabilities( + const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverCapabilities) +{ + // just assume serverCapabilities.capabilities.typeDefinitionProvider is a bool for now + // handle the ReferenceOptions later if needed (it adds the possibility to communicate the + // current progress). + serverCapabilities.capabilities.referencesProvider = true; +} + +void QQmlFindUsagesSupport::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) +{ + protocol->registerReferenceRequestHandler(getRequestHandler()); +} + +void QQmlFindUsagesSupport::process(QQmlFindUsagesSupport::RequestPointerArgument request) +{ + QList<QLspSpecification::Location> results; + ResponseScopeGuard guard(results, request->m_response); + + auto itemsFound = itemsForRequest(request); + if (guard.setErrorFrom(itemsFound)) + return; + + QQmlLSUtils::ItemLocation &front = + std::get<QList<QQmlLSUtils::ItemLocation>>(itemsFound).front(); + + auto usages = QQmlLSUtils::findUsagesOf(front.domItem); + + QQmlJS::Dom::DomItem files = front.domItem.top().field(QQmlJS::Dom::Fields::qmlFileWithPath); + + QHash<QString, QString> codeCache; + + // note: ignore usages in filenames here as that is not supported by the protocol. + for (const auto &usage : usages.usagesInFile()) { + QLspSpecification::Location location; + location.uri = QUrl::fromLocalFile(usage.filename).toEncoded(); + + auto cacheEntry = codeCache.find(usage.filename); + if (cacheEntry == codeCache.end()) { + auto file = files.key(usage.filename) + .field(QQmlJS::Dom::Fields::currentItem) + .ownerAs<QQmlJS::Dom::QmlFile>(); + if (!file) { + qDebug() << "File" << usage.filename << "not found in DOM! Available files are" + << files.keys(); + continue; + } + cacheEntry = codeCache.insert(usage.filename, file->code()); + } + + location.range = + QQmlLSUtils::qmlLocationToLspLocation(cacheEntry.value(), usage.sourceLocation); + + results.append(location); + } +} +QT_END_NAMESPACE + diff --git a/src/qmlls/qqmlfindusagessupport_p.h b/src/qmlls/qqmlfindusagessupport_p.h new file mode 100644 index 0000000000..ee4803ea93 --- /dev/null +++ b/src/qmlls/qqmlfindusagessupport_p.h @@ -0,0 +1,47 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QMLFINDUSAGESUPPORT_P_H +#define QMLFINDUSAGESUPPORT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlcodemodel_p.h" +#include "qqmlbasemodule_p.h" + +QT_BEGIN_NAMESPACE +struct ReferencesRequest : public BaseRequest<QLspSpecification::ReferenceParams, + QLspSpecification::Responses::ReferenceResponseType> +{ +}; + +class QQmlFindUsagesSupport : public QQmlBaseModule<ReferencesRequest> +{ + Q_OBJECT +public: + QQmlFindUsagesSupport(QmlLsp::QQmlCodeModel *codeModel); + + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; + + void process(RequestPointerArgument request) override; + + void typeDefinitionRequestHandler(const QByteArray &, + const QLspSpecification::TypeDefinitionParams ¶ms, + ReferencesRequest::Response &&response); +}; +QT_END_NAMESPACE + +#endif // QMLFINDUSAGESUPPORT_P_H diff --git a/src/qmlls/qqmlformatting.cpp b/src/qmlls/qqmlformatting.cpp new file mode 100644 index 0000000000..cf0cdf5147 --- /dev/null +++ b/src/qmlls/qqmlformatting.cpp @@ -0,0 +1,95 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <qqmlformatting_p.h> +#include <qqmlcodemodel_p.h> +#include <qqmllsutils_p.h> + +#include <QtQmlDom/private/qqmldomitem_p.h> +#include <QtQmlDom/private/qqmldomindentinglinewriter_p.h> +#include <QtQmlDom/private/qqmldomoutwriter_p.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(formatLog, "qt.languageserver.formatting") + +QQmlDocumentFormatting::QQmlDocumentFormatting(QmlLsp::QQmlCodeModel *codeModel) + : QQmlBaseModule(codeModel) +{ +} + +QString QQmlDocumentFormatting::name() const +{ + return u"QQmlDocumentFormatting"_s; +} + +void QQmlDocumentFormatting::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) +{ + protocol->registerDocumentFormattingRequestHandler(getRequestHandler()); +} + +void QQmlDocumentFormatting::setupCapabilities( + const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverCapabilities) +{ + // TODO: Allow customized formatting in future + serverCapabilities.capabilities.documentFormattingProvider = true; +} + +void QQmlDocumentFormatting::process(RequestPointerArgument request) +{ + QList<QLspSpecification::TextEdit> result; + ResponseScopeGuard guard(result, request->m_response); + + using namespace QQmlJS::Dom; + QmlLsp::OpenDocument doc = m_codeModel->openDocumentByUrl( + QQmlLSUtils::lspUriToQmlUrl(request->m_parameters.textDocument.uri)); + + DomItem file = doc.snapshot.doc.fileObject(GoTo::MostLikely); + if (!file) { + guard.setError(QQmlLSUtils::ErrorMessage{ + 0, u"Could not find the file %1"_s.arg(doc.snapshot.doc.canonicalFilePath()) }); + return; + } + if (!file.field(Fields::isValid).value().toBool(false)) { + guard.setError(QQmlLSUtils::ErrorMessage{ 0, u"Cannot format invalid documents!"_s }); + return; + } + if (auto envPtr = file.environment().ownerAs<DomEnvironment>()) + envPtr->clearReferenceCache(); + + auto qmlFile = file.ownerAs<QmlFile>(); + if (!qmlFile || !qmlFile->isValid()) { + file.iterateErrors( + [](const DomItem &, const ErrorMessage &msg) { + errorToQDebug(msg); + return true; + }, + true); + guard.setError(QQmlLSUtils::ErrorMessage{ + 0, u"Failed to parse %1"_s.arg(file.canonicalFilePath()) }); + return; + } + + // TODO: implement formatting options + // For now, qmlformat's default options. + LineWriterOptions options; + options.updateOptions = LineWriterOptions::Update::None; + options.attributesSequence = LineWriterOptions::AttributesSequence::Preserve; + + QLspSpecification::TextEdit formattedText; + LineWriter lw([&formattedText](QStringView s) {formattedText.newText += s.toUtf8(); }, QString(), options); + OutWriter ow(lw); + file.writeOutForFile(ow, WriteOutCheck::None); + ow.flush(); + const auto &code = qmlFile->code(); + const auto [endLine, endColumn] = QQmlLSUtils::textRowAndColumnFrom(code, code.length()); + + Q_UNUSED(endColumn); + formattedText.range = QLspSpecification::Range{ QLspSpecification::Position{ 0, 0 }, + QLspSpecification::Position{ endLine + 1, 0 } }; + + result.append(formattedText); +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmlformatting_p.h b/src/qmlls/qqmlformatting_p.h new file mode 100644 index 0000000000..19fc30683e --- /dev/null +++ b/src/qmlls/qqmlformatting_p.h @@ -0,0 +1,46 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLFORMATTING_P_H +#define QQMLFORMATTING_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlbasemodule_p.h" +#include "qqmlcodemodel_p.h" + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(formatLog) + +struct DocumentFormattingRequest + : public BaseRequest<QLspSpecification::DocumentFormattingParams, + QLspSpecification::Responses::DocumentFormattingResponseType> +{ +}; + +class QQmlDocumentFormatting : public QQmlBaseModule<DocumentFormattingRequest> +{ + Q_OBJECT +public: + QQmlDocumentFormatting(QmlLsp::QQmlCodeModel *codeModel); + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; + void process(RequestPointerArgument req) override; +}; + +QT_END_NAMESPACE + +#endif // QQMLFORMATTING_P_H diff --git a/src/qmlls/qqmlgotodefinitionsupport.cpp b/src/qmlls/qqmlgotodefinitionsupport.cpp new file mode 100644 index 0000000000..366fbe7d09 --- /dev/null +++ b/src/qmlls/qqmlgotodefinitionsupport.cpp @@ -0,0 +1,70 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmlgotodefinitionsupport_p.h" +#include "qqmllsutils_p.h" +#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> +#include <QtQmlDom/private/qqmldomexternalitems_p.h> +#include <QtQmlDom/private/qqmldomtop_p.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +QmlGoToDefinitionSupport::QmlGoToDefinitionSupport(QmlLsp::QQmlCodeModel *codeModel) + : BaseT(codeModel) +{ +} + +QString QmlGoToDefinitionSupport::name() const +{ + return u"QmlDefinitionSupport"_s; +} + +void QmlGoToDefinitionSupport::setupCapabilities( + const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverCapabilities) +{ + // just assume serverCapabilities.capabilities.typeDefinitionProvider is a bool for now + // handle the TypeDefinitionOptions and TypeDefinitionRegistrationOptions cases later on, if + // needed (as they just allow more fancy go-to-type-definition action). + serverCapabilities.capabilities.definitionProvider = true; +} + +void QmlGoToDefinitionSupport::registerHandlers(QLanguageServer *, + QLanguageServerProtocol *protocol) +{ + protocol->registerDefinitionRequestHandler(getRequestHandler()); +} + +void QmlGoToDefinitionSupport::process(RequestPointerArgument request) +{ + QList<QLspSpecification::Location> results; + ResponseScopeGuard guard(results, request->m_response); + + auto itemsFound = itemsForRequest(request); + + if (guard.setErrorFrom(itemsFound)) + return; + + auto &front = std::get<QList<QQmlLSUtils::ItemLocation>>(itemsFound).front(); + + auto location = QQmlLSUtils::findDefinitionOf(front.domItem); + if (!location) + return; + + QLspSpecification::Location l; + l.uri = QUrl::fromLocalFile(location->filename).toEncoded(); + + QQmlJS::Dom::DomItem file = front.domItem.goToFile(location->filename); + auto fileOfBasePtr = file.ownerAs<QQmlJS::Dom::QmlFile>(); + if (!fileOfBasePtr) { + qDebug() << "Could not find file" << location->filename << "in the dom!"; + return; + } + const QString qmlCode = fileOfBasePtr->code(); + l.range = QQmlLSUtils::qmlLocationToLspLocation(qmlCode, location->sourceLocation); + + results.append(l); +} +QT_END_NAMESPACE diff --git a/src/qmlls/qqmlgotodefinitionsupport_p.h b/src/qmlls/qqmlgotodefinitionsupport_p.h new file mode 100644 index 0000000000..d4553e3b67 --- /dev/null +++ b/src/qmlls/qqmlgotodefinitionsupport_p.h @@ -0,0 +1,49 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLGOTODEFINITIONSUPPORT_P_H +#define QQMLGOTODEFINITIONSUPPORT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlcodemodel_p.h" +#include "qqmlbasemodule_p.h" + +QT_BEGIN_NAMESPACE + +struct DefinitionRequest : public BaseRequest<QLspSpecification::DefinitionParams, + QLspSpecification::Responses::DefinitionResponseType> +{ +}; + +class QmlGoToDefinitionSupport : public QQmlBaseModule<DefinitionRequest> +{ + Q_OBJECT +public: + QmlGoToDefinitionSupport(QmlLsp::QQmlCodeModel *codeModel); + + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; + + void process(RequestPointerArgument request) override; + + void typeDefinitionRequestHandler(const QByteArray &, + const QLspSpecification::DefinitionParams ¶ms, + RequestPointerArgument response); +}; + +QT_END_NAMESPACE + +#endif // QQMLGOTODEFINITIONSUPPORT_P_H diff --git a/src/qmlls/qqmlgototypedefinitionsupport.cpp b/src/qmlls/qqmlgototypedefinitionsupport.cpp new file mode 100644 index 0000000000..b29cf3b833 --- /dev/null +++ b/src/qmlls/qqmlgototypedefinitionsupport.cpp @@ -0,0 +1,74 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmlgototypedefinitionsupport_p.h" +#include "qqmllsutils_p.h" +#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> +#include <QtQmlDom/private/qqmldomexternalitems_p.h> +#include <QtQmlDom/private/qqmldomtop_p.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +QmlGoToTypeDefinitionSupport::QmlGoToTypeDefinitionSupport(QmlLsp::QQmlCodeModel *codeModel) + : BaseT(codeModel) +{ +} + +QString QmlGoToTypeDefinitionSupport::name() const +{ + return u"QmlNavigationSupport"_s; +} + +void QmlGoToTypeDefinitionSupport::setupCapabilities( + const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverCapabilities) +{ + // just assume serverCapabilities.capabilities.typeDefinitionProvider is a bool for now + // handle the TypeDefinitionOptions and TypeDefinitionRegistrationOptions cases later on, if + // needed (as they just allow more fancy go-to-type-definition action). + serverCapabilities.capabilities.typeDefinitionProvider = true; +} + +void QmlGoToTypeDefinitionSupport::registerHandlers(QLanguageServer *, + QLanguageServerProtocol *protocol) +{ + protocol->registerTypeDefinitionRequestHandler(getRequestHandler()); +} + +void QmlGoToTypeDefinitionSupport::process(RequestPointerArgument request) +{ + QList<QLspSpecification::Location> results; + ResponseScopeGuard guard(results, request->m_response); + + auto itemsFound = itemsForRequest(request); + if (guard.setErrorFrom(itemsFound)) + return; + + auto &front = std::get<QList<QQmlLSUtils::ItemLocation>>(itemsFound).front(); + + auto base = QQmlLSUtils::findTypeDefinitionOf(front.domItem); + + if (!base) { + qDebug() << u"Could not obtain the base from the item"_s; + return; + } + + QQmlJS::Dom::DomItem fileOfBase = front.domItem.goToFile(base->filename); + auto fileOfBasePtr = fileOfBase.ownerAs<QQmlJS::Dom::QmlFile>(); + if (!fileOfBasePtr) { + qDebug() << u"Could not obtain the file of the base."_s; + return; + } + + QLspSpecification::Location l; + l.uri = QUrl::fromLocalFile(fileOfBasePtr->canonicalFilePath()).toEncoded(); + + const QString qmlCode = fileOfBasePtr->code(); + l.range = QQmlLSUtils::qmlLocationToLspLocation(qmlCode, base->sourceLocation); + + results.append(l); +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmlgototypedefinitionsupport_p.h b/src/qmlls/qqmlgototypedefinitionsupport_p.h new file mode 100644 index 0000000000..a8af07fd91 --- /dev/null +++ b/src/qmlls/qqmlgototypedefinitionsupport_p.h @@ -0,0 +1,50 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QMLGOTOTYPEDEFINITIONSUPPORT_P_H +#define QMLGOTOTYPEDEFINITIONSUPPORT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlcodemodel_p.h" +#include "qqmlbasemodule_p.h" + +QT_BEGIN_NAMESPACE + +struct TypeDefinitionRequest + : public BaseRequest<QLspSpecification::TypeDefinitionParams, + QLspSpecification::Responses::TypeDefinitionResponseType> +{ +}; + +class QmlGoToTypeDefinitionSupport : public QQmlBaseModule<TypeDefinitionRequest> +{ + Q_OBJECT +public: + QmlGoToTypeDefinitionSupport(QmlLsp::QQmlCodeModel *codeModel); + + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; + + void process(RequestPointerArgument request) override; + + void typeDefinitionRequestHandler(const QByteArray &, + const QLspSpecification::TypeDefinitionParams ¶ms, + TypeDefinitionRequest::Response &&response); +}; + +QT_END_NAMESPACE + +#endif // QMLGOTOTYPEDEFINITIONSUPPORT_P_H diff --git a/src/qmlls/qqmlhighlightsupport.cpp b/src/qmlls/qqmlhighlightsupport.cpp new file mode 100644 index 0000000000..b3892fbb38 --- /dev/null +++ b/src/qmlls/qqmlhighlightsupport.cpp @@ -0,0 +1,212 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <qqmlhighlightsupport_p.h> +#include <qqmlsemantictokens_p.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; +using namespace QLspSpecification; +using namespace QQmlJS::Dom; + +/*! +\internal +Make a list of enum names to register the supported token +types and modifiers. It is case-sensitive in the protocol +thus we need to lower the first characters of the enum names. +*/ +template <typename EnumType> +static QList<QByteArray> enumToByteArray() +{ + QList<QByteArray> result; + QMetaEnum metaEnum = QMetaEnum::fromType<EnumType>(); + for (auto i = 0; i < metaEnum.keyCount(); ++i) { + auto &&enumName = QByteArray(metaEnum.key(i)); + enumName.front() = std::tolower(enumName.front()); + result.emplace_back(std::move(enumName)); + } + + return result; +} + +static QList<QByteArray> tokenTypesList() +{ + return enumToByteArray<SemanticTokenTypes>(); +} + +static QList<QByteArray> tokenModifiersList() +{ + return enumToByteArray<SemanticTokenModifiers>(); +} + +/*! +\internal +A wrapper class that handles the semantic tokens request for a whole file as described in +https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#semanticTokens_fullRequest +Sends a QLspSpecification::SemanticTokens data as response that is generated for the entire file. +*/ +SemanticTokenFullHandler::SemanticTokenFullHandler(QmlLsp::QQmlCodeModel *codeModel) + : QQmlBaseModule(codeModel) +{ +} + +void SemanticTokenFullHandler::process( + QQmlBaseModule<SemanticTokensRequest>::RequestPointerArgument request) +{ + if (!request) { + qCWarning(semanticTokens) << "No semantic token request is available!"; + return; + } + + Responses::SemanticTokensResultType result; + ResponseScopeGuard guard(result, request->m_response); + const auto doc = m_codeModel->openDocumentByUrl( + QQmlLSUtils::lspUriToQmlUrl(request->m_parameters.textDocument.uri)); + DomItem file = doc.snapshot.doc.fileObject(GoTo::MostLikely); + Highlights highlights; + auto &&encoded = highlights.collectTokens(file, std::nullopt); + auto ®isteredTokens = m_codeModel->registeredTokens(); + if (!encoded.isEmpty()) { + HighlightingUtils::updateResultID(registeredTokens.resultId); + result = SemanticTokens{ registeredTokens.resultId, encoded }; + registeredTokens.lastTokens = std::move(encoded); + } else { + result = nullptr; + } +} + +void SemanticTokenFullHandler::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) +{ + protocol->registerSemanticTokensRequestHandler(getRequestHandler()); +} + +/*! +\internal +A wrapper class that handles the semantic tokens delta request for a file +https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#semanticTokens_deltaRequest +Sends either SemanticTokens or SemanticTokensDelta data as response. +This is generally requested when the text document is edited after receiving full highlighting data. +*/ +SemanticTokenDeltaHandler::SemanticTokenDeltaHandler(QmlLsp::QQmlCodeModel *codeModel) + : QQmlBaseModule(codeModel) +{ +} + +void SemanticTokenDeltaHandler::process( + QQmlBaseModule<SemanticTokensDeltaRequest>::RequestPointerArgument request) +{ + if (!request) { + qCWarning(semanticTokens) << "No semantic token request is available!"; + return; + } + + Responses::SemanticTokensDeltaResultType result; + ResponseScopeGuard guard(result, request->m_response); + const auto doc = m_codeModel->openDocumentByUrl( + QQmlLSUtils::lspUriToQmlUrl(request->m_parameters.textDocument.uri)); + DomItem file = doc.snapshot.validDoc.fileObject(GoTo::MostLikely); + Highlights highlights; + auto newEncoded = highlights.collectTokens(file, std::nullopt); + auto ®isteredTokens = m_codeModel->registeredTokens(); + const auto lastResultId = registeredTokens.resultId; + HighlightingUtils::updateResultID(registeredTokens.resultId); + + // Return full token list if result ids not align + // otherwise compute the delta. + if (lastResultId == request->m_parameters.previousResultId) { + result = QLspSpecification::SemanticTokensDelta { + registeredTokens.resultId, + HighlightingUtils::computeDiff(registeredTokens.lastTokens, newEncoded) + }; + } else if (!newEncoded.isEmpty()){ + result = QLspSpecification::SemanticTokens{ registeredTokens.resultId, newEncoded }; + } else { + result = nullptr; + } + registeredTokens.lastTokens = std::move(newEncoded); +} + +void SemanticTokenDeltaHandler::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) +{ + protocol->registerSemanticTokensDeltaRequestHandler(getRequestHandler()); +} + +/*! +\internal +A wrapper class that handles the semantic tokens range request for a file +https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#semanticTokens_rangeRequest +Sends a QLspSpecification::SemanticTokens data as response that is generated for a range of file. +*/ +SemanticTokenRangeHandler::SemanticTokenRangeHandler(QmlLsp::QQmlCodeModel *codeModel) + : QQmlBaseModule(codeModel) +{ +} + +void SemanticTokenRangeHandler::process( + QQmlBaseModule<SemanticTokensRangeRequest>::RequestPointerArgument request) +{ + if (!request) { + qCWarning(semanticTokens) << "No semantic token request is available!"; + return; + } + + Responses::SemanticTokensRangeResultType result; + ResponseScopeGuard guard(result, request->m_response); + const auto doc = m_codeModel->openDocumentByUrl( + QQmlLSUtils::lspUriToQmlUrl(request->m_parameters.textDocument.uri)); + DomItem file = doc.snapshot.doc.fileObject(GoTo::MostLikely); + const auto qmlFile = file.as<QmlFile>(); + if (!qmlFile) + return; + const QString &code = qmlFile->code(); + const auto range = request->m_parameters.range; + int startOffset = int(QQmlLSUtils::textOffsetFrom(code, range.start.line, range.end.character)); + int endOffset = int(QQmlLSUtils::textOffsetFrom(code, range.end.line, range.end.character)); + Highlights highlights; + auto &&encoded = highlights.collectTokens(file, HighlightsRange{startOffset, endOffset}); + auto ®isteredTokens = m_codeModel->registeredTokens(); + if (!encoded.isEmpty()) { + HighlightingUtils::updateResultID(registeredTokens.resultId); + result = SemanticTokens{ registeredTokens.resultId, std::move(encoded) }; + } else { + result = nullptr; + } +} + +void SemanticTokenRangeHandler::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) +{ + protocol->registerSemanticTokensRangeRequestHandler(getRequestHandler()); +} + +QQmlHighlightSupport::QQmlHighlightSupport(QmlLsp::QQmlCodeModel *codeModel) + : m_full(codeModel), m_delta(codeModel), m_range(codeModel) +{ +} + +QString QQmlHighlightSupport::name() const +{ + return "QQmlHighlightSupport"_L1; +} + +void QQmlHighlightSupport::registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) +{ + m_full.registerHandlers(server, protocol); + m_delta.registerHandlers(server, protocol); + m_range.registerHandlers(server, protocol); +} + +void QQmlHighlightSupport::setupCapabilities( + const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverCapabilities) +{ + QLspSpecification::SemanticTokensOptions options; + options.range = true; + options.full = QJsonObject({ { u"delta"_s, true } }); + options.legend.tokenTypes = tokenTypesList(); + options.legend.tokenModifiers = tokenModifiersList(); + + serverCapabilities.capabilities.semanticTokensProvider = options; +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmlhighlightsupport_p.h b/src/qmlls/qqmlhighlightsupport_p.h new file mode 100644 index 0000000000..ef84b94ef3 --- /dev/null +++ b/src/qmlls/qqmlhighlightsupport_p.h @@ -0,0 +1,96 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLHIGHLIGHTSUPPORT_P_H +#define QQMLHIGHLIGHTSUPPORT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlbasemodule_p.h" +#include "qqmlcodemodel_p.h" + +QT_BEGIN_NAMESPACE + +// We don't need these overrides as we register the request handlers in a single +// module QQmlHighlightSupport. This is an unusual pattern because QQmlBaseModule +// and QLanguageServerModule abstractions are designed to handle a single module +// which has a single request handlers. That is not the case for the semanticTokens +// module which has a one server module but also has three different handlers. +#define HIDE_UNUSED_OVERRIDES \ + private: \ + QString name() const override \ + { \ + return {}; \ + } \ + void setupCapabilities(const QLspSpecification::InitializeParams &, \ + QLspSpecification::InitializeResult &) override \ + { \ + } + +using SemanticTokensRequest = BaseRequest<QLspSpecification::SemanticTokensParams, + QLspSpecification::Responses::SemanticTokensResponseType>; + +using SemanticTokensDeltaRequest = + BaseRequest<QLspSpecification::SemanticTokensDeltaParams, + QLspSpecification::Responses::SemanticTokensDeltaResponseType>; + +using SemanticTokensRangeRequest = + BaseRequest<QLspSpecification::SemanticTokensRangeParams, + QLspSpecification::Responses::SemanticTokensRangeResponseType>; + +class SemanticTokenFullHandler : public QQmlBaseModule<SemanticTokensRequest> +{ +public: + SemanticTokenFullHandler(QmlLsp::QQmlCodeModel *codeModel); + void process(QQmlBaseModule<SemanticTokensRequest>::RequestPointerArgument req) override; + void registerHandlers(QLanguageServer *, QLanguageServerProtocol *) override; + HIDE_UNUSED_OVERRIDES +}; + +class SemanticTokenDeltaHandler : public QQmlBaseModule<SemanticTokensDeltaRequest> +{ +public: + SemanticTokenDeltaHandler(QmlLsp::QQmlCodeModel *codeModel); + void process(QQmlBaseModule<SemanticTokensDeltaRequest>::RequestPointerArgument req) override; + void registerHandlers(QLanguageServer *, QLanguageServerProtocol *) override; + HIDE_UNUSED_OVERRIDES +}; + +class SemanticTokenRangeHandler : public QQmlBaseModule<SemanticTokensRangeRequest> +{ +public: + SemanticTokenRangeHandler(QmlLsp::QQmlCodeModel *codeModel); + void process(QQmlBaseModule<SemanticTokensRangeRequest>::RequestPointerArgument req) override; + void registerHandlers(QLanguageServer *, QLanguageServerProtocol *) override; + HIDE_UNUSED_OVERRIDES +}; + +class QQmlHighlightSupport : public QLanguageServerModule +{ +public: + QQmlHighlightSupport(QmlLsp::QQmlCodeModel *codeModel); + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; +private: + SemanticTokenFullHandler m_full; + SemanticTokenDeltaHandler m_delta; + SemanticTokenRangeHandler m_range; +}; + +#undef HIDE_UNUSED_OVERRIDES + +QT_END_NAMESPACE + +#endif // QQMLHIGHLIGHTSUPPORT_P_H diff --git a/src/qmlls/qqmlhover.cpp b/src/qmlls/qqmlhover.cpp new file mode 100644 index 0000000000..d2acd7b5d9 --- /dev/null +++ b/src/qmlls/qqmlhover.cpp @@ -0,0 +1,82 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmlhover_p.h" +#include <QtQmlLS/private/qqmllshelputils_p.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(hoverLog, "qt.languageserver.hover") + +QQmlHover::QQmlHover(QmlLsp::QQmlCodeModel *codeModel) + : QQmlBaseModule(codeModel), m_helpManager(std::make_unique<HelpManager>()) +{ + // if set thorugh the commandline + if (!codeModel->documentationRootPath().isEmpty()) + m_helpManager->setDocumentationRootPath(codeModel->documentationRootPath()); + + connect(codeModel, &QmlLsp::QQmlCodeModel::documentationRootPathChanged, this, [this](const QString &path) { + m_helpManager->setDocumentationRootPath(path); + }); +} + +QQmlHover::~QQmlHover() = default; + +QString QQmlHover::name() const +{ + return u"QQmlHover"_s; +} + +void QQmlHover::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) +{ + protocol->registerHoverRequestHandler(getRequestHandler()); +} + +void QQmlHover::setupCapabilities( + const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverCapabilities) +{ + serverCapabilities.capabilities.hoverProvider = true; +} + +void QQmlHover::process(RequestPointerArgument request) +{ + if (!m_helpManager) { + qCWarning(hoverLog) + << "No help manager is available, documentation hints will not function!"; + return; + } + using namespace QQmlJS::Dom; + QLspSpecification::Hover result; + ResponseScopeGuard guard(result, request->m_response); + if (!request) { + qCWarning(hoverLog) << "No hover information is available!"; + return; + } + const auto textDocument = request->m_parameters.textDocument; + const auto position = request->m_parameters.position; + const auto doc = m_codeModel->openDocumentByUrl(QQmlLSUtils::lspUriToQmlUrl(textDocument.uri)); + DomItem file = doc.snapshot.doc.fileObject(GoTo::MostLikely); + if (!file) { + guard.setError(QQmlLSUtils::ErrorMessage{ + 0, u"Could not find the file %1"_s.arg(doc.snapshot.doc.canonicalFilePath()) }); + return; + } + + const auto documentation = m_helpManager->documentationForItem(file, position); + if (!documentation.has_value()) { + qCDebug(hoverLog) + << QStringLiteral( + "No documentation hints found for the item at (line, col): (%1,%2)") + .arg(position.line) + .arg(position.character); + return; + } + QLspSpecification::MarkupContent content; + // TODO: We need to do post-formatting what we fetch from documentation. + content.kind = QLspSpecification::MarkupKind::Markdown; + content.value = documentation.value(); + result.contents = std::move(content); +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmlhover_p.h b/src/qmlls/qqmlhover_p.h new file mode 100644 index 0000000000..ba964adc8b --- /dev/null +++ b/src/qmlls/qqmlhover_p.h @@ -0,0 +1,48 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLHOVER_P_H +#define QQMLHOVER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlbasemodule_p.h" +#include "qqmlcodemodel_p.h" + +QT_BEGIN_NAMESPACE + +struct HoverRequest + : public BaseRequest<QLspSpecification::HoverParams, + QLspSpecification::Responses::HoverResponseType> +{ +}; +class HelpManager; +class QQmlHover : public QQmlBaseModule<HoverRequest> +{ + Q_OBJECT +public: + QQmlHover(QmlLsp::QQmlCodeModel *codeModel); + ~QQmlHover() override; + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; + void process(RequestPointerArgument req) override; + +private: + std::unique_ptr<HelpManager> m_helpManager; +}; + +QT_END_NAMESPACE + +#endif // QQMLHOVER_P_H diff --git a/src/qmlls/qqmllanguageserver.cpp b/src/qmlls/qqmllanguageserver.cpp new file mode 100644 index 0000000000..1ef142b7b5 --- /dev/null +++ b/src/qmlls/qqmllanguageserver.cpp @@ -0,0 +1,185 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmllanguageserver_p.h" +#include "qtextsynchronization_p.h" +#include "qlanguageserver_p.h" +#include "qlspcustomtypes_p.h" + +#include <QtCore/qdir.h> + +#include <iostream> +#include <algorithm> + +QT_BEGIN_NAMESPACE + +namespace QmlLsp { + +using namespace QLspSpecification; +using namespace Qt::StringLiterals; +/*! +\internal +\class QmlLsp::QQmlLanguageServer +\brief Sets up a QmlLanguageServer. + +This class sets up a QML language server. + +Use the following function to send replies: + +\code +std::function<void(const QByteArray &)> sendData +\endcode + +And, feed the data that the function receives to the \c {server()->receive()} +method. + +Call this method only from a single thread, and do not block. To achieve this, +avoid direct calls, and connect the method as a slot, while reading from another +thread. + +The various tasks of the language server are divided between +QLanguageServerModule instances. Each instance is responsible for handling a +certain subset of client requests. For example, one instance handles completion +requests, another one updates the code in the code model when the client sends a +new file version, and so on. The QLanguageServerModule instances are +constructed and registered with QLanguageServer in the constructor of +this class. + +Generally, do all operations in the object thread and always call handlers from +it. However, the operations can delegate the response to another thread, as the +response handler is thread safe. All the methods of the \c server() object are +also thread safe. + +The code model starts other threads to update its state. See its documentation +for more information. +*/ +QQmlLanguageServer::QQmlLanguageServer(std::function<void(const QByteArray &)> sendData, + QQmlToolingSettings *settings) + : m_codeModel(nullptr, settings), + m_server(sendData), + m_textSynchronization(&m_codeModel), + m_lint(&m_server, &m_codeModel), + m_workspace(&m_codeModel), + m_completionSupport(&m_codeModel), + m_navigationSupport(&m_codeModel), + m_definitionSupport(&m_codeModel), + m_referencesSupport(&m_codeModel), + m_documentFormatting(&m_codeModel), + m_renameSupport(&m_codeModel), + m_rangeFormatting(&m_codeModel), + m_hover(&m_codeModel), + m_highlightSupport(&m_codeModel) +{ + m_server.addServerModule(this); + m_server.addServerModule(&m_textSynchronization); + m_server.addServerModule(&m_lint); + m_server.addServerModule(&m_workspace); + m_server.addServerModule(&m_completionSupport); + m_server.addServerModule(&m_navigationSupport); + m_server.addServerModule(&m_definitionSupport); + m_server.addServerModule(&m_referencesSupport); + m_server.addServerModule(&m_documentFormatting); + m_server.addServerModule(&m_renameSupport); + m_server.addServerModule(&m_rangeFormatting); + m_server.addServerModule(&m_hover); + m_server.addServerModule(&m_highlightSupport); + m_server.finishSetup(); + qCWarning(lspServerLog) << "Did Setup"; +} + +void QQmlLanguageServer::registerHandlers(QLanguageServer *server, + QLanguageServerProtocol *protocol) +{ + Q_UNUSED(protocol); + QObject::connect(server, &QLanguageServer::lifecycleError, this, + &QQmlLanguageServer::errorExit); + QObject::connect(server, &QLanguageServer::exit, this, &QQmlLanguageServer::exit); + QObject::connect(server, &QLanguageServer::runStatusChanged, this, [](QLanguageServer::RunStatus r) { + qCDebug(lspServerLog) << "runStatus" << int(r); + }); + protocol->typedRpc()->registerNotificationHandler<Notifications::AddBuildDirsParams>( + QByteArray(Notifications::AddBuildDirsMethod), + [this](const QByteArray &, const Notifications::AddBuildDirsParams ¶ms) { + for (const auto &buildDirs : params.buildDirsToSet) { + QStringList dirPaths; + dirPaths.resize(buildDirs.buildDirs.size()); + std::transform(buildDirs.buildDirs.begin(), buildDirs.buildDirs.end(), + dirPaths.begin(), [](const QByteArray &utf8Str) { + return QString::fromUtf8(utf8Str); + }); + m_codeModel.setBuildPathsForRootUrl(buildDirs.baseUri, dirPaths); + } + }); +} + +void QQmlLanguageServer::setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &serverInfo) +{ + QJsonObject expCap; + if (serverInfo.capabilities.experimental.has_value() && serverInfo.capabilities.experimental->isObject()) + expCap = serverInfo.capabilities.experimental->toObject(); + expCap.insert(u"addBuildDirs"_s, QJsonObject({ { u"supported"_s, true } })); + serverInfo.capabilities.experimental = expCap; + + if (clientInfo.workspaceFolders) { + if (auto workspaceList = + std::get_if<QList<WorkspaceFolder>>(&*clientInfo.workspaceFolders)) { + QList<QByteArray> workspaceUris; + std::transform(workspaceList->cbegin(), workspaceList->cend(), + std::back_inserter(workspaceUris), + [](const auto &workspaceFolder) { return workspaceFolder.uri; }); + m_codeModel.setRootUrls(workspaceUris); + } + } +} + +QString QQmlLanguageServer::name() const +{ + return u"QQmlLanguageServer"_s; +} + +void QQmlLanguageServer::errorExit() +{ + qCWarning(lspServerLog) << "Error exit"; + fclose(stdin); +} + +void QQmlLanguageServer::exit() +{ + m_returnValue = 0; + fclose(stdin); +} + +int QQmlLanguageServer::returnValue() const +{ + return m_returnValue; +} + +QQmlCodeModel *QQmlLanguageServer::codeModel() +{ + return &m_codeModel; +} + +QLanguageServer *QQmlLanguageServer::server() +{ + return &m_server; +} + +TextSynchronization *QQmlLanguageServer::textSynchronization() +{ + return &m_textSynchronization; +} + +QmlLintSuggestions *QQmlLanguageServer::lint() +{ + return &m_lint; +} + +WorkspaceHandlers *QQmlLanguageServer::worspace() +{ + return &m_workspace; +} + +} // namespace QmlLsp + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmllanguageserver_p.h b/src/qmlls/qqmllanguageserver_p.h new file mode 100644 index 0000000000..3347acd670 --- /dev/null +++ b/src/qmlls/qqmllanguageserver_p.h @@ -0,0 +1,83 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLLANGUAGESERVER_P_H +#define QQMLLANGUAGESERVER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlcodemodel_p.h" +#include "qqmlfindusagessupport_p.h" +#include "qtextsynchronization_p.h" +#include "qqmllintsuggestions_p.h" +#include "qworkspace_p.h" +#include "qqmlcompletionsupport_p.h" +#include "qqmlgototypedefinitionsupport_p.h" +#include "qqmlformatting_p.h" +#include "qqmlrangeformatting_p.h" +#include "qqmlgotodefinitionsupport_p.h" +#include "qqmlrenamesymbolsupport_p.h" +#include "qqmlhover_p.h" +#include "qqmlhighlightsupport_p.h" + +QT_BEGIN_NAMESPACE + +class QQmlToolingSettings; + +namespace QmlLsp { + +class QQmlLanguageServer : public QLanguageServerModule +{ + Q_OBJECT +public: + QQmlLanguageServer(std::function<void(const QByteArray &)> sendData, + QQmlToolingSettings *settings = nullptr); + + QString name() const final; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) final; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &serverInfo) final; + + int returnValue() const; + + QQmlCodeModel *codeModel(); + QLanguageServer *server(); + TextSynchronization *textSynchronization(); + QmlLintSuggestions *lint(); + WorkspaceHandlers *worspace(); + +public Q_SLOTS: + void exit(); + void errorExit(); + +private: + QQmlCodeModel m_codeModel; + QLanguageServer m_server; + TextSynchronization m_textSynchronization; + QmlLintSuggestions m_lint; + WorkspaceHandlers m_workspace; + QmlCompletionSupport m_completionSupport; + QmlGoToTypeDefinitionSupport m_navigationSupport; + QmlGoToDefinitionSupport m_definitionSupport; + QQmlFindUsagesSupport m_referencesSupport; + QQmlDocumentFormatting m_documentFormatting; + QQmlRenameSymbolSupport m_renameSupport; + QQmlRangeFormatting m_rangeFormatting; + QQmlHover m_hover; + QQmlHighlightSupport m_highlightSupport; + int m_returnValue = 1; +}; + +} // namespace QmlLsp +QT_END_NAMESPACE +#endif // QQMLLANGUAGESERVER_P_H diff --git a/src/qmlls/qqmllintsuggestions.cpp b/src/qmlls/qqmllintsuggestions.cpp new file mode 100644 index 0000000000..914437df3f --- /dev/null +++ b/src/qmlls/qqmllintsuggestions.cpp @@ -0,0 +1,379 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmllintsuggestions_p.h" + +#include <QtLanguageServer/private/qlanguageserverspec_p.h> +#include <QtQmlCompiler/private/qqmljslinter_p.h> +#include <QtQmlCompiler/private/qqmljslogger_p.h> +#include <QtQmlDom/private/qqmldom_utils_p.h> +#include <QtQmlDom/private/qqmldomtop_p.h> +#include <QtCore/qdebug.h> +#include <QtCore/qdir.h> +#include <QtCore/qfileinfo.h> +#include <QtCore/qlibraryinfo.h> +#include <QtCore/qtimer.h> +#include <QtCore/qxpfunctional.h> +#include <chrono> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lintLog, "qt.languageserver.lint") + +using namespace QLspSpecification; +using namespace QQmlJS::Dom; +using namespace Qt::StringLiterals; + +namespace QmlLsp { + +static DiagnosticSeverity severityFromMsgType(QtMsgType t) +{ + switch (t) { + case QtDebugMsg: + return DiagnosticSeverity::Hint; + case QtInfoMsg: + return DiagnosticSeverity::Information; + case QtWarningMsg: + return DiagnosticSeverity::Warning; + case QtCriticalMsg: + case QtFatalMsg: + break; + } + return DiagnosticSeverity::Error; +} + +static void codeActionHandler( + const QByteArray &, const CodeActionParams ¶ms, + LSPPartialResponse<std::variant<QList<std::variant<Command, CodeAction>>, std::nullptr_t>, + QList<std::variant<Command, CodeAction>>> &&response) +{ + QList<std::variant<Command, CodeAction>> responseData; + + for (const Diagnostic &diagnostic : params.context.diagnostics) { + if (!diagnostic.data.has_value()) + continue; + + const auto &data = diagnostic.data.value(); + + int version = data[u"version"].toInt(); + QJsonArray suggestions = data[u"suggestions"].toArray(); + + QList<WorkspaceEdit::DocumentChange> edits; + QString message; + for (const QJsonValue &suggestion : suggestions) { + QString replacement = suggestion[u"replacement"].toString(); + message += suggestion[u"message"].toString() + u"\n"; + + TextEdit textEdit; + textEdit.range = { Position { suggestion[u"lspBeginLine"].toInt(), + suggestion[u"lspBeginCharacter"].toInt() }, + Position { suggestion[u"lspEndLine"].toInt(), + suggestion[u"lspEndCharacter"].toInt() } }; + textEdit.newText = replacement.toUtf8(); + + TextDocumentEdit textDocEdit; + textDocEdit.textDocument = { params.textDocument, version }; + textDocEdit.edits.append(textEdit); + + edits.append(textDocEdit); + } + message.chop(1); + WorkspaceEdit edit; + edit.documentChanges = edits; + + CodeAction action; + // VS Code and QtC ignore everything that is not a 'quickfix'. + action.kind = u"quickfix"_s.toUtf8(); + action.edit = edit; + action.title = message.toUtf8(); + + responseData.append(action); + } + + response.sendResponse(responseData); +} + +void QmlLintSuggestions::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) +{ + protocol->registerCodeActionRequestHandler(&codeActionHandler); +} + +void QmlLintSuggestions::setupCapabilities(const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverInfo) +{ + serverInfo.capabilities.codeActionProvider = true; +} + +QmlLintSuggestions::QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel) + : m_server(server), m_codeModel(codeModel) +{ + QObject::connect(m_codeModel, &QmlLsp::QQmlCodeModel::updatedSnapshot, this, + &QmlLintSuggestions::diagnose, Qt::DirectConnection); +} + +static void advancePositionPastLocation_helper(const QString &fileContents, const QQmlJS::SourceLocation &location, Position &position) { + const int startOffset = location.offset; + const int length = location.length; + int i = startOffset; + int iEnd = i + length; + if (iEnd > int(fileContents.size())) + iEnd = fileContents.size(); + while (i < iEnd) { + if (fileContents.at(i) == u'\n') { + ++position.line; + position.character = 0; + if (i + 1 < iEnd && fileContents.at(i) == u'\r') + ++i; + } else { + ++position.character; + } + ++i; + } +}; + +static Diagnostic createMissingBuildDirDiagnostic() +{ + Diagnostic diagnostic; + diagnostic.severity = DiagnosticSeverity::Warning; + Range &range = diagnostic.range; + Position &position = range.start; + position.line = 0; + position.character = 0; + Position &positionEnd = range.end; + positionEnd.line = 1; + diagnostic.message = + "qmlls could not find a build directory, without a build directory " + "containing a current build there could be spurious warnings, you might " + "want to pass the --build-dir <buildDir> option to qmlls, or set the " + "environment variable QMLLS_BUILD_DIRS."; + diagnostic.source = QByteArray("qmllint"); + return diagnostic; +} + +using AdvanceFunc = qxp::function_ref<void(const QQmlJS::SourceLocation &, Position &)>; +static Diagnostic messageToDiagnostic_helper(AdvanceFunc advancePositionPastLocation, + std::optional<int> version, const Message &message) +{ + Diagnostic diagnostic; + diagnostic.severity = severityFromMsgType(message.type); + Range &range = diagnostic.range; + Position &position = range.start; + + QQmlJS::SourceLocation srcLoc = message.loc; + + if (srcLoc.isValid()) { + position.line = srcLoc.startLine - 1; + position.character = srcLoc.startColumn - 1; + range.end = position; + advancePositionPastLocation(message.loc, range.end); + } + + if (message.fixSuggestion && !message.fixSuggestion->fixDescription().isEmpty()) { + diagnostic.message = QString(message.message) + .append(u": "_s) + .append(message.fixSuggestion->fixDescription()) + .simplified() + .toUtf8(); + } else { + diagnostic.message = message.message.toUtf8(); + } + + diagnostic.source = QByteArray("qmllint"); + + auto suggestion = message.fixSuggestion; + if (!suggestion.has_value()) + return diagnostic; + + // We need to interject the information about where the fix suggestions end + // here since we don't have access to the textDocument to calculate it later. + const QQmlJS::SourceLocation cut = suggestion->location(); + + const int line = cut.isValid() ? cut.startLine - 1 : 0; + const int column = cut.isValid() ? cut.startColumn - 1 : 0; + + QJsonObject object; + object.insert("lspBeginLine"_L1, line); + object.insert("lspBeginCharacter"_L1, column); + + Position end = { line, column }; + + if (srcLoc.isValid()) + advancePositionPastLocation(cut, end); + object.insert("lspEndLine"_L1, end.line); + object.insert("lspEndCharacter"_L1, end.character); + + object.insert("message"_L1, suggestion->fixDescription()); + object.insert("replacement"_L1, suggestion->replacement()); + + QJsonArray fixedSuggestions; + fixedSuggestions.append(object); + QJsonObject data; + data[u"suggestions"] = fixedSuggestions; + + Q_ASSERT(version.has_value()); + data[u"version"] = version.value(); + + diagnostic.data = data; + + return diagnostic; +}; + +static bool isSnapshotNew(std::optional<int> snapshotVersion, std::optional<int> processedVersion) +{ + if (!snapshotVersion) + return false; + if (!processedVersion || *snapshotVersion > *processedVersion) + return true; + return false; +} + +using namespace std::chrono_literals; + +QmlLintSuggestions::VersionToDiagnose +QmlLintSuggestions::chooseVersionToDiagnoseHelper(const QByteArray &url) +{ + const std::chrono::milliseconds maxInvalidTime = 400ms; + QmlLsp::OpenDocumentSnapshot snapshot = m_codeModel->snapshotByUrl(url); + + LastLintUpdate &lastUpdate = m_lastUpdate[url]; + + // ignore updates when already processed + if (lastUpdate.version && *lastUpdate.version == snapshot.docVersion) { + qCDebug(lspServerLog) << "skipped update of " << url << "unchanged valid doc"; + return NoDocumentAvailable{}; + } + + // try out a valid version, if there is one + if (isSnapshotNew(snapshot.validDocVersion, lastUpdate.version)) + return VersionedDocument{ snapshot.validDocVersion, snapshot.validDoc }; + + // try out an invalid version, if there is one + if (isSnapshotNew(snapshot.docVersion, lastUpdate.version)) { + if (auto since = lastUpdate.invalidUpdatesSince) { + // did we wait enough to get a valid document? + if (std::chrono::steady_clock::now() - *since > maxInvalidTime) { + return VersionedDocument{ snapshot.docVersion, snapshot.doc }; + } + } else { + // first time hitting the invalid document: + lastUpdate.invalidUpdatesSince = std::chrono::steady_clock::now(); + } + + // wait some time for extra keystrokes before diagnose + return TryAgainLater{ maxInvalidTime }; + } + return NoDocumentAvailable{}; +} + +QmlLintSuggestions::VersionToDiagnose +QmlLintSuggestions::chooseVersionToDiagnose(const QByteArray &url) +{ + QMutexLocker l(&m_mutex); + auto versionToDiagnose = chooseVersionToDiagnoseHelper(url); + if (auto versionedDocument = std::get_if<VersionedDocument>(&versionToDiagnose)) { + // update immediately, and do not keep track of sent version, thus in extreme cases sent + // updates could be out of sync + LastLintUpdate &lastUpdate = m_lastUpdate[url]; + lastUpdate.version = versionedDocument->version; + lastUpdate.invalidUpdatesSince.reset(); + } + return versionToDiagnose; +} + +void QmlLintSuggestions::diagnose(const QByteArray &url) +{ + auto versionedDocument = chooseVersionToDiagnose(url); + + std::visit(qOverloadedVisitor{ + [](NoDocumentAvailable) {}, + [this, &url](const TryAgainLater &tryAgainLater) { + QTimer::singleShot(tryAgainLater.time, Qt::VeryCoarseTimer, this, + [this, url]() { diagnose(url); }); + }, + [this, &url](const VersionedDocument &versionedDocument) { + diagnoseHelper(url, versionedDocument); + }, + + }, + versionedDocument); +} + +void QmlLintSuggestions::diagnoseHelper(const QByteArray &url, + const VersionedDocument &versionedDocument) +{ + auto [version, doc] = versionedDocument; + + PublishDiagnosticsParams diagnosticParams; + diagnosticParams.uri = url; + diagnosticParams.version = version; + + qCDebug(lintLog) << "has doc, do real lint"; + QStringList imports = m_codeModel->buildPathsForFileUrl(url); + imports.append(m_codeModel->importPaths()); + const QString filename = doc.canonicalFilePath(); + // add source directory as last import as fallback in case there is no qmldir in the build + // folder this mimics qmllint behaviors + imports.append(QFileInfo(filename).dir().absolutePath()); + // add m_server->clientInfo().rootUri & co? + bool silent = true; + const QString fileContents = doc.field(Fields::code).value().toString(); + const QStringList qmltypesFiles; + const QStringList resourceFiles = resourceFilesFromBuildFolders(imports); + + QList<QQmlJS::LoggerCategory> categories; + + QQmlJSLinter linter(imports); + + linter.lintFile(filename, &fileContents, silent, nullptr, imports, qmltypesFiles, + resourceFiles, categories); + + // ### TODO: C++20 replace with bind_front + auto advancePositionPastLocation = [&fileContents](const QQmlJS::SourceLocation &location, Position &position) + { + advancePositionPastLocation_helper(fileContents, location, position); + }; + auto messageToDiagnostic = [&advancePositionPastLocation, + versionedDocument](const Message &message) { + return messageToDiagnostic_helper(advancePositionPastLocation, versionedDocument.version, + message); + }; + + QList<Diagnostic> diagnostics; + doc.iterateErrors( + [&diagnostics, &advancePositionPastLocation](const DomItem &, const ErrorMessage &msg) { + Diagnostic diagnostic; + diagnostic.severity = severityFromMsgType(QtMsgType(int(msg.level))); + // do something with msg.errorGroups ? + auto &location = msg.location; + Range &range = diagnostic.range; + range.start.line = location.startLine - 1; + range.start.character = location.startColumn - 1; + range.end = range.start; + advancePositionPastLocation(location, range.end); + diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size()); + diagnostic.source = "domParsing"; + diagnostic.message = msg.message.toUtf8(); + diagnostics.append(diagnostic); + return true; + }, + true); + + if (const QQmlJSLogger *logger = linter.logger()) { + qsizetype nDiagnostics = diagnostics.size(); + for (const auto &messages : { logger->infos(), logger->warnings(), logger->errors() }) + for (const Message &message : messages) + diagnostics.append(messageToDiagnostic(message)); + if (diagnostics.size() != nDiagnostics && imports.size() == 1) + diagnostics.append(createMissingBuildDirDiagnostic()); + } + + diagnosticParams.diagnostics = diagnostics; + + m_server->protocol()->notifyPublishDiagnostics(diagnosticParams); + qCDebug(lintLog) << "lint" << QString::fromUtf8(url) << "found" + << diagnosticParams.diagnostics.size() << "issues" + << QTypedJson::toJsonValue(diagnosticParams); +} + +} // namespace QmlLsp +QT_END_NAMESPACE diff --git a/src/qmlls/qqmllintsuggestions_p.h b/src/qmlls/qqmllintsuggestions_p.h new file mode 100644 index 0000000000..28ab3ccb5d --- /dev/null +++ b/src/qmlls/qqmllintsuggestions_p.h @@ -0,0 +1,72 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QMLLINTSUGGESTIONS_P_H +#define QMLLINTSUGGESTIONS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlcodemodel_p.h" + +#include <chrono> +#include <optional> + +QT_BEGIN_NAMESPACE +namespace QmlLsp { +struct LastLintUpdate +{ + std::optional<int> version; + std::optional<std::chrono::steady_clock::time_point> invalidUpdatesSince; +}; + +class QmlLintSuggestions : public QLanguageServerModule +{ + Q_OBJECT +public: + QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel); + + QString name() const override { return QLatin1StringView("QmlLint Suggestions"); } +public Q_SLOTS: + void diagnose(const QByteArray &uri); + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; + +private: + struct VersionedDocument + { + std::optional<int> version; + QQmlJS::Dom::DomItem item; + }; + struct TryAgainLater + { + std::chrono::milliseconds time; + }; + struct NoDocumentAvailable + { + }; + + using VersionToDiagnose = std::variant<VersionedDocument, TryAgainLater, NoDocumentAvailable>; + + VersionToDiagnose chooseVersionToDiagnose(const QByteArray &url); + VersionToDiagnose chooseVersionToDiagnoseHelper(const QByteArray &url); + void diagnoseHelper(const QByteArray &uri, const VersionedDocument &document); + + QMutex m_mutex; + QHash<QByteArray, LastLintUpdate> m_lastUpdate; + QLanguageServer *m_server; + QmlLsp::QQmlCodeModel *m_codeModel; +}; +} // namespace QmlLsp +QT_END_NAMESPACE +#endif // QMLLINTSUGGESTIONS_P_H diff --git a/src/qmlls/qqmllscompletion.cpp b/src/qmlls/qqmllscompletion.cpp new file mode 100644 index 0000000000..5f54597c78 --- /dev/null +++ b/src/qmlls/qqmllscompletion.cpp @@ -0,0 +1,1889 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmllscompletion_p.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(QQmlLSCompletionLog, "qt.languageserver.completions") + +using namespace QLspSpecification; +using namespace QQmlJS::Dom; +using namespace Qt::StringLiterals; + +/*! +\class QQmlLSCompletion +\internal +\brief QQmlLSCompletion provides completions for all kinds of QML and JS constructs. + +Use the \l{completions} method to obtain completions at a certain DomItem. + +All the other methods in this class are helper methods: some compute completions for specific QML +and JS constructs and some are shared between multiple QML or JS constructs to avoid code +duplication. Most of the helper methods add their completion items via a BackInsertIterator. + +Some helper methods are called "suggest*" and will try to suggest code that does not exist yet. For +example, any JS statement can be expected inside a Blockstatement so suggestJSStatementCompletion() +is used to suggest JS statements inside of BlockStatements. Another example might be +suggestReachableTypes() that will suggest Types for type annotations, attached types or Qml Object +hierarchies, or suggestCaseAndDefaultStatementCompletion() that will only suggest "case" and +"default" clauses for switch statements. + +Some helper methods are called "inside*" and will try to suggest code inside an existing structure. +For example, insideForStatementCompletion() will try to suggest completion for the different code +pieces initializer, condition, increment and statement that exist inside of: +\badcode +for(initializer; condition; increment) + statement +\endcode +*/ + +CompletionItem QQmlLSCompletion::makeSnippet(QUtf8StringView qualifier, QUtf8StringView label, + QUtf8StringView insertText) +{ + CompletionItem res; + if (!qualifier.isEmpty()) { + res.label = qualifier.data(); + res.label += '.'; + } + res.label += label.data(); + res.insertTextFormat = InsertTextFormat::Snippet; + if (!qualifier.isEmpty()) { + res.insertText = qualifier.data(); + *res.insertText += '.'; + *res.insertText += insertText.data(); + } else { + res.insertText = insertText.data(); + } + res.kind = int(CompletionItemKind::Snippet); + res.insertTextMode = InsertTextMode::AdjustIndentation; + return res; +} + +CompletionItem QQmlLSCompletion::makeSnippet(QUtf8StringView label, QUtf8StringView insertText) +{ + return makeSnippet(QByteArray(), label, insertText); +} + +/*! +\internal +\brief Compare left and right locations to the position denoted by ctx, see special cases below. + +Statements and expressions need to provide different completions depending on where the cursor is. +For example, lets take following for-statement: +\badcode +for (let i = 0; <here> ; ++i) {} +\endcode +We want to provide script expression completion (method names, property names, available JS +variables names, QML objects ids, and so on) at the place denoted by \c{<here>}. +The question is: how do we know that the cursor is really at \c{<here>}? In the case of the +for-loop, we can compare the position of the cursor with the first and the second semicolon of the +for loop. + +If the first semicolon does not exist, it has an invalid sourcelocation and the cursor is +definitively \e{not} at \c{<here>}. Therefore, return false when \c{left} is invalid. + +If the second semicolon does not exist, then just ignore it: it might not have been written yet. +*/ +bool QQmlLSCompletion::betweenLocations(QQmlJS::SourceLocation left, + const QQmlLSCompletionPosition &positionInfo, + QQmlJS::SourceLocation right) const +{ + if (!left.isValid()) + return false; + // note: left.end() == ctx.offset() means that the cursor lies exactly after left + if (!(left.end() <= positionInfo.offset())) + return false; + if (!right.isValid()) + return true; + + // note: ctx.offset() == right.begin() means that the cursor lies exactly before right + return positionInfo.offset() <= right.begin(); +} + +/*! +\internal +Returns true if ctx denotes an offset lying behind left.end(), and false otherwise. +*/ +bool QQmlLSCompletion::afterLocation(QQmlJS::SourceLocation left, + const QQmlLSCompletionPosition &positionInfo) const +{ + return betweenLocations(left, positionInfo, QQmlJS::SourceLocation{}); +} + +/*! +\internal +Returns true if ctx denotes an offset lying before right.begin(), and false otherwise. +*/ +bool QQmlLSCompletion::beforeLocation(const QQmlLSCompletionPosition &ctx, + QQmlJS::SourceLocation right) const +{ + if (!right.isValid()) + return true; + + // note: ctx.offset() == right.begin() means that the cursor lies exactly before right + if (ctx.offset() <= right.begin()) + return true; + + return false; +} + +bool QQmlLSCompletion::ctxBeforeStatement(const QQmlLSCompletionPosition &positionInfo, + const DomItem &parentForContext, + FileLocationRegion firstRegion) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + const bool result = beforeLocation(positionInfo, regions[firstRegion]); + return result; +} + +void +QQmlLSCompletion::suggestBindingCompletion(const DomItem &itemAtPosition, BackInsertIterator it) const +{ + suggestReachableTypes(itemAtPosition, LocalSymbolsType::AttachedType, CompletionItemKind::Class, + it); + + const QQmlJSScope::ConstPtr scope = [&]() { + if (!QQmlLSUtils::isFieldMemberAccess(itemAtPosition)) + return itemAtPosition.qmlObject().semanticScope(); + + const DomItem owner = itemAtPosition.directParent().field(Fields::left); + auto expressionType = QQmlLSUtils::resolveExpressionType( + owner, QQmlLSUtils::ResolveActualTypeForFieldMemberExpression); + return expressionType ? expressionType->semanticScope : QQmlJSScope::ConstPtr{}; + }(); + + if (!scope) + return; + + propertyCompletion(scope, nullptr, it); + signalHandlerCompletion(scope, nullptr, it); +} + +void QQmlLSCompletion::insideImportCompletionHelper(const DomItem &file, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const +{ + // returns completions for import statements, ctx is supposed to be in an import statement + const CompletionContextStrings &ctx = positionInfo.cursorPosition; + ImportCompletionType importCompletionType = ImportCompletionType::None; + QRegularExpression spaceRe(uR"(\W+)"_s); + QList<QStringView> linePieces = ctx.preLine().split(spaceRe, Qt::SkipEmptyParts); + qsizetype effectiveLength = linePieces.size() + + ((!ctx.preLine().isEmpty() && ctx.preLine().last().isSpace()) ? 1 : 0); + if (effectiveLength < 2) { + CompletionItem comp; + comp.label = "import"; + comp.kind = int(CompletionItemKind::Keyword); + it = comp; + } + if (linePieces.isEmpty() || linePieces.first() != u"import") + return; + if (effectiveLength == 2) { + // the cursor is after the import, possibly in a partial module name + importCompletionType = ImportCompletionType::Module; + } else if (effectiveLength == 3) { + if (linePieces.last() != u"as") { + // the cursor is after the module, possibly in a partial version token (or partial as) + CompletionItem comp; + comp.label = "as"; + comp.kind = int(CompletionItemKind::Keyword); + it = comp; + importCompletionType = ImportCompletionType::Version; + } + } + DomItem env = file.environment(); + if (std::shared_ptr<DomEnvironment> envPtr = env.ownerAs<DomEnvironment>()) { + switch (importCompletionType) { + case ImportCompletionType::None: + break; + case ImportCompletionType::Module: { + QDuplicateTracker<QString> modulesSeen; + for (const QString &uri : envPtr->moduleIndexUris(env)) { + QStringView base = ctx.base(); // if we allow spaces we should get rid of them + if (uri.startsWith(base)) { + QStringList rest = uri.mid(base.size()).split(u'.'); + if (rest.isEmpty()) + continue; + + const QString label = rest.first(); + if (!modulesSeen.hasSeen(label)) { + CompletionItem comp; + comp.label = label.toUtf8(); + comp.kind = int(CompletionItemKind::Module); + it = comp; + } + } + } + break; + } + case ImportCompletionType::Version: + if (ctx.base().isEmpty()) { + for (int majorV : + envPtr->moduleIndexMajorVersions(env, linePieces.at(1).toString())) { + CompletionItem comp; + comp.label = QString::number(majorV).toUtf8(); + comp.kind = int(CompletionItemKind::Constant); + it = comp; + } + } else { + bool hasMajorVersion = ctx.base().endsWith(u'.'); + int majorV = -1; + if (hasMajorVersion) + majorV = ctx.base().mid(0, ctx.base().size() - 1).toInt(&hasMajorVersion); + if (!hasMajorVersion) + break; + if (std::shared_ptr<ModuleIndex> mIndex = + envPtr->moduleIndexWithUri(env, linePieces.at(1).toString(), majorV)) { + for (int minorV : mIndex->minorVersions()) { + CompletionItem comp; + comp.label = QString::number(minorV).toUtf8(); + comp.kind = int(CompletionItemKind::Constant); + it = comp; + } + } + } + break; + } + } +} + +void QQmlLSCompletion::idsCompletions(const DomItem &component, BackInsertIterator it) const +{ + qCDebug(QQmlLSCompletionLog) << "adding ids completions"; + for (const QString &k : component.field(Fields::ids).keys()) { + CompletionItem comp; + comp.label = k.toUtf8(); + comp.kind = int(CompletionItemKind::Value); + it = comp; + } +} + +static bool testScopeSymbol(const QQmlJSScope::ConstPtr &scope, LocalSymbolsTypes options, + CompletionItemKind kind) +{ + const bool currentIsSingleton = scope->isSingleton(); + const bool currentIsAttached = !scope->attachedType().isNull(); + if ((options & LocalSymbolsType::Singleton) && currentIsSingleton) { + return true; + } + if ((options & LocalSymbolsType::AttachedType) && currentIsAttached) { + return true; + } + const bool isObjectType = scope->isReferenceType(); + if (options & LocalSymbolsType::ObjectType && !currentIsSingleton && isObjectType) { + return kind != CompletionItemKind::Constructor || scope->isCreatable(); + } + if (options & LocalSymbolsType::ValueType && !currentIsSingleton && !isObjectType) { + return true; + } + return false; +} + +/*! +\internal +Obtain the types reachable from \c{el} as a CompletionItems. +*/ +void QQmlLSCompletion::suggestReachableTypes(const DomItem &el, LocalSymbolsTypes options, + CompletionItemKind kind, BackInsertIterator it) const +{ + auto file = el.containingFile().as<QmlFile>(); + if (!file) + return; + auto resolver = file->typeResolver(); + if (!resolver) + return; + + const QString requiredQualifiers = QQmlLSUtils::qualifiersFrom(el); + const auto keyValueRange = resolver->importedTypes().asKeyValueRange(); + for (const auto &type : keyValueRange) { + // ignore special QQmlJSImporterMarkers + const bool isMarkerType = type.first.contains(u"$internal$.") + || type.first.contains(u"$anonymous$.") || type.first.contains(u"$module$."); + if (isMarkerType || !type.first.startsWith(requiredQualifiers)) + continue; + + auto &scope = type.second.scope; + if (!scope) + continue; + + if (!testScopeSymbol(scope, options, kind)) + continue; + + CompletionItem completion; + completion.label = QStringView(type.first).sliced(requiredQualifiers.size()).toUtf8(); + completion.kind = int(kind); + it = completion; + } +} + +void QQmlLSCompletion::jsIdentifierCompletion(const QQmlJSScope::ConstPtr &scope, + QDuplicateTracker<QString> *usedNames, + BackInsertIterator it) const +{ + for (const auto &[name, jsIdentifier] : scope->ownJSIdentifiers().asKeyValueRange()) { + CompletionItem completion; + if (usedNames && usedNames->hasSeen(name)) { + continue; + } + completion.label = name.toUtf8(); + completion.kind = int(CompletionItemKind::Variable); + QString detail = u"has type "_s; + if (jsIdentifier.typeName) { + if (jsIdentifier.isConst) { + detail.append(u"const "); + } + detail.append(*jsIdentifier.typeName); + } else { + detail.append(jsIdentifier.isConst ? u"const"_s : u"var"_s); + } + completion.detail = detail.toUtf8(); + it = completion; + } +} + +void QQmlLSCompletion::methodCompletion(const QQmlJSScope::ConstPtr &scope, + QDuplicateTracker<QString> *usedNames, + BackInsertIterator it) const +{ + // JS functions in current and base scopes + for (const auto &[name, method] : scope->methods().asKeyValueRange()) { + if (method.access() != QQmlJSMetaMethod::Public) + continue; + if (usedNames && usedNames->hasSeen(name)) { + continue; + } + CompletionItem completion; + completion.label = name.toUtf8(); + completion.kind = int(CompletionItemKind::Method); + it = completion; + // TODO: QQmlLSUtils::reachableSymbols seems to be able to do documentation and detail + // and co, it should also be done here if possible. + } +} + +void QQmlLSCompletion::propertyCompletion(const QQmlJSScope::ConstPtr &scope, + QDuplicateTracker<QString> *usedNames, + BackInsertIterator it) const +{ + for (const auto &[name, property] : scope->properties().asKeyValueRange()) { + if (usedNames && usedNames->hasSeen(name)) { + continue; + } + CompletionItem completion; + completion.label = name.toUtf8(); + completion.kind = int(CompletionItemKind::Property); + QString detail{ u"has type "_s }; + if (!property.isWritable()) + detail.append(u"readonly "_s); + detail.append(property.typeName().isEmpty() ? u"var"_s : property.typeName()); + completion.detail = detail.toUtf8(); + it = completion; + } +} + +void QQmlLSCompletion::enumerationCompletion(const QQmlJSScope::ConstPtr &scope, + QDuplicateTracker<QString> *usedNames, + BackInsertIterator it) const +{ + for (const QQmlJSMetaEnum &enumerator : scope->enumerations()) { + if (usedNames && usedNames->hasSeen(enumerator.name())) { + continue; + } + CompletionItem completion; + completion.label = enumerator.name().toUtf8(); + completion.kind = static_cast<int>(CompletionItemKind::Enum); + it = completion; + } +} + +void QQmlLSCompletion::enumerationValueCompletionHelper(const QStringList &enumeratorKeys, + BackInsertIterator it) const +{ + for (const QString &enumeratorKey : enumeratorKeys) { + CompletionItem completion; + completion.label = enumeratorKey.toUtf8(); + completion.kind = static_cast<int>(CompletionItemKind::EnumMember); + it = completion; + } +} + +/*! +\internal +Creates completion items for enumerationvalues. +If enumeratorName is a valid enumerator then only do completion for the requested enumerator, and +otherwise do completion for \b{all other possible} enumerators. + +For example: +``` +id: someItem +enum Hello { World } +enum MyEnum { ValueOne, ValueTwo } + +// Hello does refer to a enumerator: +property var a: Hello.<complete only World here> + +// someItem does not refer to a enumerator: +property var b: someItem.<complete World, ValueOne and ValueTwo here> +``` +*/ + +void QQmlLSCompletion::enumerationValueCompletion(const QQmlJSScope::ConstPtr &scope, + const QString &enumeratorName, + BackInsertIterator result) const +{ + auto enumerator = scope->enumeration(enumeratorName); + if (enumerator.isValid()) { + enumerationValueCompletionHelper(enumerator.keys(), result); + return; + } + + for (const QQmlJSMetaEnum &enumerator : scope->enumerations()) { + enumerationValueCompletionHelper(enumerator.keys(), result); + } +} + +/*! +\internal +Calls F on all JavaScript-parents of scope. For example, you can use this method to +collect all the JavaScript Identifiers from following code: +``` +{ // this block statement contains only 'x' + let x = 3; + { // this block statement contains only 'y', and 'x' has to be retrieved via its parent. + let y = 4; + } +} +``` +*/ +template<typename F> +void collectFromAllJavaScriptParents(const F &&f, const QQmlJSScope::ConstPtr &scope) +{ + for (QQmlJSScope::ConstPtr current = scope; current; current = current->parentScope()) { + f(current); + if (current->scopeType() == QQmlSA::ScopeType::QMLScope) + return; + } +} + +/*! +\internal +Generate autocompletions for JS expressions, suggest possible properties, methods, etc. + +If scriptIdentifier is inside a Field Member Expression, like \c{onCompleted} in +\c{Component.onCompleted} for example, then this method will only suggest properties, methods, etc +from the correct type. For the previous example that would be properties, methods, etc. from the +Component attached type. +*/ +void QQmlLSCompletion::suggestJSExpressionCompletion(const DomItem &scriptIdentifier, + BackInsertIterator result) const +{ + QDuplicateTracker<QString> usedNames; + QQmlJSScope::ConstPtr nearestScope; + + // note: there is an edge case, where the user asks for completion right after the dot + // of some qualified expression like `root.hello`. In this case, scriptIdentifier is actually + // the BinaryExpression instead of the left-hand-side that has not be written down yet. + const bool askForCompletionOnDot = QQmlLSUtils::isFieldMemberExpression(scriptIdentifier); + const bool hasQualifier = + QQmlLSUtils::isFieldMemberAccess(scriptIdentifier) || askForCompletionOnDot; + + if (!hasQualifier) { + for (QUtf8StringView view : std::array<QUtf8StringView, 3>{ "null", "false", "true" }) { + CompletionItem completion; + completion.label = view.data(); + completion.kind = int(CompletionItemKind::Value); + result = completion; + } + idsCompletions(scriptIdentifier.component(), result); + suggestReachableTypes(scriptIdentifier, + LocalSymbolsType::Singleton | LocalSymbolsType::AttachedType, + CompletionItemKind::Class, result); + + auto scope = scriptIdentifier.nearestSemanticScope(); + if (!scope) + return; + nearestScope = scope; + + enumerationCompletion(nearestScope, &usedNames, result); + } else { + const DomItem owner = + (askForCompletionOnDot ? scriptIdentifier : scriptIdentifier.directParent()) + .field(Fields::left); + auto expressionType = QQmlLSUtils::resolveExpressionType( + owner, QQmlLSUtils::ResolveActualTypeForFieldMemberExpression); + if (!expressionType || !expressionType->semanticScope) + return; + nearestScope = expressionType->semanticScope; + // Use root element scope to use find the enumerations + // This should be changed when we support usages in external files + if (expressionType->type == QQmlLSUtils::QmlComponentIdentifier) + nearestScope = owner.rootQmlObject(GoTo::MostLikely).semanticScope(); + if (expressionType->name) { + // note: you only get enumeration values in qualified expressions, never alone + enumerationValueCompletion(nearestScope, *expressionType->name, result); + + // skip enumeration types if already inside an enumeration type + if (auto enumerator = nearestScope->enumeration(*expressionType->name); + !enumerator.isValid()) { + enumerationCompletion(nearestScope, &usedNames, result); + } + + if (expressionType->type == QQmlLSUtils::EnumeratorIdentifier) + return; + } + } + + Q_ASSERT(nearestScope); + + methodCompletion(nearestScope, &usedNames, result); + propertyCompletion(nearestScope, &usedNames, result); + + if (!hasQualifier) { + // collect all of the stuff from parents + collectFromAllJavaScriptParents( + [this, &usedNames, result](const QQmlJSScope::ConstPtr &scope) { + jsIdentifierCompletion(scope, &usedNames, result); + }, + nearestScope); + collectFromAllJavaScriptParents( + [this, &usedNames, result](const QQmlJSScope::ConstPtr &scope) { + methodCompletion(scope, &usedNames, result); + }, + nearestScope); + collectFromAllJavaScriptParents( + [this, &usedNames, result](const QQmlJSScope::ConstPtr &scope) { + propertyCompletion(scope, &usedNames, result); + }, + nearestScope); + + auto file = scriptIdentifier.containingFile().as<QmlFile>(); + if (!file) + return; + auto resolver = file->typeResolver(); + if (!resolver) + return; + + const auto globals = resolver->jsGlobalObject(); + methodCompletion(globals, &usedNames, result); + propertyCompletion(globals, &usedNames, result); + } +} + +static const QQmlJSScope *resolve(const QQmlJSScope *current, const QStringList &names) +{ + for (const QString &name : names) { + if (auto property = current->property(name); property.isValid()) { + if (auto propertyType = property.type().get()) { + current = propertyType; + continue; + } + } + return {}; + } + return current; +} + +bool QQmlLSCompletion::cursorInFrontOfItem(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo) +{ + auto fileLocations = FileLocations::treeOf(parentForContext)->info().fullRegion; + return positionInfo.offset() <= fileLocations.offset; +} + +bool QQmlLSCompletion::cursorAfterColon(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo) +{ + auto location = FileLocations::treeOf(currentItem)->info(); + auto region = location.regions.constFind(ColonTokenRegion); + + if (region == location.regions.constEnd()) + return false; + + if (region.value().isValid() && region.value().offset < positionInfo.offset()) { + return true; + } + return false; +} + +/*! +\internal +\brief Mapping from pragma names to allowed pragma values. + +This mapping of pragma names to pragma values is not complete. In fact, it only contains the +pragma names and values that one should see autocompletion for. +Some pragmas like FunctionSignatureBehavior or Strict or the Reference/Value of ValueTypeBehavior, +for example, should currently not be proposed as completion items by qmlls. + +An empty QList-value in the QMap means that the pragma does not accept pragma values. +*/ +static const QMap<QString, QList<QString>> valuesForPragmas{ + { u"ComponentBehavior"_s, { u"Unbound"_s, u"Bound"_s } }, + { u"NativeMethodBehavior"_s, { u"AcceptThisObject"_s, u"RejectThisObject"_s } }, + { u"ListPropertyAssignBehavior"_s, { u"Append"_s, u"Replace"_s, u"ReplaceIfNotDefault"_s } }, + { u"Singleton"_s, {} }, + { u"ValueTypeBehavior"_s, { u"Addressable"_s, u"Inaddressable"_s } }, +}; + +void QQmlLSCompletion::insidePragmaCompletion(QQmlJS::Dom::DomItem currentItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + if (cursorAfterColon(currentItem, positionInfo)) { + const QString name = currentItem.field(Fields::name).value().toString(); + auto values = valuesForPragmas.constFind(name); + if (values == valuesForPragmas.constEnd()) + return; + + for (const auto &value : *values) { + CompletionItem comp; + comp.label = value.toUtf8(); + comp.kind = static_cast<int>(CompletionItemKind::Value); + result = comp; + } + return; + } + + for (const auto &pragma : valuesForPragmas.asKeyValueRange()) { + CompletionItem comp; + comp.label = pragma.first.toUtf8(); + if (!pragma.second.isEmpty()) { + comp.insertText = QString(pragma.first).append(u": ").toUtf8(); + } + comp.kind = static_cast<int>(CompletionItemKind::Value); + result = comp; + } +} + +void QQmlLSCompletion::insideQmlObjectCompletion(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation leftBrace = regions[LeftBraceRegion]; + const QQmlJS::SourceLocation rightBrace = regions[RightBraceRegion]; + + if (beforeLocation(positionInfo, leftBrace)) { + LocalSymbolsTypes options; + options.setFlag(LocalSymbolsType::ObjectType); + suggestReachableTypes(positionInfo.itemAtPosition, options, CompletionItemKind::Constructor, + result); + suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result); + + if (QQmlLSUtils::isFieldMemberExpression(positionInfo.itemAtPosition)) { + /*! + \internal + In the case that a missing identifier is followed by an assignment to the default + property, the parser will create a QmlObject out of both binding and default + binding. For example, in \code property int x: root. Item {} \endcode the parser will + create one binding containing one QmlObject of type `root.Item`, instead of two + bindings (one for `x` and one for the default property). For this special case, if + completion is requested inside `root.Item`, then try to also suggest JS expressions. + + Note: suggestJSExpressionCompletion() will suggest nothing if the + fieldMemberExpression starts with the name of a qualified module or a filename, so + this only adds invalid suggestions in the case that there is something shadowing the + qualified module name or filename, like a property name for example. + + Note 2: This does not happen for field member accesses. For example, in + \code + property int x: root.x + Item {} + \endcode + The parser will create both bindings correctly. + */ + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + } + return; + } + + if (betweenLocations(leftBrace, positionInfo, rightBrace)) { + // default/required property completion + for (QUtf8StringView view : + std::array<QUtf8StringView, 6>{ "", "readonly ", "default ", "default required ", + "required default ", "required " }) { + // readonly properties require an initializer + if (view != QUtf8StringView("readonly ")) { + result = makeSnippet( + QByteArray(view.data()).append("property type name;"), + QByteArray(view.data()).append("property ${1:type} ${0:name};")); + } + + result = makeSnippet( + QByteArray(view.data()).append("property type name: value;"), + QByteArray(view.data()).append("property ${1:type} ${2:name}: ${0:value};")); + } + + // signal + result = makeSnippet("signal name(arg1:type1, ...)", "signal ${1:name}($0)"); + + // signal without parameters + result = makeSnippet("signal name;", "signal ${0:name};"); + + // make already existing property required + result = makeSnippet("required name;", "required ${0:name};"); + + // function + result = makeSnippet("function name(args...): returnType { statements...}", + "function ${1:name}($2): ${3:returnType} {\n\t$0\n}"); + + // enum + result = makeSnippet("enum name { Values...}", "enum ${1:name} {\n\t${0:values}\n}"); + + // inline component + result = makeSnippet("component Name: BaseType { ... }", + "component ${1:name}: ${2:baseType} {\n\t$0\n}"); + + // add bindings + const DomItem containingObject = parentForContext.qmlObject(); + suggestBindingCompletion(containingObject, result); + + // add Qml Types for default binding + const DomItem containingFile = parentForContext.containingFile(); + suggestReachableTypes(containingFile, LocalSymbolsType::ObjectType, + CompletionItemKind::Constructor, result); + suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insidePropertyDefinitionCompletion( + const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + auto info = FileLocations::treeOf(currentItem)->info(); + const QQmlJS::SourceLocation propertyKeyword = info.regions[PropertyKeywordRegion]; + + // do completions for the keywords + if (positionInfo.offset() < propertyKeyword.offset + propertyKeyword.length) { + const QQmlJS::SourceLocation readonlyKeyword = info.regions[ReadonlyKeywordRegion]; + const QQmlJS::SourceLocation defaultKeyword = info.regions[DefaultKeywordRegion]; + const QQmlJS::SourceLocation requiredKeyword = info.regions[RequiredKeywordRegion]; + + bool completeReadonly = true; + bool completeRequired = true; + bool completeDefault = true; + + // if there is already a readonly keyword before the cursor: do not auto complete it again + if (readonlyKeyword.isValid() && readonlyKeyword.offset < positionInfo.offset()) { + completeReadonly = false; + // also, required keywords do not like readonly keywords + completeRequired = false; + } + + // same for required + if (requiredKeyword.isValid() && requiredKeyword.offset < positionInfo.offset()) { + completeRequired = false; + // also, required keywords do not like readonly keywords + completeReadonly = false; + } + + // same for default + if (defaultKeyword.isValid() && defaultKeyword.offset < positionInfo.offset()) { + completeDefault = false; + } + auto addCompletionKeyword = [&result](QUtf8StringView view, bool complete) { + if (!complete) + return; + CompletionItem item; + item.label = view.data(); + item.kind = int(CompletionItemKind::Keyword); + result = item; + }; + addCompletionKeyword(u8"readonly", completeReadonly); + addCompletionKeyword(u8"required", completeRequired); + addCompletionKeyword(u8"default", completeDefault); + addCompletionKeyword(u8"property", true); + + return; + } + + const QQmlJS::SourceLocation propertyIdentifier = info.regions[IdentifierRegion]; + if (propertyKeyword.end() <= positionInfo.offset() + && positionInfo.offset() < propertyIdentifier.offset) { + suggestReachableTypes(currentItem, + LocalSymbolsType::ObjectType | LocalSymbolsType::ValueType, + CompletionItemKind::Class, result); + } + // do not autocomplete the rest + return; +} + +void QQmlLSCompletion::insideBindingCompletion(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const DomItem containingBinding = currentItem.filterUp( + [](DomType type, const QQmlJS::Dom::DomItem &) { return type == DomType::Binding; }, + FilterUpOptions::ReturnOuter); + + // do scriptidentifiercompletion after the ':' of a binding + if (cursorAfterColon(containingBinding, positionInfo)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + + if (auto type = QQmlLSUtils::resolveExpressionType(currentItem, + QQmlLSUtils::ResolveOwnerType)) { + const QStringList names = currentItem.field(Fields::name).toString().split(u'.'); + const QQmlJSScope *current = resolve(type->semanticScope.get(), names); + // add type names when binding to an object type or a property with var type + if (!current || current->accessSemantics() == QQmlSA::AccessSemantics::Reference) { + LocalSymbolsTypes options; + options.setFlag(LocalSymbolsType::ObjectType); + suggestReachableTypes(positionInfo.itemAtPosition, options, + CompletionItemKind::Constructor, result); + suggestSnippetsForRightHandSideOfBinding(positionInfo.itemAtPosition, result); + } + } + return; + } + + // ignore the binding if asking for completion in front of the binding + if (cursorInFrontOfItem(containingBinding, positionInfo)) { + insideQmlObjectCompletion(currentItem.containingObject(), positionInfo, result); + return; + } + + const DomItem containingObject = currentItem.qmlObject(); + + suggestBindingCompletion(positionInfo.itemAtPosition, result); + + // add Qml Types for default binding + suggestReachableTypes(positionInfo.itemAtPosition, LocalSymbolsType::ObjectType, + CompletionItemKind::Constructor, result); + suggestSnippetsForLeftHandSideOfBinding(positionInfo.itemAtPosition, result); +} + +void QQmlLSCompletion::insideImportCompletion(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const DomItem containingFile = currentItem.containingFile(); + insideImportCompletionHelper(containingFile, positionInfo, result); + + // when in front of the import statement: propose types for root Qml Object completion + if (cursorInFrontOfItem(currentItem, positionInfo)) { + suggestReachableTypes(containingFile, LocalSymbolsType::ObjectType, + CompletionItemKind::Constructor, result); + } +} + +void QQmlLSCompletion::insideQmlFileCompletion(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const DomItem containingFile = currentItem.containingFile(); + // completions for code outside the root Qml Object + // global completions + if (positionInfo.cursorPosition.atLineStart()) { + if (positionInfo.cursorPosition.base().isEmpty()) { + for (const QStringView &s : std::array<QStringView, 2>({ u"pragma", u"import" })) { + CompletionItem comp; + comp.label = s.toUtf8(); + comp.kind = int(CompletionItemKind::Keyword); + result = comp; + } + } + } + // Types for root Qml Object completion + suggestReachableTypes(containingFile, LocalSymbolsType::ObjectType, + CompletionItemKind::Constructor, result); +} + +/*! +\internal +Generate the snippets for let, var and const variable declarations. +*/ +void QQmlLSCompletion::suggestVariableDeclarationStatementCompletion( + BackInsertIterator result, AppendOption option) const +{ + // let/var/const statement + for (auto view : std::array<QUtf8StringView, 3>{ "let", "var", "const" }) { + auto snippet = makeSnippet(QByteArray(view.data()).append(" variable = value"), + QByteArray(view.data()).append(" ${1:variable} = $0")); + if (option == AppendSemicolon) { + snippet.insertText->append(";"); + snippet.label.append(";"); + } + result = snippet; + } +} + +/*! +\internal +Generate the snippets for case and default statements. +*/ +void QQmlLSCompletion::suggestCaseAndDefaultStatementCompletion(BackInsertIterator result) const +{ + // case snippet + result = makeSnippet("case value: statements...", "case ${1:value}:\n\t$0"); + // case + brackets snippet + result = makeSnippet("case value: { statements... }", "case ${1:value}: {\n\t$0\n}"); + + // default snippet + result = makeSnippet("default: statements...", "default:\n\t$0"); + // default + brackets snippet + result = makeSnippet("default: { statements... }", "default: {\n\t$0\n}"); +} + +/*! +\internal +Break and continue can be inserted only in following situations: +\list + \li Break and continue inside a loop. + \li Break inside a (nested) LabelledStatement + \li Break inside a (nested) SwitchStatement +\endlist +*/ +void QQmlLSCompletion::suggestContinueAndBreakStatementIfNeeded(const DomItem &itemAtPosition, + BackInsertIterator result) const +{ + bool alreadyInLabel = false; + bool alreadyInSwitch = false; + for (DomItem current = itemAtPosition; current; current = current.directParent()) { + switch (current.internalKind()) { + case DomType::ScriptExpression: + // reached end of script expression + return; + + case DomType::ScriptForStatement: + case DomType::ScriptForEachStatement: + case DomType::ScriptWhileStatement: + case DomType::ScriptDoWhileStatement: { + CompletionItem continueKeyword; + continueKeyword.label = "continue"; + continueKeyword.kind = int(CompletionItemKind::Keyword); + result = continueKeyword; + + // do not add break twice + if (!alreadyInSwitch && !alreadyInLabel) { + CompletionItem breakKeyword; + breakKeyword.label = "break"; + breakKeyword.kind = int(CompletionItemKind::Keyword); + result = breakKeyword; + } + // early exit: cannot suggest more completions + return; + } + case DomType::ScriptSwitchStatement: { + // check if break was already inserted + if (alreadyInSwitch || alreadyInLabel) + break; + alreadyInSwitch = true; + + CompletionItem breakKeyword; + breakKeyword.label = "break"; + breakKeyword.kind = int(CompletionItemKind::Keyword); + result = breakKeyword; + break; + } + case DomType::ScriptLabelledStatement: { + // check if break was already inserted because of switch or loop + if (alreadyInSwitch || alreadyInLabel) + break; + alreadyInLabel = true; + + CompletionItem breakKeyword; + breakKeyword.label = "break"; + breakKeyword.kind = int(CompletionItemKind::Keyword); + result = breakKeyword; + break; + } + default: + break; + } + } +} + +/*! +\internal +Generates snippets or keywords for all possible JS statements where it makes sense. To use whenever +any JS statement can be expected, but when no JS statement is there yet. + +Only generates JS expression completions when itemAtPosition is a qualified name. + +Here is a list of statements that do \e{not} get any snippets: +\list + \li BlockStatement does not need a code snippet, editors automatically include the closing +bracket anyway. \li EmptyStatement completion would only generate a single \c{;} \li +ExpressionStatement completion cannot generate any snippet, only identifiers \li WithStatement +completion is not recommended: qmllint will warn about usage of with statements \li +LabelledStatement completion might need to propose labels (TODO?) \li DebuggerStatement completion +does not strike as being very useful \endlist +*/ +void QQmlLSCompletion::suggestJSStatementCompletion(const DomItem &itemAtPosition, + BackInsertIterator result) const +{ + suggestJSExpressionCompletion(itemAtPosition, result); + + if (QQmlLSUtils::isFieldMemberAccess(itemAtPosition)) + return; + + // expression statements + suggestVariableDeclarationStatementCompletion(result); + // block statement + result = makeSnippet("{ statements... }", "{\n\t$0\n}"); + + // if + brackets statement + result = makeSnippet("if (condition) { statements }", "if ($1) {\n\t$0\n}"); + + // do statement + result = makeSnippet("do { statements } while (condition);", "do {\n\t$1\n} while ($0);"); + + // while + brackets statement + result = makeSnippet("while (condition) { statements...}", "while ($1) {\n\t$0\n}"); + + // for + brackets loop statement + result = makeSnippet("for (initializer; condition; increment) { statements... }", + "for ($1;$2;$3) {\n\t$0\n}"); + + // for ... in + brackets loop statement + result = makeSnippet("for (property in object) { statements... }", "for ($1 in $2) {\n\t$0\n}"); + + // for ... of + brackets loop statement + result = makeSnippet("for (element of array) { statements... }", "for ($1 of $2) {\n\t$0\n}"); + + // try + catch statement + result = makeSnippet("try { statements... } catch(error) { statements... }", + "try {\n\t$1\n} catch($2) {\n\t$0\n}"); + + // try + finally statement + result = makeSnippet("try { statements... } finally { statements... }", + "try {\n\t$1\n} finally {\n\t$0\n}"); + + // try + catch + finally statement + result = makeSnippet( + "try { statements... } catch(error) { statements... } finally { statements... }", + "try {\n\t$1\n} catch($2) {\n\t$3\n} finally {\n\t$0\n}"); + + // one can always assume that JS code in QML is inside a function, so always propose `return` + for (auto &&view : { "return"_ba, "throw"_ba }) { + CompletionItem item; + item.label = std::move(view); + item.kind = int(CompletionItemKind::Keyword); + result = item; + } + + // rules for case+default statements: + // 1) when inside a CaseBlock, or + // 2) inside a CaseClause, as an (non-nested) element of the CaseClause statementlist. + // 3) inside a DefaultClause, as an (non-nested) element of the DefaultClause statementlist, + // + // switch (x) { + // // (1) + // case 1: + // myProperty = 5; + // // (2) -> could be another statement of current case, but also a new case or default! + // default: + // myProperty = 5; + // // (3) -> could be another statement of current default, but also a new case or default! + // } + const DomType currentKind = itemAtPosition.internalKind(); + const DomType parentKind = itemAtPosition.directParent().internalKind(); + if (currentKind == DomType::ScriptCaseBlock || currentKind == DomType::ScriptCaseClause + || currentKind == DomType::ScriptDefaultClause + || (currentKind == DomType::List + && (parentKind == DomType::ScriptCaseClause + || parentKind == DomType::ScriptDefaultClause))) { + suggestCaseAndDefaultStatementCompletion(result); + } + suggestContinueAndBreakStatementIfNeeded(itemAtPosition, result); +} + +void QQmlLSCompletion::insideForStatementCompletion(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; + const QQmlJS::SourceLocation firstSemicolon = regions[FirstSemicolonTokenRegion]; + const QQmlJS::SourceLocation secondSemicolon = regions[SecondSemicolonRegion]; + const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; + + if (betweenLocations(leftParenthesis, positionInfo, firstSemicolon)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + suggestVariableDeclarationStatementCompletion(result, + AppendOption::AppendNothing); + return; + } + if (betweenLocations(firstSemicolon, positionInfo, secondSemicolon) + || betweenLocations(secondSemicolon, positionInfo, rightParenthesis)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } + + if (afterLocation(rightParenthesis, positionInfo)) { + suggestJSStatementCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideScriptLiteralCompletion(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + Q_UNUSED(currentItem); + if (positionInfo.cursorPosition.base().isEmpty()) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideCallExpression(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(currentItem)->info().regions; + const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; + const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; + if (beforeLocation(positionInfo, leftParenthesis)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } + if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideIfStatement(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(currentItem)->info().regions; + const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; + const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; + const QQmlJS::SourceLocation elseKeyword = regions[ElseKeywordRegion]; + + if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } + if (betweenLocations(rightParenthesis, positionInfo, elseKeyword)) { + suggestJSStatementCompletion(positionInfo.itemAtPosition, result); + return; + } + if (afterLocation(elseKeyword, positionInfo)) { + suggestJSStatementCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideReturnStatement(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(currentItem)->info().regions; + const QQmlJS::SourceLocation returnKeyword = regions[ReturnKeywordRegion]; + + if (afterLocation(returnKeyword, positionInfo)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideWhileStatement(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(currentItem)->info().regions; + const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; + const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; + + if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } + if (afterLocation(rightParenthesis, positionInfo)) { + suggestJSStatementCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideDoWhileStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + const QQmlJS::SourceLocation doKeyword = regions[DoKeywordRegion]; + const QQmlJS::SourceLocation whileKeyword = regions[WhileKeywordRegion]; + const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; + const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; + + if (betweenLocations(doKeyword, positionInfo, whileKeyword)) { + suggestJSStatementCompletion(positionInfo.itemAtPosition, result); + return; + } + if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideForEachStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation inOf = regions[InOfTokenRegion]; + const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; + const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; + + if (betweenLocations(leftParenthesis, positionInfo, inOf)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + suggestVariableDeclarationStatementCompletion(result); + return; + } + if (betweenLocations(inOf, positionInfo, rightParenthesis)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } + + if (afterLocation(rightParenthesis, positionInfo)) { + suggestJSStatementCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideSwitchStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; + const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; + + if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideCaseClause(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation caseKeyword = regions[CaseKeywordRegion]; + const QQmlJS::SourceLocation colonToken = regions[ColonTokenRegion]; + + if (betweenLocations(caseKeyword, positionInfo, colonToken)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } + if (afterLocation(colonToken, positionInfo)) { + suggestJSStatementCompletion(positionInfo.itemAtPosition, result); + return; + } + +} + +/*! +\internal +Checks if a case or default clause does happen before ctx in the code. +*/ +bool QQmlLSCompletion::isCaseOrDefaultBeforeCtx(const DomItem ¤tClause, + const QQmlLSCompletionPosition &positionInfo, + FileLocationRegion keywordRegion) const +{ + Q_ASSERT(keywordRegion == QQmlJS::Dom::CaseKeywordRegion + || keywordRegion == QQmlJS::Dom::DefaultKeywordRegion); + + if (!currentClause) + return false; + + const auto token = FileLocations::treeOf(currentClause)->info().regions[keywordRegion]; + if (afterLocation(token, positionInfo)) + return true; + + return false; +} + +/*! +\internal + +Search for a `case ...:` or a `default: ` clause happening before ctx, and return the +corresponding DomItem of type DomType::CaseClauses or DomType::DefaultClause. + +Return an empty DomItem if neither case nor default was found. +*/ +DomItem +QQmlLSCompletion::previousCaseOfCaseBlock(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo) const +{ + const DomItem caseClauses = parentForContext.field(Fields::caseClauses); + for (int i = 0; i < caseClauses.indexes(); ++i) { + const DomItem currentClause = caseClauses.index(i); + if (isCaseOrDefaultBeforeCtx(currentClause, positionInfo, QQmlJS::Dom::CaseKeywordRegion)) { + return currentClause; + } + } + + const DomItem defaultClause = parentForContext.field(Fields::defaultClause); + if (isCaseOrDefaultBeforeCtx(defaultClause, positionInfo, QQmlJS::Dom::DefaultKeywordRegion)) + return parentForContext.field(Fields::defaultClause); + + const DomItem moreCaseClauses = parentForContext.field(Fields::moreCaseClauses); + for (int i = 0; i < moreCaseClauses.indexes(); ++i) { + const DomItem currentClause = moreCaseClauses.index(i); + if (isCaseOrDefaultBeforeCtx(currentClause, positionInfo, QQmlJS::Dom::CaseKeywordRegion)) { + return currentClause; + } + } + + return {}; +} + +void QQmlLSCompletion::insideCaseBlock(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation leftBrace = regions[LeftBraceRegion]; + const QQmlJS::SourceLocation rightBrace = regions[RightBraceRegion]; + + if (!betweenLocations(leftBrace, positionInfo, rightBrace)) + return; + + // TODO: looks fishy + // if there is a previous case or default clause, you can still add statements to it + if (const auto previousCase = previousCaseOfCaseBlock(parentForContext, positionInfo)) { + suggestJSStatementCompletion(previousCase, result); + return; + } + + // otherwise, only complete case and default + suggestCaseAndDefaultStatementCompletion(result); +} + +void QQmlLSCompletion::insideDefaultClause(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation colonToken = regions[ColonTokenRegion]; + + if (afterLocation(colonToken, positionInfo)) { + suggestJSStatementCompletion(positionInfo.itemAtPosition, result); + return ; + } +} + +void QQmlLSCompletion::insideBinaryExpressionCompletion( + const DomItem &parentForContext, const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation operatorLocation = regions[OperatorTokenRegion]; + + if (beforeLocation(positionInfo, operatorLocation)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } + if (afterLocation(operatorLocation, positionInfo)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +/*! +\internal +Doing completion in variable declarations requires taking a look at all different cases: + +\list + \li Normal variable names, like \c{let helloWorld = 123;} + In this case, only autocomplete scriptexpressionidentifiers after the '=' token. + Do not propose existing names for the variable name, because the variable name needs to be + an identifier that is not used anywhere (to avoid shadowing and confusing code), + + \li Deconstructed arrays, like \c{let [ helloWorld, ] = [ 123, ];} + In this case, only autocomplete scriptexpressionidentifiers after the '=' token. + Do not propose already existing identifiers inside the left hand side array. + + \li Deconstructed arrays with initializers, like \c{let [ helloWorld = someVar, ] = [ 123, ];} + Note: this assigns the value of someVar to helloWorld if the right hand side's first element + is undefined or does not exist. + + In this case, only autocomplete scriptexpressionidentifiers after the '=' tokens. + Only propose already existing identifiers inside the left hand side array when behind a '=' + token. + + \li Deconstructed Objects, like \c{let { helloWorld, } = { helloWorld: 123, };} + In this case, only autocomplete scriptexpressionidentifiers after the '=' token. + Do not propose already existing identifiers inside the left hand side object. + + \li Deconstructed Objects with initializers, like \c{let { helloWorld = someVar, } = {};} + Note: this assigns the value of someVar to helloWorld if the right hand side's object does + not have a property called 'helloWorld'. + + In this case, only autocomplete scriptexpressionidentifiers after the '=' token. + Only propose already existing identifiers inside the left hand side object when behind a '=' + token. + + \li Finally, you are allowed to nest and combine all above possibilities together for all your + deconstruction needs, so the exact same completion needs to be done for + DomType::ScriptPatternElement too. + +\endlist +*/ +void QQmlLSCompletion::insideScriptPattern(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation equal = regions[EqualTokenRegion]; + + if (!afterLocation(equal, positionInfo)) + return; + + // otherwise, only complete case and default + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); +} + +/*! +\internal +See comment on insideScriptPattern(). +*/ +void QQmlLSCompletion::insideVariableDeclarationEntry(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + insideScriptPattern(parentForContext, positionInfo, result); +} + +void QQmlLSCompletion::insideThrowStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation throwKeyword = regions[ThrowKeywordRegion]; + + if (afterLocation(throwKeyword, positionInfo)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideLabelledStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation colon = regions[ColonTokenRegion]; + + if (afterLocation(colon, positionInfo)) { + suggestJSStatementCompletion(positionInfo.itemAtPosition, result); + return; + } + // note: the case "beforeLocation(ctx, colon)" probably never happens: + // this is because without the colon, the parser will probably not parse this as a + // labelledstatement but as a normal expression statement. + // So this case only happens when the colon already exists, and the user goes back to the + // label name and requests completion for that label. +} + +/*! +\internal +Collect the current set of labels that some DomItem can jump to. +*/ +static void collectLabels(const DomItem &context, QQmlLSCompletion::BackInsertIterator result) +{ + for (DomItem current = context; current; current = current.directParent()) { + if (current.internalKind() == DomType::ScriptLabelledStatement) { + const QString label = current.field(Fields::label).value().toString(); + if (label.isEmpty()) + continue; + CompletionItem item; + item.label = label.toUtf8(); + item.kind = int(CompletionItemKind::Value); // variable? + // TODO: more stuff here? + result = item; + } else if (current.internalKind() == DomType::ScriptExpression) { + // quick exit when leaving the JS part + return; + } + } + return; +} + +void QQmlLSCompletion::insideContinueStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation continueKeyword = regions[ContinueKeywordRegion]; + + if (afterLocation(continueKeyword, positionInfo)) { + collectLabels(parentForContext, result); + return; + } +} + +void QQmlLSCompletion::insideBreakStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation breakKeyword = regions[BreakKeywordRegion]; + + if (afterLocation(breakKeyword, positionInfo)) { + collectLabels(parentForContext, result); + return; + } +} + +void QQmlLSCompletion::insideConditionalExpression(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation questionMark = regions[QuestionMarkTokenRegion]; + const QQmlJS::SourceLocation colon = regions[ColonTokenRegion]; + + if (beforeLocation(positionInfo, questionMark)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } + if (betweenLocations(questionMark, positionInfo, colon)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } + if (afterLocation(colon, positionInfo)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideUnaryExpression(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation operatorToken = regions[OperatorTokenRegion]; + + if (afterLocation(operatorToken, positionInfo)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insidePostExpression(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation operatorToken = regions[OperatorTokenRegion]; + + if (beforeLocation(positionInfo, operatorToken)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::insideParenthesizedExpression(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator result) const +{ + const auto regions = FileLocations::treeOf(parentForContext)->info().regions; + + const QQmlJS::SourceLocation leftParenthesis = regions[LeftParenthesisRegion]; + const QQmlJS::SourceLocation rightParenthesis = regions[RightParenthesisRegion]; + + if (betweenLocations(leftParenthesis, positionInfo, rightParenthesis)) { + suggestJSExpressionCompletion(positionInfo.itemAtPosition, result); + return; + } +} + +void QQmlLSCompletion::signalHandlerCompletion(const QQmlJSScope::ConstPtr &scope, + QDuplicateTracker<QString> *usedNames, + BackInsertIterator result) const +{ + const auto keyValues = scope->methods().asKeyValueRange(); + for (const auto &[name, method] : keyValues) { + if (method.access() != QQmlJSMetaMethod::Public + || method.methodType() != QQmlJSMetaMethodType::Signal) { + continue; + } + if (usedNames && usedNames->hasSeen(name)) { + continue; + } + + CompletionItem completion; + completion.label = QQmlSignalNames::signalNameToHandlerName(name).toUtf8(); + completion.kind = int(CompletionItemKind::Method); + result = completion; + } +} + +/*! +\internal +Decide which completions can be used at currentItem and compute them. +*/ +QList<CompletionItem> +QQmlLSCompletion::completions(const DomItem ¤tItem, + const CompletionContextStrings &contextStrings) const +{ + QList<CompletionItem> result; + collectCompletions(currentItem, contextStrings, std::back_inserter(result)); + return result; +} + +void QQmlLSCompletion::collectCompletions(const DomItem ¤tItem, + const CompletionContextStrings &contextStrings, + BackInsertIterator result) const +{ + /*! + Completion is not provided on a script identifier expression because script identifier + expressions lack context information. Instead, find the first parent that has enough + context information and provide completion for this one. + For example, a script identifier expression \c{le} in + \badcode + for (;le;) { ... } + \endcode + will get completion for a property called \c{leProperty}, while the same script identifier + expression in + \badcode + for (le;;) { ... } + \endcode + will, in addition to \c{leProperty}, also get completion for the \c{let} statement snippet. + In this example, the parent used for the completion is the for-statement, of type + DomType::ScriptForStatement. + + In addition of the parent for the context, use positionInfo to have exact information on where + the cursor is (to compare with the SourceLocations of tokens) and which item is at this position + (required to provide completion at the correct position, for example for attached properties). + */ + const QQmlLSCompletionPosition positionInfo{ currentItem, contextStrings }; + for (DomItem currentParent = currentItem; currentParent; + currentParent = currentParent.directParent()) { + const DomType currentType = currentParent.internalKind(); + + switch (currentType) { + case DomType::Id: + // suppress completions for ids + return; + case DomType::Pragma: + insidePragmaCompletion(currentParent, positionInfo, result); + return; + case DomType::ScriptType: { + if (currentParent.directParent().internalKind() == DomType::QmlObject) { + insideQmlObjectCompletion(currentParent.directParent(), positionInfo, result); + return; + } + + LocalSymbolsTypes options; + options.setFlag(LocalSymbolsType::ObjectType); + options.setFlag(LocalSymbolsType::ValueType); + suggestReachableTypes(currentItem, options, CompletionItemKind::Class, result); + return; + } + case DomType::ScriptFormalParameter: + // no autocompletion inside of function parameter definition + return; + case DomType::Binding: + insideBindingCompletion(currentParent, positionInfo, result); + return; + case DomType::Import: + insideImportCompletion(currentParent, positionInfo, result); + return; + case DomType::ScriptForStatement: + insideForStatementCompletion(currentParent, positionInfo, result); + return; + case DomType::ScriptBlockStatement: + suggestJSStatementCompletion(positionInfo.itemAtPosition, result); + return; + case DomType::QmlFile: + insideQmlFileCompletion(currentParent, positionInfo, result); + return; + case DomType::QmlObject: + insideQmlObjectCompletion(currentParent, positionInfo, result); + return; + case DomType::MethodInfo: + // suppress completions + return; + case DomType::PropertyDefinition: + insidePropertyDefinitionCompletion(currentParent, positionInfo, result); + return; + case DomType::ScriptBinaryExpression: + // ignore field member expressions: these need additional context from its parents + if (QQmlLSUtils::isFieldMemberExpression(currentParent)) + continue; + insideBinaryExpressionCompletion(currentParent, positionInfo, result); + return; + case DomType::ScriptLiteral: + insideScriptLiteralCompletion(currentParent, positionInfo, result); + return; + case DomType::ScriptCallExpression: + insideCallExpression(currentParent, positionInfo, result); + return; + case DomType::ScriptIfStatement: + insideIfStatement(currentParent, positionInfo, result); + return; + case DomType::ScriptReturnStatement: + insideReturnStatement(currentParent, positionInfo, result); + return; + case DomType::ScriptWhileStatement: + insideWhileStatement(currentParent, positionInfo, result); + return; + case DomType::ScriptDoWhileStatement: + insideDoWhileStatement(currentParent, positionInfo, result); + return; + case DomType::ScriptForEachStatement: + insideForEachStatement(currentParent, positionInfo, result); + return; + case DomType::ScriptTryCatchStatement: + /*! + \internal + The Ecmascript standard specifies that there can only be a block statement between \c + try and \c catch(...), \c try and \c finally and \c catch(...) and \c finally, so all of + these completions are already handled by the DomType::ScriptBlockStatement completion. + The only place in the try statement where there is no BlockStatement and therefore needs + its own completion is inside the catch parameter, but that is + \quotation + An optional identifier or pattern to hold the caught exception for the associated catch + block. + \endquotation + citing + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch?retiredLocale=de#exceptionvar. + This means that no completion is needed inside a catch-expression, as it should contain + an identifier that is not yet used anywhere. + Therefore, no completion is required at all when inside a try-statement but outside a + block-statement. + */ + return; + case DomType::ScriptSwitchStatement: + insideSwitchStatement(currentParent, positionInfo, result); + return; + case DomType::ScriptCaseClause: + insideCaseClause(currentParent, positionInfo, result); + return; + case DomType::ScriptDefaultClause: + if (ctxBeforeStatement(positionInfo, currentParent, QQmlJS::Dom::DefaultKeywordRegion)) + continue; + insideDefaultClause(currentParent, positionInfo, result); + return; + case DomType::ScriptCaseBlock: + insideCaseBlock(currentParent, positionInfo, result); + return; + case DomType::ScriptVariableDeclaration: + // not needed: thats a list of ScriptVariableDeclarationEntry, and those entries cannot + // be suggested because they all start with `{`, `[` or an identifier that should not be + // in use yet. + return; + case DomType::ScriptVariableDeclarationEntry: + insideVariableDeclarationEntry(currentParent, positionInfo, result); + return; + case DomType::ScriptProperty: + // fallthrough: a ScriptProperty is a ScriptPattern but inside a JS Object. It gets the + // same completions as a ScriptPattern. + case DomType::ScriptPattern: + insideScriptPattern(currentParent, positionInfo, result); + return; + case DomType::ScriptThrowStatement: + insideThrowStatement(currentParent, positionInfo, result); + return; + case DomType::ScriptLabelledStatement: + insideLabelledStatement(currentParent, positionInfo, result); + return; + case DomType::ScriptContinueStatement: + insideContinueStatement(currentParent, positionInfo, result); + return; + case DomType::ScriptBreakStatement: + insideBreakStatement(currentParent, positionInfo, result); + return; + case DomType::ScriptConditionalExpression: + insideConditionalExpression(currentParent, positionInfo, result); + return; + case DomType::ScriptUnaryExpression: + insideUnaryExpression(currentParent, positionInfo, result); + return; + case DomType::ScriptPostExpression: + insidePostExpression(currentParent, positionInfo, result); + return; + case DomType::ScriptParenthesizedExpression: + insideParenthesizedExpression(currentParent, positionInfo, result); + return; + + // TODO: Implement those statements. + // In the meanwhile, suppress completions to avoid weird behaviors. + case DomType::ScriptArray: + case DomType::ScriptObject: + case DomType::ScriptElision: + case DomType::ScriptArrayEntry: + return; + + default: + continue; + } + Q_UNREACHABLE(); + } + + // no completion could be found + qCDebug(QQmlLSUtilsLog) << "No completion was found for current request."; + return; +} + +QQmlLSCompletion::QQmlLSCompletion(const QFactoryLoader &pluginLoader) +{ + const auto keys = pluginLoader.metaDataKeys(); + for (qsizetype i = 0; i < keys.size(); ++i) { + auto instance = std::unique_ptr<QQmlLSPlugin>( + qobject_cast<QQmlLSPlugin *>(pluginLoader.instance(i))); + if (!instance) + continue; + if (auto completionInstance = instance->createCompletionPlugin()) + m_plugins.push_back(std::move(completionInstance)); + } +} + +/*! +\internal +Helper method to call a method on all loaded plugins. +*/ +void QQmlLSCompletion::collectFromPlugins(qxp::function_ref<CompletionFromPluginFunction> f, + BackInsertIterator result) const +{ + for (const auto &plugin : m_plugins) { + Q_ASSERT(plugin); + f(plugin.get(), result); + } +} + +void QQmlLSCompletion::suggestSnippetsForLeftHandSideOfBinding(const DomItem &itemAtPosition, + BackInsertIterator result) const +{ + collectFromPlugins( + [&itemAtPosition](QQmlLSCompletionPlugin *p, BackInsertIterator result) { + p->suggestSnippetsForLeftHandSideOfBinding(itemAtPosition, result); + }, + result); +} + +void QQmlLSCompletion::suggestSnippetsForRightHandSideOfBinding(const DomItem &itemAtPosition, + BackInsertIterator result) const +{ + collectFromPlugins( + [&itemAtPosition](QQmlLSCompletionPlugin *p, BackInsertIterator result) { + p->suggestSnippetsForRightHandSideOfBinding(itemAtPosition, result); + }, + result); +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmllscompletion_p.h b/src/qmlls/qqmllscompletion_p.h new file mode 100644 index 0000000000..a371fd0315 --- /dev/null +++ b/src/qmlls/qqmllscompletion_p.h @@ -0,0 +1,228 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLLSCOMPLETION_H +#define QQMLLSCOMPLETION_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qqmlcompletioncontextstrings_p.h" +#include "qqmllsutils_p.h" +#include "qqmllsplugin_p.h" + +#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> +#include <QtQmlDom/private/qqmldomexternalitems_p.h> +#include <QtQmlDom/private/qqmldomtop_p.h> +#include <QtCore/private/qduplicatetracker_p.h> +#include <QtCore/private/qfactoryloader_p.h> +#include <QtCore/qpluginloader.h> +#include <QtCore/qxpfunctional.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(QQmlLSCompletionLog) + + +class QQmlLSCompletion +{ + using DomItem = QQmlJS::Dom::DomItem; +public: + enum class ImportCompletionType { None, Module, Version }; + enum AppendOption { AppendSemicolon, AppendNothing }; + + QQmlLSCompletion(const QFactoryLoader &pluginLoader); + + using CompletionItem = QLspSpecification::CompletionItem; + using BackInsertIterator = std::back_insert_iterator<QList<CompletionItem>>; + QList<CompletionItem> completions(const DomItem ¤tItem, + const CompletionContextStrings &ctx) const; + + static CompletionItem makeSnippet(QUtf8StringView qualifier, QUtf8StringView label, + QUtf8StringView insertText); + + static CompletionItem makeSnippet(QUtf8StringView label, QUtf8StringView insertText); + +private: + struct QQmlLSCompletionPosition + { + DomItem itemAtPosition; + CompletionContextStrings cursorPosition; + qsizetype offset() const { return cursorPosition.offset(); } + }; + + void collectCompletions(const DomItem ¤tItem, const CompletionContextStrings &ctx, + BackInsertIterator result) const; + + bool betweenLocations(QQmlJS::SourceLocation left, const QQmlLSCompletionPosition &positionInfo, + QQmlJS::SourceLocation right) const; + bool afterLocation(QQmlJS::SourceLocation left, + const QQmlLSCompletionPosition &positionInfo) const; + bool beforeLocation(const QQmlLSCompletionPosition &ctx, QQmlJS::SourceLocation right) const; + bool ctxBeforeStatement(const QQmlLSCompletionPosition &positionInfo, + const DomItem &parentForContext, + QQmlJS::Dom::FileLocationRegion firstRegion) const; + bool isCaseOrDefaultBeforeCtx(const DomItem ¤tClause, + const QQmlLSCompletionPosition &positionInfo, + QQmlJS::Dom::FileLocationRegion keywordRegion) const; + DomItem previousCaseOfCaseBlock(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo) const; + + void idsCompletions(const DomItem &component, BackInsertIterator it) const; + + void suggestReachableTypes(const DomItem &context, + QQmlJS::Dom::LocalSymbolsTypes typeCompletionType, + QLspSpecification::CompletionItemKind kind, + BackInsertIterator it) const; + + void suggestJSStatementCompletion(const DomItem ¤tItem, BackInsertIterator it) const; + void suggestCaseAndDefaultStatementCompletion(BackInsertIterator it) const; + void suggestVariableDeclarationStatementCompletion( + BackInsertIterator it, AppendOption option = AppendSemicolon) const; + + void suggestJSExpressionCompletion(const DomItem &context, BackInsertIterator it) const; + + void suggestBindingCompletion(const DomItem &itemAtPosition, BackInsertIterator it) const; + + void insideImportCompletionHelper(const DomItem &file, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + + void jsIdentifierCompletion(const QQmlJSScope::ConstPtr &scope, + QDuplicateTracker<QString> *usedNames, BackInsertIterator it) const; + + void methodCompletion(const QQmlJSScope::ConstPtr &scope, QDuplicateTracker<QString> *usedNames, + BackInsertIterator it) const; + void propertyCompletion(const QQmlJSScope::ConstPtr &scope, + QDuplicateTracker<QString> *usedNames, BackInsertIterator it) const; + void enumerationCompletion(const QQmlJSScope::ConstPtr &scope, + QDuplicateTracker<QString> *usedNames, BackInsertIterator it) const; + void enumerationValueCompletionHelper(const QStringList &enumeratorKeys, + BackInsertIterator it) const; + + void enumerationValueCompletion(const QQmlJSScope::ConstPtr &scope, + const QString &enumeratorName, BackInsertIterator it) const; + + static bool cursorInFrontOfItem(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo); + static bool cursorAfterColon(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo); + void insidePragmaCompletion(QQmlJS::Dom::DomItem currentItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideQmlObjectCompletion(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insidePropertyDefinitionCompletion(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideBindingCompletion(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideImportCompletion(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideQmlFileCompletion(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void suggestContinueAndBreakStatementIfNeeded(const DomItem &itemAtPosition, + BackInsertIterator it) const; + void insideScriptLiteralCompletion(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideCallExpression(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideIfStatement(const DomItem ¤tItem, const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideReturnStatement(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideWhileStatement(const DomItem ¤tItem, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideDoWhileStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideForStatementCompletion(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideForEachStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideSwitchStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition positionInfo, + BackInsertIterator it) const; + void insideCaseClause(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideCaseBlock(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, BackInsertIterator it) const; + void insideDefaultClause(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideBinaryExpressionCompletion(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideScriptPattern(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideVariableDeclarationEntry(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideThrowStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideLabelledStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideContinueStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideBreakStatement(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideConditionalExpression(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideUnaryExpression(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insidePostExpression(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void insideParenthesizedExpression(const DomItem &parentForContext, + const QQmlLSCompletionPosition &positionInfo, + BackInsertIterator it) const; + void signalHandlerCompletion(const QQmlJSScope::ConstPtr &scope, + QDuplicateTracker<QString> *usedNames, + BackInsertIterator it) const; + + void suggestSnippetsForLeftHandSideOfBinding(const DomItem &items, + BackInsertIterator result) const; + + void suggestSnippetsForRightHandSideOfBinding(const DomItem &items, + BackInsertIterator result) const; + +private: + using CompletionFromPluginFunction = void(QQmlLSCompletionPlugin *plugin, + BackInsertIterator result); + void collectFromPlugins(const qxp::function_ref<CompletionFromPluginFunction> f, + BackInsertIterator result) const; + + QStringList m_loadPaths; + + std::vector<std::unique_ptr<QQmlLSCompletionPlugin>> m_plugins; +}; + +QT_END_NAMESPACE + +#endif // QQMLLSCOMPLETION_H diff --git a/src/qmlls/qqmllscompletionplugin.cpp b/src/qmlls/qqmllscompletionplugin.cpp new file mode 100644 index 0000000000..fd47c691f7 --- /dev/null +++ b/src/qmlls/qqmllscompletionplugin.cpp @@ -0,0 +1,4 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmllscompletionplugin_p.h" diff --git a/src/qmlls/qqmllscompletionplugin_p.h b/src/qmlls/qqmllscompletionplugin_p.h new file mode 100644 index 0000000000..0dde7bec76 --- /dev/null +++ b/src/qmlls/qqmllscompletionplugin_p.h @@ -0,0 +1,42 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLLSCOMPLETIONPLUGIN_H +#define QQMLLSCOMPLETIONPLUGIN_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <iterator> + +#include <QtQmlDom/private/qqmldomelements_p.h> +#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> + +QT_BEGIN_NAMESPACE + +class QQmlLSCompletionPlugin +{ +public: + QQmlLSCompletionPlugin() = default; + virtual ~QQmlLSCompletionPlugin() = default; + + using BackInsertIterator = std::back_insert_iterator<QList<QLspSpecification::CompletionItem>>; + + virtual void suggestSnippetsForLeftHandSideOfBinding(const QQmlJS::Dom::DomItem &items, + BackInsertIterator result) const = 0; + + virtual void suggestSnippetsForRightHandSideOfBinding(const QQmlJS::Dom::DomItem &items, + BackInsertIterator result) const = 0; +}; + +QT_END_NAMESPACE + +#endif // QQMLLSCOMPLETIONPLUGIN_H diff --git a/src/qmlls/qqmllshelpplugininterface.cpp b/src/qmlls/qqmllshelpplugininterface.cpp new file mode 100644 index 0000000000..62944d69e0 --- /dev/null +++ b/src/qmlls/qqmllshelpplugininterface.cpp @@ -0,0 +1,4 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmllshelpplugininterface_p.h" diff --git a/src/qmlls/qqmllshelpplugininterface_p.h b/src/qmlls/qqmllshelpplugininterface_p.h new file mode 100644 index 0000000000..280d7c3e5d --- /dev/null +++ b/src/qmlls/qqmllshelpplugininterface_p.h @@ -0,0 +1,64 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLLSHELPPLUGININTERFACE_H +#define QQMLLSHELPPLUGININTERFACE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qstring.h> +#include <QtCore/qurl.h> +#include <QtCore/qobject.h> +#include <vector> + +QT_BEGIN_NAMESPACE + +class QQmlLSHelpProviderBase +{ +public: + struct DocumentLink + { + QUrl url; + QString title; + }; + +public: + virtual ~QQmlLSHelpProviderBase() = default; + virtual bool registerDocumentation(const QString &documentationFileName) = 0; + [[nodiscard]] virtual QByteArray fileData(const QUrl &url) const = 0; + [[nodiscard]] virtual std::vector<DocumentLink> documentsForIdentifier(const QString &id) const = 0; + [[nodiscard]] virtual std::vector<DocumentLink> + documentsForIdentifier(const QString &id, const QString &filterName) const = 0; + [[nodiscard]] virtual std::vector<DocumentLink> documentsForKeyword(const QString &keyword) const = 0; + [[nodiscard]] virtual std::vector<DocumentLink> + documentsForKeyword(const QString &keyword, const QString &filterName) const = 0; + [[nodiscard]] virtual QStringList registeredNamespaces() const = 0; + [[nodiscard]] virtual QString error() const = 0; +}; + +class QQmlLSHelpPluginInterface +{ +public: + QQmlLSHelpPluginInterface() = default; + virtual ~QQmlLSHelpPluginInterface() = default; + Q_DISABLE_COPY_MOVE(QQmlLSHelpPluginInterface) + + virtual std::unique_ptr<QQmlLSHelpProviderBase> initialize(const QString &collectionFile, + QObject *parent) = 0; +}; + +#define QQmlLSHelpPluginInterface_iid "org.qt-project.Qt.QmlLS.HelpPlugin/1.0" +Q_DECLARE_INTERFACE(QQmlLSHelpPluginInterface, QQmlLSHelpPluginInterface_iid) + +QT_END_NAMESPACE + +#endif // QQMLLSHELPPLUGININTERFACE_H diff --git a/src/qmlls/qqmllshelputils.cpp b/src/qmlls/qqmllshelputils.cpp new file mode 100644 index 0000000000..b74e8d0402 --- /dev/null +++ b/src/qmlls/qqmllshelputils.cpp @@ -0,0 +1,255 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmllshelputils_p.h" + +#include <QtQmlLS/private/qqmllsutils_p.h> +#include <QtCore/private/qfactoryloader_p.h> +#include <QtCore/qlibraryinfo.h> +#include <QtCore/qdiriterator.h> +#include <QtCore/qdir.h> +#include <QtQmlCompiler/private/qqmljstyperesolver_p.h> +#include <optional> + +QT_BEGIN_NAMESPACE + +Q_STATIC_LOGGING_CATEGORY(QQmlLSHelpUtilsLog, "qt.languageserver.helpUtils") + +using namespace QQmlJS::Dom; + +static QStringList documentationFiles(const QString &qtInstallationPath) +{ + QStringList result; + QDirIterator dirIterator(qtInstallationPath, QStringList{ "*.qch"_L1 }, QDir::Files); + while (dirIterator.hasNext()) { + const auto fileInfo = dirIterator.nextFileInfo(); + result << fileInfo.absoluteFilePath(); + } + return result; +} + +HelpManager::HelpManager() +{ + const QFactoryLoader pluginLoader(QQmlLSHelpPluginInterface_iid, u"/help"_s); + const auto keys = pluginLoader.metaDataKeys(); + for (qsizetype i = 0; i < keys.size(); ++i) { + auto instance = qobject_cast<QQmlLSHelpPluginInterface *>(pluginLoader.instance(i)); + if (instance) { + m_helpPlugin = + instance->initialize(QDir::tempPath() + "/collectionFile.qhc"_L1, nullptr); + break; + } + } +} + +void HelpManager::setDocumentationRootPath(const QString &path) +{ + if (m_docRootPath == path) + return; + m_docRootPath = path; + + const auto foundQchFiles = documentationFiles(path); + if (foundQchFiles.isEmpty()) { + qCWarning(QQmlLSHelpUtilsLog) + << "No documentation files found in the Qt doc installation path: " << path; + return; + } + + return registerDocumentations(foundQchFiles); +} + +QString HelpManager::documentationRootPath() const +{ + return m_docRootPath; +} + +void HelpManager::registerDocumentations(const QStringList &docs) const +{ + if (!m_helpPlugin) + return; + std::for_each(docs.cbegin(), docs.cend(), + [this](const auto &file) { m_helpPlugin->registerDocumentation(file); }); +} + +std::optional<QByteArray> HelpManager::extractDocumentation(const DomItem &item) const +{ + if (item.internalKind() == DomType::ScriptIdentifierExpression) { + const auto resolvedType = + QQmlLSUtils::resolveExpressionType(item, QQmlLSUtils::ResolveOwnerType); + if (!resolvedType) + return std::nullopt; + + return extractDocumentationForIdentifiers(item, resolvedType.value()); + } else { + return extractDocumentationForDomElements(item); + } + + Q_UNREACHABLE_RETURN(std::nullopt); +} + +std::optional<QByteArray> +HelpManager::extractDocumentationForIdentifiers(const DomItem &item, + QQmlLSUtils::ExpressionType expr) const +{ + const auto qmlFile = item.containingFile().as<QmlFile>(); + if (!qmlFile) + return std::nullopt; + const auto links = collectDocumentationLinks(expr.semanticScope, qmlFile->typeResolver(), + expr.name.value()); + switch (expr.type) { + case QQmlLSUtils::QmlObjectIdIdentifier: + case QQmlLSUtils::JavaScriptIdentifier: + case QQmlLSUtils::GroupedPropertyIdentifier: + case QQmlLSUtils::PropertyIdentifier: { + ExtractDocumentation extractor(DomType::PropertyDefinition); + return tryExtract(extractor, links, expr.name.value()); + } + case QQmlLSUtils::PropertyChangedSignalIdentifier: + case QQmlLSUtils::PropertyChangedHandlerIdentifier: + case QQmlLSUtils::SignalIdentifier: + case QQmlLSUtils::SignalHandlerIdentifier: + case QQmlLSUtils::MethodIdentifier: { + ExtractDocumentation extractor(DomType::MethodInfo); + return tryExtract(extractor, links, expr.name.value()); + } + case QQmlLSUtils::SingletonIdentifier: + case QQmlLSUtils::AttachedTypeIdentifier: + case QQmlLSUtils::QmlComponentIdentifier: { + ExtractDocumentation extractor(DomType::QmlObject); + return tryExtract(extractor, links, expr.name.value()); + } + + // Not implemented yet + case QQmlLSUtils::EnumeratorIdentifier: + case QQmlLSUtils::EnumeratorValueIdentifier: + default: + qCDebug(QQmlLSHelpUtilsLog) + << "Documentation extraction for" << expr.name.value() << "was not implemented"; + return std::nullopt; + } + Q_UNREACHABLE_RETURN(std::nullopt); +} + +std::optional<QByteArray> HelpManager::extractDocumentationForDomElements(const DomItem &item) const +{ + const auto qmlFile = item.containingFile().as<QmlFile>(); + if (!qmlFile) + return std::nullopt; + + const auto name = item.field(Fields::name).value().toString(); + std::vector<QQmlLSHelpProviderBase::DocumentLink> links; + switch (item.internalKind()) { + case DomType::QmlObject: { + links = collectDocumentationLinks(item.nearestSemanticScope(), qmlFile->typeResolver(), + name); + break; + } + case DomType::PropertyDefinition: { + const auto scope = + QQmlLSUtils::findDefiningScopeForProperty(item.nearestSemanticScope(), name); + links = collectDocumentationLinks(scope, qmlFile->typeResolver(), name); + break; + } + case DomType::Binding: { + const auto scope = + QQmlLSUtils::findDefiningScopeForBinding(item.nearestSemanticScope(), name); + links = collectDocumentationLinks(scope, qmlFile->typeResolver(), name); + break; + } + case DomType::MethodInfo: { + const auto scope = + QQmlLSUtils::findDefiningScopeForMethod(item.nearestSemanticScope(), name); + links = collectDocumentationLinks(scope, qmlFile->typeResolver(), name); + break; + } + default: + qCDebug(QQmlLSHelpUtilsLog) + << item.internalKindStr() << "was not implemented for documentation extraction"; + return std::nullopt; + } + + ExtractDocumentation extractor(item.internalKind()); + return tryExtract(extractor, links, name); +} + +std::optional<QByteArray> +HelpManager::tryExtract(ExtractDocumentation &extractor, + const std::vector<QQmlLSHelpProviderBase::DocumentLink> &links, + const QString &name) const +{ + if (!m_helpPlugin) + return std::nullopt; + + for (auto &&link : links) { + const auto fileData = m_helpPlugin->fileData(link.url); + if (fileData.isEmpty()) { + qCDebug(QQmlLSHelpUtilsLog) << "No documentation found for" << link.url; + continue; + } + const auto &documentation = extractor.execute(QString::fromUtf8(fileData), name, + HtmlExtractor::ExtractionMode::Simplified); + if (documentation.isEmpty()) + continue; + return documentation.toUtf8(); + } + + return std::nullopt; +} + +std::optional<QByteArray> +HelpManager::documentationForItem(const DomItem &file, QLspSpecification::Position position) const +{ + if (!m_helpPlugin) + return std::nullopt; + + if (m_helpPlugin->registeredNamespaces().empty()) + return std::nullopt; + + std::optional<QByteArray> result; + const auto [line, character] = position; + const auto itemLocations = QQmlLSUtils::itemsFromTextLocation(file, line, character); + + // Process found item's internalKind and fetch its documentation. + for (const auto &entry : itemLocations) { + result = extractDocumentation(entry.domItem); + if (result.has_value()) + break; + } + + return result; +} + +/* + * Returns the list of potential documentation links for the given item. + * A keyword is not necessarily a unique name, so we need to find the scope where + * the keyword is defined. If the item is a property, method or binding, it will + * search for the defining scope and return the documentation links by looking at + * the imported names. If the item is a QmlObject, it will return the documentation + * links for qmlobject name. + */ +std::vector<QQmlLSHelpProviderBase::DocumentLink> +HelpManager::collectDocumentationLinks( + const QQmlJSScope::ConstPtr &scope, const std::shared_ptr<QQmlJSTypeResolver> &typeResolver, + const QString &name) const +{ + if (!m_helpPlugin) + return {}; + const auto potentialDocumentationLinks = + [this](const QQmlJSScope::ConstPtr &scope, + const std::shared_ptr<QQmlJSTypeResolver> &typeResolver) + -> std::vector<QQmlLSHelpProviderBase::DocumentLink> { + if (!scope || !typeResolver) + return {}; + + std::vector<QQmlLSHelpProviderBase::DocumentLink> links; + const auto docLinks = m_helpPlugin->documentsForKeyword(typeResolver->nameForType(scope)); + std::copy(docLinks.cbegin(), docLinks.cend(), std::back_inserter(links)); + return links; + }; + + // If the scope is not found for the defined scope, return all the links related to this name. + const auto result = potentialDocumentationLinks(scope, typeResolver); + return result.empty() ? m_helpPlugin->documentsForKeyword(name) : result; +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmllshelputils_p.h b/src/qmlls/qqmllshelputils_p.h new file mode 100644 index 0000000000..1f7e6f1f8b --- /dev/null +++ b/src/qmlls/qqmllshelputils_p.h @@ -0,0 +1,59 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLLSHELPUTILS_P_H +#define QQMLLSHELPUTILS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQmlLS/private/qqmllshelpplugininterface_p.h> +#include <QtQmlLS/private/qqmllsutils_p.h> +#include <QtQmlDom/private/qqmldomtop_p.h> +#include <QtQmlLS/private/qdochtmlparser_p.h> +#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> + +#include <vector> +#include <string> + +QT_BEGIN_NAMESPACE + +class HelpManager final +{ +public: + HelpManager(); + void setDocumentationRootPath(const QString &path); + [[nodiscard]] QString documentationRootPath() const; + [[nodiscard]] std::optional<QByteArray> documentationForItem( + const QQmlJS::Dom::DomItem &file, QLspSpecification::Position position) const; + +private: + [[nodiscard]] std::optional<QByteArray> extractDocumentationForIdentifiers( + const QQmlJS::Dom::DomItem &item, QQmlLSUtils::ExpressionType) const; + [[nodiscard]] std::optional<QByteArray> extractDocumentationForDomElements( + const QQmlJS::Dom::DomItem &item) const; + [[nodiscard]] std::optional<QByteArray> extractDocumentation( + const QQmlJS::Dom::DomItem &item) const; + [[nodiscard]] std::optional<QByteArray> tryExtract(ExtractDocumentation &extractor, + const std::vector<QQmlLSHelpProviderBase::DocumentLink> &links, + const QString &name) const; + [[nodiscard]] std::vector<QQmlLSHelpProviderBase::DocumentLink> collectDocumentationLinks( + const QQmlJSScope::ConstPtr &scope, + const std::shared_ptr<QQmlJSTypeResolver> &typeResolver, + const QString &name) const; + void registerDocumentations(const QStringList &docs) const; + std::unique_ptr<QQmlLSHelpProviderBase> m_helpPlugin; + QString m_docRootPath; +}; + +QT_END_NAMESPACE + +#endif // QQMLLSHELPUTILS_P_H diff --git a/src/qmlls/qqmllsplugin_p.h b/src/qmlls/qqmllsplugin_p.h new file mode 100644 index 0000000000..07699ce2c5 --- /dev/null +++ b/src/qmlls/qqmllsplugin_p.h @@ -0,0 +1,42 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QMLLSPLUGIN_P_H +#define QMLLSPLUGIN_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <memory> + +#include <QtCore/qtclasshelpermacros.h> +#include <QtCore/qobject.h> +#include <QtQmlLS/private/qqmllscompletionplugin_p.h> + +QT_BEGIN_NAMESPACE + +class QQmlLSPlugin +{ +public: + QQmlLSPlugin() = default; + virtual ~QQmlLSPlugin() = default; + + Q_DISABLE_COPY_MOVE(QQmlLSPlugin) + + virtual std::unique_ptr<QQmlLSCompletionPlugin> createCompletionPlugin() const = 0; +}; + +#define QmlLSPluginInterface_iid "org.qt-project.Qt.QmlLS.Plugin/1.0" +Q_DECLARE_INTERFACE(QQmlLSPlugin, QmlLSPluginInterface_iid) + +QT_END_NAMESPACE + +#endif // QMLLSPLUGIN_P_H diff --git a/src/qmlls/qqmllsutils.cpp b/src/qmlls/qqmllsutils.cpp new file mode 100644 index 0000000000..9e535ed185 --- /dev/null +++ b/src/qmlls/qqmllsutils.cpp @@ -0,0 +1,2186 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmllsutils_p.h" + +#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> +#include <QtCore/qthreadpool.h> +#include <QtCore/private/qduplicatetracker_p.h> +#include <QtCore/QRegularExpression> +#include <QtQmlDom/private/qqmldomexternalitems_p.h> +#include <QtQmlDom/private/qqmldomtop_p.h> +#include <QtQmlDom/private/qqmldomscriptelements_p.h> +#include <QtQmlDom/private/qqmldom_utils_p.h> +#include <QtQml/private/qqmlsignalnames_p.h> +#include <QtQml/private/qqmljslexer_p.h> +#include <QtQmlCompiler/private/qqmljsutils_p.h> + +#include <algorithm> +#include <iterator> +#include <memory> +#include <optional> +#include <set> +#include <stack> +#include <type_traits> +#include <utility> +#include <variant> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(QQmlLSUtilsLog, "qt.languageserver.utils") + +using namespace QQmlJS::Dom; +using namespace Qt::StringLiterals; + +namespace QQmlLSUtils { +QString qualifiersFrom(const DomItem &el) +{ + const bool isAccess = QQmlLSUtils::isFieldMemberAccess(el); + if (!isAccess && !QQmlLSUtils::isFieldMemberExpression(el)) + return {}; + + const DomItem fieldMemberExpressionBeginning = el.filterUp( + [](DomType, const DomItem &item) { return !QQmlLSUtils::isFieldMemberAccess(item); }, + FilterUpOptions::ReturnOuter); + QStringList qualifiers = + QQmlLSUtils::fieldMemberExpressionBits(fieldMemberExpressionBeginning, el); + + QString result; + for (const QString &qualifier : qualifiers) + result.append(qualifier).append(QChar(u'.')); + return result; +} + +/*! + \internal + Helper to check if item is a Field Member Expression \c {<someExpression>.propertyName}. +*/ +bool isFieldMemberExpression(const DomItem &item) +{ + return item.internalKind() == DomType::ScriptBinaryExpression + && item.field(Fields::operation).value().toInteger() + == ScriptElements::BinaryExpression::FieldMemberAccess; +} + +/*! + \internal + Helper to check if item is a Field Member Access \c memberAccess in + \c {<someExpression>.memberAccess}. +*/ +bool isFieldMemberAccess(const DomItem &item) +{ + auto parent = item.directParent(); + if (!isFieldMemberExpression(parent)) + return false; + + DomItem rightHandSide = parent.field(Fields::right); + return item == rightHandSide; +} + +/*! + \internal + Get the bits of a field member expression, like \c{a}, \c{b} and \c{c} for \c{a.b.c}. + + stopAtChild can either be an FieldMemberExpression, a ScriptIdentifierExpression or a default + constructed DomItem: This exits early before processing Field::right of an + FieldMemberExpression stopAtChild, or before processing a ScriptIdentifierExpression stopAtChild. + No early exits if stopAtChild is default constructed. +*/ +QStringList fieldMemberExpressionBits(const DomItem &item, const DomItem &stopAtChild) +{ + const bool isAccess = isFieldMemberAccess(item); + const bool isExpression = isFieldMemberExpression(item); + + // assume it is a non-qualified name + if (!isAccess && !isExpression) + return { item.value().toString() }; + + const DomItem stopMarker = + isFieldMemberExpression(stopAtChild) ? stopAtChild : stopAtChild.directParent(); + + QStringList result; + DomItem current = + isAccess ? item.directParent() : (isFieldMemberExpression(item) ? item : DomItem{}); + + for (; isFieldMemberExpression(current); current = current.field(Fields::right)) { + result << current.field(Fields::left).value().toString(); + + if (current == stopMarker) + return result; + } + result << current.value().toString(); + + return result; +} + +/*! + \internal + The language server protocol calls "URI" what QML calls "URL". + According to RFC 3986, a URL is a special case of URI that not only + identifies a resource but also shows how to access it. + In QML, however, URIs are distinct from URLs. URIs are the + identifiers of modules, for example "QtQuick.Controls". + In order to not confuse the terms we interpret language server URIs + as URLs in the QML code model. + This method marks a point of translation between the terms, but does + not have to change the actual URI/URL. + + \sa QQmlLSUtils::qmlUriToLspUrl + */ +QByteArray lspUriToQmlUrl(const QByteArray &uri) +{ + return uri; +} + +QByteArray qmlUrlToLspUri(const QByteArray &url) +{ + return url; +} + +/*! + \internal + \brief Converts a QQmlJS::SourceLocation to a LSP Range. + + QQmlJS::SourceLocation starts counting lines and rows at 1, but the LSP Range starts at 0. + Also, the QQmlJS::SourceLocation contains startLine, startColumn and length while the LSP Range + contains startLine, startColumn, endLine and endColumn, which must be computed from the actual + qml code. + */ +QLspSpecification::Range qmlLocationToLspLocation(const QString &code, + QQmlJS::SourceLocation qmlLocation) +{ + QLspSpecification::Range range; + + range.start.line = qmlLocation.startLine - 1; + range.start.character = qmlLocation.startColumn - 1; + + auto end = QQmlLSUtils::textRowAndColumnFrom(code, qmlLocation.end()); + range.end.line = end.line; + range.end.character = end.character; + return range; +} + +/*! + \internal + \brief Convert a text position from (line, column) into an offset. + + Row, Column and the offset are all 0-based. + For example, \c{s[textOffsetFrom(s, 5, 55)]} returns the character of s at line 5 and column 55. + + \sa QQmlLSUtils::textRowAndColumnFrom +*/ +qsizetype textOffsetFrom(const QString &text, int row, int column) +{ + int targetLine = row; + qsizetype i = 0; + while (i != text.size() && targetLine != 0) { + QChar c = text.at(i++); + if (c == u'\n') { + --targetLine; + } + if (c == u'\r') { + if (i != text.size() && text.at(i) == u'\n') + ++i; + --targetLine; + } + } + qsizetype leftChars = column; + while (i != text.size() && leftChars) { + QChar c = text.at(i); + if (c == u'\n' || c == u'\r') + break; + ++i; + if (!c.isLowSurrogate()) + --leftChars; + } + return i; +} + +/*! + \internal + \brief Convert a text position from an offset into (line, column). + + Row, Column and the offset are all 0-based. + For example, \c{textRowAndColumnFrom(s, 55)} returns the line and columns of the + character at \c {s[55]}. + + \sa QQmlLSUtils::textOffsetFrom +*/ +TextPosition textRowAndColumnFrom(const QString &text, qsizetype offset) +{ + int row = 0; + int column = 0; + qsizetype currentLineOffset = 0; + for (qsizetype i = 0; i < offset; i++) { + QChar c = text[i]; + if (c == u'\n') { + row++; + currentLineOffset = i + 1; + } else if (c == u'\r') { + if (i > 0 && text[i - 1] == u'\n') + currentLineOffset++; + } + } + column = offset - currentLineOffset; + + return { row, column }; +} + +static QList<ItemLocation>::const_iterator +handlePropertyDefinitionAndBindingOverlap(const QList<ItemLocation> &items, qsizetype offsetInFile) +{ + auto smallest = std::min_element( + items.begin(), items.end(), [](const ItemLocation &a, const ItemLocation &b) { + return a.fileLocation->info().fullRegion.length + < b.fileLocation->info().fullRegion.length; + }); + + if (smallest->domItem.internalKind() == DomType::Binding) { + // weird edge case: the filelocations of property definitions and property bindings are + // actually overlapping, which means that qmlls cannot distinguish between bindings and + // bindings in property definitions. Those need to be treated differently for + // autocompletion, for example. + // Therefore: when inside a binding and a propertydefinition, choose the property definition + // if offsetInFile is before the colon, like for example: + // property var helloProperty: Rectangle { /*...*/ } + // |----return propertydef---|-- return Binding ---| + + // get the smallest property definition to avoid getting the property definition that the + // current QmlObject is getting bound to! + auto smallestPropertyDefinition = std::min_element( + items.begin(), items.end(), [](const ItemLocation &a, const ItemLocation &b) { + // make property definition smaller to avoid getting smaller items that are not + // property definitions + const bool aIsPropertyDefinition = + a.domItem.internalKind() == DomType::PropertyDefinition; + const bool bIsPropertyDefinition = + b.domItem.internalKind() == DomType::PropertyDefinition; + return aIsPropertyDefinition > bIsPropertyDefinition + && a.fileLocation->info().fullRegion.length + < b.fileLocation->info().fullRegion.length; + }); + + if (smallestPropertyDefinition->domItem.internalKind() != DomType::PropertyDefinition) + return smallest; + + const auto propertyDefinitionColon = + smallestPropertyDefinition->fileLocation->info().regions[ColonTokenRegion]; + const auto smallestColon = smallest->fileLocation->info().regions[ColonTokenRegion]; + // sanity check: is it the definition of the current binding? check if they both have their + // ':' at the same location + if (propertyDefinitionColon.isValid() && propertyDefinitionColon == smallestColon + && offsetInFile < smallestColon.offset) { + return smallestPropertyDefinition; + } + } + return smallest; +} + +static QList<ItemLocation> filterItemsFromTextLocation(const QList<ItemLocation> &items, + qsizetype offsetInFile) +{ + if (items.size() < 2) + return items; + + // if there are multiple items, take the smallest one + its neighbors + // this allows to prefer inline components over main components, when both contain the + // current textposition, and to disregard internal structures like property maps, which + // "contain" everything from their first-appearing to last-appearing property (e.g. also + // other stuff in between those two properties). + + QList<ItemLocation> filteredItems; + + auto smallest = handlePropertyDefinitionAndBindingOverlap(items, offsetInFile); + + filteredItems.append(*smallest); + + const QQmlJS::SourceLocation smallestLoc = smallest->fileLocation->info().fullRegion; + const quint32 smallestBegin = smallestLoc.begin(); + const quint32 smallestEnd = smallestLoc.end(); + + for (auto it = items.begin(); it != items.end(); it++) { + if (it == smallest) + continue; + + const QQmlJS::SourceLocation itLoc = it->fileLocation->info().fullRegion; + const quint32 itBegin = itLoc.begin(); + const quint32 itEnd = itLoc.end(); + if (itBegin == smallestEnd || smallestBegin == itEnd) { + filteredItems.append(*it); + } + } + return filteredItems; +} + +/*! + \internal + \brief Find the DomItem representing the object situated in file at given line and + character/column. + + If line and character point between two objects, two objects might be returned. + If line and character point to whitespace, it might return an inner node of the QmlDom-Tree. + */ +QList<ItemLocation> itemsFromTextLocation(const DomItem &file, int line, int character) +{ + QList<ItemLocation> itemsFound; + std::shared_ptr<QmlFile> filePtr = file.ownerAs<QmlFile>(); + if (!filePtr) + return itemsFound; + FileLocations::Tree t = filePtr->fileLocationsTree(); + Q_ASSERT(t); + QString code = filePtr->code(); // do something more advanced wrt to changes wrt to this->code? + QList<ItemLocation> toDo; + qsizetype targetPos = textOffsetFrom(code, line, character); + Q_ASSERT(targetPos >= 0); + auto containsTarget = [targetPos](QQmlJS::SourceLocation l) { + if constexpr (sizeof(qsizetype) <= sizeof(quint32)) { + return l.begin() <= quint32(targetPos) && quint32(targetPos) <= l.end(); + } else { + return l.begin() <= targetPos && targetPos <= l.end(); + } + }; + if (containsTarget(t->info().fullRegion)) { + ItemLocation loc; + loc.domItem = file; + loc.fileLocation = t; + toDo.append(loc); + } + while (!toDo.isEmpty()) { + ItemLocation iLoc = toDo.last(); + toDo.removeLast(); + + bool inParentButOutsideChildren = true; + + auto subEls = iLoc.fileLocation->subItems(); + for (auto it = subEls.begin(); it != subEls.end(); ++it) { + auto subLoc = std::static_pointer_cast<AttachedInfoT<FileLocations>>(it.value()); + Q_ASSERT(subLoc); + + if (containsTarget(subLoc->info().fullRegion)) { + ItemLocation subItem; + subItem.domItem = iLoc.domItem.path(it.key()); + if (!subItem.domItem) { + qCDebug(QQmlLSUtilsLog) + << "A DomItem child is missing or the FileLocationsTree structure does " + "not follow the DomItem Structure."; + continue; + } + // the parser inserts empty Script Expressions for bindings that are not completely + // written out yet. Ignore them here. + if (subItem.domItem.internalKind() == DomType::ScriptExpression + && subLoc->info().fullRegion.length == 0) { + continue; + } + subItem.fileLocation = subLoc; + toDo.append(subItem); + inParentButOutsideChildren = false; + } + } + if (inParentButOutsideChildren) { + itemsFound.append(iLoc); + } + } + + // filtering step: + auto filtered = filterItemsFromTextLocation(itemsFound, targetPos); + return filtered; +} + +DomItem baseObject(const DomItem &object) +{ + DomItem prototypes; + DomItem qmlObject = object.qmlObject(); + // object is (or is inside) an inline component definition + if (object.internalKind() == DomType::QmlComponent || !qmlObject) { + prototypes = object.component() + .field(Fields::objects) + .index(0) + .field(QQmlJS::Dom::Fields::prototypes); + } else { + // object is (or is inside) a QmlObject + prototypes = qmlObject.field(QQmlJS::Dom::Fields::prototypes); + } + switch (prototypes.indexes()) { + case 0: + return {}; + case 1: + break; + default: + qDebug() << "Multiple prototypes found for " << object.name() << ", taking the first one."; + break; + } + QQmlJS::Dom::DomItem base = prototypes.index(0).proceedToScope(); + return base; +} + +static std::optional<Location> locationFromDomItem(const DomItem &item, FileLocationRegion region) +{ + Location location; + location.filename = item.canonicalFilePath(); + + auto tree = FileLocations::treeOf(item); + // tree is null for C++ defined types, for example + if (!tree) + return {}; + + location.sourceLocation = FileLocations::region(tree, region); + if (!location.sourceLocation.isValid() && region != QQmlJS::Dom::MainRegion) + location.sourceLocation = FileLocations::region(tree, MainRegion); + return location; +} + +/*! + \internal + \brief Returns the location of the type definition pointed by object. + + For a \c PropertyDefinition, return the location of the type of the property. + For a \c Binding, return the bound item's type location if an QmlObject is bound, and otherwise + the type of the property. + For a \c QmlObject, return the location of the QmlObject's base. + For an \c Id, return the location of the object to which the id resolves. + For a \c Methodparameter, return the location of the type of the parameter. + Otherwise, return std::nullopt. + */ +std::optional<Location> findTypeDefinitionOf(const DomItem &object) +{ + DomItem typeDefinition; + + switch (object.internalKind()) { + case QQmlJS::Dom::DomType::QmlComponent: + typeDefinition = object.field(Fields::objects).index(0); + break; + case QQmlJS::Dom::DomType::QmlObject: + typeDefinition = baseObject(object); + break; + case QQmlJS::Dom::DomType::Binding: { + auto binding = object.as<Binding>(); + Q_ASSERT(binding); + + // try to grab the type from the bound object + if (binding->valueKind() == BindingValueKind::Object) { + typeDefinition = baseObject(object.field(Fields::value)); + break; + } else { + // use the type of the property it is bound on for scriptexpression etc. + DomItem propertyDefinition; + const QString bindingName = binding->name(); + object.containingObject().visitLookup( + bindingName, + [&propertyDefinition](const DomItem &item) { + if (item.internalKind() == QQmlJS::Dom::DomType::PropertyDefinition) { + propertyDefinition = item; + return false; + } + return true; + }, + LookupType::PropertyDef); + typeDefinition = propertyDefinition.field(Fields::type).proceedToScope(); + break; + } + Q_UNREACHABLE(); + } + case QQmlJS::Dom::DomType::Id: + typeDefinition = object.field(Fields::referredObject).proceedToScope(); + break; + case QQmlJS::Dom::DomType::PropertyDefinition: + case QQmlJS::Dom::DomType::MethodParameter: + case QQmlJS::Dom::DomType::MethodInfo: + typeDefinition = object.field(Fields::type).proceedToScope(); + break; + case QQmlJS::Dom::DomType::ScriptIdentifierExpression: { + if (DomItem type = object.filterUp( + [](DomType k, const DomItem &) { return k == DomType::ScriptType; }, + FilterUpOptions::ReturnOuter)) { + + const QString name = fieldMemberExpressionBits(type.field(Fields::typeName)).join(u'.'); + switch (type.directParent().internalKind()) { + case DomType::QmlObject: + // is the type name of a QmlObject, like Item in `Item {...}` + typeDefinition = baseObject(type.directParent()); + break; + case DomType::QmlComponent: + typeDefinition = type.directParent(); + return locationFromDomItem(typeDefinition, FileLocationRegion::IdentifierRegion); + break; + default: + // is a type annotation, like Item in `function f(x: Item) { ... }` + typeDefinition = object.path(Paths::lookupTypePath(name)); + if (typeDefinition.internalKind() == DomType::Export) { + typeDefinition = typeDefinition.field(Fields::type).get(); + } + } + break; + } + if (DomItem id = object.filterUp( + [](DomType k, const DomItem &) { return k == DomType::Id; }, + FilterUpOptions::ReturnOuter)) { + + typeDefinition = id.field(Fields::referredObject).proceedToScope(); + break; + } + + auto scope = resolveExpressionType( + object, ResolveOptions::ResolveActualTypeForFieldMemberExpression); + if (!scope) + return {}; + + if (scope->type == QmlObjectIdIdentifier) { + return Location{ scope->semanticScope->filePath(), + scope->semanticScope->sourceLocation() }; + } + + typeDefinition = sourceLocationToDomItem(object.containingFile(), + scope->semanticScope->sourceLocation()); + return locationFromDomItem(typeDefinition.component(), + FileLocationRegion::IdentifierRegion); + } + default: + qDebug() << "QQmlLSUtils::findTypeDefinitionOf: Found unimplemented Type" + << object.internalKindStr(); + return {}; + } + + return locationFromDomItem(typeDefinition, FileLocationRegion::MainRegion); +} + +static bool findDefinitionFromItem(const DomItem &item, const QString &name) +{ + if (const QQmlJSScope::ConstPtr &scope = item.semanticScope()) { + qCDebug(QQmlLSUtilsLog) << "Searching for definition in" << item.internalKindStr(); + if (auto jsIdentifier = scope->ownJSIdentifier(name)) { + qCDebug(QQmlLSUtilsLog) << "Found scope" << scope->baseTypeName(); + return true; + } + } + return false; +} + +static DomItem findJSIdentifierDefinition(const DomItem &item, const QString &name) +{ + DomItem definitionOfItem; + item.visitUp([&name, &definitionOfItem](const DomItem &i) { + if (findDefinitionFromItem(i, name)) { + definitionOfItem = i; + return false; + } + // early exit: no JS definitions/usages outside the ScriptExpression DOM element. + if (i.internalKind() == DomType::ScriptExpression) + return false; + return true; + }); + + if (definitionOfItem) + return definitionOfItem; + + // special case: somebody asks for usages of a function parameter from its definition + // function parameters are defined in the method's scope + if (DomItem res = item.filterUp([](DomType k, const DomItem &) { return k == DomType::MethodInfo; }, + FilterUpOptions::ReturnOuter)) { + DomItem candidate = res.field(Fields::body).field(Fields::scriptElement); + if (findDefinitionFromItem(candidate, name)) { + return candidate; + } + } + + return definitionOfItem; +} + +/*! +\internal +Represents a signal, signal handler, property, property changed signal or a property changed +handler. + */ +struct SignalOrProperty +{ + /*! + \internal The name of the signal or property, independent of whether this is a changed signal + or handler. + */ + QString name; + IdentifierType type; +}; + +/*! +\internal +\brief Find out if \c{name} is a signal, signal handler, property, property changed signal, or a +property changed handler in the given QQmlJSScope. + +Heuristic to find if name is a property, property changed signal, .... because those can appear +under different names, for example \c{mySignal} and \c{onMySignal} for a signal. +This will give incorrect results as soon as properties/signals/methods are called \c{onMySignal}, +\c{on<some already existing property>Changed}, ..., but the good news is that the engine also +will act weird in these cases (e.g. one cannot bind to a property called like an already existing +signal or a property changed handler). +For future reference: you can always add additional checks to check the existence of those buggy +properties/signals/methods by looking if they exist in the QQmlJSScope. +*/ +static std::optional<SignalOrProperty> resolveNameInQmlScope(const QString &name, + const QQmlJSScope::ConstPtr &owner) +{ + if (owner->hasProperty(name)) { + return SignalOrProperty{ name, PropertyIdentifier }; + } + + if (const auto propertyName = QQmlSignalNames::changedHandlerNameToPropertyName(name)) { + if (owner->hasProperty(*propertyName)) { + return SignalOrProperty{ *propertyName, PropertyChangedHandlerIdentifier }; + } + } + + if (const auto signalName = QQmlSignalNames::handlerNameToSignalName(name)) { + if (auto methods = owner->methods(*signalName); !methods.isEmpty()) { + if (methods.front().methodType() == QQmlJSMetaMethodType::Signal) { + return SignalOrProperty{ *signalName, SignalHandlerIdentifier }; + } + } + } + + if (const auto propertyName = QQmlSignalNames::changedSignalNameToPropertyName(name)) { + if (owner->hasProperty(*propertyName)) { + return SignalOrProperty{ *propertyName, PropertyChangedSignalIdentifier }; + } + } + + if (auto methods = owner->methods(name); !methods.isEmpty()) { + if (methods.front().methodType() == QQmlJSMetaMethodType::Signal) { + return SignalOrProperty{ name, SignalIdentifier }; + } + return SignalOrProperty{ name, MethodIdentifier }; + } + return std::nullopt; +} + +/*! +\internal +Returns a list of names, that when belonging to the same targetType, should be considered equal. +This is used to find signal handlers as usages of their corresponding signals, for example. +*/ +static QStringList namesOfPossibleUsages(const QString &name, + const DomItem &item, + const QQmlJSScope::ConstPtr &targetType) +{ + QStringList namesToCheck = { name }; + if (item.internalKind() == DomType::EnumItem || item.internalKind() == DomType::EnumDecl) + return namesToCheck; + + auto namings = resolveNameInQmlScope(name, targetType); + if (!namings) + return namesToCheck; + switch (namings->type) { + case PropertyIdentifier: { + // for a property, also find bindings to its onPropertyChanged handler + propertyChanged + // signal + const QString propertyChangedHandler = + QQmlSignalNames::propertyNameToChangedHandlerName(namings->name); + namesToCheck.append(propertyChangedHandler); + + const QString propertyChangedSignal = + QQmlSignalNames::propertyNameToChangedSignalName(namings->name); + namesToCheck.append(propertyChangedSignal); + break; + } + case PropertyChangedHandlerIdentifier: { + // for a property changed handler, also find the usages of its property + propertyChanged + // signal + namesToCheck.append(namings->name); + namesToCheck.append(QQmlSignalNames::propertyNameToChangedSignalName(namings->name)); + break; + } + case PropertyChangedSignalIdentifier: { + // for a property changed signal, also find the usages of its property + onPropertyChanged + // handlers + namesToCheck.append(namings->name); + namesToCheck.append(QQmlSignalNames::propertyNameToChangedHandlerName(namings->name)); + break; + } + case SignalIdentifier: { + // for a signal, also find bindings to its onSignalHandler. + namesToCheck.append(QQmlSignalNames::signalNameToHandlerName(namings->name)); + break; + } + case SignalHandlerIdentifier: { + // for a signal handler, also find the usages of the signal it handles + namesToCheck.append(namings->name); + break; + } + default: { + break; + } + } + return namesToCheck; +} + +template<typename Predicate> +QQmlJSScope::ConstPtr findDefiningScopeIf( + const QQmlJSScope::ConstPtr &referrerScope, Predicate &&check) +{ + QQmlJSScope::ConstPtr result; + QQmlJSUtils::searchBaseAndExtensionTypes( + referrerScope, [&](const QQmlJSScope::ConstPtr &scope) { + if (check(scope)) { + result = scope; + return true; + } + return false; + }); + + return result; +} + +/*! +\internal +\brief Finds the scope where a property is first defined. + +Starts looking for the name starting from the given scope and traverse through base and +extension types. +*/ +QQmlJSScope::ConstPtr findDefiningScopeForProperty(const QQmlJSScope::ConstPtr &referrerScope, + const QString &nameToCheck) +{ + return findDefiningScopeIf(referrerScope, [&nameToCheck](const QQmlJSScope::ConstPtr &scope) { + return scope->hasOwnProperty(nameToCheck); + }); +} + +/*! +\internal +See also findDefiningScopeForProperty(). + +Special case: you can also bind to a signal handler. +*/ +QQmlJSScope::ConstPtr findDefiningScopeForBinding(const QQmlJSScope::ConstPtr &referrerScope, + const QString &nameToCheck) +{ + return findDefiningScopeIf(referrerScope, [&nameToCheck](const QQmlJSScope::ConstPtr &scope) { + return scope->hasOwnProperty(nameToCheck) || scope->hasOwnMethod(nameToCheck); + }); +} + +/*! +\internal +See also findDefiningScopeForProperty(). +*/ +QQmlJSScope::ConstPtr findDefiningScopeForMethod(const QQmlJSScope::ConstPtr &referrerScope, + const QString &nameToCheck) +{ + return findDefiningScopeIf(referrerScope, [&nameToCheck](const QQmlJSScope::ConstPtr &scope) { + return scope->hasOwnMethod(nameToCheck); + }); +} + +/*! +\internal +See also findDefiningScopeForProperty(). +*/ +QQmlJSScope::ConstPtr findDefiningScopeForEnumeration(const QQmlJSScope::ConstPtr &referrerScope, + const QString &nameToCheck) +{ + return findDefiningScopeIf(referrerScope, [&nameToCheck](const QQmlJSScope::ConstPtr &scope) { + return scope->hasOwnEnumeration(nameToCheck); + }); +} + +/*! +\internal +See also findDefiningScopeForProperty(). +*/ +QQmlJSScope::ConstPtr findDefiningScopeForEnumerationKey(const QQmlJSScope::ConstPtr &referrerScope, + const QString &nameToCheck) +{ + return findDefiningScopeIf(referrerScope, [&nameToCheck](const QQmlJSScope::ConstPtr &scope) { + return scope->hasOwnEnumerationKey(nameToCheck); + }); +} + +/*! + Filter away the parts of the Dom not needed for find usages, by following the profiler's + information. + 1. "propertyInfos" tries to require all inherited properties of some QmlObject. That is super + slow (profiler says it eats 90% of the time needed by `tst_qmlls_utils findUsages`!) and is not + needed for usages. + 2. "get" tries to resolve references, like base types saved in prototypes for example, and is not + needed to find usages. Profiler says it eats 70% of the time needed by `tst_qmlls_utils + findUsages`. + 3. "defaultPropertyName" also recurses through base types and is not needed to find usages. +*/ +static FieldFilter filterForFindUsages() +{ + FieldFilter filter{ {}, + { + { QString(), QString::fromUtf16(Fields::propertyInfos) }, + { QString(), QString::fromUtf16(Fields::defaultPropertyName) }, + { QString(), QString::fromUtf16(Fields::get) }, + } }; + return filter; +}; + +static void findUsagesOfNonJSIdentifiers(const DomItem &item, const QString &name, Usages &result) +{ + const auto expressionType = resolveExpressionType(item, ResolveOwnerType); + if (!expressionType) + return; + + // for Qml file components: add their filename as an usage for the renaming operation + if (expressionType->type == QmlComponentIdentifier + && !expressionType->semanticScope->isInlineComponent()) { + result.appendFilenameUsage(expressionType->semanticScope->filePath()); + } + + const QStringList namesToCheck = namesOfPossibleUsages(name, item, expressionType->semanticScope); + + const auto addLocationIfTypeMatchesTarget = + [&result, &expressionType](const DomItem &toBeResolved, FileLocationRegion subRegion) { + const auto currentType = + resolveExpressionType(toBeResolved, ResolveOptions::ResolveOwnerType); + if (!currentType) + return; + + const QQmlJSScope::ConstPtr target = expressionType->semanticScope; + const QQmlJSScope::ConstPtr current = currentType->semanticScope; + if (target == current) { + auto tree = FileLocations::treeOf(toBeResolved); + QQmlJS::SourceLocation sourceLocation; + + sourceLocation = FileLocations::region(tree, subRegion); + if (!sourceLocation.isValid()) + return; + + Location location{ toBeResolved.canonicalFilePath(), sourceLocation }; + result.appendUsage(location); + } + }; + + auto findUsages = [&addLocationIfTypeMatchesTarget, &name, + &namesToCheck](Path, const DomItem ¤t, bool) -> bool { + bool continueForChildren = true; + if (auto scope = current.semanticScope()) { + // is the current property shadowed by some JS identifier? ignore current + its children + if (scope->ownJSIdentifier(name)) { + return false; + } + } + switch (current.internalKind()) { + case DomType::QmlObject: + case DomType::Binding: + case DomType::MethodInfo: + case DomType::PropertyDefinition: { + const QString propertyName = current.field(Fields::name).value().toString(); + if (namesToCheck.contains(propertyName)) + addLocationIfTypeMatchesTarget(current, IdentifierRegion); + return continueForChildren; + } + case DomType::ScriptIdentifierExpression: { + const QString identifierName = current.field(Fields::identifier).value().toString(); + if (namesToCheck.contains(identifierName)) + addLocationIfTypeMatchesTarget(current, MainRegion); + return continueForChildren; + } + case DomType::ScriptLiteral: { + const QString literal = current.field(Fields::value).value().toString(); + if (namesToCheck.contains(literal)) + addLocationIfTypeMatchesTarget(current, MainRegion); + return continueForChildren; + } + case DomType::EnumItem: { + // Only look for the first enum defined. The inner enums + // have no way to be accessed. + const auto parentPath = current.containingObject().pathFromOwner(); + const auto index = parentPath.last().headIndex(); + if (index != 0) + return continueForChildren; + const QString enumValue = current.field(Fields::name).value().toString(); + if (namesToCheck.contains(enumValue)) + addLocationIfTypeMatchesTarget(current, IdentifierRegion); + return continueForChildren; + } + case DomType::EnumDecl: { + // Only look for the first enum defined. The inner enums + // have no way to be accessed. + const auto parentPath = current.pathFromOwner(); + const auto index = parentPath.last().headIndex(); + if (index != 0) + return continueForChildren; + const QString enumValue = current.field(Fields::name).value().toString(); + if (namesToCheck.contains(enumValue)) + addLocationIfTypeMatchesTarget(current, IdentifierRegion); + return continueForChildren; + } + default: + return continueForChildren; + }; + + Q_UNREACHABLE_RETURN(continueForChildren); + }; + + const DomItem qmlFiles = item.top().field(Fields::qmlFileWithPath); + const auto filter = filterForFindUsages(); + for (const QString &file : qmlFiles.keys()) { + const DomItem currentFileComponents = + qmlFiles.key(file).field(Fields::currentItem).field(Fields::components); + currentFileComponents.visitTree(Path(), emptyChildrenVisitor, + VisitOption::Recurse | VisitOption::VisitSelf, findUsages, + emptyChildrenVisitor, filter); + } +} + +static Location locationFromJSIdentifierDefinition(const DomItem &definitionOfItem, + const QString &name) +{ + Q_ASSERT_X(!definitionOfItem.semanticScope().isNull() + && definitionOfItem.semanticScope()->ownJSIdentifier(name).has_value(), + "QQmlLSUtils::locationFromJSIdentifierDefinition", + "JS definition does not actually define the JS identifier. " + "Did you obtain definitionOfItem from findJSIdentifierDefinition() ?"); + QQmlJS::SourceLocation location = + definitionOfItem.semanticScope()->ownJSIdentifier(name).value().location; + + Location result = { definitionOfItem.canonicalFilePath(), location }; + return result; +} + +static void findUsagesHelper(const DomItem &item, const QString &name, Usages &result) +{ + qCDebug(QQmlLSUtilsLog) << "Looking for JS identifier with name" << name; + DomItem definitionOfItem = findJSIdentifierDefinition(item, name); + + // if there is no definition found: check if name was a property or an id instead + if (!definitionOfItem) { + qCDebug(QQmlLSUtilsLog) << "No defining JS-Scope found!"; + findUsagesOfNonJSIdentifiers(item, name, result); + return; + } + + definitionOfItem.visitTree( + Path(), emptyChildrenVisitor, VisitOption::VisitAdopted | VisitOption::Recurse, + [&name, &result](Path, const DomItem &item, bool) -> bool { + qCDebug(QQmlLSUtilsLog) << "Visiting a " << item.internalKindStr(); + if (item.internalKind() == DomType::ScriptIdentifierExpression + && item.field(Fields::identifier).value().toString() == name) { + // add this usage + auto fileLocation = FileLocations::treeOf(item); + if (!fileLocation) { + qCWarning(QQmlLSUtilsLog) << "Failed finding filelocation of found usage"; + return true; + } + const QQmlJS::SourceLocation location = fileLocation->info().fullRegion; + const QString fileName = item.canonicalFilePath(); + result.appendUsage({ fileName, location }); + return true; + } else if (const QQmlJSScope::ConstPtr scope = item.semanticScope(); + scope && scope->ownJSIdentifier(name)) { + // current JS identifier has been redefined, do not visit children + return false; + } + return true; + }, + emptyChildrenVisitor, filterForFindUsages()); + + const Location definition = locationFromJSIdentifierDefinition(definitionOfItem, name); + result.appendUsage(definition); +} + +Usages findUsagesOf(const DomItem &item) +{ + Usages result; + + switch (item.internalKind()) { + case DomType::ScriptIdentifierExpression: { + const QString name = item.field(Fields::identifier).value().toString(); + findUsagesHelper(item, name, result); + break; + } + case DomType::ScriptVariableDeclarationEntry: { + const QString name = item.field(Fields::identifier).value().toString(); + findUsagesHelper(item, name, result); + break; + } + case DomType::EnumDecl: + case DomType::EnumItem: + case DomType::QmlObject: + case DomType::PropertyDefinition: + case DomType::Binding: + case DomType::MethodInfo: { + const QString name = item.field(Fields::name).value().toString(); + findUsagesHelper(item, name, result); + break; + } + case DomType::QmlComponent: { + QString name = item.field(Fields::name).value().toString(); + + // get rid of extra qualifiers + if (const auto dotIndex = name.indexOf(u'.'); dotIndex != -1) + name = name.sliced(dotIndex + 1); + findUsagesHelper(item, name, result); + break; + } + default: + qCDebug(QQmlLSUtilsLog) << item.internalKindStr() + << "was not implemented for QQmlLSUtils::findUsagesOf"; + return result; + } + + result.sort(); + + if (QQmlLSUtilsLog().isDebugEnabled()) { + qCDebug(QQmlLSUtilsLog) << "Found following usages in files:"; + for (auto r : result.usagesInFile()) { + qCDebug(QQmlLSUtilsLog) + << r.filename << " @ " << r.sourceLocation.startLine << ":" + << r.sourceLocation.startColumn << " with length " << r.sourceLocation.length; + } + qCDebug(QQmlLSUtilsLog) << "And following usages in file names:" + << result.usagesInFilename(); + } + + return result; +} + +static std::optional<IdentifierType> hasMethodOrSignal(const QQmlJSScope::ConstPtr &scope, + const QString &name) +{ + auto methods = scope->methods(name); + if (methods.isEmpty()) + return {}; + + const bool isSignal = methods.front().methodType() == QQmlJSMetaMethodType::Signal; + IdentifierType type = + isSignal ? IdentifierType::SignalIdentifier : IdentifierType::MethodIdentifier; + return type; +} + +/*! +\internal +Searches for a method by traversing the parent scopes. + +We assume here that it is possible to call methods from parent scope to simplify things, as the +linting module already warns about calling methods from parent scopes. + +Note: in QML, one can only call methods from the current scope, and from the QML file root scope. +Everything else needs a qualifier. +*/ +static std::optional<ExpressionType> +methodFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QString &name, + ResolveOptions options) +{ + for (QQmlJSScope::ConstPtr current = referrerScope; current; current = current->parentScope()) { + if (auto type = hasMethodOrSignal(current, name)) { + switch (options) { + case ResolveOwnerType: + return ExpressionType{ name, findDefiningScopeForMethod(current, name), *type }; + case ResolveActualTypeForFieldMemberExpression: + // QQmlJSScopes were not implemented for methods yet, but JS functions have methods + // and properties see + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function + // for the list of properties/methods of functions. Therefore return a null scope. + // see also code below for non-qualified method access + return ExpressionType{ name, {}, *type }; + } + } + + if (const auto signalName = QQmlSignalNames::handlerNameToSignalName(name)) { + if (auto type = hasMethodOrSignal(current, *signalName)) { + switch (options) { + case ResolveOwnerType: + return ExpressionType{ name, findDefiningScopeForMethod(current, *signalName), + SignalHandlerIdentifier }; + case ResolveActualTypeForFieldMemberExpression: + // Properties and methods of JS methods are not supported yet + return ExpressionType{ name, {}, SignalHandlerIdentifier }; + } + } + } + } + return {}; +} + + +/*! +\internal +See comment on methodFromReferrerScope: the same applies to properties. +*/ +static std::optional<ExpressionType> +propertyFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QString &propertyName, + ResolveOptions options) +{ + for (QQmlJSScope::ConstPtr current = referrerScope; current; current = current->parentScope()) { + const auto resolved = resolveNameInQmlScope(propertyName, current); + if (!resolved) + continue; + + if (auto property = current->property(resolved->name); property.isValid()) { + switch (options) { + case ResolveOwnerType: + return ExpressionType{ propertyName, + findDefiningScopeForProperty(current, propertyName), + resolved->type }; + case ResolveActualTypeForFieldMemberExpression: + return ExpressionType{ propertyName, property.type(), resolved->type }; + } + } + } + return {}; +} + +/*! +\internal +See comment on methodFromReferrerScope: the same applies to property bindings. + +If resolver is not null then it is used to resolve the id with which a generalized grouped +properties starts. +*/ +static std::optional<ExpressionType> +propertyBindingFromReferrerScope(const QQmlJSScope::ConstPtr &referrerScope, const QString &name, + ResolveOptions options, QQmlJSTypeResolver *resolverForIds) +{ + const auto bindings = referrerScope->propertyBindings(name); + if (bindings.isEmpty()) + return {}; + + const auto binding = bindings.begin(); + const auto bindingType = binding->bindingType(); + const bool bindingIsAttached = bindingType == QQmlSA::BindingType::AttachedProperty; + if (!bindingIsAttached && bindingType != QQmlSA::BindingType::GroupProperty) + return {}; + + // Generalized grouped properties, like Bindings or PropertyChanges, for example, have bindings + // starting in an id (like `someId.someProperty: ...`). + // If `someid` is not a property and is a deferred name, then it should be an id. + if (!bindingIsAttached && !referrerScope->hasProperty(name) + && referrerScope->isNameDeferred(name)) { + if (!resolverForIds) + return {}; + + return ExpressionType { + name, + resolverForIds->typeForId(referrerScope, name, AssumeComponentsAreBound), + QmlObjectIdIdentifier + }; + } + + const auto typeIdentifier = + bindingIsAttached ? AttachedTypeIdentifier : GroupedPropertyIdentifier; + + const auto getScope = [bindingIsAttached, binding]() -> QQmlJSScope::ConstPtr { + if (bindingIsAttached) + return binding->attachingType(); + + return binding->groupType(); + }; + + switch (options) { + case ResolveOwnerType: { + return ExpressionType{ name, + // note: always return the type of the attached type as the owner. + // Find usages on "Keys.", for example, should yield all usages of + // the "Keys" attached property. + bindingIsAttached + ? getScope() + : findDefiningScopeForProperty(referrerScope, name), + typeIdentifier }; + } + case ResolveActualTypeForFieldMemberExpression: + return ExpressionType{ name, getScope(), typeIdentifier }; + } + Q_UNREACHABLE_RETURN({}); +} + +/*! \internal + Finds the scope within the special elements like Connections, + PropertyChanges, Bindings or AnchorChanges. +*/ +static QQmlJSScope::ConstPtr findScopeOfSpecialItems( + const QQmlJSScope::ConstPtr &scope, const DomItem &item) +{ + if (!scope) + return {}; + + const QSet<QString> specialItems = {u"QQmlConnections"_s, + u"QQuickPropertyChanges"_s, + u"QQmlBind"_s, + u"QQuickAnchorChanges"_s}; + + const auto special = QQmlJSUtils::searchBaseAndExtensionTypes( + scope, [&specialItems](const QQmlJSScope::ConstPtr &visitedScope) { + const auto typeName = visitedScope->internalName(); + if (specialItems.contains(typeName)) + return true; + return false; + }); + + if (!special) + return {}; + + // Perform target name search if there is binding to property "target" + QString targetName; + if (scope->hasOwnPropertyBindings(u"target"_s)) { + // TODO: propagate the whole binding. + // We can figure out the meaning of target in more cases. + + DomItem current = item.qmlObject(); + auto target = current.bindings().key(u"target"_s).index(0); + if (target) { + targetName = target.field(Fields::value) + .field(Fields::scriptElement) + .field(Fields::identifier) + .value() + .toString(); + } + } + + if (!targetName.isEmpty()) { + // target: someId + auto resolver = item.containingFile().ownerAs<QmlFile>()->typeResolver(); + if (!resolver) + return {}; + + // Note: It does not have to be an ID. It can be a property. + return resolver->scopedType(scope, targetName); + } + + if (item.internalKind() == DomType::Binding && + item.field(Fields::bindingType).value().toInteger() == int(BindingType::OnBinding)) { + // Binding on sth : {} syntax + // Target scope is the current scope + return scope; + } + return scope->parentScope(); +} + +static std::optional<ExpressionType> resolveFieldMemberExpressionType(const DomItem &item, + ResolveOptions options) +{ + const QString name = item.field(Fields::identifier).value().toString(); + DomItem parent = item.directParent(); + auto owner = resolveExpressionType(parent.field(Fields::left), + ResolveOptions::ResolveActualTypeForFieldMemberExpression); + if (!owner) + return {}; + + if (auto scope = methodFromReferrerScope(owner->semanticScope, name, options)) + return *scope; + + if (auto scope = propertyBindingFromReferrerScope(owner->semanticScope, name, options, nullptr)) + return *scope; + + if (auto scope = propertyFromReferrerScope(owner->semanticScope, name, options)) + return *scope; + + // Ignore enum usages from other files for now. + if (owner->type == QmlComponentIdentifier) { + // Check if name is a enum value <TypeName>.<EnumValue> + // Enumerations should live under the root element scope of the file that defines the enum, + // therefore use the DomItem to find the root element of the qml file instead of directly + // using owner->semanticScope. + const auto scope = item.goToFile(owner->semanticScope->filePath()) + .rootQmlObject(GoTo::MostLikely) + .semanticScope(); + if (scope->hasEnumerationKey(name)) { + return ExpressionType{ name, scope, EnumeratorValueIdentifier }; + } + // Or it is a enum name <TypeName>.<EnumName>.<EnumValue> + else if (scope->hasEnumeration(name)) { + return ExpressionType{ name, scope, EnumeratorIdentifier }; + } + + // check inline components <TypeName>.<InlineComponentName> + for (auto it = owner->semanticScope->childScopesBegin(), + end = owner->semanticScope->childScopesEnd(); + it != end; ++it) { + if ((*it)->inlineComponentName() == name) { + return ExpressionType{ name, *it, QmlComponentIdentifier }; + } + } + return {}; + } + + qCDebug(QQmlLSUtilsLog) << "Could not find identifier expression for" << item.internalKindStr(); + return owner; +} + +static std::optional<ExpressionType> resolveIdentifierExpressionType(const DomItem &item, + ResolveOptions options) +{ + if (isFieldMemberAccess(item)) { + return resolveFieldMemberExpressionType(item, options); + } + + const QString name = item.field(Fields::identifier).value().toString(); + + if (const DomItem definitionOfItem = findJSIdentifierDefinition(item, name)) { + Q_ASSERT_X(!definitionOfItem.semanticScope().isNull() + && definitionOfItem.semanticScope()->ownJSIdentifier(name), + "QQmlLSUtils::findDefinitionOf", + "JS definition does not actually define the JS identifer. " + "It should be empty."); + const auto scope = definitionOfItem.semanticScope(); + const auto jsIdentifier = scope->ownJSIdentifier(name); + return ExpressionType { + name, + jsIdentifier->scope ? QQmlJSScope::ConstPtr(jsIdentifier->scope.toStrongRef()) : scope, + IdentifierType::JavaScriptIdentifier + }; + } + + const auto referrerScope = item.nearestSemanticScope(); + if (!referrerScope) + return {}; + + // check if its a method + if (auto scope = methodFromReferrerScope(referrerScope, name, options)) + return scope; + + const auto resolver = item.containingFile().ownerAs<QmlFile>()->typeResolver(); + if (!resolver) + return {}; + + // check if its found as a property binding + if (const auto scope = propertyBindingFromReferrerScope( + referrerScope, name, options, resolver.get())) { + return *scope; + } + + // check if its an (unqualified) property + if (const auto scope = propertyFromReferrerScope(referrerScope, name, options)) + return *scope; + + // Returns the baseType, can't use it with options. + if (const auto scope = resolver->typeForName(name)) { + if (scope->isSingleton()) + return ExpressionType{ name, scope, IdentifierType::SingletonIdentifier }; + + if (const auto attachedScope = scope->attachedType()) + return ExpressionType{ name, attachedScope, IdentifierType::AttachedTypeIdentifier }; + + // its a (inline) component! + return ExpressionType{ name, scope, QmlComponentIdentifier }; + } + + // check if its an id + if (const QQmlJSScope::ConstPtr fromId + = resolver->typeForId(referrerScope, name, AssumeComponentsAreBound)) { + return ExpressionType{ name, fromId, QmlObjectIdIdentifier }; + } + + const QQmlJSScope::ConstPtr jsGlobal = resolver->jsGlobalObject(); + // check if its a JS global method + if (const auto scope = methodFromReferrerScope(jsGlobal, name, options)) + return scope; + + // check if its an JS global property + if (const auto scope = propertyFromReferrerScope(jsGlobal, name, options)) + return *scope; + + return {}; +} + +static std::optional<ExpressionType> +resolveSignalOrPropertyExpressionType(const QString &name, const QQmlJSScope::ConstPtr &scope, + ResolveOptions options) +{ + const auto signalOrProperty = resolveNameInQmlScope(name, scope); + if (!signalOrProperty) + return {}; + + switch (signalOrProperty->type) { + case PropertyIdentifier: + switch (options) { + case ResolveOwnerType: + return ExpressionType{ name, findDefiningScopeForProperty(scope, name), + signalOrProperty->type }; + case ResolveActualTypeForFieldMemberExpression: + return ExpressionType{ name, scope->property(name).type(), signalOrProperty->type }; + } + Q_UNREACHABLE_RETURN({}); + case PropertyChangedHandlerIdentifier: + switch (options) { + case ResolveOwnerType: + return ExpressionType{ name, + findDefiningScopeForProperty(scope, signalOrProperty->name), + signalOrProperty->type }; + case ResolveActualTypeForFieldMemberExpression: + // Properties and methods are not implemented on methods. + Q_UNREACHABLE_RETURN({}); + } + Q_UNREACHABLE_RETURN({}); + case SignalHandlerIdentifier: + case PropertyChangedSignalIdentifier: + case SignalIdentifier: + case MethodIdentifier: + switch (options) { + case ResolveOwnerType: { + return ExpressionType{ name, findDefiningScopeForMethod(scope, name), + signalOrProperty->type }; + } + case ResolveActualTypeForFieldMemberExpression: + // Properties and methods are not implemented on methods. + Q_UNREACHABLE_RETURN({}); + } + Q_UNREACHABLE_RETURN({}); + default: + Q_UNREACHABLE_RETURN({}); + } +} + +/*! + \internal + Resolves the type of the given DomItem, when possible (e.g., when there are enough type + annotations). +*/ +std::optional<ExpressionType> resolveExpressionType(const QQmlJS::Dom::DomItem &item, + ResolveOptions options) +{ + switch (item.internalKind()) { + case DomType::ScriptIdentifierExpression: { + return resolveIdentifierExpressionType(item, options); + } + case DomType::PropertyDefinition: { + auto propertyDefinition = item.as<PropertyDefinition>(); + if (propertyDefinition && propertyDefinition->semanticScope()) { + const auto &scope = propertyDefinition->semanticScope(); + switch (options) { + case ResolveOwnerType: + return ExpressionType{ propertyDefinition->name, scope, PropertyIdentifier }; + case ResolveActualTypeForFieldMemberExpression: + // There should not be any PropertyDefinition inside a FieldMemberExpression. + Q_UNREACHABLE_RETURN({}); + } + Q_UNREACHABLE_RETURN({}); + } + return {}; + } + case DomType::Binding: { + auto binding = item.as<Binding>(); + if (binding) { + std::optional<QQmlJSScope::ConstPtr> owner = item.qmlObject().semanticScope(); + if (!owner) + return {}; + const QString name = binding->name(); + + if (name == u"id") + return ExpressionType{ name, owner.value(), QmlObjectIdIdentifier }; + + if (const QQmlJSScope::ConstPtr targetScope + = findScopeOfSpecialItems(owner.value(), item)) { + const auto signalOrProperty = resolveNameInQmlScope(name, targetScope); + if (!signalOrProperty) + return {}; + switch (options) { + case ResolveOwnerType: + return ExpressionType{ + name, findDefiningScopeForBinding(targetScope, signalOrProperty->name), + signalOrProperty->type + }; + case ResolveActualTypeForFieldMemberExpression: + // Bindings can't be inside of FieldMemberExpressions. + Q_UNREACHABLE_RETURN({}); + } + } + if (auto result = resolveSignalOrPropertyExpressionType(name, owner.value(), options)) { + return result; + } + qDebug(QQmlLSUtilsLog) << "QQmlLSUtils::resolveExpressionType() could not resolve the" + "type of a Binding."; + } + + return {}; + } + case DomType::QmlObject: { + auto object = item.as<QmlObject>(); + if (!object) + return {}; + if (auto scope = object->semanticScope()) { + const auto name = item.name(); + const bool isComponent = name.front().isUpper(); + if (isComponent) + scope = scope->baseType(); + const IdentifierType type = + isComponent ? QmlComponentIdentifier : GroupedPropertyIdentifier; + switch (options) { + case ResolveOwnerType: + return ExpressionType{ name, scope, type }; + case ResolveActualTypeForFieldMemberExpression: + return ExpressionType{ name, scope, type }; + } + } + return {}; + } + case DomType::QmlComponent: { + auto component = item.as<QmlComponent>(); + if (!component) + return {}; + const auto scope = component->semanticScope(); + if (!scope) + return {}; + + QString name = item.name(); + if (auto dotIndex = name.indexOf(u'.'); dotIndex != -1) + name = name.sliced(dotIndex + 1); + switch (options) { + case ResolveOwnerType: + return ExpressionType{ name, scope, QmlComponentIdentifier }; + case ResolveActualTypeForFieldMemberExpression: + return ExpressionType{ name, scope, QmlComponentIdentifier }; + } + Q_UNREACHABLE_RETURN({}); + } + case DomType::MethodInfo: { + const auto object = item.as<MethodInfo>(); + if (object && object->semanticScope()) { + std::optional<QQmlJSScope::ConstPtr> scope = object->semanticScope(); + if (!scope) + return {}; + + if (const QQmlJSScope::ConstPtr targetScope = + findScopeOfSpecialItems(scope.value()->parentScope(), item)) { + const auto signalOrProperty = resolveNameInQmlScope(object->name, targetScope); + if (!signalOrProperty) + return {}; + + switch (options) { + case ResolveOwnerType: + return ExpressionType{ object->name, + findDefiningScopeForMethod(targetScope, + signalOrProperty->name), + signalOrProperty->type }; + case ResolveActualTypeForFieldMemberExpression: + // not supported for methods + return {}; + } + } + + // in case scope is the semantic scope for the function bodies: grab the owner's scope + // this happens for all methods but not for signals (they do not have a body) + if (scope.value()->scopeType() == QQmlJSScope::ScopeType::JSFunctionScope) + scope = scope.value()->parentScope(); + + if (const auto result = resolveSignalOrPropertyExpressionType( + object->name, scope.value(), options)) { + return result; + } + qDebug(QQmlLSUtilsLog) << "QQmlLSUtils::resolveExpressionType() could not resolve the" + "type of a MethodInfo."; + } + + return {}; + } + case DomType::ScriptBinaryExpression: { + if (isFieldMemberExpression(item)) { + return resolveExpressionType(item.field(Fields::right), options); + } + return {}; + } + case DomType::ScriptLiteral: { + /* special case + Binding { target: someId; property: "someProperty"} + */ + const auto scope = item.qmlObject().semanticScope(); + const auto name = item.field(Fields::value).value().toString(); + if (const QQmlJSScope::ConstPtr targetScope = findScopeOfSpecialItems(scope, item)) { + const auto signalOrProperty = resolveNameInQmlScope(name, targetScope); + if (!signalOrProperty) + return {}; + switch (options) { + case ResolveOwnerType: + return ExpressionType{ + name, findDefiningScopeForProperty(targetScope, signalOrProperty->name), + signalOrProperty->type + }; + case ResolveActualTypeForFieldMemberExpression: + // ScriptLiteral's can't be inside of FieldMemberExpression's, especially when they + // are inside a special item. + Q_UNREACHABLE_RETURN({}); + } + } + return {}; + } + case DomType::EnumItem: { + const QString enumValue = item.field(Fields::name).value().toString(); + const QQmlJSScope::ConstPtr referrerScope + = item.rootQmlObject(GoTo::MostLikely).semanticScope(); + if (!referrerScope->hasEnumerationKey(enumValue)) + return {}; + switch (options) { + // special case: use the owner's scope here, as enums do not have their own + // QQmlJSScope. + case ResolveActualTypeForFieldMemberExpression: + case ResolveOwnerType: + return ExpressionType { + enumValue, + findDefiningScopeForEnumerationKey(referrerScope, enumValue), + EnumeratorValueIdentifier + }; + } + Q_UNREACHABLE_RETURN({}); + } + case DomType::EnumDecl: { + const QString enumName = item.field(Fields::name).value().toString(); + const QQmlJSScope::ConstPtr referrerScope + = item.rootQmlObject(GoTo::MostLikely).semanticScope(); + if (!referrerScope->hasEnumeration(enumName)) + return {}; + switch (options) { + // special case: use the owner's scope here, as enums do not have their own QQmlJSScope. + case ResolveActualTypeForFieldMemberExpression: + case ResolveOwnerType: + return ExpressionType { + enumName, + findDefiningScopeForEnumeration(referrerScope, enumName), + EnumeratorIdentifier + }; + } + + Q_UNREACHABLE_RETURN({}); + } + default: { + qCDebug(QQmlLSUtilsLog) << "Type" << item.internalKindStr() + << "is unimplemented in QQmlLSUtils::resolveExpressionType"; + return {}; + } + } + Q_UNREACHABLE(); +} + +DomItem sourceLocationToDomItem(const DomItem &file, const QQmlJS::SourceLocation &location) +{ + // QQmlJS::SourceLocation starts counting at 1 but the utils and the LSP start at 0. + auto items = QQmlLSUtils::itemsFromTextLocation(file, location.startLine - 1, + location.startColumn - 1); + switch (items.size()) { + case 0: + return {}; + case 1: + return items.front().domItem; + case 2: { + // special case: because location points to the beginning of the type definition, + // itemsFromTextLocation might also return the type on its left, in case it is directly + // adjacent to it. In this case always take the right (=with the higher column-number) + // item. + auto &first = items.front(); + auto &second = items.back(); + Q_ASSERT_X(first.fileLocation->info().fullRegion.startLine + == second.fileLocation->info().fullRegion.startLine, + "QQmlLSUtils::findTypeDefinitionOf(DomItem)", + "QQmlLSUtils::itemsFromTextLocation returned non-adjacent items."); + if (first.fileLocation->info().fullRegion.startColumn + > second.fileLocation->info().fullRegion.startColumn) + return first.domItem; + else + return second.domItem; + break; + } + default: + qDebug() << "Found multiple candidates for type of scriptidentifierexpression"; + break; + } + return {}; +} + +static std::optional<Location> +findMethodDefinitionOf(const DomItem &file, QQmlJS::SourceLocation location, const QString &name) +{ + DomItem owner = QQmlLSUtils::sourceLocationToDomItem(file, location).qmlObject(); + DomItem method = owner.field(Fields::methods).key(name).index(0); + auto fileLocation = FileLocations::treeOf(method); + if (!fileLocation) + return {}; + + auto regions = fileLocation->info().regions; + + if (auto it = regions.constFind(IdentifierRegion); it != regions.constEnd()) { + Location result; + result.sourceLocation = *it; + result.filename = method.canonicalFilePath(); + return result; + } + + return {}; +} + +static std::optional<Location> +findPropertyDefinitionOf(const DomItem &file, QQmlJS::SourceLocation propertyDefinitionLocation, + const QString &name) +{ + DomItem propertyOwner = + QQmlLSUtils::sourceLocationToDomItem(file, propertyDefinitionLocation).qmlObject(); + DomItem propertyDefinition = propertyOwner.field(Fields::propertyDefs).key(name).index(0); + auto fileLocation = FileLocations::treeOf(propertyDefinition); + if (!fileLocation) + return {}; + + auto regions = fileLocation->info().regions; + + if (auto it = regions.constFind(IdentifierRegion); it != regions.constEnd()) { + Location result; + result.sourceLocation = *it; + result.filename = propertyDefinition.canonicalFilePath(); + return result; + } + + return {}; +} + +std::optional<Location> findDefinitionOf(const DomItem &item) +{ + auto resolvedExpression = resolveExpressionType(item, ResolveOptions::ResolveOwnerType); + + if (!resolvedExpression || !resolvedExpression->name || !resolvedExpression->semanticScope) { + qCDebug(QQmlLSUtilsLog) << "QQmlLSUtils::findDefinitionOf: Type could not be resolved."; + return {}; + } + + switch (resolvedExpression->type) { + case JavaScriptIdentifier: { + const QQmlJS::SourceLocation location = + resolvedExpression->semanticScope->ownJSIdentifier(*resolvedExpression->name) + .value() + .location; + + return Location{ resolvedExpression->semanticScope->filePath(), location }; + } + + case PropertyIdentifier: { + const DomItem ownerFile = item.goToFile(resolvedExpression->semanticScope->filePath()); + const QQmlJS::SourceLocation ownerLocation = + resolvedExpression->semanticScope->sourceLocation(); + return findPropertyDefinitionOf(ownerFile, ownerLocation, *resolvedExpression->name); + } + case PropertyChangedSignalIdentifier: + case PropertyChangedHandlerIdentifier: + case SignalIdentifier: + case SignalHandlerIdentifier: + case MethodIdentifier: { + const DomItem ownerFile = item.goToFile(resolvedExpression->semanticScope->filePath()); + const QQmlJS::SourceLocation ownerLocation = + resolvedExpression->semanticScope->sourceLocation(); + return findMethodDefinitionOf(ownerFile, ownerLocation, *resolvedExpression->name); + } + case QmlObjectIdIdentifier: { + DomItem qmlObject = QQmlLSUtils::sourceLocationToDomItem( + item.containingFile(), resolvedExpression->semanticScope->sourceLocation()); + // in the Dom, the id is saved in a QMultiHash inside the Component of an QmlObject. + const DomItem domId = qmlObject.component() + .field(Fields::ids) + .key(*resolvedExpression->name) + .index(0) + .field(Fields::value); + if (!domId) { + qCDebug(QQmlLSUtilsLog) + << "QmlComponent in Dom structure has no id, was it misconstructed?"; + return {}; + } + + Location result; + result.sourceLocation = FileLocations::treeOf(domId)->info().fullRegion; + result.filename = domId.canonicalFilePath(); + return result; + } + case QmlComponentIdentifier: { + Location result; + result.sourceLocation = resolvedExpression->semanticScope->sourceLocation(); + result.filename = resolvedExpression->semanticScope->filePath(); + return result; + } + case SingletonIdentifier: + case EnumeratorIdentifier: + case EnumeratorValueIdentifier: + case AttachedTypeIdentifier: + case GroupedPropertyIdentifier: + qCDebug(QQmlLSUtilsLog) << "QQmlLSUtils::findDefinitionOf was not implemented for type" + << resolvedExpression->type; + return {}; + } + Q_UNREACHABLE_RETURN({}); +} + +static QQmlJSScope::ConstPtr propertyOwnerFrom(const QQmlJSScope::ConstPtr &type, + const QString &name) +{ + Q_ASSERT(!name.isEmpty()); + Q_ASSERT(type); + + QQmlJSScope::ConstPtr typeWithDefinition = type; + while (typeWithDefinition && !typeWithDefinition->hasOwnProperty(name)) + typeWithDefinition = typeWithDefinition->baseType(); + + if (!typeWithDefinition) { + qCDebug(QQmlLSUtilsLog) + << "QQmlLSUtils::checkNameForRename cannot find property definition," + " ignoring."; + } + + return typeWithDefinition; +} + +static QQmlJSScope::ConstPtr methodOwnerFrom(const QQmlJSScope::ConstPtr &type, + const QString &name) +{ + Q_ASSERT(!name.isEmpty()); + Q_ASSERT(type); + + QQmlJSScope::ConstPtr typeWithDefinition = type; + while (typeWithDefinition && !typeWithDefinition->hasOwnMethod(name)) + typeWithDefinition = typeWithDefinition->baseType(); + + if (!typeWithDefinition) { + qCDebug(QQmlLSUtilsLog) + << "QQmlLSUtils::checkNameForRename cannot find method definition," + " ignoring."; + } + + return typeWithDefinition; +} + +static QQmlJSScope::ConstPtr expressionTypeWithDefinition(const ExpressionType &ownerType) +{ + switch (ownerType.type) { + case PropertyIdentifier: + return propertyOwnerFrom(ownerType.semanticScope, *ownerType.name); + case PropertyChangedHandlerIdentifier: { + const auto propertyName = + QQmlSignalNames::changedHandlerNameToPropertyName(*ownerType.name); + return propertyOwnerFrom(ownerType.semanticScope, *propertyName); + break; + } + case PropertyChangedSignalIdentifier: { + const auto propertyName = QQmlSignalNames::changedSignalNameToPropertyName(*ownerType.name); + return propertyOwnerFrom(ownerType.semanticScope, *propertyName); + } + case MethodIdentifier: + case SignalIdentifier: + return methodOwnerFrom(ownerType.semanticScope, *ownerType.name); + case SignalHandlerIdentifier: { + const auto signalName = QQmlSignalNames::handlerNameToSignalName(*ownerType.name); + return methodOwnerFrom(ownerType.semanticScope, *signalName); + } + case JavaScriptIdentifier: + case QmlObjectIdIdentifier: + case SingletonIdentifier: + case EnumeratorIdentifier: + case EnumeratorValueIdentifier: + case AttachedTypeIdentifier: + case GroupedPropertyIdentifier: + case QmlComponentIdentifier: + return ownerType.semanticScope; + } + return {}; +} + +std::optional<ErrorMessage> checkNameForRename(const DomItem &item, const QString &dirtyNewName, + const std::optional<ExpressionType> &ownerType) +{ + if (!ownerType) { + if (const auto resolved = resolveExpressionType(item, ResolveOwnerType)) + return checkNameForRename(item, dirtyNewName, resolved); + } + + // general checks for ECMAscript identifiers + if (!isValidEcmaScriptIdentifier(dirtyNewName)) + return ErrorMessage{ 0, u"Invalid EcmaScript identifier!"_s }; + + const auto userSemanticScope = item.nearestSemanticScope(); + + if (!ownerType || !userSemanticScope) { + return ErrorMessage{ 0, u"Requested item cannot be renamed"_s }; + } + + // type specific checks + switch (ownerType->type) { + case PropertyChangedSignalIdentifier: { + if (!QQmlSignalNames::isChangedSignalName(dirtyNewName)) { + return ErrorMessage{ 0, u"Invalid name for a property changed signal."_s }; + } + break; + } + case PropertyChangedHandlerIdentifier: { + if (!QQmlSignalNames::isChangedHandlerName(dirtyNewName)) { + return ErrorMessage{ 0, u"Invalid name for a property changed handler identifier."_s }; + } + break; + } + case SignalHandlerIdentifier: { + if (!QQmlSignalNames::isHandlerName(dirtyNewName)) { + return ErrorMessage{ 0, u"Invalid name for a signal handler identifier."_s }; + } + break; + } + // TODO: any other specificities? + case QmlObjectIdIdentifier: + if (dirtyNewName.front().isLetter() && !dirtyNewName.front().isLower()) { + return ErrorMessage{ 0, u"Object id names cannot start with an upper case letter."_s }; + } + break; + case JavaScriptIdentifier: + case PropertyIdentifier: + case SignalIdentifier: + case MethodIdentifier: + default: + break; + }; + + auto typeWithDefinition = expressionTypeWithDefinition(*ownerType); + + if (!typeWithDefinition) { + return ErrorMessage{ + 0, + u"Renaming has not been implemented for the requested item."_s, + }; + } + + // is it not defined in QML? + if (!typeWithDefinition->isComposite()) { + return ErrorMessage{ 0, u"Cannot rename items defined in non-QML files."_s }; + } + + // is it defined in the current module? + const QString moduleOfDefinition = ownerType->semanticScope->moduleName(); + const QString moduleOfCurrentItem = userSemanticScope->moduleName(); + if (moduleOfDefinition != moduleOfCurrentItem) { + return ErrorMessage{ + 0, + u"Cannot rename items defined in the \"%1\" module from a usage in the \"%2\" module."_s + .arg(moduleOfDefinition, moduleOfCurrentItem), + }; + } + + return {}; +} + +static std::optional<QString> oldNameFrom(const DomItem &item) +{ + switch (item.internalKind()) { + case DomType::ScriptIdentifierExpression: { + return item.field(Fields::identifier).value().toString(); + } + case DomType::ScriptVariableDeclarationEntry: { + return item.field(Fields::identifier).value().toString(); + } + case DomType::PropertyDefinition: + case DomType::Binding: + case DomType::MethodInfo: { + return item.field(Fields::name).value().toString(); + } + default: + qCDebug(QQmlLSUtilsLog) << item.internalKindStr() + << "was not implemented for QQmlLSUtils::renameUsagesOf"; + return std::nullopt; + } + Q_UNREACHABLE_RETURN(std::nullopt); +} + +static std::optional<QString> newNameFrom(const QString &dirtyNewName, IdentifierType alternative) +{ + // When renaming signal/property changed handlers and property changed signals: + // Get the actual corresponding signal name (for signal handlers) or property name (for + // property changed signal + handlers) that will be used for the renaming. + switch (alternative) { + case SignalHandlerIdentifier: { + return QQmlSignalNames::handlerNameToSignalName(dirtyNewName); + } + case PropertyChangedHandlerIdentifier: { + return QQmlSignalNames::changedHandlerNameToPropertyName(dirtyNewName); + } + case PropertyChangedSignalIdentifier: { + return QQmlSignalNames::changedSignalNameToPropertyName(dirtyNewName); + } + case SignalIdentifier: + case PropertyIdentifier: + default: + return std::nullopt; + } + Q_UNREACHABLE_RETURN(std::nullopt); +} + +/*! +\internal +\brief Rename the appearance of item to newName. + +Special cases: +\list + \li Renaming a property changed signal or property changed handler does the same as renaming + the underlying property, except that newName gets + \list + \li its "on"-prefix and "Changed"-suffix chopped of if item is a property changed handlers + \li its "Changed"-suffix chopped of if item is a property changed signals + \endlist + \li Renaming a signal handler does the same as renaming a signal, but the "on"-prefix in newName + is chopped of. + + All of the chopping operations are done using the static helpers from QQmlSignalNames. +\endlist +*/ +RenameUsages renameUsagesOf(const DomItem &item, const QString &dirtyNewName, + const std::optional<ExpressionType> &targetType) +{ + RenameUsages result; + const Usages locations = findUsagesOf(item); + if (locations.isEmpty()) + return result; + + auto oldName = oldNameFrom(item); + if (!oldName) + return result; + + QQmlJSScope::ConstPtr semanticScope; + if (targetType) { + semanticScope = targetType->semanticScope; + } else if (const auto resolved = + QQmlLSUtils::resolveExpressionType(item, ResolveOptions::ResolveOwnerType)) { + semanticScope = resolved->semanticScope; + } else { + return result; + } + + QString newName; + if (const auto resolved = resolveNameInQmlScope(*oldName, semanticScope)) { + newName = newNameFrom(dirtyNewName, resolved->type).value_or(dirtyNewName); + oldName = resolved->name; + } else { + newName = dirtyNewName; + } + + const qsizetype oldNameLength = oldName->length(); + const qsizetype oldHandlerNameLength = + QQmlSignalNames::signalNameToHandlerName(*oldName).length(); + const qsizetype oldChangedSignalNameLength = + QQmlSignalNames::propertyNameToChangedSignalName(*oldName).length(); + const qsizetype oldChangedHandlerNameLength = + QQmlSignalNames::propertyNameToChangedHandlerName(*oldName).length(); + + const QString newHandlerName = QQmlSignalNames::signalNameToHandlerName(newName); + const QString newChangedSignalName = QQmlSignalNames::propertyNameToChangedSignalName(newName); + const QString newChangedHandlerName = + QQmlSignalNames::propertyNameToChangedHandlerName(newName); + + // set the new name at the found usages, but add "on"-prefix and "Changed"-suffix if needed + for (const auto &location : locations.usagesInFile()) { + const qsizetype currentLength = location.sourceLocation.length; + Edit edit; + edit.location = location; + if (oldNameLength == currentLength) { + // normal case, nothing to do + edit.replacement = newName; + + } else if (oldHandlerNameLength == currentLength) { + // signal handler location + edit.replacement = newHandlerName; + + } else if (oldChangedSignalNameLength == currentLength) { + // property changed signal location + edit.replacement = newChangedSignalName; + + } else if (oldChangedHandlerNameLength == currentLength) { + // property changed handler location + edit.replacement = newChangedHandlerName; + + } else { + qCDebug(QQmlLSUtilsLog) << "Found usage with wrong identifier length, ignoring..."; + continue; + } + result.appendRename(edit); + } + + for (const auto &filename : locations.usagesInFilename()) { + // assumption: we only rename files ending in .qml or .ui.qml in qmlls + QString extension; + if (filename.endsWith(u".ui.qml"_s)) + extension = u".ui.qml"_s; + else if (filename.endsWith(u".qml"_s)) + extension = u".qml"_s; + else + continue; + + QFileInfo info(filename); + // do not rename the file if it has a custom type name in the qmldir + if (!info.isFile() || info.baseName() != oldName) + continue; + + const QString newFilename = + QDir::cleanPath(filename + "/.."_L1) + '/'_L1 + newName + extension; + result.appendRename({ filename, newFilename }); + } + + return result; +} + +Location Location::from(const QString &fileName, const QString &code, quint32 startLine, + quint32 startCharacter, quint32 length) +{ + quint32 offset = QQmlLSUtils::textOffsetFrom(code, startLine - 1, startCharacter - 1); + + Location location{ fileName, + QQmlJS::SourceLocation{ offset, length, startLine, startCharacter } }; + return location; +} + +Edit Edit::from(const QString &fileName, const QString &code, quint32 startLine, + quint32 startCharacter, quint32 length, const QString &newName) +{ + Edit rename; + rename.location = Location::from(fileName, code, startLine, startCharacter, length); + rename.replacement = newName; + return rename; +} + +bool isValidEcmaScriptIdentifier(QStringView identifier) +{ + QQmlJS::Lexer lexer(nullptr); + lexer.setCode(identifier.toString(), 0); + const int token = lexer.lex(); + if (token != static_cast<int>(QQmlJS::Lexer::T_IDENTIFIER)) + return false; + // make sure there is nothing following the lexed identifier + const int eofToken = lexer.lex(); + return eofToken == static_cast<int>(QQmlJS::Lexer::EOF_SYMBOL); +} + + +/*! +\internal +Returns the name of the cmake program along with the arguments needed to build the +qmltyperegistration. This command generates the .qmltypes, qmldir and .qrc files required for qmlls +to provide correct information on C++ defined QML elements. + +We assume here that CMake is available in the path. This should be the case for linux and macOS by +default. +For windows, having CMake in the path is not too unrealistic, for example, +https://doc.qt.io/qt-6/windows-building.html#step-2-install-build-requirements claims that you need +to have CMake in your path to build Qt. So a developer machine running qmlls has a high chance of +having CMake in their path, if CMake is installed and used. +*/ +QPair<QString, QStringList> cmakeBuildCommand(const QString &path) +{ + const QPair<QString, QStringList> result{ + u"cmake"_s, { u"--build"_s, path, u"-t"_s, u"all_qmltyperegistrations"_s } + }; + return result; +} + +void Usages::sort() +{ + std::sort(m_usagesInFile.begin(), m_usagesInFile.end()); + std::sort(m_usagesInFilename.begin(), m_usagesInFilename.end()); +} + +bool Usages::isEmpty() const +{ + return m_usagesInFilename.isEmpty() && m_usagesInFile.isEmpty(); +} + +Usages::Usages(const QList<Location> &usageInFile, const QList<QString> &usageInFilename) + : m_usagesInFile(usageInFile), m_usagesInFilename(usageInFilename) +{ + std::sort(m_usagesInFile.begin(), m_usagesInFile.end()); + std::sort(m_usagesInFilename.begin(), m_usagesInFilename.end()); +} + +RenameUsages::RenameUsages(const QList<Edit> &renamesInFile, + const QList<FileRename> &renamesInFilename) + : m_renamesInFile(renamesInFile), m_renamesInFilename(renamesInFilename) +{ + std::sort(m_renamesInFile.begin(), m_renamesInFile.end()); + std::sort(m_renamesInFilename.begin(), m_renamesInFilename.end()); +} + +} // namespace QQmlLSUtils + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmllsutils_p.h b/src/qmlls/qqmllsutils_p.h new file mode 100644 index 0000000000..84cf31c368 --- /dev/null +++ b/src/qmlls/qqmllsutils_p.h @@ -0,0 +1,274 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QLANGUAGESERVERUTILS_P_H +#define QLANGUAGESERVERUTILS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> +#include <QtQmlDom/private/qqmldomexternalitems_p.h> +#include <QtQmlDom/private/qqmldomtop_p.h> +#include <algorithm> +#include <optional> +#include <tuple> +#include <variant> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(QQmlLSUtilsLog); + +namespace QQmlLSUtils { + +struct ItemLocation +{ + QQmlJS::Dom::DomItem domItem; + QQmlJS::Dom::FileLocations::Tree fileLocation; +}; + +struct TextPosition +{ + int line; + int character; +}; + +enum IdentifierType : char { + JavaScriptIdentifier, + PropertyIdentifier, + PropertyChangedSignalIdentifier, + PropertyChangedHandlerIdentifier, + SignalIdentifier, + SignalHandlerIdentifier, + MethodIdentifier, + QmlObjectIdIdentifier, + SingletonIdentifier, + EnumeratorIdentifier, + EnumeratorValueIdentifier, + AttachedTypeIdentifier, + GroupedPropertyIdentifier, + QmlComponentIdentifier, +}; + +struct ErrorMessage +{ + int code; + QString message; +}; + +struct ExpressionType +{ + std::optional<QString> name; + QQmlJSScope::ConstPtr semanticScope; + IdentifierType type; +}; + +struct Location +{ + QString filename; + QQmlJS::SourceLocation sourceLocation; + + static Location from(const QString &fileName, const QString &code, quint32 startLine, + quint32 startCharacter, quint32 length); + + friend bool operator<(const Location &a, const Location &b) + { + return std::make_tuple(a.filename, a.sourceLocation.begin(), a.sourceLocation.end()) + < std::make_tuple(b.filename, b.sourceLocation.begin(), b.sourceLocation.end()); + } + friend bool operator==(const Location &a, const Location &b) + { + return std::make_tuple(a.filename, a.sourceLocation.begin(), a.sourceLocation.end()) + == std::make_tuple(b.filename, b.sourceLocation.begin(), b.sourceLocation.end()); + } +}; + +/*! +Represents a rename operation where the file itself needs to be renamed. +\internal +*/ +struct FileRename +{ + QString oldFilename; + QString newFilename; + + friend bool comparesEqual(const FileRename &a, const FileRename &b) noexcept + { + return std::tie(a.oldFilename, a.newFilename) == std::tie(b.oldFilename, b.newFilename); + } + friend Qt::strong_ordering compareThreeWay(const FileRename &a, const FileRename &b) noexcept + { + if (a.oldFilename != b.oldFilename) + return compareThreeWay(a.oldFilename, b.oldFilename); + return compareThreeWay(a.newFilename, b.newFilename); + } + Q_DECLARE_STRONGLY_ORDERED(FileRename); +}; + +struct Edit +{ + Location location; + QString replacement; + + static Edit from(const QString &fileName, const QString &code, quint32 startLine, + quint32 startCharacter, quint32 length, const QString &newName); + + friend bool operator<(const Edit &a, const Edit &b) + { + return std::make_tuple(a.location, a.replacement) + < std::make_tuple(b.location, b.replacement); + } + friend bool operator==(const Edit &a, const Edit &b) + { + return std::make_tuple(a.location, a.replacement) + == std::make_tuple(b.location, b.replacement); + } +}; + +/*! +Represents the locations where some highlighting should take place, like in the "find all +references" feature of the LSP. Those locations are pointing to parts of a Qml file or to a Qml +file name. + +The file names are not reported as usage to the LSP and are currently only needed for the renaming +operation to be able to rename files. + +\internal +*/ +class Usages +{ +public: + void sort(); + bool isEmpty() const; + + friend bool comparesEqual(const Usages &a, const Usages &b) + { + return a.m_usagesInFile == b.m_usagesInFile && a.m_usagesInFilename == b.m_usagesInFilename; + } + Q_DECLARE_EQUALITY_COMPARABLE(Usages) + + Usages() = default; + Usages(const QList<Location> &usageInFile, const QList<QString> &usageInFilename); + + QList<Location> usagesInFile() const { return m_usagesInFile; }; + QList<QString> usagesInFilename() const { return m_usagesInFilename; }; + + void appendUsage(const Location &edit) + { + if (!m_usagesInFile.contains(edit)) + m_usagesInFile.append(edit); + }; + void appendFilenameUsage(const QString &edit) + { + + if (!m_usagesInFilename.contains(edit)) + m_usagesInFilename.append(edit); + }; + +private: + QList<Location> m_usagesInFile; + QList<QString> m_usagesInFilename; +}; + +/*! +Represents the locations where a renaming should take place. Parts of text inside a file can be +renamed and also filename themselves can be renamed. + +\internal +*/ +class RenameUsages +{ +public: + friend bool comparesEqual(const RenameUsages &a, const RenameUsages &b) + { + return std::tie(a.m_renamesInFile, a.m_renamesInFilename) + == std::tie(b.m_renamesInFile, b.m_renamesInFilename); + } + Q_DECLARE_EQUALITY_COMPARABLE(RenameUsages) + + RenameUsages() = default; + RenameUsages(const QList<Edit> &renamesInFile, const QList<FileRename> &renamesInFilename); + + QList<Edit> renameInFile() const { return m_renamesInFile; }; + QList<FileRename> renameInFilename() const { return m_renamesInFilename; }; + + void appendRename(const Edit &edit) { m_renamesInFile.append(edit); }; + void appendRename(const FileRename &edit) { m_renamesInFilename.append(edit); }; + +private: + QList<Edit> m_renamesInFile; + QList<FileRename> m_renamesInFilename; +}; + +/*! + \internal + Choose whether to resolve the owner type or the entire type (the latter is only required to + resolve the types of qualified names and property accesses). + + For properties, methods, enums and co: + * ResolveOwnerType returns the base type of the owner that owns the property, method, enum + and co. For example, resolving "x" in "myRectangle.x" will return the Item as the owner, as + Item is the base type of Rectangle that defines the "x" property. + * ResolveActualTypeForFieldMemberExpression is used to resolve field member expressions, and + might lose some information about the owner. For example, resolving "x" in "myRectangle.x" + will return the JS type for float that was used to define the "x" property. + */ +enum ResolveOptions { + ResolveOwnerType, + ResolveActualTypeForFieldMemberExpression, +}; + +using DomItem = QQmlJS::Dom::DomItem; + +qsizetype textOffsetFrom(const QString &code, int row, int character); +TextPosition textRowAndColumnFrom(const QString &code, qsizetype offset); +QList<ItemLocation> itemsFromTextLocation(const DomItem &file, int line, int character); +DomItem sourceLocationToDomItem(const DomItem &file, const QQmlJS::SourceLocation &location); +QByteArray lspUriToQmlUrl(const QByteArray &uri); +QByteArray qmlUrlToLspUri(const QByteArray &url); +QLspSpecification::Range qmlLocationToLspLocation(const QString &code, + QQmlJS::SourceLocation qmlLocation); +DomItem baseObject(const DomItem &qmlObject); +std::optional<Location> findTypeDefinitionOf(const DomItem &item); +std::optional<Location> findDefinitionOf(const DomItem &item); +Usages findUsagesOf(const DomItem &item); + +std::optional<ErrorMessage> +checkNameForRename(const DomItem &item, const QString &newName, + const std::optional<ExpressionType> &targetType = std::nullopt); +RenameUsages renameUsagesOf(const DomItem &item, const QString &newName, + const std::optional<ExpressionType> &targetType = std::nullopt); +std::optional<ExpressionType> resolveExpressionType(const DomItem &item, ResolveOptions); +bool isValidEcmaScriptIdentifier(QStringView view); + +QPair<QString, QStringList> cmakeBuildCommand(const QString &path); + +bool isFieldMemberExpression(const DomItem &item); +bool isFieldMemberAccess(const DomItem &item); +QStringList fieldMemberExpressionBits(const DomItem &item, const DomItem &stopAtChild = {}); + +QString qualifiersFrom(const DomItem &el); + +QQmlJSScope::ConstPtr findDefiningScopeForProperty(const QQmlJSScope::ConstPtr &referrerScope, + const QString &nameToCheck); +QQmlJSScope::ConstPtr findDefiningScopeForBinding(const QQmlJSScope::ConstPtr &referrerScope, + const QString &nameToCheck); +QQmlJSScope::ConstPtr findDefiningScopeForMethod(const QQmlJSScope::ConstPtr &referrerScope, + const QString &nameToCheck); +QQmlJSScope::ConstPtr findDefiningScopeForEnumeration(const QQmlJSScope::ConstPtr &referrerScope, + const QString &nameToCheck); +QQmlJSScope::ConstPtr findDefiningScopeForEnumerationKey(const QQmlJSScope::ConstPtr &referrerScope, + const QString &nameToCheck); +} // namespace QQmlLSUtils + +QT_END_NAMESPACE + +#endif // QLANGUAGESERVERUTILS_P_H diff --git a/src/qmlls/qqmlrangeformatting.cpp b/src/qmlls/qqmlrangeformatting.cpp new file mode 100644 index 0000000000..fab8260d6b --- /dev/null +++ b/src/qmlls/qqmlrangeformatting.cpp @@ -0,0 +1,142 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <qqmlrangeformatting_p.h> +#include <qqmlcodemodel_p.h> +#include <qqmllsutils_p.h> + +#include <QtQmlDom/private/qqmldomitem_p.h> +#include <QtQmlDom/private/qqmldomindentinglinewriter_p.h> +#include <QtQmlDom/private/qqmldomcodeformatter_p.h> +#include <QtQmlDom/private/qqmldomoutwriter_p.h> +#include <QtQmlDom/private/qqmldommock_p.h> +#include <QtQmlDom/private/qqmldomcompare_p.h> + +QT_BEGIN_NAMESPACE + +QQmlRangeFormatting::QQmlRangeFormatting(QmlLsp::QQmlCodeModel *codeModel) + : QQmlBaseModule(codeModel) +{ +} + +QString QQmlRangeFormatting::name() const +{ + return u"QQmlRangeFormatting"_s; +} + +void QQmlRangeFormatting::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) +{ + protocol->registerDocumentRangeFormattingRequestHandler(getRequestHandler()); +} + +void QQmlRangeFormatting::setupCapabilities(const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverCapabilities) +{ + serverCapabilities.capabilities.documentRangeFormattingProvider = true; +} + +void QQmlRangeFormatting::process(RequestPointerArgument request) +{ + using namespace QQmlJS::Dom; + QList<QLspSpecification::TextEdit> result{}; + + QmlLsp::OpenDocument doc = m_codeModel->openDocumentByUrl( + QQmlLSUtils::lspUriToQmlUrl(request->m_parameters.textDocument.uri)); + + DomItem file = doc.snapshot.doc.fileObject(GoTo::MostLikely); + if (!file) { + qWarning() << u"Could not find the file"_s << doc.snapshot.doc.toString(); + return; + } + + if (auto envPtr = file.environment().ownerAs<DomEnvironment>()) + envPtr->clearReferenceCache(); + + auto qmlFile = file.ownerAs<QmlFile>(); + auto code = qmlFile->code(); + + // Range requested to be formatted + const auto selectedRange = request->m_parameters.range; + const auto selectedRangeStartLine = selectedRange.start.line; + const auto selectedRangeEndLine = selectedRange.end.line; + Q_ASSERT(selectedRangeStartLine >= 0); + Q_ASSERT(selectedRangeEndLine >= 0); + + LineWriterOptions options; + options.updateOptions = LineWriterOptions::Update::None; + options.attributesSequence = LineWriterOptions::AttributesSequence::Preserve; + + QTextStream in(&code); + FormatTextStatus status = FormatTextStatus::initialStatus(); + FormatPartialStatus partialStatus({}, options.formatOptions, status); + + // Get the token status of the previous line without performing write operation + int lineNumber = 0; + while (!in.atEnd()) { + const auto line = in.readLine(); + partialStatus = formatCodeLine(line, options.formatOptions, partialStatus.currentStatus); + if (++lineNumber >= selectedRangeStartLine) + break; + } + + QString resultText; + QTextStream out(&resultText); + IndentingLineWriter lw([&out](QStringView writtenText) { out << writtenText.toUtf8(); }, + QString(), options, partialStatus.currentStatus); + OutWriter ow(lw); + ow.indentNextlines = true; + + // TODO: This is a workaround and will/should be handled by the actual formatter + // once we improve the range-formatter design in QTBUG-116139 + const auto removeSpaces = [](const QString &line) { + QString result; + QTextStream out(&result); + bool previousIsSpace = false; + + int newLineCount = 0; + for (int i = 0; i < line.length(); ++i) { + QChar c = line.at(i); + if (c.isSpace()) { + if (c == '\n'_L1 && newLineCount < 2) { + out << '\n'_L1; + ++newLineCount; + } else if (c == '\r'_L1 && (i + 1) < line.length() && line.at(i + 1) == '\n'_L1 + && newLineCount < 2) { + out << "\r\n"; + ++newLineCount; + ++i; + } else { + if (!previousIsSpace) + out << ' '_L1; + } + previousIsSpace = true; + } else { + out << c; + previousIsSpace = false; + newLineCount = 0; + } + } + + out.flush(); + return result; + }; + + const auto startOffset = QQmlLSUtils::textOffsetFrom(code, selectedRangeStartLine, 0); + const auto endOffset = QQmlLSUtils::textOffsetFrom(code, selectedRangeEndLine + 1, 0); + const auto &toFormat = code.mid(startOffset, endOffset - startOffset); + ow.write(removeSpaces(toFormat)); + ow.flush(); + ow.eof(); + + const auto documentLineCount = QQmlLSUtils::textRowAndColumnFrom(code, code.length()).line; + code.replace(startOffset, toFormat.length(), resultText); + + QLspSpecification::TextEdit add; + add.newText = code.toUtf8(); + add.range = { { 0, 0 }, { documentLineCount + 1 } }; + result.append(add); + + request->m_response.sendResponse(result); +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmlrangeformatting_p.h b/src/qmlls/qqmlrangeformatting_p.h new file mode 100644 index 0000000000..5c51ea4f12 --- /dev/null +++ b/src/qmlls/qqmlrangeformatting_p.h @@ -0,0 +1,44 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLRANGEFORMATTING_P_H +#define QQMLRANGEFORMATTING_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlbasemodule_p.h" +#include "qqmlcodemodel_p.h" + +QT_BEGIN_NAMESPACE + +struct RangeFormattingRequest + : public BaseRequest<QLspSpecification::DocumentRangeFormattingParams, + QLspSpecification::Responses::DocumentRangeFormattingResponseType> +{ +}; + +class QQmlRangeFormatting : public QQmlBaseModule<RangeFormattingRequest> +{ + Q_OBJECT +public: + QQmlRangeFormatting(QmlLsp::QQmlCodeModel *codeModel); + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; + void process(RequestPointerArgument req) override; +}; + +QT_END_NAMESPACE + +#endif // QQMLRANGEFORMATTING_P_H diff --git a/src/qmlls/qqmlrenamesymbolsupport.cpp b/src/qmlls/qqmlrenamesymbolsupport.cpp new file mode 100644 index 0000000000..a1e16ad87b --- /dev/null +++ b/src/qmlls/qqmlrenamesymbolsupport.cpp @@ -0,0 +1,119 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qqmllsutils_p.h" +#include "qqmlrenamesymbolsupport_p.h" +#include <utility> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; +QQmlRenameSymbolSupport::QQmlRenameSymbolSupport(QmlLsp::QQmlCodeModel *model) : BaseT(model) { } + +QString QQmlRenameSymbolSupport::name() const +{ + return u"QmlRenameSymbolSupport"_s; +} + +void QQmlRenameSymbolSupport::setupCapabilities( + const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverCapabilities) +{ + // use a bool for now. Alternatively, if the client supports "prepareSupport", one could + // use a RenameOptions here. See following page for more information about prepareSupport: + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_prepareRename + serverCapabilities.capabilities.renameProvider = true; +} + +void QQmlRenameSymbolSupport::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol) +{ + protocol->registerRenameRequestHandler(getRequestHandler()); +} + +void QQmlRenameSymbolSupport::process(QQmlRenameSymbolSupport::RequestPointerArgument request) +{ + QLspSpecification::WorkspaceEdit result; + ResponseScopeGuard guard(result, request->m_response); + + auto itemsFound = itemsForRequest(request); + if (guard.setErrorFrom(itemsFound)) + return; + + QQmlLSUtils::ItemLocation &front = + std::get<QList<QQmlLSUtils::ItemLocation>>(itemsFound).front(); + + const QString newName = QString::fromUtf8(request->m_parameters.newName); + auto expressionType = + QQmlLSUtils::resolveExpressionType(front.domItem, QQmlLSUtils::ResolveOwnerType); + + if (!expressionType) { + guard.setError(QQmlLSUtils::ErrorMessage{ 0, u"Cannot rename the requested object"_s }); + return; + } + + if (guard.setErrorFrom(QQmlLSUtils::checkNameForRename(front.domItem, newName, expressionType))) + return; + + auto &editsByFileForResult = result.documentChanges.emplace(); + + // The QLspSpecification::WorkspaceEdit requires the changes to be grouped by files, so + // collect them into editsByFileUris. + QMap<QUrl, QList<QLspSpecification::TextEdit>> editsByFileUris; + + auto renames = QQmlLSUtils::renameUsagesOf(front.domItem, newName, expressionType); + + QQmlJS::Dom::DomItem files = front.domItem.top().field(QQmlJS::Dom::Fields::qmlFileWithPath); + + QHash<QString, QString> codeCache; + + for (const auto &rename : renames.renameInFile()) { + QLspSpecification::TextEdit edit; + + const QUrl uri = QUrl::fromLocalFile(rename.location.filename); + + auto cacheEntry = codeCache.find(rename.location.filename); + if (cacheEntry == codeCache.end()) { + auto file = files.key(rename.location.filename) + .field(QQmlJS::Dom::Fields::currentItem) + .ownerAs<QQmlJS::Dom::QmlFile>(); + if (!file) { + qDebug() << "File" << rename.location.filename + << "not found in DOM! Available files are" << files.keys(); + continue; + } + cacheEntry = codeCache.insert(rename.location.filename, file->code()); + } + + edit.range = QQmlLSUtils::qmlLocationToLspLocation(cacheEntry.value(), + rename.location.sourceLocation); + edit.newText = rename.replacement.toUtf8(); + + editsByFileUris[uri].append(edit); + } + + for (auto it = editsByFileUris.keyValueBegin(); it != editsByFileUris.keyValueEnd(); ++it) { + QLspSpecification::TextDocumentEdit editsForCurrentFile; + editsForCurrentFile.textDocument.uri = it->first.toEncoded(); + + // TODO: do we need to take care of the optional versioning in + // editsForCurrentFile.textDocument.version? see + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#optionalVersionedTextDocumentIdentifier + // for more details + + for (const auto &x : std::as_const(it->second)) { + editsForCurrentFile.edits.append(x); + } + editsByFileForResult.append(editsForCurrentFile); + } + + // if files need to be renamed, then do it after the text edits + for (const auto &rename : renames.renameInFilename()) { + QLspSpecification::RenameFile currentRenameFile; + currentRenameFile.kind = "rename"; + currentRenameFile.oldUri = QUrl::fromLocalFile(rename.oldFilename).toEncoded(); + currentRenameFile.newUri = QUrl::fromLocalFile(rename.newFilename).toEncoded(); + editsByFileForResult.append(currentRenameFile); + } +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmlrenamesymbolsupport_p.h b/src/qmlls/qqmlrenamesymbolsupport_p.h new file mode 100644 index 0000000000..0f1d8be252 --- /dev/null +++ b/src/qmlls/qqmlrenamesymbolsupport_p.h @@ -0,0 +1,44 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLRENAMESYMBOLSUPPORT_P_H +#define QQMLRENAMESYMBOLSUPPORT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlanguageserver_p.h" +#include "qqmlcodemodel_p.h" +#include "qqmlbasemodule_p.h" + +QT_BEGIN_NAMESPACE +struct RenameRequest : public BaseRequest<QLspSpecification::RenameParams, + QLspSpecification::Responses::RenameResponseType> +{ +}; + +class QQmlRenameSymbolSupport : public QQmlBaseModule<RenameRequest> +{ + Q_OBJECT +public: + QQmlRenameSymbolSupport(QmlLsp::QQmlCodeModel *codeModel); + + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; + + void process(RequestPointerArgument request) override; +}; + +QT_END_NAMESPACE + +#endif // QQMLRENAMESYMBOLSUPPORT_P_H diff --git a/src/qmlls/qqmlsemantictokens.cpp b/src/qmlls/qqmlsemantictokens.cpp new file mode 100644 index 0000000000..03a7168149 --- /dev/null +++ b/src/qmlls/qqmlsemantictokens.cpp @@ -0,0 +1,771 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include <qqmlsemantictokens_p.h> + +#include <QtQmlLS/private/qqmllsutils_p.h> +#include <QtQmlDom/private/qqmldomscriptelements_p.h> +#include <QtQmlDom/private/qqmldomfieldfilter_p.h> + +#include <QtLanguageServer/private/qlanguageserverprotocol_p.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(semanticTokens, "qt.languageserver.semanticTokens") + +using namespace QQmlJS::AST; +using namespace QQmlJS::Dom; +using namespace QLspSpecification; + +static int tokenTypeFromRegion(QQmlJS::Dom::FileLocationRegion region) +{ + switch (region) { + case AsTokenRegion: + case BreakKeywordRegion: + case DoKeywordRegion: + case CaseKeywordRegion: + case CatchKeywordRegion: + case ComponentKeywordRegion: + case ContinueKeywordRegion: + case ElseKeywordRegion: + case EnumKeywordRegion: + case ForKeywordRegion: + case FinallyKeywordRegion: + case FunctionKeywordRegion: + case ImportTokenRegion: + case OnTokenRegion: + case PragmaKeywordRegion: + case ReturnKeywordRegion: + case SignalKeywordRegion: + case ThrowKeywordRegion: + case TryKeywordRegion: + case WhileKeywordRegion: + case PropertyKeywordRegion: + case InOfTokenRegion: + case DefaultKeywordRegion: + case ReadonlyKeywordRegion: + case RequiredKeywordRegion: + case IfKeywordRegion: + case SwitchKeywordRegion: + return int(SemanticTokenTypes::Keyword); + case QuestionMarkTokenRegion: + case EllipsisTokenRegion: + case OperatorTokenRegion: + return int(SemanticTokenTypes::Operator); + case QQmlJS::Dom::TypeIdentifierRegion: + return int(SemanticTokenTypes::Type); + case PragmaValuesRegion: + case IdentifierRegion: + case IdNameRegion: + return int(SemanticTokenTypes::Variable); + case ImportUriRegion: + return int(SemanticTokenTypes::Namespace); + case IdTokenRegion: + case OnTargetRegion: + return int(SemanticTokenTypes::Property); + case VersionRegion: + case EnumValueRegion: + return int(SemanticTokenTypes::Number); + default: + return int(SemanticTokenTypes::Variable); + } + Q_UNREACHABLE_RETURN({}); +} + +static FieldFilter highlightingFilter() +{ + QMultiMap<QString, QString> fieldFilterAdd{}; + QMultiMap<QString, QString> fieldFilterRemove{ + { QString(), QString::fromUtf16(Fields::propertyInfos) }, + { QString(), QString::fromUtf16(Fields::fileLocationsTree) }, + { QString(), QString::fromUtf16(Fields::importScope) }, + { QString(), QString::fromUtf16(Fields::defaultPropertyName) }, + { QString(), QString::fromUtf16(Fields::get) }, + }; + return FieldFilter{ fieldFilterAdd, fieldFilterRemove }; +} + +HighlightingVisitor::HighlightingVisitor(Highlights &highlights, + const std::optional<HighlightsRange> &range) + : m_highlights(highlights), m_range(range) +{ +} + +bool HighlightingVisitor::operator()(Path, const DomItem &item, bool) +{ + if (m_range.has_value()) { + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return true; + const auto regions = fLocs->info().regions; + if (!HighlightingUtils::rangeOverlapsWithSourceLocation(regions[MainRegion], + m_range.value())) + return true; + } + switch (item.internalKind()) { + case DomType::Comment: { + highlightComment(item); + return true; + } + case DomType::Import: { + highlightImport(item); + return true; + } + case DomType::Binding: { + highlightBinding(item); + return true; + } + case DomType::Pragma: { + highlightPragma(item); + return true; + } + case DomType::EnumDecl: { + highlightEnumDecl(item); + return true; + } + case DomType::EnumItem: { + highlightEnumItem(item); + return true; + } + case DomType::QmlObject: { + highlightQmlObject(item); + return true; + } + case DomType::QmlComponent: { + highlightComponent(item); + return true; + } + case DomType::PropertyDefinition: { + highlightPropertyDefinition(item); + return true; + } + case DomType::MethodInfo: { + highlightMethod(item); + return true; + } + case DomType::ScriptLiteral: { + highlightScriptLiteral(item); + return true; + } + case DomType::ScriptIdentifierExpression: { + highlightIdentifier(item); + return true; + } + default: + if (item.ownerAs<ScriptExpression>()) + highlightScriptExpressions(item); + return true; + } + Q_UNREACHABLE_RETURN(false); +} + +void HighlightingVisitor::highlightComment(const DomItem &item) +{ + const auto comment = item.as<Comment>(); + Q_ASSERT(comment); + const auto locs = HighlightingUtils::sourceLocationsFromMultiLineToken( + comment->info().comment(), comment->info().sourceLocation()); + for (const auto &loc : locs) + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Comment)); +} + +void HighlightingVisitor::highlightImport(const DomItem &item) +{ + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return; + const auto regions = fLocs->info().regions; + const auto import = item.as<Import>(); + Q_ASSERT(import); + m_highlights.addHighlight(regions, ImportTokenRegion); + if (import->uri.isModule()) + m_highlights.addHighlight(regions[ImportUriRegion], int(SemanticTokenTypes::Namespace)); + else + m_highlights.addHighlight(regions[ImportUriRegion], int(SemanticTokenTypes::String)); + if (regions.contains(VersionRegion)) + m_highlights.addHighlight(regions, VersionRegion); + if (regions.contains(AsTokenRegion)) { + m_highlights.addHighlight(regions, AsTokenRegion); + m_highlights.addHighlight(regions[IdNameRegion], int(SemanticTokenTypes::Namespace)); + } +} + +void HighlightingVisitor::highlightBinding(const DomItem &item) +{ + const auto binding = item.as<Binding>(); + Q_ASSERT(binding); + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) { + qCDebug(semanticTokens) << "Can't find the locations for" << item.internalKind(); + return; + } + const auto regions = fLocs->info().regions; + // If dotted name, then defer it to be handled in ScriptIdentifierExpression + if (binding->name().contains("."_L1)) + return; + + if (binding->bindingType() != BindingType::Normal) { + m_highlights.addHighlight(regions, OnTokenRegion); + m_highlights.addHighlight(regions[IdentifierRegion], int(SemanticTokenTypes::Property)); + return; + } + + return highlightBySemanticAnalysis(item, regions[IdentifierRegion]); +} + +void HighlightingVisitor::highlightPragma(const DomItem &item) +{ + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return; + const auto regions = fLocs->info().regions; + m_highlights.addHighlight(regions, PragmaKeywordRegion); + m_highlights.addHighlight(regions, IdentifierRegion); + const auto pragma = item.as<Pragma>(); + for (auto i = 0; i < pragma->values.size(); ++i) { + DomItem value = item.field(Fields::values).index(i); + const auto valueRegions = FileLocations::treeOf(value)->info().regions; + m_highlights.addHighlight(valueRegions, PragmaValuesRegion); + } + return; +} + +void HighlightingVisitor::highlightEnumDecl(const DomItem &item) +{ + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return; + const auto regions = fLocs->info().regions; + m_highlights.addHighlight(regions, EnumKeywordRegion); + m_highlights.addHighlight(regions[IdentifierRegion], int(SemanticTokenTypes::Enum)); +} + +void HighlightingVisitor::highlightEnumItem(const DomItem &item) +{ + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return; + const auto regions = fLocs->info().regions; + m_highlights.addHighlight(regions[IdentifierRegion], int(SemanticTokenTypes::EnumMember)); + if (regions.contains(EnumValueRegion)) + m_highlights.addHighlight(regions, EnumValueRegion); +} + +void HighlightingVisitor::highlightQmlObject(const DomItem &item) +{ + const auto qmlObject = item.as<QmlObject>(); + Q_ASSERT(qmlObject); + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return; + const auto regions = fLocs->info().regions; + // Handle ids here + if (!qmlObject->idStr().isEmpty()) { + m_highlights.addHighlight(regions, IdTokenRegion); + m_highlights.addHighlight(regions, IdNameRegion); + } + // If dotted name, then defer it to be handled in ScriptIdentifierExpression + if (qmlObject->name().contains("."_L1)) + return; + + m_highlights.addHighlight(regions[IdentifierRegion], int(SemanticTokenTypes::Type)); +} + +void HighlightingVisitor::highlightComponent(const DomItem &item) +{ + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return; + const auto regions = fLocs->info().regions; + m_highlights.addHighlight(regions, ComponentKeywordRegion); + m_highlights.addHighlight(regions[IdentifierRegion], int(SemanticTokenTypes::Type)); +} + +void HighlightingVisitor::highlightPropertyDefinition(const DomItem &item) +{ + const auto propertyDef = item.as<PropertyDefinition>(); + Q_ASSERT(propertyDef); + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return; + const auto regions = fLocs->info().regions; + int modifier = 0; + HighlightingUtils::addModifier(SemanticTokenModifiers::Definition, &modifier); + if (propertyDef->isDefaultMember) { + HighlightingUtils::addModifier(SemanticTokenModifiers::DefaultLibrary, + &modifier); + m_highlights.addHighlight(regions[DefaultKeywordRegion], + int(SemanticTokenTypes::Keyword)); + } + if (propertyDef->isRequired) { + HighlightingUtils::addModifier(SemanticTokenModifiers::Abstract, &modifier); + m_highlights.addHighlight(regions[RequiredKeywordRegion], + int(SemanticTokenTypes::Keyword)); + } + if (propertyDef->isReadonly) { + HighlightingUtils::addModifier(SemanticTokenModifiers::Readonly, &modifier); + m_highlights.addHighlight(regions[ReadonlyKeywordRegion], + int(SemanticTokenTypes::Keyword)); + } + m_highlights.addHighlight(regions, PropertyKeywordRegion); + if (propertyDef->isAlias()) + m_highlights.addHighlight(regions[TypeIdentifierRegion], + int(SemanticTokenTypes::Keyword)); + else + m_highlights.addHighlight(regions, TypeIdentifierRegion); + m_highlights.addHighlight(regions[IdentifierRegion], int(SemanticTokenTypes::Property), + modifier); +} + +void HighlightingVisitor::highlightMethod(const DomItem &item) +{ + const auto method = item.as<MethodInfo>(); + Q_ASSERT(method); + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return; + const auto regions = fLocs->info().regions; + switch (method->methodType) { + case MethodInfo::Signal: { + m_highlights.addHighlight(regions, SignalKeywordRegion); + m_highlights.addHighlight(regions[IdentifierRegion], int(SemanticTokenTypes::Method)); + break; + } + case MethodInfo::Method: { + m_highlights.addHighlight(regions, FunctionKeywordRegion); + m_highlights.addHighlight(regions[IdentifierRegion], int(SemanticTokenTypes::Method)); + m_highlights.addHighlight(regions[TypeIdentifierRegion], int(SemanticTokenTypes::Type)); + break; + } + default: + Q_UNREACHABLE(); + } + + for (auto i = 0; i < method->parameters.size(); ++i) { + DomItem parameter = item.field(Fields::parameters).index(i); + const auto paramRegions = FileLocations::treeOf(parameter)->info().regions; + m_highlights.addHighlight(paramRegions[IdentifierRegion], + int(SemanticTokenTypes::Parameter)); + m_highlights.addHighlight(paramRegions[TypeIdentifierRegion], int(SemanticTokenTypes::Type)); + } + return; +} + +void HighlightingVisitor::highlightScriptLiteral(const DomItem &item) +{ + const auto literal = item.as<ScriptElements::Literal>(); + Q_ASSERT(literal); + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return; + const auto regions = fLocs->info().regions; + if (std::holds_alternative<QString>(literal->literalValue())) { + const QString value = u'\"' + std::get<QString>(literal->literalValue()) + u'\"'; + const auto &locs = HighlightingUtils::sourceLocationsFromMultiLineToken( + value, regions[MainRegion]); + for (const auto &loc : locs) + m_highlights.addHighlight(loc, int(SemanticTokenTypes::String)); + } else if (std::holds_alternative<double>(literal->literalValue())) + m_highlights.addHighlight(regions[MainRegion], int(SemanticTokenTypes::Number)); + else if (std::holds_alternative<bool>(literal->literalValue())) + m_highlights.addHighlight(regions[MainRegion], int(SemanticTokenTypes::Keyword)); + else if (std::holds_alternative<std::nullptr_t>(literal->literalValue())) + m_highlights.addHighlight(regions[MainRegion], int(SemanticTokenTypes::Keyword)); + else + qCWarning(semanticTokens) << "Invalid literal variant"; +} + +void HighlightingVisitor::highlightIdentifier(const DomItem &item) +{ + using namespace QLspSpecification; + const auto id = item.as<ScriptElements::IdentifierExpression>(); + Q_ASSERT(id); + const auto loc = id->mainRegionLocation(); + // Many of the scriptIdentifiers expressions are already handled by + // other cases. In those cases, if the location offset is already in the list + // we don't need to perform expensive resolveExpressionType operation. + if (m_highlights.highlights().contains(loc.offset)) + return; + + highlightBySemanticAnalysis(item, loc); +} + +void HighlightingVisitor::highlightBySemanticAnalysis(const DomItem &item, QQmlJS::SourceLocation loc) +{ + const auto expression = QQmlLSUtils::resolveExpressionType( + item, QQmlLSUtils::ResolveOptions::ResolveOwnerType); + + if (!expression) { + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Variable)); + return; + } + switch (expression->type) { + case QQmlLSUtils::QmlComponentIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Type)); + return; + case QQmlLSUtils::JavaScriptIdentifier: { + SemanticTokenTypes tokenType = SemanticTokenTypes::Variable; + int modifier = 0; + if (const auto jsIdentifier + = expression->semanticScope->jsIdentifier(expression->name.value())) { + switch (jsIdentifier.value().kind) { + case QQmlJSScope::JavaScriptIdentifier::Parameter: + tokenType = SemanticTokenTypes::Parameter; + break; + case QQmlJSScope::JavaScriptIdentifier::LexicalScoped: // let or const + case QQmlJSScope::JavaScriptIdentifier::FunctionScoped: // var + case QQmlJSScope::JavaScriptIdentifier::Injected: + default: + tokenType = SemanticTokenTypes::Variable; + break; + } + if (jsIdentifier.value().isConst) { + HighlightingUtils::addModifier(SemanticTokenModifiers::Readonly, + &modifier); + } + } + m_highlights.addHighlight(loc, int(tokenType), modifier); + return; + } + case QQmlLSUtils::PropertyIdentifier: { + if (const auto scope = expression->semanticScope) { + const auto property = scope->property(expression->name.value()); + int modifier = 0; + if (!property.isWritable()) { + HighlightingUtils::addModifier(SemanticTokenModifiers::Readonly, + &modifier); + } + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Property), modifier); + } + return; + } + case QQmlLSUtils::PropertyChangedSignalIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Method)); + return; + case QQmlLSUtils::PropertyChangedHandlerIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Method)); + return; + case QQmlLSUtils::SignalIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Method)); + return; + case QQmlLSUtils::SignalHandlerIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Method)); + return; + case QQmlLSUtils::MethodIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Method)); + return; + case QQmlLSUtils::QmlObjectIdIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Variable)); + return; + case QQmlLSUtils::SingletonIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Type)); + return; + case QQmlLSUtils::EnumeratorIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Enum)); + return; + case QQmlLSUtils::EnumeratorValueIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::EnumMember)); + return; + case QQmlLSUtils::AttachedTypeIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Type)); + return; + case QQmlLSUtils::GroupedPropertyIdentifier: + m_highlights.addHighlight(loc, int(SemanticTokenTypes::Property)); + return; + default: + qCWarning(semanticTokens) + << QString::fromLatin1("Semantic token for %1 has not been implemented yet") + .arg(int(expression->type)); + } + Q_UNREACHABLE_RETURN(); +} + +void HighlightingVisitor::highlightScriptExpressions(const DomItem &item) +{ + const auto fLocs = FileLocations::treeOf(item); + if (!fLocs) + return; + const auto regions = fLocs->info().regions; + switch (item.internalKind()) { + case DomType::ScriptLiteral: + highlightScriptLiteral(item); + return; + case DomType::ScriptForStatement: + m_highlights.addHighlight(regions, ForKeywordRegion); + m_highlights.addHighlight(regions[TypeIdentifierRegion], + int(SemanticTokenTypes::Keyword)); + return; + + case DomType::ScriptVariableDeclaration: { + m_highlights.addHighlight(regions[TypeIdentifierRegion], + int(SemanticTokenTypes::Keyword)); + return; + } + case DomType::ScriptReturnStatement: + m_highlights.addHighlight(regions, ReturnKeywordRegion); + return; + case DomType::ScriptCaseClause: + m_highlights.addHighlight(regions, CaseKeywordRegion); + return; + case DomType::ScriptDefaultClause: + m_highlights.addHighlight(regions, DefaultKeywordRegion); + return; + case DomType::ScriptSwitchStatement: + m_highlights.addHighlight(regions, SwitchKeywordRegion); + return; + case DomType::ScriptWhileStatement: + m_highlights.addHighlight(regions, WhileKeywordRegion); + return; + case DomType::ScriptDoWhileStatement: + m_highlights.addHighlight(regions, DoKeywordRegion); + m_highlights.addHighlight(regions, WhileKeywordRegion); + return; + case DomType::ScriptTryCatchStatement: + m_highlights.addHighlight(regions, TryKeywordRegion); + m_highlights.addHighlight(regions, CatchKeywordRegion); + m_highlights.addHighlight(regions, FinallyKeywordRegion); + return; + case DomType::ScriptForEachStatement: + m_highlights.addHighlight(regions[TypeIdentifierRegion], + int(SemanticTokenTypes::Keyword)); + m_highlights.addHighlight(regions, ForKeywordRegion); + m_highlights.addHighlight(regions, InOfTokenRegion); + return; + case DomType::ScriptThrowStatement: + m_highlights.addHighlight(regions, ThrowKeywordRegion); + return; + case DomType::ScriptBreakStatement: + m_highlights.addHighlight(regions, BreakKeywordRegion); + return; + case DomType::ScriptContinueStatement: + m_highlights.addHighlight(regions, ContinueKeywordRegion); + return; + case DomType::ScriptIfStatement: + m_highlights.addHighlight(regions, IfKeywordRegion); + m_highlights.addHighlight(regions, ElseKeywordRegion); + return; + case DomType::ScriptLabelledStatement: + m_highlights.addHighlight(regions, IdentifierRegion); + return; + case DomType::ScriptConditionalExpression: + m_highlights.addHighlight(regions, QuestionMarkTokenRegion); + m_highlights.addHighlight(regions, ColonTokenRegion); + return; + case DomType::ScriptUnaryExpression: + case DomType::ScriptPostExpression: + m_highlights.addHighlight(regions, OperatorTokenRegion); + return; + case DomType::ScriptType: + m_highlights.addHighlight(regions[IdentifierRegion], int(SemanticTokenTypes::Type)); + m_highlights.addHighlight(regions[TypeIdentifierRegion], int(SemanticTokenTypes::Type)); + return; + default: + qCDebug(semanticTokens) + << "Script Expressions with kind" << item.internalKind() << "not implemented"; + return; + } + Q_UNREACHABLE_RETURN(); +} + +/*! +\internal +\brief Returns multiple source locations for a given raw comment + +Needed by semantic highlighting of comments. LSP clients usually don't support multiline +tokens. In QML, we can have multiline tokens like string literals and comments. +This method generates multiple source locations of sub-elements of token split by a newline +delimiter. +*/ +QList<QQmlJS::SourceLocation> +HighlightingUtils::sourceLocationsFromMultiLineToken(QStringView stringLiteral, + const QQmlJS::SourceLocation &locationInDocument) +{ + auto lineBreakLength = qsizetype(std::char_traits<char>::length("\n")); + const auto lineLengths = [&lineBreakLength](QStringView literal) { + std::vector<qsizetype> lineLengths; + qsizetype startIndex = 0; + qsizetype pos = literal.indexOf(u'\n'); + while (pos != -1) { + // TODO: QTBUG-106813 + // Since a document could be opened in normalized form + // we can't use platform dependent newline handling here. + // Thus, we check manually if the literal contains \r so that we split + // the literal at the correct offset. + if (pos - 1 > 0 && literal[pos - 1] == u'\r') { + // Handle Windows line endings + lineBreakLength = qsizetype(std::char_traits<char>::length("\r\n")); + // Move pos to the index of '\r' + pos = pos - 1; + } + lineLengths.push_back(pos - startIndex); + // Advance the lookup index, so it won't find the same index. + startIndex = pos + lineBreakLength; + pos = literal.indexOf('\n'_L1, startIndex); + } + // Push the last line + if (startIndex < literal.length()) { + lineLengths.push_back(literal.length() - startIndex); + } + return lineLengths; + }; + + QList<QQmlJS::SourceLocation> result; + // First token location should start from the "stringLiteral"'s + // location in the qml document. + QQmlJS::SourceLocation lineLoc = locationInDocument; + for (const auto lineLength : lineLengths(stringLiteral)) { + lineLoc.length = lineLength; + result.push_back(lineLoc); + + // update for the next line + lineLoc.offset += lineLoc.length + lineBreakLength; + ++lineLoc.startLine; + lineLoc.startColumn = 1; + } + return result; +} + +QList<int> HighlightingUtils::encodeSemanticTokens(Highlights &highlights) +{ + QList<int> result; + const auto highlightingTokens = highlights.highlights(); + constexpr auto tokenEncodingLength = 5; + result.reserve(tokenEncodingLength * highlightingTokens.size()); + + int prevLine = 0; + int prevColumn = 0; + + std::for_each(highlightingTokens.constBegin(), highlightingTokens.constEnd(), [&](const auto &token) { + Q_ASSERT(token.startLine >= prevLine); + if (token.startLine != prevLine) + prevColumn = 0; + result.emplace_back(token.startLine - prevLine); + result.emplace_back(token.startColumn - prevColumn); + result.emplace_back(token.length); + result.emplace_back(token.tokenType); + result.emplace_back(token.tokenModifier); + prevLine = token.startLine; + prevColumn = token.startColumn; + }); + + return result; +} + +/*! +\internal +Computes the modifier value. Modifier is read as binary value in the protocol. The location +of the bits set are interpreted as the indices of the tokenModifiers list registered by the +server. Then, the client modifies the highlighting of the token. + +tokenModifiersList: ["declaration", definition, readonly, static ,,,] + +To set "definition" and "readonly", we need to send 0b00000110 +*/ +void HighlightingUtils::addModifier(SemanticTokenModifiers modifier, int *baseModifier) +{ + if (!baseModifier) + return; + *baseModifier |= (1 << int(modifier)); +} + +/*! +\internal +Check if the ranges overlap by ensuring that one range starts before the other ends +*/ +bool HighlightingUtils::rangeOverlapsWithSourceLocation(const QQmlJS::SourceLocation &loc, + const HighlightsRange &r) +{ + int startOffsetItem = int(loc.offset); + int endOffsetItem = startOffsetItem + int(loc.length); + return (startOffsetItem <= r.endOffset) && (r.startOffset <= endOffsetItem); +} + +/* +\internal +Increments the resultID by one. +*/ +void HighlightingUtils::updateResultID(QByteArray &resultID) +{ + int length = resultID.length(); + for (int i = length - 1; i >= 0; --i) { + if (resultID[i] == '9') { + resultID[i] = '0'; + } else { + resultID[i] = resultID[i] + 1; + return; + } + } + resultID.prepend('1'); +} + +/* +\internal +A utility method that computes the difference of two list. The first argument is the encoded token data +of the file before edited. The second argument is the encoded token data after the file is edited. Returns +a list of SemanticTokensEdit as expected by the protocol. +*/ +QList<SemanticTokensEdit> HighlightingUtils::computeDiff(const QList<int> &oldData, const QList<int> &newData) +{ + // Find the iterators pointing the first mismatch, from the start + const auto [oldStart, newStart] = + std::mismatch(oldData.cbegin(), oldData.cend(), newData.cbegin(), newData.cend()); + + // Find the iterators pointing the first mismatch, from the end + // but the iterators shouldn't pass over the start iterators found above. + const auto [r1, r2] = std::mismatch(oldData.crbegin(), std::make_reverse_iterator(oldStart), + newData.crbegin(), std::make_reverse_iterator(newStart)); + const auto oldEnd = r1.base(); + const auto newEnd = r2.base(); + + // no change + if (oldStart == oldEnd && newStart == newEnd) + return {}; + + SemanticTokensEdit edit; + edit.start = int(std::distance(newData.cbegin(), newStart)); + edit.deleteCount = int(std::distance(oldStart, oldEnd)); + + if (newStart >= newData.cbegin() && newEnd <= newData.cend() && newStart < newEnd) + edit.data.emplace(newStart, newEnd); + + return { std::move(edit) }; +} + + +void Highlights::addHighlight(const QQmlJS::SourceLocation &loc, int tokenType, int tokenModifier) +{ + if (!loc.isValid()) { + qCDebug(semanticTokens) << "Invalid locations: Cannot add highlight to token"; + return; + } + + if (!m_highlights.contains(loc.offset)) + m_highlights.insert(loc.offset, QT_PREPEND_NAMESPACE(Token)(loc, tokenType, tokenModifier)); +} + +void Highlights::addHighlight(const QMap<FileLocationRegion, QQmlJS::SourceLocation> ®ions, + FileLocationRegion region, int modifier) +{ + if (!regions.contains(region)) { + qCDebug(semanticTokens) << "Invalid region: Cannot add highlight to token"; + return; + } + + const auto loc = regions.value(region); + return addHighlight(loc, tokenTypeFromRegion(region), modifier); +} + +QList<int> Highlights::collectTokens(const QQmlJS::Dom::DomItem &item, + const std::optional<HighlightsRange> &range) +{ + using namespace QQmlJS::Dom; + HighlightingVisitor highlightDomElements(*this, range); + // In QmlFile level, visitTree visits even FileLocations tree which takes quite a time to + // finish. HighlightingFilter is added to prevent unnecessary visits. + item.visitTree(Path(), highlightDomElements, VisitOption::Default, emptyChildrenVisitor, + emptyChildrenVisitor, highlightingFilter()); + + return HighlightingUtils::encodeSemanticTokens(*this); +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qqmlsemantictokens_p.h b/src/qmlls/qqmlsemantictokens_p.h new file mode 100644 index 0000000000..193c39baea --- /dev/null +++ b/src/qmlls/qqmlsemantictokens_p.h @@ -0,0 +1,131 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QQMLSEMANTICTOKENS_P_H +#define QQMLSEMANTICTOKENS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtLanguageServer/private/qlanguageserverspec_p.h> +#include <QtQmlDom/private/qqmldomitem_p.h> +#include <QtCore/qlist.h> +#include <QtCore/qmap.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(semanticTokens) + +// Represents a semantic highlighting token +// startLine and startColumn are 0-based as in LSP spec. +struct Token +{ + Token() = default; + Token(const QQmlJS::SourceLocation &loc, int tokenType, int tokenModifier = 0) + : offset(loc.offset), + length(loc.length), + startLine(loc.startLine - 1), + startColumn(loc.startColumn - 1), + tokenType(tokenType), + tokenModifier(tokenModifier) + { + } + + inline friend bool operator<(const Token &lhs, const Token &rhs) + { + return lhs.offset < rhs.offset; + } + + inline friend bool operator==(const Token &lhs, const Token &rhs) + { + return lhs.offset == rhs.offset && lhs.length == rhs.length + && lhs.startLine == rhs.startLine && lhs.startColumn == rhs.startColumn + && lhs.tokenType == rhs.tokenType && lhs.tokenModifier == rhs.tokenModifier; + } + + int offset; + int length; + int startLine; + int startColumn; + int tokenType; + int tokenModifier; +}; + +using HighlightsContainer = QMap<int, QT_PREPEND_NAMESPACE(Token)>; + +/*! +\internal +Offsets start from zero. +*/ +struct HighlightsRange +{ + int startOffset; + int endOffset; +}; + +class Highlights +{ +public: + void addHighlight(const QQmlJS::SourceLocation &loc, int tokenType, int tokenModifier = 0); + void addHighlight(const QMap<QQmlJS::Dom::FileLocationRegion, QQmlJS::SourceLocation> ®ions, + QQmlJS::Dom::FileLocationRegion region, int tokenModifier = 0); + QList<int> collectTokens(const QQmlJS::Dom::DomItem &item, + const std::optional<HighlightsRange> &range); + + HighlightsContainer &highlights() { return m_highlights; } + const HighlightsContainer &highlights() const { return m_highlights; } + +private: + HighlightsContainer m_highlights; +}; + +struct HighlightingUtils +{ + static QList<int> encodeSemanticTokens(Highlights &highlights); + static QList<QQmlJS::SourceLocation> + sourceLocationsFromMultiLineToken(QStringView code, + const QQmlJS::SourceLocation &tokenLocation); + static void addModifier(QLspSpecification::SemanticTokenModifiers modifier, int *baseModifier); + static bool rangeOverlapsWithSourceLocation(const QQmlJS::SourceLocation &loc, const HighlightsRange &r); + static QList<QLspSpecification::SemanticTokensEdit> computeDiff(const QList<int> &, const QList<int> &); + static void updateResultID(QByteArray &resultID); +}; + +class HighlightingVisitor +{ +public: + HighlightingVisitor(Highlights &highlights, const std::optional<HighlightsRange> &range); + bool operator()(QQmlJS::Dom::Path, const QQmlJS::Dom::DomItem &item, bool); + +private: + void highlightComment(const QQmlJS::Dom::DomItem &item); + void highlightImport(const QQmlJS::Dom::DomItem &item); + void highlightBinding(const QQmlJS::Dom::DomItem &item); + void highlightPragma(const QQmlJS::Dom::DomItem &item); + void highlightEnumItem(const QQmlJS::Dom::DomItem &item); + void highlightEnumDecl(const QQmlJS::Dom::DomItem &item); + void highlightQmlObject(const QQmlJS::Dom::DomItem &item); + void highlightComponent(const QQmlJS::Dom::DomItem &item); + void highlightPropertyDefinition(const QQmlJS::Dom::DomItem &item); + void highlightMethod(const QQmlJS::Dom::DomItem &item); + void highlightScriptLiteral(const QQmlJS::Dom::DomItem &item); + void highlightIdentifier(const QQmlJS::Dom::DomItem &item); + void highlightBySemanticAnalysis(const QQmlJS::Dom::DomItem &item, QQmlJS::SourceLocation loc); + void highlightScriptExpressions(const QQmlJS::Dom::DomItem &item); + +private: + Highlights &m_highlights; + std::optional<HighlightsRange> m_range; +}; + +QT_END_NAMESPACE + +#endif // QQMLSEMANTICTOKENS_P_H diff --git a/src/qmlls/qtextblock.cpp b/src/qmlls/qtextblock.cpp new file mode 100644 index 0000000000..6fc1fa6a5c --- /dev/null +++ b/src/qmlls/qtextblock.cpp @@ -0,0 +1,101 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qtextblock_p.h" +#include "qtextdocument_p.h" + +#include <QtCore/qstring.h> + +namespace Utils { + +bool TextBlock::isValid() const +{ + return m_document; +} + +void TextBlock::setBlockNumber(int blockNumber) +{ + m_blockNumber = blockNumber; +} + +int TextBlock::blockNumber() const +{ + return m_blockNumber; +} + +void TextBlock::setPosition(int position) +{ + m_position = position; +} + +int TextBlock::position() const +{ + return m_position; +} + +void TextBlock::setLength(int length) +{ + m_length = length; +} + +int TextBlock::length() const +{ + return m_length; +} + +TextBlock TextBlock::next() const +{ + return m_document->findBlockByNumber(m_blockNumber + 1); +} + +TextBlock TextBlock::previous() const +{ + return m_document->findBlockByNumber(m_blockNumber - 1); +} + +int TextBlock::userState() const +{ + return m_document->userState(m_blockNumber); +} + +void TextBlock::setUserState(int state) +{ + m_document->setUserState(m_blockNumber, state); +} + +void TextBlock::setDocument(TextDocument *document) +{ + m_document = document; +} + +TextDocument *TextBlock::document() const +{ + return m_document; +} + +QString TextBlock::text() const +{ + return document()->toPlainText().mid(position(), length()); +} + +int TextBlock::revision() const +{ + return m_revision; +} + +void TextBlock::setRevision(int rev) +{ + m_revision = rev; +} + +bool operator==(const TextBlock &t1, const TextBlock &t2) +{ + return t1.document() == t2.document() && t1.blockNumber() == t2.blockNumber(); +} + +bool operator!=(const TextBlock &t1, const TextBlock &t2) +{ + return !(t1 == t2); +} + +} // namespace Utils diff --git a/src/qmlls/qtextblock_p.h b/src/qmlls/qtextblock_p.h new file mode 100644 index 0000000000..138d28c033 --- /dev/null +++ b/src/qmlls/qtextblock_p.h @@ -0,0 +1,73 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QTEXTBLOCK_P_H +#define QTEXTBLOCK_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qstring.h> + +namespace Utils { + +class TextDocument; +class TextBlockUserData; + +class TextBlock +{ +public: + bool isValid() const; + + void setBlockNumber(int blockNumber); + int blockNumber() const; + + void setPosition(int position); + int position() const; + + void setLength(int length); + int length() const; + + TextBlock next() const; + TextBlock previous() const; + + int userState() const; + void setUserState(int state); + + bool isVisible() const; + void setVisible(bool visible); + + void setLineCount(int count); + int lineCount() const; + + void setDocument(TextDocument *document); + TextDocument *document() const; + + QString text() const; + + int revision() const; + void setRevision(int rev); + + friend bool operator==(const TextBlock &t1, const TextBlock &t2); + friend bool operator!=(const TextBlock &t1, const TextBlock &t2); + +private: + TextDocument *m_document = nullptr; + int m_revision = 0; + + int m_position = 0; + int m_length = 0; + int m_blockNumber = -1; +}; + +} // namespace Utils + +#endif // TEXTBLOCK_P_H diff --git a/src/qmlls/qtextcursor.cpp b/src/qmlls/qtextcursor.cpp new file mode 100644 index 0000000000..79289c3fa5 --- /dev/null +++ b/src/qmlls/qtextcursor.cpp @@ -0,0 +1,122 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qtextcursor_p.h" +#include "qtextdocument_p.h" +#include "qtextblock_p.h" + +namespace Utils { + +class TextFrame; +class TextTable; +class TextTableCell; + +TextCursor::TextCursor(TextDocument *document) : m_document(document) { } + +bool TextCursor::movePosition(TextCursor::MoveOperation op, TextCursor::MoveMode mode, int n) +{ + Q_UNUSED(n); + switch (op) { + case NoMove: + return true; + case Start: + m_position = 0; + break; + case PreviousCharacter: + while (--n >= 0) { + if (m_position == 0) + return false; + --m_position; + } + break; + case End: + m_position = m_document->characterCount(); + break; + case NextCharacter: + while (--n >= 0) { + if (m_position == m_document->characterCount()) + return false; + ++m_position; + } + break; + } + + if (mode == MoveAnchor) + m_anchor = m_position; + + return false; +} + +int TextCursor::position() const +{ + return m_position; +} + +void TextCursor::setPosition(int pos, Utils::TextCursor::MoveMode mode) +{ + m_position = pos; + if (mode == MoveAnchor) + m_anchor = pos; +} + +QString TextCursor::selectedText() const +{ + return m_document->toPlainText().mid(qMin(m_position, m_anchor), qAbs(m_position - m_anchor)); +} + +void TextCursor::clearSelection() +{ + m_anchor = m_position; +} + +TextDocument *TextCursor::document() const +{ + return m_document; +} + +void TextCursor::insertText(const QString &text) +{ + const QString orig = m_document->toPlainText(); + const QString left = orig.left(qMin(m_position, m_anchor)); + const QString right = orig.mid(qMax(m_position, m_anchor)); + m_document->setPlainText(left + text + right); +} + +TextBlock TextCursor::block() const +{ + TextBlock current = m_document->firstBlock(); + while (current.isValid()) { + if (current.position() <= position() + && current.position() + current.length() > current.position()) + break; + current = current.next(); + } + return current; +} + +int TextCursor::positionInBlock() const +{ + return m_position - block().position(); +} + +int TextCursor::blockNumber() const +{ + return block().blockNumber(); +} + +void TextCursor::removeSelectedText() +{ + insertText(QString()); +} + +int TextCursor::selectionEnd() const +{ + return qMax(m_position, m_anchor); +} + +bool TextCursor::isNull() const +{ + return m_document == nullptr; +} + +} // namespace Utils diff --git a/src/qmlls/qtextcursor_p.h b/src/qmlls/qtextcursor_p.h new file mode 100644 index 0000000000..275acb93ec --- /dev/null +++ b/src/qmlls/qtextcursor_p.h @@ -0,0 +1,72 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef TEXTCURSOR_H +#define TEXTCURSOR_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qstring.h> + +namespace Utils { + +class TextDocument; +class TextBlock; + +class TextCursor +{ +public: + enum MoveOperation { + NoMove, + Start, + PreviousCharacter, + End, + NextCharacter, + }; + + enum MoveMode { MoveAnchor, KeepAnchor }; + + enum SelectionType { Document }; + + TextCursor(); + TextCursor(const TextBlock &block); + TextCursor(TextDocument *document); + + bool movePosition(MoveOperation op, MoveMode = MoveAnchor, int n = 1); + int position() const; + void setPosition(int pos, MoveMode mode = MoveAnchor); + QString selectedText() const; + void clearSelection(); + int anchor() const; + TextDocument *document() const; + void insertText(const QString &text); + TextBlock block() const; + int positionInBlock() const; + int blockNumber() const; + + void select(SelectionType selection); + + bool hasSelection() const; + + void removeSelectedText(); + int selectionEnd() const; + + bool isNull() const; + +private: + TextDocument *m_document = nullptr; + int m_position = 0; + int m_anchor = 0; +}; +} // namespace Utils + +#endif // TEXTCURSOR_H diff --git a/src/qmlls/qtextdocument.cpp b/src/qmlls/qtextdocument.cpp new file mode 100644 index 0000000000..54e200274e --- /dev/null +++ b/src/qmlls/qtextdocument.cpp @@ -0,0 +1,124 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qtextdocument_p.h" +#include "qtextblock_p.h" + +namespace Utils { + +TextDocument::TextDocument(const QString &text) +{ + setPlainText(text); +} + +TextBlock TextDocument::findBlockByNumber(int blockNumber) const +{ + return (blockNumber >= 0 && blockNumber < m_blocks.size()) + ? m_blocks.at(blockNumber).textBlock + : TextBlock(); +} + +TextBlock TextDocument::findBlockByLineNumber(int lineNumber) const +{ + return findBlockByNumber(lineNumber); +} + +QChar TextDocument::characterAt(int pos) const +{ + return m_content.at(pos); +} + +int TextDocument::characterCount() const +{ + return m_content.size(); +} + +TextBlock TextDocument::begin() const +{ + return m_blocks.isEmpty() ? TextBlock() : m_blocks.at(0).textBlock; +} + +TextBlock TextDocument::firstBlock() const +{ + return begin(); +} + +TextBlock TextDocument::lastBlock() const +{ + return m_blocks.isEmpty() ? TextBlock() : m_blocks.last().textBlock; +} + +std::optional<int> TextDocument::version() const +{ + return m_version; +} + +void TextDocument::setVersion(std::optional<int> v) +{ + m_version = v; +} + +QString TextDocument::toPlainText() const +{ + return m_content; +} + +void TextDocument::setPlainText(const QString &text) +{ + m_content = text; + m_blocks.clear(); + + const auto appendToBlocks = [this](int blockNumber, int start, int length) { + Block block; + block.textBlock.setBlockNumber(blockNumber); + block.textBlock.setPosition(start); + block.textBlock.setDocument(this); + block.textBlock.setLength(length); + m_blocks.append(block); + }; + + int blockStart = 0; + int blockNumber = -1; + while (blockStart < text.size()) { + int blockEnd = text.indexOf(u'\n', blockStart) + 1; + if (blockEnd == 0) + blockEnd = text.size(); + appendToBlocks(++blockNumber, blockStart, blockEnd - blockStart); + blockStart = blockEnd; + } + // Add an empty block if the text ends with \n. This is required for retrieving + // the actual line of the text editor if requested, for example, in findBlockByNumber. + // Consider a case with text aa\nbb\n\n. You are on 4th line of the text editor and even + // if it is an empty line, we introduce a text block for it to maybe use later. + if (text.endsWith(u'\n')) + appendToBlocks(++blockNumber, blockStart, 0); +} + +bool TextDocument::isModified() const +{ + return m_modified; +} + +void TextDocument::setModified(bool modified) +{ + m_modified = modified; +} + +void TextDocument::setUserState(int blockNumber, int state) +{ + if (blockNumber >= 0 && blockNumber < m_blocks.size()) + m_blocks[blockNumber].userState = state; +} + +int TextDocument::userState(int blockNumber) const +{ + return (blockNumber >= 0 && blockNumber < m_blocks.size()) ? m_blocks[blockNumber].userState + : -1; +} + +QMutex *TextDocument::mutex() const +{ + return &m_mutex; +} + +} // namespace Utils diff --git a/src/qmlls/qtextdocument_p.h b/src/qmlls/qtextdocument_p.h new file mode 100644 index 0000000000..4df516e54a --- /dev/null +++ b/src/qmlls/qtextdocument_p.h @@ -0,0 +1,78 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QTEXTDOCUMENT_P_H +#define QTEXTDOCUMENT_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qtextblock_p.h" + +#include <QtCore/qchar.h> +#include <QtCore/qvector.h> +#include <QtCore/qscopedpointer.h> +#include <QtCore/qmutex.h> + +#include <optional> + +namespace Utils { + +class TextBlockUserData; + +class TextDocument +{ +public: + TextDocument() = default; + explicit TextDocument(const QString &text); + + TextBlock findBlockByNumber(int blockNumber) const; + TextBlock findBlockByLineNumber(int lineNumber) const; + QChar characterAt(int pos) const; + int characterCount() const; + TextBlock begin() const; + TextBlock firstBlock() const; + TextBlock lastBlock() const; + + std::optional<int> version() const; + void setVersion(std::optional<int>); + + QString toPlainText() const; + void setPlainText(const QString &text); + + bool isModified() const; + void setModified(bool modified); + + void setUndoRedoEnabled(bool enable); + + void clear(); + + void setUserState(int blockNumber, int state); + int userState(int blockNumber) const; + QMutex *mutex() const; + +private: + struct Block + { + TextBlock textBlock; + int userState = -1; + }; + + QVector<Block> m_blocks; + + QString m_content; + bool m_modified = false; + std::optional<int> m_version; + mutable QMutex m_mutex; +}; +} // namespace Utils + +#endif // TEXTDOCUMENT_P_H diff --git a/src/qmlls/qtextsynchronization.cpp b/src/qmlls/qtextsynchronization.cpp new file mode 100644 index 0000000000..5a1e39e855 --- /dev/null +++ b/src/qmlls/qtextsynchronization.cpp @@ -0,0 +1,99 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qtextsynchronization_p.h" +#include "qqmllsutils_p.h" +#include "qtextdocument_p.h" + +using namespace QLspSpecification; +using namespace Qt::StringLiterals; + +QT_BEGIN_NAMESPACE + +TextSynchronization::TextSynchronization(QmlLsp::QQmlCodeModel *codeModel, QObject *parent) + : QLanguageServerModule(parent), m_codeModel(codeModel) +{ +} + +void TextSynchronization::didCloseTextDocument(const DidCloseTextDocumentParams ¶ms) +{ + m_codeModel->closeOpenFile(QQmlLSUtils::lspUriToQmlUrl(params.textDocument.uri)); +} + +void TextSynchronization::didOpenTextDocument(const DidOpenTextDocumentParams ¶ms) +{ + const TextDocumentItem &item = params.textDocument; + const QString fileName = m_codeModel->url2Path(QQmlLSUtils::lspUriToQmlUrl(item.uri)); + m_codeModel->newOpenFile(QQmlLSUtils::lspUriToQmlUrl(item.uri), item.version, + QString::fromUtf8(item.text)); +} + +void TextSynchronization::didDidChangeTextDocument(const DidChangeTextDocumentParams ¶ms) +{ + QByteArray url = QQmlLSUtils::lspUriToQmlUrl(params.textDocument.uri); + const QString fileName = m_codeModel->url2Path(url); + auto openDoc = m_codeModel->openDocumentByUrl(url); + std::shared_ptr<Utils::TextDocument> document = openDoc.textDocument; + if (!document) { + qCWarning(lspServerLog) << "Ingnoring changes to non open or closed document" + << QString::fromUtf8(url); + return; + } + const auto &changes = params.contentChanges; + { + QMutexLocker l(document->mutex()); + for (const auto &change : changes) { + if (!change.range) { + document->setPlainText(QString::fromUtf8(change.text)); + continue; + } + + const auto &range = *change.range; + const auto &rangeStart = range.start; + const int start = + document->findBlockByNumber(rangeStart.line).position() + rangeStart.character; + const auto &rangeEnd = range.end; + const int end = + document->findBlockByNumber(rangeEnd.line).position() + rangeEnd.character; + + document->setPlainText(document->toPlainText().replace(start, end - start, + QString::fromUtf8(change.text))); + } + document->setVersion(params.textDocument.version); + qCDebug(lspServerLog).noquote() + << "text is\n:----------" << document->toPlainText() << "\n_________"; + } + m_codeModel->addOpenToUpdate(url); + m_codeModel->openNeedUpdate(); +} + +void TextSynchronization::registerHandlers(QLanguageServer *server, QLanguageServerProtocol *) +{ + QObject::connect(server->notifySignals(), + &QLspNotifySignals::receivedDidOpenTextDocumentNotification, this, + &TextSynchronization::didOpenTextDocument); + + QObject::connect(server->notifySignals(), + &QLspNotifySignals::receivedDidChangeTextDocumentNotification, this, + &TextSynchronization::didDidChangeTextDocument); + + QObject::connect(server->notifySignals(), + &QLspNotifySignals::receivedDidCloseTextDocumentNotification, this, + &TextSynchronization::didCloseTextDocument); +} + +QString TextSynchronization::name() const +{ + return u"TextSynchonization"_s; +} + +void TextSynchronization::setupCapabilities(const QLspSpecification::InitializeParams &, + QLspSpecification::InitializeResult &serverInfo) +{ + TextDocumentSyncOptions syncOptions; + syncOptions.openClose = true; + syncOptions.change = TextDocumentSyncKind::Incremental; + serverInfo.capabilities.textDocumentSync = syncOptions; +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qtextsynchronization_p.h b/src/qmlls/qtextsynchronization_p.h new file mode 100644 index 0000000000..2c26b84aea --- /dev/null +++ b/src/qmlls/qtextsynchronization_p.h @@ -0,0 +1,43 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QTEXTSYNCHRONIZATION_P_H +#define QTEXTSYNCHRONIZATION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qqmlcodemodel_p.h" +#include "qlanguageserver_p.h" + +QT_BEGIN_NAMESPACE + +class TextSynchronization : public QLanguageServerModule +{ + Q_OBJECT +public: + TextSynchronization(QmlLsp::QQmlCodeModel *codeModel, QObject *parent = nullptr); + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; + +public Q_SLOTS: + void didOpenTextDocument(const QLspSpecification::DidOpenTextDocumentParams ¶ms); + void didDidChangeTextDocument(const QLspSpecification::DidChangeTextDocumentParams ¶ms); + void didCloseTextDocument(const QLspSpecification::DidCloseTextDocumentParams ¶ms); + +private: + QmlLsp::QQmlCodeModel *m_codeModel; +}; + +QT_END_NAMESPACE +#endif // QTEXTSYNCHRONIZATION_P_H diff --git a/src/qmlls/qworkspace.cpp b/src/qmlls/qworkspace.cpp new file mode 100644 index 0000000000..ea8f2d6005 --- /dev/null +++ b/src/qmlls/qworkspace.cpp @@ -0,0 +1,172 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qworkspace_p.h" +#include "qqmllanguageserver_p.h" +#include "qqmllsutils_p.h" + +#include <QtLanguageServer/private/qlanguageserverspectypes_p.h> +#include <QtLanguageServer/private/qlspnotifysignals_p.h> + +#include <QtCore/qfile.h> +#include <variant> + +QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; +using namespace QLspSpecification; + +void WorkspaceHandlers::registerHandlers(QLanguageServer *server, QLanguageServerProtocol *) +{ + QObject::connect(server->notifySignals(), + &QLspNotifySignals::receivedDidChangeWorkspaceFoldersNotification, this, + [server, this](const DidChangeWorkspaceFoldersParams ¶ms) { + const WorkspaceFoldersChangeEvent &event = params.event; + + const QList<WorkspaceFolder> &removed = event.removed; + QList<QByteArray> toRemove; + for (const WorkspaceFolder &folder : removed) { + toRemove.append(QQmlLSUtils::lspUriToQmlUrl(folder.uri)); + m_codeModel->removeDirectory(m_codeModel->url2Path( + QQmlLSUtils::lspUriToQmlUrl(folder.uri))); + } + m_codeModel->removeRootUrls(toRemove); + const QList<WorkspaceFolder> &added = event.added; + QList<QByteArray> toAdd; + QStringList pathsToAdd; + for (const WorkspaceFolder &folder : added) { + toAdd.append(QQmlLSUtils::lspUriToQmlUrl(folder.uri)); + pathsToAdd.append(m_codeModel->url2Path( + QQmlLSUtils::lspUriToQmlUrl(folder.uri))); + } + m_codeModel->addRootUrls(toAdd); + m_codeModel->addDirectoriesToIndex(pathsToAdd, server); + }); + + QObject::connect(server->notifySignals(), + &QLspNotifySignals::receivedDidChangeWatchedFilesNotification, this, + [this](const DidChangeWatchedFilesParams ¶ms) { + const QList<FileEvent> &changes = params.changes; + for (const FileEvent &change : changes) { + const QString filename = + m_codeModel->url2Path(QQmlLSUtils::lspUriToQmlUrl(change.uri)); + switch (FileChangeType(change.type)) { + case FileChangeType::Created: + // m_codeModel->addFile(filename); + break; + case FileChangeType::Changed: { + QFile file(filename); + if (file.open(QIODevice::ReadOnly)) + // m_modelManager->setFileContents(filename, file.readAll()); + break; + break; + } + case FileChangeType::Deleted: + // m_modelManager->removeFile(filename); + break; + } + } + // update due to dep changes... + }); + + QObject::connect(server, &QLanguageServer::clientInitialized, this, + &WorkspaceHandlers::clientInitialized); +} + +QString WorkspaceHandlers::name() const +{ + return u"Workspace"_s; +} + +void WorkspaceHandlers::setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &serverInfo) +{ + if (!clientInfo.capabilities.workspace + || !clientInfo.capabilities.workspace->value(u"workspaceFolders"_s).toBool(false)) + return; + WorkspaceFoldersServerCapabilities folders; + folders.supported = true; + folders.changeNotifications = true; + if (!serverInfo.capabilities.workspace) + serverInfo.capabilities.workspace = QJsonObject(); + serverInfo.capabilities.workspace->insert(u"workspaceFolders"_s, + QTypedJson::toJsonValue(folders)); +} + +void WorkspaceHandlers::clientInitialized(QLanguageServer *server) +{ + QLanguageServerProtocol *protocol = server->protocol(); + const auto clientInfo = server->clientInfo(); + QList<Registration> registrations; + if (clientInfo.capabilities.workspace + && clientInfo.capabilities.workspace + ->value(u"didChangeWatchedFiles"_s)[u"dynamicRegistration"_s] + .toBool(false)) { + const int watchAll = + int(WatchKind::Create) | int(WatchKind::Change) | int(WatchKind::Delete); + DidChangeWatchedFilesRegistrationOptions watchedFilesParams; + FileSystemWatcher qmlWatcher; + qmlWatcher.globPattern = QByteArray("*.{qml,js,mjs}"); + qmlWatcher.kind = watchAll; + FileSystemWatcher qmldirWatcher; + qmldirWatcher.globPattern = "qmldir"; + qmldirWatcher.kind = watchAll; + FileSystemWatcher qmltypesWatcher; + qmltypesWatcher.globPattern = QByteArray("*.qmltypes"); + qmltypesWatcher.kind = watchAll; + watchedFilesParams.watchers = QList<FileSystemWatcher>({ + std::move(qmlWatcher), + std::move(qmldirWatcher), + std::move(qmltypesWatcher) + }); + registrations.append(Registration { + // use ClientCapabilitiesInfo::WorkspaceDidChangeWatchedFiles as id too + ClientCapabilitiesInfo::WorkspaceDidChangeWatchedFiles, + ClientCapabilitiesInfo::WorkspaceDidChangeWatchedFiles, + QTypedJson::toJsonValue(watchedFilesParams) }); + } + + if (!registrations.isEmpty()) { + RegistrationParams params; + params.registrations = registrations; + protocol->requestRegistration( + params, + []() { + // successful registration + }, + [protocol](const ResponseError &err) { + LogMessageParams msg; + msg.message = QByteArray("registration of file udates failed, will miss file " + "changes done outside the editor due to error "); + msg.message.append(QString::number(err.code).toUtf8()); + if (!err.message.isEmpty()) + msg.message.append(" "); + msg.message.append(err.message); + msg.type = MessageType::Warning; + qCWarning(lspServerLog) << QString::fromUtf8(msg.message); + protocol->notifyLogMessage(msg); + }); + } + + QSet<QString> rootPaths; + if (std::holds_alternative<QByteArray>(clientInfo.rootUri)) { + QString path = m_codeModel->url2Path( + QQmlLSUtils::lspUriToQmlUrl(std::get<QByteArray>(clientInfo.rootUri))); + rootPaths.insert(path); + } else if (clientInfo.rootPath && std::holds_alternative<QByteArray>(*clientInfo.rootPath)) { + QString path = QString::fromUtf8(std::get<QByteArray>(*clientInfo.rootPath)); + rootPaths.insert(path); + } + + if (clientInfo.workspaceFolders + && std::holds_alternative<QList<WorkspaceFolder>>(*clientInfo.workspaceFolders)) { + for (const WorkspaceFolder &workspace : + std::as_const(std::get<QList<WorkspaceFolder>>(*clientInfo.workspaceFolders))) { + const QUrl workspaceUrl(QString::fromUtf8(QQmlLSUtils::lspUriToQmlUrl(workspace.uri))); + rootPaths.insert(workspaceUrl.toLocalFile()); + } + } + if (m_status == Status::Indexing) + m_codeModel->addDirectoriesToIndex(QStringList(rootPaths.begin(), rootPaths.end()), server); +} + +QT_END_NAMESPACE diff --git a/src/qmlls/qworkspace_p.h b/src/qmlls/qworkspace_p.h new file mode 100644 index 0000000000..b703249c9b --- /dev/null +++ b/src/qmlls/qworkspace_p.h @@ -0,0 +1,43 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWORKSPACE_P_H +#define QWORKSPACE_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qqmlcodemodel_p.h" +#include "qlanguageserver_p.h" + +QT_BEGIN_NAMESPACE + +class WorkspaceHandlers : public QLanguageServerModule +{ + Q_OBJECT +public: + enum class Status { NoIndex, Indexing }; + WorkspaceHandlers(QmlLsp::QQmlCodeModel *codeModel) : m_codeModel(codeModel) { } + QString name() const override; + void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override; + void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo, + QLspSpecification::InitializeResult &) override; +public Q_SLOTS: + void clientInitialized(QLanguageServer *); + +private: + QmlLsp::QQmlCodeModel *m_codeModel = nullptr; + Status m_status = Status::NoIndex; +}; + +QT_END_NAMESPACE + +#endif // QWORKSPACE_P_H |