diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/CMakeLists.txt | 3 | ||||
-rw-r--r-- | tools/qmlls/CMakeLists.txt | 29 | ||||
-rw-r--r-- | tools/qmlls/qlanguageserver.cpp | 434 | ||||
-rw-r--r-- | tools/qmlls/qlanguageserver.h | 128 | ||||
-rw-r--r-- | tools/qmlls/qlanguageserver_p.h | 88 | ||||
-rw-r--r-- | tools/qmlls/qmllanguageservertool.cpp | 213 | ||||
-rw-r--r-- | tools/qmlls/qmllintsuggestions.cpp | 221 | ||||
-rw-r--r-- | tools/qmlls/qmllintsuggestions.h | 71 | ||||
-rw-r--r-- | tools/qmlls/qqmlcodemodel.cpp | 610 | ||||
-rw-r--r-- | tools/qmlls/qqmlcodemodel.h | 149 | ||||
-rw-r--r-- | tools/qmlls/qqmllanguageserver.cpp | 145 | ||||
-rw-r--r-- | tools/qmlls/qqmllanguageserver.h | 82 | ||||
-rw-r--r-- | tools/qmlls/textblock.cpp | 137 | ||||
-rw-r--r-- | tools/qmlls/textblock.h | 97 | ||||
-rw-r--r-- | tools/qmlls/textcursor.cpp | 158 | ||||
-rw-r--r-- | tools/qmlls/textcursor.h | 96 | ||||
-rw-r--r-- | tools/qmlls/textdocument.cpp | 152 | ||||
-rw-r--r-- | tools/qmlls/textdocument.h | 102 | ||||
-rw-r--r-- | tools/qmlls/textsynchronization.cpp | 132 | ||||
-rw-r--r-- | tools/qmlls/textsynchronization.h | 68 |
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 ¶ms) { + Q_D(QLanguageServer); + QJsonValue id = QTypedJson::toJsonValue(params.id); + QMutexLocker l(&d->mutex); + if (d->requestsInProgress.contains(id)) + d->requestsInProgress[id].canceled = true; + else + qCWarning(lspServerLog) + << "Ignoring cancellation of non in progress request" << id; + }); + + protocol->registerInitializeRequestHandler( + [this](const QByteArray &, + const QLspSpecification::Requests::InitializeParamsType ¶ms, + QLspSpecification::Responses::InitializeResponseType &&response) { + qCDebug(lspServerLog) << "init"; + Q_D(QLanguageServer); + RunStatus rStatus; + { + QMutexLocker l(&d->mutex); + rStatus = d->runStatus; + if (rStatus == RunStatus::DidSetup) + d->runStatus = RunStatus::Initializing; + } + if (rStatus != RunStatus::DidSetup) { + if (rStatus == RunStatus::NotSetup || rStatus == RunStatus::SettingUp) + response.sendErrorResponse( + int(QLspSpecification::ErrorCodes::InvalidRequest), + u"Initialization request received on non setup language server"_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 ¶ms) +{ + m_codeModel->closeOpenFile(params.textDocument.uri); +} + +void TextSynchronization::didOpenTextDocument(const DidOpenTextDocumentParams ¶ms) +{ + 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 ¶ms) +{ + 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 ¶ms); + void didDidChangeTextDocument(const QLspSpecification::DidChangeTextDocumentParams ¶ms); + void didCloseTextDocument(const QLspSpecification::DidCloseTextDocumentParams ¶ms); + +private: + QmlLsp::QQmlCodeModel *m_codeModel; +}; + +QT_END_NAMESPACE +#endif // TEXTSYNCH_H |