aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/CMakeLists.txt3
-rw-r--r--tools/qmlls/CMakeLists.txt29
-rw-r--r--tools/qmlls/qlanguageserver.cpp434
-rw-r--r--tools/qmlls/qlanguageserver.h128
-rw-r--r--tools/qmlls/qlanguageserver_p.h88
-rw-r--r--tools/qmlls/qmllanguageservertool.cpp213
-rw-r--r--tools/qmlls/qmllintsuggestions.cpp221
-rw-r--r--tools/qmlls/qmllintsuggestions.h71
-rw-r--r--tools/qmlls/qqmlcodemodel.cpp610
-rw-r--r--tools/qmlls/qqmlcodemodel.h149
-rw-r--r--tools/qmlls/qqmllanguageserver.cpp145
-rw-r--r--tools/qmlls/qqmllanguageserver.h82
-rw-r--r--tools/qmlls/textblock.cpp137
-rw-r--r--tools/qmlls/textblock.h97
-rw-r--r--tools/qmlls/textcursor.cpp158
-rw-r--r--tools/qmlls/textcursor.h96
-rw-r--r--tools/qmlls/textdocument.cpp152
-rw-r--r--tools/qmlls/textdocument.h102
-rw-r--r--tools/qmlls/textsynchronization.cpp132
-rw-r--r--tools/qmlls/textsynchronization.h68
20 files changed, 3115 insertions, 0 deletions
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 1b7b4641d1..f3170a9d4d 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -13,6 +13,9 @@ if(QT_FEATURE_qml_devtools)
add_subdirectory(qmlimportscanner)
add_subdirectory(qmlformat)
add_subdirectory(qmltc)
+ if (TARGET Qt::LanguageServerPrivate)
+ add_subdirectory(qmlls)
+ endif()
endif()
if(QT_FEATURE_qml_devtools AND QT_FEATURE_xmlstreamwriter)
# special case begin
diff --git a/tools/qmlls/CMakeLists.txt b/tools/qmlls/CMakeLists.txt
new file mode 100644
index 0000000000..ac42d239de
--- /dev/null
+++ b/tools/qmlls/CMakeLists.txt
@@ -0,0 +1,29 @@
+#####################################################################
+## qmlls Tool:
+#####################################################################
+
+qt_get_tool_target_name(target_name qmlls)
+qt_internal_add_tool(${target_name}
+ TARGET_DESCRIPTION "QML languageserver"
+ TOOLS_TARGET Qml # special case
+ SOURCES
+ qlanguageserver.h qlanguageserver_p.h qlanguageserver.cpp
+ qqmllanguageserver.h qqmllanguageserver.cpp
+ qmllanguageservertool.cpp
+ textblock.h textblock.cpp
+ textcursor.h textcursor.cpp
+ textcursor.cpp textcursor.h
+ textdocument.cpp textdocument.h
+ qmllintsuggestions.h qmllintsuggestions.cpp
+ textsynchronization.cpp textsynchronization.h
+ qqmlcodemodel.h qqmlcodemodel.cpp
+ ../shared/qqmltoolingsettings.h
+ ../shared/qqmltoolingsettings.cpp
+ PUBLIC_LIBRARIES
+ Qt::QmlPrivate
+ Qt::CorePrivate
+ Qt::QmlDomPrivate
+ Qt::LanguageServerPrivate
+ Qt::QmlLintPrivate
+)
+qt_internal_return_unless_building_tools()
diff --git a/tools/qmlls/qlanguageserver.cpp b/tools/qmlls/qlanguageserver.cpp
new file mode 100644
index 0000000000..c7046ec5e8
--- /dev/null
+++ b/tools/qmlls/qlanguageserver.cpp
@@ -0,0 +1,434 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qlanguageserver_p.h"
+#include <QtLanguageServer/private/qlspnotifysignals_p.h>
+#include <QtJsonRpc/private/qjsonrpcprotocol_p_p.h>
+
+QT_BEGIN_NAMESPACE
+
+using namespace QLspSpecification;
+
+Q_LOGGING_CATEGORY(lspServerLog, "qt.languageserver.server")
+
+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);
+}
+
+QLanguageServer::~QLanguageServer() { }
+
+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);
+ 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"_qs
+ .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)"_qs.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 : qAsConst(d->modules))
+ module->setupCapabilities(clientInfo, serverInfo);
+}
+
+const QLspSpecification::InitializeParams &QLanguageServer::clientInfo() const
+{
+ const Q_D(QLanguageServer);
+ switch (d->runStatus) {
+ case RunStatus::NotSetup:
+ case RunStatus::SettingUp:
+ case RunStatus::DidSetup:
+ case RunStatus::Initializing:
+ if (int(runStatus()) < int(RunStatus::DidInitialize))
+ qCWarning(lspServerLog) << "asked for Language Server clientInfo before initialization";
+ break;
+ case RunStatus::DidInitialize:
+ case RunStatus::WaitPending:
+ case RunStatus::Stopping:
+ case RunStatus::Stopped:
+ break;
+ }
+ return d->clientInfo;
+}
+
+const QLspSpecification::InitializeResult &QLanguageServer::serverInfo() const
+{
+ const Q_D(QLanguageServer);
+ switch (d->runStatus) {
+ case RunStatus::NotSetup:
+ case RunStatus::SettingUp:
+ case RunStatus::DidSetup:
+ case RunStatus::Initializing:
+ qCWarning(lspServerLog) << "asked for Language Server serverInfo before initialization";
+ break;
+ case RunStatus::DidInitialize:
+ case RunStatus::WaitPending:
+ case RunStatus::Stopping:
+ case RunStatus::Stopped:
+ break;
+ }
+ return d->serverInfo;
+}
+
+void QLanguageServer::receiveData(const QByteArray &d)
+{
+ protocol()->receiveData(d);
+}
+
+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"_qs
+ .toUtf8());
+ else
+ response.sendErrorResponse(
+ int(QLspSpecification::ErrorCodes::InvalidRequest),
+ u"Received multiple initialization requests"_qs.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/tools/qmlls/qlanguageserver.h b/tools/qmlls/qlanguageserver.h
new file mode 100644
index 0000000000..218658a90d
--- /dev/null
+++ b/tools/qmlls/qlanguageserver.h
@@ -0,0 +1,128 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#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);
+ ~QLanguageServer();
+ enum class RunStatus {
+ NotSetup,
+ SettingUp,
+ DidSetup,
+ Initializing,
+ DidInitialize, // normal state of execution
+ WaitPending,
+ Stopping,
+ Stopped
+ };
+
+ 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 slots:
+ void receiveData(const QByteArray &d);
+signals:
+ void runStatusChanged(RunStatus);
+ void clientInitialized(QLanguageServer *server);
+ void shutdown();
+ void exit();
+ void lifecycleError();
+
+private:
+ void registerMethods(QJsonRpc::TypedRpc &typedRpc);
+ void executeShutdown();
+ Q_DISABLE_COPY(QLanguageServer)
+ Q_DECLARE_PRIVATE(QLanguageServer)
+};
+
+QT_END_NAMESPACE
+
+#endif // QLANGUAGESERVER_P_H
diff --git a/tools/qmlls/qlanguageserver_p.h b/tools/qmlls/qlanguageserver_p.h
new file mode 100644
index 0000000000..2e170c9b87
--- /dev/null
+++ b/tools/qmlls/qlanguageserver_p.h
@@ -0,0 +1,88 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#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.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/tools/qmlls/qmllanguageservertool.cpp b/tools/qmlls/qmllanguageservertool.cpp
new file mode 100644
index 0000000000..93c50c8614
--- /dev/null
+++ b/tools/qmlls/qmllanguageservertool.cpp
@@ -0,0 +1,213 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qqmllanguageserver.h"
+#include <QtCore/qdebug.h>
+#include <QtCore/qfile.h>
+#include <QtCore/qdir.h>
+#include <QtCore/qfileinfo.h>
+#include <QtCore/qcoreapplication.h>
+#include "../shared/qqmltoolingsettings.h"
+#include <QtCore/qdiriterator.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qjsonarray.h>
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qmutex.h>
+#include <QtCore/QMutexLocker>
+#include <QtCore/qscopedpointer.h>
+#include <QtCore/qrunnable.h>
+#include <QtCore/qthreadpool.h>
+#include <QtCore/qtimer.h>
+
+#include <QtQmlCompiler/private/qqmljsresourcefilemapper_p.h>
+#include <QtQmlCompiler/private/qqmljscompiler_p.h>
+#include <QtQmlCompiler/private/qqmljslogger_p.h>
+#include <QtQmlCompiler/private/qqmljsscope_p.h>
+#include <QtQmlCompiler/private/qqmljsimporter_p.h>
+#if QT_CONFIG(commandlineparser)
+# include <QtCore/qcommandlineparser.h>
+#endif
+
+#ifndef QT_BOOTSTRAPPED
+# include <QtCore/qlibraryinfo.h>
+#endif
+
+#include "qlanguageserver_p.h"
+
+#include <iostream>
+#ifdef Q_OS_WIN32
+# include <fcntl.h>
+# include <io.h>
+#endif
+
+using namespace QmlLsp;
+
+QFile *logFile = nullptr;
+QBasicMutex *logFileLock = nullptr;
+
+class StdinReader : public QObject
+{
+ Q_OBJECT
+public:
+ void run()
+ {
+ auto guard = qScopeGuard([this]() { emit eof(); });
+ char data[256];
+ auto buffer = static_cast<char *>(data);
+ while (std::cin.get(buffer[0])) { // should poll/select and process events
+ const int read = std::cin.readsome(buffer + 1, 255) + 1;
+ emit receivedData(QByteArray(buffer, read));
+ }
+ }
+signals:
+ void receivedData(const QByteArray &data);
+ void eof();
+};
+
+// To debug:
+//
+// * simple logging can be redirected to a file
+// passing -l <file> to the qmlls command
+//
+// * more complex debugging can use named pipes:
+//
+// mkfifo qmllsIn
+// mkfifo qmllsOut
+//
+// this together with a qmllsEcho script that can be defined as
+//
+// #!/bin/sh
+// cat -u < ~/qmllsOut &
+// cat -u > ~/qmllsIn
+//
+// allows to use qmllsEcho as lsp server, and still easily start
+// it in a terminal
+//
+// qmlls < ~/qmllsIn > ~/qmllsOut
+//
+// * statup can be slowed down to have the time to attach via the
+// -w <nSeconds> flag.
+
+int main(int argv, char *argc[])
+{
+#ifdef Q_OS_WIN32
+ // windows does not open stdin/stdout in binary mode by default
+ int err = _setmode(_fileno(stdout), _O_BINARY);
+ if (err == -1)
+ perror("Cannot set mode for stdout");
+ err = _setmode(_fileno(stdin), _O_BINARY);
+ if (err == -1)
+ perror("Cannot set mode for stdin");
+#endif
+
+ qSetGlobalQHashSeed(0);
+ QCoreApplication app(argv, argc);
+ QCoreApplication::setApplicationName("qmllanguageserver");
+ QCoreApplication::setApplicationVersion(QT_VERSION_STR);
+#if QT_CONFIG(commandlineparser)
+ QCommandLineParser parser;
+ QQmlToolingSettings settings(QLatin1String("qmllanguageserver"));
+ parser.setApplicationDescription(QLatin1String(R"(QML languageserver
+)"));
+
+
+ QCommandLineOption waitOption(QStringList() << "w"
+ << "wait",
+ QLatin1String("Waits the given number of seconds before startup"),
+ QLatin1String("waitSeconds"));
+ parser.addOption(waitOption);
+
+ QCommandLineOption verboseOption(
+ QStringList() << "v"
+ << "verbose",
+ QLatin1String("Outputs extra information on the operations being performed"));
+ parser.addOption(verboseOption);
+
+ QCommandLineOption logFileOption(QStringList() << "l"
+ << "log-file",
+ QLatin1String("Writes logging to the given file"),
+ QLatin1String("logFile"));
+ parser.addOption(logFileOption);
+
+ parser.process(app);
+ if (parser.isSet(logFileOption)) {
+ QString fileName = parser.value(logFileOption);
+ qInfo() << "will log to" << fileName;
+ logFile = new QFile(fileName);
+ logFileLock = new QMutex;
+ logFile->open(QFile::WriteOnly | QFile::Truncate | QFile::Text);
+ qInstallMessageHandler([](QtMsgType t, const QMessageLogContext &, const QString &msg) {
+ QMutexLocker l(logFileLock);
+ logFile->write(QString::number(int(t)).toUtf8());
+ logFile->write(" ");
+ logFile->write(msg.toUtf8());
+ logFile->write("\n");
+ logFile->flush();
+ });
+ }
+ if (parser.isSet(verboseOption))
+ QLoggingCategory::setFilterRules("qt.languageserver*.debug=true\n");
+ if (parser.isSet(waitOption)) {
+ int waitSeconds = parser.value(waitOption).toInt();
+ if (waitSeconds > 0)
+ qDebug() << "waiting";
+ QThread::sleep(waitSeconds);
+ qDebug() << "starting";
+ }
+#endif
+ QMutex writeMutex;
+ QQmlLanguageServer qmlServer([&writeMutex](const QByteArray &data) {
+ QMutexLocker l(&writeMutex);
+ std::cout.write(data.constData(), data.length());
+ std::cout.flush();
+ });
+ StdinReader *r = new StdinReader;
+ QObject::connect(r, &StdinReader::receivedData, qmlServer.server(),
+ &QLanguageServer::receiveData);
+ QObject::connect(r, &StdinReader::eof, &app, []() {
+ QTimer::singleShot(100, []() {
+ QCoreApplication::processEvents();
+ QCoreApplication::exit();
+ });
+ });
+ QThreadPool::globalInstance()->start([r]() { r->run(); });
+ app.exec();
+ return qmlServer.returnValue();
+}
+
+#include "qmllanguageservertool.moc"
diff --git a/tools/qmlls/qmllintsuggestions.cpp b/tools/qmlls/qmllintsuggestions.cpp
new file mode 100644
index 0000000000..d9f68b1ee0
--- /dev/null
+++ b/tools/qmlls/qmllintsuggestions.cpp
@@ -0,0 +1,221 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qmllintsuggestions.h"
+#include <QtLanguageServer/private/qlanguageserverspec_p.h>
+#include <QtQmlLint/private/qqmllinter_p.h>
+#include <QtQmlCompiler/private/qqmljslogger_p.h>
+#include <QtCore/qlibraryinfo.h>
+#include <QtCore/qtimer.h>
+#include <QtCore/qdebug.h>
+
+using namespace QLspSpecification;
+using namespace QQmlJS::Dom;
+
+Q_LOGGING_CATEGORY(lintLog, "qt.languageserver.lint")
+
+QT_BEGIN_NAMESPACE
+namespace QmlLsp {
+
+static DiagnosticSeverity severityFromString(const QStringView &str)
+{
+ if (str.compare(u"debug", Qt::CaseInsensitive) == 0)
+ return DiagnosticSeverity::Hint;
+ else if (str.compare(u"warning", Qt::CaseInsensitive) == 0)
+ return DiagnosticSeverity::Warning;
+ else if (str.compare(u"critical", Qt::CaseInsensitive) == 0)
+ return DiagnosticSeverity::Error;
+ else if (str.compare(u"fatal", Qt::CaseInsensitive) == 0)
+ return DiagnosticSeverity::Error;
+ else if (str.compare(u"info", Qt::CaseInsensitive) == 0)
+ return DiagnosticSeverity::Information;
+ else
+ return DiagnosticSeverity::Information;
+}
+
+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;
+}
+
+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);
+}
+
+void QmlLintSuggestions::diagnose(const QByteArray &uri)
+{
+ const int maxInvalidMsec = 4000;
+ qCDebug(lintLog) << "diagnose start";
+ QmlLsp::OpenDocumentSnapshot snapshot = m_codeModel->snapshotByUri(uri);
+ QList<Diagnostic> diagnostics;
+ std::optional<int> version;
+ DomItem doc;
+ {
+ QMutexLocker l(&m_mutex);
+ LastLintUpdate &lastUpdate = m_lastUpdate[uri];
+ if (lastUpdate.version && *lastUpdate.version == version) {
+ qCDebug(lspServerLog) << "skipped update of " << uri << "unchanged valid doc";
+ return;
+ }
+ if (snapshot.validDocVersion
+ && (!lastUpdate.version || *snapshot.validDocVersion > *lastUpdate.version)) {
+ doc = snapshot.validDoc;
+ version = snapshot.validDocVersion;
+ } else if (snapshot.docVersion
+ && (!lastUpdate.version || *snapshot.docVersion > *lastUpdate.version)) {
+ if (!lastUpdate.version || !snapshot.validDocVersion
+ || (lastUpdate.invalidUpdatesSince
+ && lastUpdate.invalidUpdatesSince->msecsTo(QDateTime::currentDateTime())
+ > maxInvalidMsec)) {
+ doc = snapshot.doc;
+ version = snapshot.docVersion;
+ } else if (!lastUpdate.invalidUpdatesSince) {
+ lastUpdate.invalidUpdatesSince = QDateTime::currentDateTime();
+ QTimer::singleShot(maxInvalidMsec, Qt::VeryCoarseTimer, this,
+ [this, uri]() { diagnose(uri); });
+ }
+ }
+ if (doc) {
+ // update immediately, and do not keep track of sent version, thus in extreme cases sent
+ // updates could be out of sync
+ lastUpdate.version = version;
+ lastUpdate.invalidUpdatesSince = {};
+ }
+ }
+ QString fileContents;
+ if (doc) {
+ qCDebug(lintLog) << "has doc, do real lint";
+ QStringList imports;
+ imports.append(QLibraryInfo::path(QLibraryInfo::QmlImportsPath));
+ // add m_server->clientInfo().rootUri & co?
+ bool useAbsolutePath = false;
+ bool silent = true;
+ QString filename = doc.canonicalFilePath();
+ fileContents = doc.field(Fields::code).value().toString();
+ QJsonArray json;
+ QStringList qmltypesFiles;
+ QStringList resourceFiles;
+ QMap<QString, QQmlJSLogger::Option> options;
+
+ QQmlLinter linter(imports, useAbsolutePath);
+
+ linter.lintFile(filename, &fileContents, silent, &json, imports, qmltypesFiles,
+ resourceFiles, options);
+ auto addLength = [&fileContents](Position &position, int startOffset, int 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;
+ }
+ };
+
+ auto jsonToDiagnostic = [&addLength](const QJsonValue &message) {
+ Diagnostic diagnostic;
+ diagnostic.severity = severityFromString(message[u"type"].toString());
+ Range &range = diagnostic.range;
+ Position &position = range.start;
+ position.line = message[u"line"].toInt(1) - 1;
+ position.character = message[u"column"].toInt(1) - 1;
+ range.end = position;
+ addLength(range.end, message[u"charOffset"].toInt(), message[u"length"].toInt());
+ diagnostic.message = message[u"message"].toString().toUtf8();
+ diagnostic.source = QByteArray("qmllint");
+ return diagnostic;
+ };
+ doc.iterateErrors(
+ [&diagnostics, &addLength](DomItem, 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;
+ addLength(range.end, location.offset, location.length);
+ diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
+ diagnostic.source = "domParsing";
+ diagnostic.message = msg.message.toUtf8();
+ diagnostics.append(diagnostic);
+ return true;
+ },
+ true);
+ for (const auto &results : qAsConst(json)) {
+ if (results[u"filename"].toString() == filename) {
+ for (const auto &message : results[u"warnings"].toArray())
+ diagnostics.append(jsonToDiagnostic(message));
+ }
+ }
+ }
+ PublishDiagnosticsParams diagnosticParams;
+ diagnosticParams.uri = uri;
+ diagnosticParams.diagnostics = diagnostics;
+ diagnosticParams.version = version;
+
+ m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
+ qCDebug(lintLog) << "lint" << QString::fromUtf8(uri) << "found"
+ << diagnosticParams.diagnostics.size() << "issues"
+ << QTypedJson::toJsonValue(diagnosticParams);
+}
+
+} // namespace QmlLsp
+QT_END_NAMESPACE
diff --git a/tools/qmlls/qmllintsuggestions.h b/tools/qmlls/qmllintsuggestions.h
new file mode 100644
index 0000000000..b3dfbd1ae4
--- /dev/null
+++ b/tools/qmlls/qmllintsuggestions.h
@@ -0,0 +1,71 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef QMLLINTSUGGESTIONS_H
+#define QMLLINTSUGGESTIONS_H
+
+#include "qlanguageserver.h"
+#include "qqmlcodemodel.h"
+
+#include <optional>
+
+QT_BEGIN_NAMESPACE
+namespace QmlLsp {
+struct LastLintUpdate
+{
+ std::optional<int> version;
+ std::optional<QDateTime> invalidUpdatesSince;
+};
+
+class QmlLintSuggestions : public QObject
+{
+ Q_OBJECT
+public:
+ QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel);
+public slots:
+ void diagnose(const QByteArray &uri);
+
+private:
+ QMutex m_mutex;
+ QHash<QByteArray, LastLintUpdate> m_lastUpdate;
+ QLanguageServer *m_server;
+ QmlLsp::QQmlCodeModel *m_codeModel;
+};
+} // namespace QmlLsp
+QT_END_NAMESPACE
+#endif // QMLLINTSUGGESTIONS_H
diff --git a/tools/qmlls/qqmlcodemodel.cpp b/tools/qmlls/qqmlcodemodel.cpp
new file mode 100644
index 0000000000..77e0298180
--- /dev/null
+++ b/tools/qmlls/qqmlcodemodel.cpp
@@ -0,0 +1,610 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qqmllanguageserver.h"
+#include "qqmlcodemodel.h"
+#include <QtCore/qfileinfo.h>
+#include <QtCore/qdir.h>
+#include <QtCore/qthreadpool.h>
+#include <QtQmlDom/private/qqmldomtop_p.h>
+#include "textdocument.h"
+
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+
+namespace QmlLsp {
+
+Q_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel")
+
+using namespace QQmlJS::Dom;
+
+/*!
+\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 snapshotByUri() returns an OpenDocumentSnapshot of an open document. Form it you can get the
+ document, its latest valid version, scope,... and 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 openDocumentByUri() 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 s 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 usially 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 bein 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.
+
+An initial implementation allowed one to register a callback to be called when a given open document
+had some chosen parts of the snapshot up to date. But I did not need anythign more that the
+updatedSnapshot() signal, so that has been removed, but something like that might become useful in
+the future.
+*/
+
+QQmlCodeModel::QQmlCodeModel(QObject *parent)
+ : QObject { parent },
+ m_currentEnv(std::make_shared<DomEnvironment>(QStringList(),
+ DomEnvironment::Option::SingleThreaded)),
+ m_validEnv(std::make_shared<DomEnvironment>(QStringList(),
+ DomEnvironment::Option::SingleThreaded))
+{
+}
+
+OpenDocumentSnapshot QQmlCodeModel::snapshotByUri(const QByteArray &uri)
+{
+ return openDocumentByUri(uri).snapshot;
+}
+
+int QQmlCodeModel::indexEvalProgress() const
+{
+ // should be called with acquired mutex
+ const int dirCost = 10;
+ int costToDo = 1;
+ for (const ToIndex &el : qAsConst(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_nUpdateInProgress = 0;
+ m_toIndex.clear();
+ m_indexInProgressCost = 0;
+ m_indexDoneCost = 0;
+}
+
+void QQmlCodeModel::indexSendProgress(int progress)
+{
+ if (progress <= m_lastIndexProgress)
+ return;
+ m_lastIndexProgress = progress;
+ // to do: send progress
+}
+
+bool QQmlCodeModel::indexCancelled()
+{
+ 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({ "*.qml", "*.js", "*.mjs" }), 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();
+ int iFile = 0;
+ for (const QString &file : qmljs) {
+ if (indexCancelled())
+ return;
+ QString fPath = dir.filePath(file);
+ QFileInfo fInfo(fPath);
+ QString cPath = fInfo.canonicalFilePath();
+ if (!cPath.isEmpty()) {
+ bool isNew = false;
+ newCurrent.loadFile(cPath, fPath,
+ [&isNew](Path, DomItem &oldValue, DomItem &newValue) {
+ if (oldValue != newValue)
+ isNew = true;
+ },
+ {});
+ newCurrent.loadPendingDependencies();
+ if (isNew) {
+ newCurrent.commitToBase();
+ m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
+ }
+ }
+ ++iFile;
+ {
+ QMutexLocker l(&m_mutex);
+ ++m_indexDoneCost;
+ --m_indexInProgressCost;
+ progress = indexEvalProgress();
+ }
+ indexSendProgress(progress);
+ }
+}
+
+void QQmlCodeModel::addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server)
+{
+ Q_UNUSED(server);
+ // to do 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);
+ auto it = m_toIndex.begin();
+ auto end = m_toIndex.end();
+ while (it != 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;
+ }
+ 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::uri2Path(const QByteArray &uri, UriLookup options)
+{
+ QString res;
+ {
+ QMutexLocker l(&m_mutex);
+ res = m_uri2path.value(uri);
+ }
+ if (!res.isEmpty() && options == UriLookup::Caching)
+ return res;
+ QUrl url(QString::fromUtf8(uri));
+ QFileInfo f(url.toLocalFile());
+ QString cPath = f.canonicalFilePath();
+ if (cPath.isEmpty())
+ cPath = f.filePath();
+ {
+ QMutexLocker l(&m_mutex);
+ if (!res.isEmpty() && res != cPath)
+ m_path2uri.remove(res);
+ m_uri2path.insert(uri, cPath);
+ m_path2uri.insert(cPath, uri);
+ }
+ return cPath;
+}
+
+void QQmlCodeModel::newOpenFile(const QByteArray &uri, int version, const QString &docText)
+{
+ {
+ QMutexLocker l(&m_mutex);
+ auto &openDoc = m_openDocuments[uri];
+ if (!openDoc.textDocument)
+ openDoc.textDocument = std::make_shared<Utils::TextDocument>();
+ QMutexLocker l2(openDoc.textDocument->mutex());
+ openDoc.textDocument->setVersion(version);
+ openDoc.textDocument->setPlainText(docText);
+ }
+ addOpenToUpdate(uri);
+ openNeedUpdate();
+}
+
+OpenDocument QQmlCodeModel::openDocumentByUri(const QByteArray &uri)
+{
+ QMutexLocker l(&m_mutex);
+ return m_openDocuments.value(uri);
+}
+
+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";
+}
+
+DomItem QQmlCodeModel::validDocForUpdate(DomItem &item)
+{
+ if (item.field(Fields::isValid).value().toBool(false)) {
+ if (auto envPtr = m_validEnv.ownerAs<DomEnvironment>()) {
+ switch (item.fileObject().internalKind()) {
+ case DomType::QmlFile:
+ envPtr->addQmlFile(item.fileObject().ownerAs<QmlFile>());
+ break;
+ case DomType::JsFile:
+ envPtr->addJsFile(item.fileObject().ownerAs<JsFile>());
+ break;
+ default:
+ qCWarning(lspServerLog)
+ << "Unexpected file type " << item.fileObject().internalKindStr();
+ return DomItem();
+ }
+ return m_validEnv.path(item.canonicalPath());
+ }
+ }
+ return DomItem();
+}
+
+void QQmlCodeModel::newDocForOpenFile(const QByteArray &uri, int version, const QString &docText)
+{
+ qCDebug(codeModelLog) << "updating doc" << uri << "to version" << version << "("
+ << docText.length() << "chars)";
+ DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
+ QString fPath = uri2Path(uri, UriLookup::ForceLookup);
+ Path p;
+ newCurrent.loadFile(
+ fPath, fPath, docText, QDateTime::currentDateTimeUtc(),
+ [&p](Path, DomItem &, DomItem &newValue) { p = newValue.fileObject().canonicalPath(); },
+ {});
+ newCurrent.loadPendingDependencies();
+ if (p) {
+ newCurrent.commitToBase();
+ DomItem item = m_currentEnv.path(p);
+ DomItem vDoc = validDocForUpdate(item);
+ {
+ QMutexLocker l(&m_mutex);
+ OpenDocument &doc = m_openDocuments[uri];
+ if (!doc.textDocument) {
+ qCWarning(lspServerLog)
+ << "ignoring update to closed document" << QString::fromUtf8(uri);
+ return;
+ } else {
+ QMutexLocker l(doc.textDocument->mutex());
+ if (doc.textDocument->version() && *doc.textDocument->version() > version) {
+ qCWarning(lspServerLog)
+ << "docUpdate: version" << version << "of document"
+ << QString::fromUtf8(uri) << "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) << "skippig update of current doc to obsolete version"
+ << version << "of document" << QString::fromUtf8(uri);
+ }
+ if (vDoc) {
+ if (!doc.snapshot.validDocVersion || *doc.snapshot.validDocVersion < version) {
+ doc.snapshot.validDocVersion = version;
+ doc.snapshot.validDoc = vDoc;
+ } else {
+ qCWarning(lspServerLog) << "skippig update of valid doc to obsolete version"
+ << version << "of document" << QString::fromUtf8(uri);
+ }
+ } else {
+ qCWarning(lspServerLog)
+ << "avoid update of validDoc to " << version << "of document"
+ << QString::fromUtf8(uri) << "as it is invalid";
+ }
+ }
+ }
+ if (codeModelLog().isDebugEnabled()) {
+ qCDebug(codeModelLog) << "finished update doc of " << uri << "to version" << version;
+ snapshotByUri(uri).dump(qDebug() << "postSnapshot",
+ OpenDocumentSnapshot::DumpOption::AllCode);
+ }
+ // we should update the scope in the future thus call addOpen(uri)
+ emit updatedSnapshot(uri);
+}
+
+void QQmlCodeModel::closeOpenFile(const QByteArray &uri)
+{
+ QMutexLocker l(&m_mutex);
+ m_openDocuments.remove(uri);
+}
+
+void QQmlCodeModel::openUpdate(const QByteArray &uri)
+{
+ 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[uri];
+ 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(uri, *rNow, docText);
+ }
+ if (updateScope) {
+ // to do
+ }
+}
+
+void QQmlCodeModel::addOpenToUpdate(const QByteArray &uri)
+{
+ QMutexLocker l(&m_mutex);
+ m_openDocumentsToUpdate.insert(uri);
+}
+
+QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options)
+{
+ dbg.noquote().nospace() << "{";
+ dbg << " uri:" << QString::fromUtf8(uri) << "\n";
+ dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_qs) << "\n";
+ if (options & DumpOption::LatestCode) {
+ dbg << " doc: ------------\n"
+ << doc.field(Fields::code).value().toString() << "\n==========\n";
+ } else {
+ dbg << u" doc:"
+ << (doc ? u"%1chars"_qs.arg(doc.field(Fields::code).value().toString().length())
+ : u"*none*"_qs)
+ << "\n";
+ }
+ dbg << " validDocVersion:"
+ << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_qs) << "\n";
+ if (options & DumpOption::ValidCode) {
+ dbg << " validDoc: ------------\n"
+ << validDoc.field(Fields::code).value().toString() << "\n==========\n";
+ } else {
+ dbg << u" validDoc:"
+ << (validDoc ? u"%1chars"_qs.arg(
+ validDoc.field(Fields::code).value().toString().length())
+ : u"*none*"_qs)
+ << "\n";
+ }
+ dbg << " scopeVersion:" << (scopeVersion ? QString::number(*scopeVersion) : u"*none*"_qs)
+ << "\n";
+ dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n";
+ dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n";
+ dbg << "}";
+ return dbg;
+}
+
+} // namespace QmlLsp
+
+QT_END_NAMESPACE
diff --git a/tools/qmlls/qqmlcodemodel.h b/tools/qmlls/qqmlcodemodel.h
new file mode 100644
index 0000000000..43d07cec80
--- /dev/null
+++ b/tools/qmlls/qqmlcodemodel.h
@@ -0,0 +1,149 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef QQMLCODEMODEL_H
+#define QQMLCODEMODEL_H
+
+#include <QObject>
+#include <QHash>
+#include <QtQmlDom/private/qqmldomitem_p.h>
+#include <QtQmlCompiler/private/qqmljsscope_p.h>
+#include "qlanguageserver_p.h"
+#include "textdocument.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)
+ QByteArray uri;
+ 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::Ptr 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;
+};
+
+class QQmlCodeModel : public QObject
+{
+ Q_OBJECT
+public:
+ enum class UriLookup { Caching, ForceLookup };
+
+ explicit QQmlCodeModel(QObject *parent = nullptr);
+ QQmlJS::Dom::DomItem currentEnv();
+ QQmlJS::Dom::DomItem validEnv();
+ OpenDocumentSnapshot snapshotByUri(const QByteArray &uri);
+ OpenDocument openDocumentByUri(const QByteArray &uri);
+
+ 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 uri2Path(const QByteArray &uri, UriLookup options = UriLookup::Caching);
+ void newOpenFile(const QByteArray &uri, int version, const QString &docText);
+ void newDocForOpenFile(const QByteArray &uri, int version, const QString &docText);
+ void closeOpenFile(const QByteArray &uri);
+signals:
+ void updatedSnapshot(const QByteArray &uri);
+private:
+ QQmlJS::Dom::DomItem validDocForUpdate(QQmlJS::Dom::DomItem &item);
+ 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 &);
+ QMutex m_mutex;
+ int m_lastIndexProgress = 0;
+ int m_nIndexInProgress = 0;
+ QList<ToIndex> m_toIndex;
+ int m_indexInProgressCost = 0;
+ int m_indexDoneCost = 0;
+ int m_nUpdateInProgress = 0;
+ QQmlJS::Dom::DomItem m_currentEnv;
+ QQmlJS::Dom::DomItem m_validEnv;
+ QByteArray m_lastOpenDocumentUpdated;
+ QSet<QByteArray> m_openDocumentsToUpdate;
+ QHash<QByteArray, QString> m_uri2path;
+ QHash<QString, QByteArray> m_path2uri;
+ QHash<QByteArray, OpenDocument> m_openDocuments;
+};
+
+} // namespace QmlLsp
+QT_END_NAMESPACE
+#endif // QQMLCODEMODEL_H
diff --git a/tools/qmlls/qqmllanguageserver.cpp b/tools/qmlls/qqmllanguageserver.cpp
new file mode 100644
index 0000000000..8bb9b4c830
--- /dev/null
+++ b/tools/qmlls/qqmllanguageserver.cpp
@@ -0,0 +1,145 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qqmllanguageserver.h"
+
+#include "textsynchronization.h"
+
+#include "qlanguageserver.h"
+#include <QtCore/qdir.h>
+
+#include <iostream>
+
+QT_BEGIN_NAMESPACE
+
+namespace QmlLsp {
+
+using namespace QLspSpecification;
+/*!
+\internal
+\class QmlLsp::QQmlLanguageServer
+\brief Class that sets up a QmlLanguageServer
+
+This class sets up a QML language server.
+It needs a function
+\code
+std::function<void(const QByteArray &)> sendData
+\endcode
+to send out its replies, and one should feed the data it receives to the server()->receive() method.
+It is expected to call this method only from a single thread, and not to block, the simplest way to
+achieve this is to avoid direct calls, and connect it as slot, while reading from another thread.
+
+The Server is build with separate QLanguageServerModule that implement a given functionality, and
+all of them are constructed and registered with the QLanguageServer in the constructor o this class.
+
+Generally all operations are expected to be done in the object thread, and handlers are always
+called from it, but they are free to delegate the response to another thread, the response handler
+is thread safe. All the methods of the server() obect are also threadsafe.
+
+The code model starts other threads to update its state, see its documentation for more information.
+*/
+QQmlLanguageServer::QQmlLanguageServer(std::function<void(const QByteArray &)> sendData)
+ : m_server(sendData),
+ m_textSynchronization(&m_codeModel),
+ m_lint(&m_server, &m_codeModel)
+{
+ m_server.addServerModule(this);
+ m_server.addServerModule(&m_textSynchronization);
+ 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, [](QLanguageServer::RunStatus r) {
+ qCDebug(lspServerLog) << "runStatus" << int(r);
+ });
+}
+
+void QQmlLanguageServer::setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
+ QLspSpecification::InitializeResult &serverInfo)
+{
+ Q_UNUSED(clientInfo);
+ Q_UNUSED(serverInfo);
+}
+
+QString QQmlLanguageServer::name() const
+{
+ return u"QQmlLanguageServer"_qs;
+}
+
+void QQmlLanguageServer::errorExit()
+{
+ qCWarning(lspServerLog) << "Error exit";
+ fclose(stdin);
+}
+
+void QQmlLanguageServer::exit()
+{
+ m_returnValue = 0;
+ fclose(stdin);
+}
+
+int QQmlLanguageServer::returnValue() const
+{
+ return m_returnValue;
+}
+
+QLanguageServer *QQmlLanguageServer::server()
+{
+ return &m_server;
+}
+
+TextSynchronization *QQmlLanguageServer::textSynchronization()
+{
+ return &m_textSynchronization;
+}
+
+QmlLintSuggestions *QQmlLanguageServer::lint()
+{
+ return &m_lint;
+}
+
+} // namespace QmlLsp
+
+QT_END_NAMESPACE
diff --git a/tools/qmlls/qqmllanguageserver.h b/tools/qmlls/qqmllanguageserver.h
new file mode 100644
index 0000000000..0e2458ff24
--- /dev/null
+++ b/tools/qmlls/qqmllanguageserver.h
@@ -0,0 +1,82 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef QQMLLANGUAGESERVER_H
+#define QQMLLANGUAGESERVER_H
+
+#include "qlanguageserver_p.h"
+#include "qqmlcodemodel.h"
+#include "textsynchronization.h"
+#include "qmllintsuggestions.h"
+
+QT_BEGIN_NAMESPACE
+namespace QmlLsp {
+
+class QQmlLanguageServer : public QLanguageServerModule
+{
+ Q_OBJECT
+public:
+ QQmlLanguageServer(std::function<void(const QByteArray &)> sendData);
+
+ 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();
+
+public slots:
+ void exit();
+ void errorExit();
+
+private:
+ QQmlCodeModel m_codeModel;
+ QLanguageServer m_server;
+ TextSynchronization m_textSynchronization;
+ QmlLintSuggestions m_lint;
+ int m_returnValue = 1;
+};
+
+} // namespace QmlLsp
+QT_END_NAMESPACE
+#endif // QQMLLANGUAGESERVER_H
diff --git a/tools/qmlls/textblock.cpp b/tools/qmlls/textblock.cpp
new file mode 100644
index 0000000000..2ea09b9e25
--- /dev/null
+++ b/tools/qmlls/textblock.cpp
@@ -0,0 +1,137 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "textblock.h"
+#include "textdocument.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 TextBlock::operator==(const TextBlock &other) const
+{
+ return document() == other.document() && blockNumber() == other.blockNumber();
+}
+
+bool TextBlock::operator!=(const TextBlock &other) const
+{
+ return !operator==(other);
+}
+
+} // namespace Utils
diff --git a/tools/qmlls/textblock.h b/tools/qmlls/textblock.h
new file mode 100644
index 0000000000..edc7e77363
--- /dev/null
+++ b/tools/qmlls/textblock.h
@@ -0,0 +1,97 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef TEXTBLOCK_H
+#define TEXTBLOCK_H
+
+#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);
+
+ bool operator==(const TextBlock &other) const;
+ bool operator!=(const TextBlock &other) const;
+
+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_H
diff --git a/tools/qmlls/textcursor.cpp b/tools/qmlls/textcursor.cpp
new file mode 100644
index 0000000000..d74a9bf0da
--- /dev/null
+++ b/tools/qmlls/textcursor.cpp
@@ -0,0 +1,158 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "textcursor.h"
+#include "textdocument.h"
+#include "textblock.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;
+}
+
+}
diff --git a/tools/qmlls/textcursor.h b/tools/qmlls/textcursor.h
new file mode 100644
index 0000000000..16f8c4807b
--- /dev/null
+++ b/tools/qmlls/textcursor.h
@@ -0,0 +1,96 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef TEXTCURSOR_H
+#define TEXTCURSOR_H
+
+#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/tools/qmlls/textdocument.cpp b/tools/qmlls/textdocument.cpp
new file mode 100644
index 0000000000..67032a6f6e
--- /dev/null
+++ b/tools/qmlls/textdocument.cpp
@@ -0,0 +1,152 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "textdocument.h"
+#include "textblock.h"
+
+namespace Utils {
+
+TextDocument::TextDocument(const QString &text)
+{
+ setPlainText(text);
+}
+
+TextBlock TextDocument::findBlockByNumber(int blockNumber) const
+{
+ return (blockNumber >= 0 && blockNumber < m_blocks.length())
+ ? 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.length();
+}
+
+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();
+
+ int blockStart = 0;
+ int blockNumber = 0;
+ while (blockStart < text.length()) {
+ Block block;
+ block.textBlock.setBlockNumber(blockNumber++);
+ block.textBlock.setPosition(blockStart);
+ block.textBlock.setDocument(this);
+
+ int blockEnd = text.indexOf('\n', blockStart) + 1;
+ if (blockEnd == 0)
+ blockEnd = text.length();
+
+ block.textBlock.setLength(blockEnd - blockStart);
+ m_blocks.append(block);
+ blockStart = blockEnd;
+ }
+}
+
+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.length())
+ m_blocks[blockNumber].userState = state;
+}
+
+int TextDocument::userState(int blockNumber) const
+{
+ return (blockNumber >= 0 && blockNumber < m_blocks.length()) ? m_blocks[blockNumber].userState
+ : -1;
+}
+
+QMutex *TextDocument::mutex() const
+{
+ return &m_mutex;
+}
+
+} // namespace Utils
diff --git a/tools/qmlls/textdocument.h b/tools/qmlls/textdocument.h
new file mode 100644
index 0000000000..dcbf937e19
--- /dev/null
+++ b/tools/qmlls/textdocument.h
@@ -0,0 +1,102 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef TEXTDOCUMENT_H
+#define TEXTDOCUMENT_H
+
+#include "textblock.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_H
diff --git a/tools/qmlls/textsynchronization.cpp b/tools/qmlls/textsynchronization.cpp
new file mode 100644
index 0000000000..d23089f7ca
--- /dev/null
+++ b/tools/qmlls/textsynchronization.cpp
@@ -0,0 +1,132 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "textsynchronization.h"
+#include "qqmllanguageserver.h"
+
+#include "textdocument.h"
+
+using namespace QLspSpecification;
+QT_BEGIN_NAMESPACE
+
+TextSynchronization::TextSynchronization(QmlLsp::QQmlCodeModel *codeModel, QObject *parent)
+ : QLanguageServerModule(parent), m_codeModel(codeModel)
+{
+}
+
+void TextSynchronization::didCloseTextDocument(const DidCloseTextDocumentParams &params)
+{
+ m_codeModel->closeOpenFile(params.textDocument.uri);
+}
+
+void TextSynchronization::didOpenTextDocument(const DidOpenTextDocumentParams &params)
+{
+ const TextDocumentItem &item = params.textDocument;
+ const QString fileName = m_codeModel->uri2Path(item.uri);
+
+ m_codeModel->newOpenFile(item.uri, item.version, item.text);
+}
+
+void TextSynchronization::didDidChangeTextDocument(const DidChangeTextDocumentParams &params)
+{
+ QByteArray uri = params.textDocument.uri;
+ const QString fileName = m_codeModel->uri2Path(uri);
+ auto openDoc = m_codeModel->openDocumentByUri(uri);
+ std::shared_ptr<Utils::TextDocument> document = openDoc.textDocument;
+ if (!document) {
+ qCWarning(lspServerLog) << "Ingnoring changes to non open or closed document"
+ << QString::fromUtf8(uri);
+ return;
+ }
+ const auto &changes = params.contentChanges;
+ {
+ QMutexLocker l(document->mutex());
+ for (const auto &change : changes) {
+ if (!change.range) {
+ document->setPlainText(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, change.text));
+ }
+ document->setVersion(params.textDocument.version);
+ }
+ m_codeModel->addOpenToUpdate(uri);
+ 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"_qs;
+}
+
+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/tools/qmlls/textsynchronization.h b/tools/qmlls/textsynchronization.h
new file mode 100644
index 0000000000..8440b2001a
--- /dev/null
+++ b/tools/qmlls/textsynchronization.h
@@ -0,0 +1,68 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef TEXTSYNCH_H
+#define TEXTSYNCH_H
+
+#include "qqmlcodemodel.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 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 // TEXTSYNCH_H