aboutsummaryrefslogtreecommitdiffstats
path: root/src/qmlls
diff options
context:
space:
mode:
Diffstat (limited to 'src/qmlls')
-rw-r--r--src/qmlls/CMakeLists.txt46
-rw-r--r--src/qmlls/qdochtmlparser.cpp227
-rw-r--r--src/qmlls/qdochtmlparser_p.h43
-rw-r--r--src/qmlls/qlanguageserver.cpp384
-rw-r--r--src/qmlls/qlanguageserver_p.h93
-rw-r--r--src/qmlls/qlanguageserver_p_p.h53
-rw-r--r--src/qmlls/qlspcustomtypes_p.h56
-rw-r--r--src/qmlls/qqmlbasemodule_p.h267
-rw-r--r--src/qmlls/qqmlcodemodel.cpp927
-rw-r--r--src/qmlls/qqmlcodemodel_p.h178
-rw-r--r--src/qmlls/qqmlcompletioncontextstrings.cpp50
-rw-r--r--src/qmlls/qqmlcompletioncontextstrings_p.h62
-rw-r--r--src/qmlls/qqmlcompletionsupport.cpp196
-rw-r--r--src/qmlls/qqmlcompletionsupport_p.h61
-rw-r--r--src/qmlls/qqmlfindusagessupport.cpp81
-rw-r--r--src/qmlls/qqmlfindusagessupport_p.h47
-rw-r--r--src/qmlls/qqmlformatting.cpp95
-rw-r--r--src/qmlls/qqmlformatting_p.h46
-rw-r--r--src/qmlls/qqmlgotodefinitionsupport.cpp70
-rw-r--r--src/qmlls/qqmlgotodefinitionsupport_p.h49
-rw-r--r--src/qmlls/qqmlgototypedefinitionsupport.cpp74
-rw-r--r--src/qmlls/qqmlgototypedefinitionsupport_p.h50
-rw-r--r--src/qmlls/qqmlhighlightsupport.cpp212
-rw-r--r--src/qmlls/qqmlhighlightsupport_p.h96
-rw-r--r--src/qmlls/qqmlhover.cpp82
-rw-r--r--src/qmlls/qqmlhover_p.h48
-rw-r--r--src/qmlls/qqmllanguageserver.cpp185
-rw-r--r--src/qmlls/qqmllanguageserver_p.h83
-rw-r--r--src/qmlls/qqmllintsuggestions.cpp379
-rw-r--r--src/qmlls/qqmllintsuggestions_p.h72
-rw-r--r--src/qmlls/qqmllscompletion.cpp1889
-rw-r--r--src/qmlls/qqmllscompletion_p.h228
-rw-r--r--src/qmlls/qqmllscompletionplugin.cpp4
-rw-r--r--src/qmlls/qqmllscompletionplugin_p.h42
-rw-r--r--src/qmlls/qqmllshelpplugininterface.cpp4
-rw-r--r--src/qmlls/qqmllshelpplugininterface_p.h64
-rw-r--r--src/qmlls/qqmllshelputils.cpp255
-rw-r--r--src/qmlls/qqmllshelputils_p.h59
-rw-r--r--src/qmlls/qqmllsplugin_p.h42
-rw-r--r--src/qmlls/qqmllsutils.cpp2186
-rw-r--r--src/qmlls/qqmllsutils_p.h274
-rw-r--r--src/qmlls/qqmlrangeformatting.cpp142
-rw-r--r--src/qmlls/qqmlrangeformatting_p.h44
-rw-r--r--src/qmlls/qqmlrenamesymbolsupport.cpp119
-rw-r--r--src/qmlls/qqmlrenamesymbolsupport_p.h44
-rw-r--r--src/qmlls/qqmlsemantictokens.cpp771
-rw-r--r--src/qmlls/qqmlsemantictokens_p.h131
-rw-r--r--src/qmlls/qtextblock.cpp101
-rw-r--r--src/qmlls/qtextblock_p.h73
-rw-r--r--src/qmlls/qtextcursor.cpp122
-rw-r--r--src/qmlls/qtextcursor_p.h72
-rw-r--r--src/qmlls/qtextdocument.cpp124
-rw-r--r--src/qmlls/qtextdocument_p.h78
-rw-r--r--src/qmlls/qtextsynchronization.cpp99
-rw-r--r--src/qmlls/qtextsynchronization_p.h43
-rw-r--r--src/qmlls/qworkspace.cpp172
-rw-r--r--src/qmlls/qworkspace_p.h43
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 &params) {
+ 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 &params,
+ 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 &params, 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 &parameters, 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 &params,
+ 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 &parameters,
+ RequestResponse &&response) {
+ requestHandler(parameters, std::move(response));
+ };
+ return handler;
+}
+
+template<typename RequestType>
+void QQmlBaseModule<RequestType>::requestHandler(const RequestParameters &parameters,
+ 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 &registeredTokens();
+ const RegisteredSemanticTokens &registeredTokens() 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 &params,
+ 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 &params, 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 &params,
+ 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 &params,
+ 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 &params,
+ 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 &registeredTokens = 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 &registeredTokens = 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 &registeredTokens = 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 &params) {
+ 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 &params,
+ 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 &currentItem,
+ 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 &currentItem, 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 &currentItem,
+ 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 &currentItem,
+ 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 &currentItem,
+ 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 &currentItem,
+ const QQmlLSCompletionPosition &positionInfo,
+ BackInsertIterator result) const
+{
+ Q_UNUSED(currentItem);
+ if (positionInfo.cursorPosition.base().isEmpty()) {
+ suggestJSExpressionCompletion(positionInfo.itemAtPosition, result);
+ return;
+ }
+}
+
+void QQmlLSCompletion::insideCallExpression(const DomItem &currentItem,
+ 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 &currentItem,
+ 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 &currentItem,
+ 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 &currentItem,
+ 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 &currentClause,
+ 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 &currentItem,
+ const CompletionContextStrings &contextStrings) const
+{
+ QList<CompletionItem> result;
+ collectCompletions(currentItem, contextStrings, std::back_inserter(result));
+ return result;
+}
+
+void QQmlLSCompletion::collectCompletions(const DomItem &currentItem,
+ 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 &currentItem,
+ 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 &currentItem, 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 &currentClause,
+ 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 &currentItem, 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 &currentItem,
+ 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 &currentItem,
+ const QQmlLSCompletionPosition &positionInfo,
+ BackInsertIterator it) const;
+ void insideBindingCompletion(const DomItem &currentItem,
+ const QQmlLSCompletionPosition &positionInfo,
+ BackInsertIterator it) const;
+ void insideImportCompletion(const DomItem &currentItem,
+ const QQmlLSCompletionPosition &positionInfo,
+ BackInsertIterator it) const;
+ void insideQmlFileCompletion(const DomItem &currentItem,
+ const QQmlLSCompletionPosition &positionInfo,
+ BackInsertIterator it) const;
+ void suggestContinueAndBreakStatementIfNeeded(const DomItem &itemAtPosition,
+ BackInsertIterator it) const;
+ void insideScriptLiteralCompletion(const DomItem &currentItem,
+ const QQmlLSCompletionPosition &positionInfo,
+ BackInsertIterator it) const;
+ void insideCallExpression(const DomItem &currentItem,
+ const QQmlLSCompletionPosition &positionInfo,
+ BackInsertIterator it) const;
+ void insideIfStatement(const DomItem &currentItem, const QQmlLSCompletionPosition &positionInfo,
+ BackInsertIterator it) const;
+ void insideReturnStatement(const DomItem &currentItem,
+ const QQmlLSCompletionPosition &positionInfo,
+ BackInsertIterator it) const;
+ void insideWhileStatement(const DomItem &currentItem,
+ 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 &current, 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> &regions,
+ 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> &regions,
+ 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 &params)
+{
+ m_codeModel->closeOpenFile(QQmlLSUtils::lspUriToQmlUrl(params.textDocument.uri));
+}
+
+void TextSynchronization::didOpenTextDocument(const DidOpenTextDocumentParams &params)
+{
+ 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 &params)
+{
+ 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 &params);
+ void didDidChangeTextDocument(const QLspSpecification::DidChangeTextDocumentParams &params);
+ void didCloseTextDocument(const QLspSpecification::DidCloseTextDocumentParams &params);
+
+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 &params) {
+ 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 &params) {
+ 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