aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFawzi Mohamed <fawzi.mohamed@qt.io>2021-12-13 03:41:19 +0100
committerFawzi Mohamed <fawzi.mohamed@qt.io>2021-12-23 18:56:00 +0100
commit9da7eb62e41a186512a05c3cfd69f1d1cb38cd2b (patch)
tree2a69a0b2a655bc2210979f137f3b7afab99fe177
parent6cd1fe470320d69b61b17a015d99c86b6e80d15d (diff)
qmlls: qml language server implementation
This is the initial commit, with just qmllint integration, but many of the tricky bits and the architecture are already there, see the description in qqmllanguageserver.cpp and qqmlcodemodel.cpp Change-Id: Ie493fed02276f938fde641e8d91c67aed0514d1f Reviewed-by: Andrei Golubev <andrei.golubev@qt.io> (cherry picked from commit 25ac957f6da559333938430eec9cc341823e22cc)
-rw-r--r--CMakeLists.txt2
-rw-r--r--dependencies.yaml3
-rw-r--r--tests/auto/CMakeLists.txt1
-rw-r--r--tests/auto/qmlls/CMakeLists.txt4
-rw-r--r--tests/auto/qmlls/qlanguageserver/CMakeLists.txt19
-rw-r--r--tests/auto/qmlls/qlanguageserver/qiopipe.cpp176
-rw-r--r--tests/auto/qmlls/qlanguageserver/qiopipe.h64
-rw-r--r--tests/auto/qmlls/qlanguageserver/tst_qlanguageserver.cpp149
-rw-r--r--tests/auto/qmlls/qmlls/CMakeLists.txt24
-rw-r--r--tests/auto/qmlls/qmlls/data/default/Yyy.qml11
-rw-r--r--tests/auto/qmlls/qmlls/data/default/Zzz.qml10
-rw-r--r--tests/auto/qmlls/qmlls/tst_qmlls.cpp174
-rw-r--r--tools/CMakeLists.txt3
-rw-r--r--tools/qmlls/CMakeLists.txt29
-rw-r--r--tools/qmlls/qlanguageserver.cpp434
-rw-r--r--tools/qmlls/qlanguageserver.h128
-rw-r--r--tools/qmlls/qlanguageserver_p.h88
-rw-r--r--tools/qmlls/qmllanguageservertool.cpp213
-rw-r--r--tools/qmlls/qmllintsuggestions.cpp221
-rw-r--r--tools/qmlls/qmllintsuggestions.h71
-rw-r--r--tools/qmlls/qqmlcodemodel.cpp610
-rw-r--r--tools/qmlls/qqmlcodemodel.h149
-rw-r--r--tools/qmlls/qqmllanguageserver.cpp145
-rw-r--r--tools/qmlls/qqmllanguageserver.h82
-rw-r--r--tools/qmlls/textblock.cpp137
-rw-r--r--tools/qmlls/textblock.h97
-rw-r--r--tools/qmlls/textcursor.cpp158
-rw-r--r--tools/qmlls/textcursor.h96
-rw-r--r--tools/qmlls/textdocument.cpp152
-rw-r--r--tools/qmlls/textdocument.h102
-rw-r--r--tools/qmlls/textsynchronization.cpp132
-rw-r--r--tools/qmlls/textsynchronization.h68
32 files changed, 3751 insertions, 1 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1f1eeffb03..7269350837 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,7 +11,7 @@ project(QtDeclarative # special case
)
find_package(Qt6 ${PROJECT_VERSION} CONFIG REQUIRED COMPONENTS BuildInternals Core) # special case
-find_package(Qt6 ${PROJECT_VERSION} QUIET CONFIG OPTIONAL_COMPONENTS Gui Network Widgets OpenGL OpenGLWidgets Sql Concurrent Test)
+find_package(Qt6 ${PROJECT_VERSION} QUIET CONFIG OPTIONAL_COMPONENTS Gui Network Widgets OpenGL OpenGLWidgets Sql Concurrent Test LanguageServer)
# Set up QT_HOST_PATH as an extra root path to look for the ShaderToolsTools package
# when cross-compiling.
diff --git a/dependencies.yaml b/dependencies.yaml
index 71ad595185..04c4030b1c 100644
--- a/dependencies.yaml
+++ b/dependencies.yaml
@@ -11,3 +11,6 @@ dependencies:
../qtsvg:
ref: d4333de46a2e34a13603aa7f0877363795911f57
required: false
+ ../qtlanguageserver:
+ ref: ab54c44550b6fc6e610d90706059464365424dbb
+ required: false
diff --git a/tests/auto/CMakeLists.txt b/tests/auto/CMakeLists.txt
index 2476a8fe76..fa570a9ddc 100644
--- a/tests/auto/CMakeLists.txt
+++ b/tests/auto/CMakeLists.txt
@@ -25,6 +25,7 @@ if(TARGET Qt::QuickWidgets)
endif()
if(TARGET Qt::QmlDomPrivate)
add_subdirectory(qmldom)
+ add_subdirectory(qmlls)
endif()
if(TARGET Qt::QuickTemplates2)
add_subdirectory(quickcontrols2)
diff --git a/tests/auto/qmlls/CMakeLists.txt b/tests/auto/qmlls/CMakeLists.txt
new file mode 100644
index 0000000000..2ce7460b80
--- /dev/null
+++ b/tests/auto/qmlls/CMakeLists.txt
@@ -0,0 +1,4 @@
+if (TARGET Qt::LanguageServerPrivate)
+ add_subdirectory(qmlls)
+ add_subdirectory(qlanguageserver)
+endif()
diff --git a/tests/auto/qmlls/qlanguageserver/CMakeLists.txt b/tests/auto/qmlls/qlanguageserver/CMakeLists.txt
new file mode 100644
index 0000000000..8ab6c0902e
--- /dev/null
+++ b/tests/auto/qmlls/qlanguageserver/CMakeLists.txt
@@ -0,0 +1,19 @@
+#####################################################################
+## tst_qlanguageserver Test:
+#####################################################################
+
+qt_internal_add_test(tst_qlanguageserver
+ SOURCES
+ ../../../../tools/qmlls/qlanguageserver.h ../../../../tools/qmlls/qlanguageserver.cpp
+ tst_qlanguageserver.cpp
+ qiopipe.h qiopipe.cpp
+ INCLUDE_DIRECTORIES
+ ../../../../tools/qmlls
+ DEFINES
+ QT_DEPRECATED_WARNINGS
+ PUBLIC_LIBRARIES
+ Qt::CorePrivate
+ Qt::LanguageServerPrivate
+ Qt::Test
+ TESTDATA ${test_data}
+)
diff --git a/tests/auto/qmlls/qlanguageserver/qiopipe.cpp b/tests/auto/qmlls/qlanguageserver/qiopipe.cpp
new file mode 100644
index 0000000000..de6d37a02f
--- /dev/null
+++ b/tests/auto/qmlls/qlanguageserver/qiopipe.cpp
@@ -0,0 +1,176 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite 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 "qiopipe.h"
+
+#include <QtCore/private/qobject_p.h>
+#include <QtCore/qpointer.h>
+
+#include <QDebug>
+
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+
+class QPipeEndPoint : public QIODevice
+{
+ Q_OBJECT
+
+public:
+ bool isSequential() const override;
+ qint64 bytesAvailable() const override;
+
+ void setRemoteEndPoint(QPipeEndPoint *other);
+
+protected:
+ qint64 readData(char *data, qint64 maxlen) override;
+ qint64 writeData(const char *data, qint64 len) override;
+
+private:
+ QByteArray m_buffer;
+ QPointer<QPipeEndPoint> m_remoteEndPoint;
+};
+
+class QIOPipePrivate : public QObjectPrivate
+{
+ Q_DECLARE_PUBLIC(QIOPipe)
+public:
+ QIOPipePrivate();
+ ~QIOPipePrivate() final;
+
+ std::unique_ptr<QPipeEndPoint> end1;
+ std::unique_ptr<QPipeEndPoint> end2;
+};
+
+QIOPipe::QIOPipe(QObject *parent) : QObject(*(new QIOPipePrivate()), parent) { }
+
+bool QIOPipe::open(QIODevice::OpenMode mode)
+{
+ Q_D(QIOPipe);
+
+ if (!d->end1->open(mode))
+ return false;
+ switch (mode & QIODevice::ReadWrite) {
+ case QIODevice::WriteOnly:
+ case QIODevice::ReadOnly:
+ return d->end2->open(mode ^ QIODevice::ReadWrite);
+ default:
+ return d->end2->open(mode);
+ }
+}
+
+QIODevice *QIOPipe::end1() const
+{
+ Q_D(const QIOPipe);
+ return d->end1.get();
+}
+
+QIODevice *QIOPipe::end2() const
+{
+ Q_D(const QIOPipe);
+ return d->end2.get();
+}
+
+QIOPipePrivate::QIOPipePrivate()
+ : end1(std::make_unique<QPipeEndPoint>()), end2(std::make_unique<QPipeEndPoint>())
+{
+ end1->setRemoteEndPoint(end2.get());
+ end2->setRemoteEndPoint(end1.get());
+}
+
+QIOPipePrivate::~QIOPipePrivate() { }
+
+bool QPipeEndPoint::isSequential() const
+{
+ return true;
+}
+
+qint64 QPipeEndPoint::bytesAvailable() const
+{
+ return m_buffer.length() + QIODevice::bytesAvailable();
+}
+
+void QPipeEndPoint::setRemoteEndPoint(QPipeEndPoint *other)
+{
+ m_remoteEndPoint = other;
+}
+
+qint64 QPipeEndPoint::readData(char *data, qint64 maxlen)
+{
+ maxlen = qMin(maxlen, static_cast<qint64>(m_buffer.length()));
+ if (maxlen <= 0)
+ return 0;
+
+ Q_ASSERT(maxlen > 0);
+ Q_ASSERT(maxlen <= std::numeric_limits<int>::max());
+ memcpy(data, m_buffer.data(), static_cast<size_t>(maxlen));
+ m_buffer = m_buffer.mid(static_cast<int>(maxlen));
+ return maxlen;
+}
+
+qint64 QPipeEndPoint::writeData(const char *data, qint64 len)
+{
+ if (!m_remoteEndPoint)
+ return -1;
+
+ if (len <= 0)
+ return 0;
+
+ QByteArray &buffer = m_remoteEndPoint->m_buffer;
+ const qint64 prevLen = buffer.length();
+ Q_ASSERT(prevLen >= 0);
+ len = qMin(len, std::numeric_limits<int>::max() - prevLen);
+
+ if (len == 0)
+ return 0;
+
+ Q_ASSERT(len > 0);
+ Q_ASSERT(prevLen + len > 0);
+ Q_ASSERT(prevLen + len <= std::numeric_limits<int>::max());
+
+ buffer.resize(static_cast<int>(prevLen + len));
+ memcpy(buffer.data() + prevLen, data, static_cast<size_t>(len));
+ emit bytesWritten(len);
+ emit m_remoteEndPoint->readyRead();
+ return len;
+}
+
+QT_END_NAMESPACE
+
+#include <qiopipe.moc>
diff --git a/tests/auto/qmlls/qlanguageserver/qiopipe.h b/tests/auto/qmlls/qlanguageserver/qiopipe.h
new file mode 100644
index 0000000000..3d6a0008f3
--- /dev/null
+++ b/tests/auto/qmlls/qlanguageserver/qiopipe.h
@@ -0,0 +1,64 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite 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 QECHODEVICE_H
+#define QECHODEVICE_H
+
+#include <QtCore/qiodevice.h>
+
+QT_BEGIN_NAMESPACE
+
+class QIOPipePrivate;
+class QIOPipe : public QObject
+{
+ Q_OBJECT
+ Q_DECLARE_PRIVATE(QIOPipe)
+
+public:
+ QIOPipe(QObject *parent = nullptr);
+
+ bool open(QIODevice::OpenMode mode);
+
+ QIODevice *end1() const;
+ QIODevice *end2() const;
+};
+
+QT_END_NAMESPACE
+
+#endif // QECHODEVICE_H
diff --git a/tests/auto/qmlls/qlanguageserver/tst_qlanguageserver.cpp b/tests/auto/qmlls/qlanguageserver/tst_qlanguageserver.cpp
new file mode 100644
index 0000000000..91d778f8fb
--- /dev/null
+++ b/tests/auto/qmlls/qlanguageserver/tst_qlanguageserver.cpp
@@ -0,0 +1,149 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** 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 General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** 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-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qiopipe.h"
+
+#include <QtLanguageServer/private/qlanguageserverjsonrpctransport_p.h>
+
+#include <QtJsonRpc/private/qjsonrpcprotocol_p.h>
+
+#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
+#include "qlanguageserver.h"
+
+#include <QtCore/qjsonarray.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qstring.h>
+#include <QtCore/qbytearray.h>
+#include <QtTest/qsignalspy.h>
+#include <QtTest/qtest.h>
+
+using namespace QLspSpecification;
+
+class TestRigEventHandler
+{
+public:
+ TestRigEventHandler(QLanguageServer *server, QIODevice *device)
+ : m_device(device), m_server(server)
+ {
+ server->finishSetup();
+ QObject::connect(device, &QIODevice::readyRead,
+ [this]() { m_server->protocol()->receiveData(m_device->readAll()); });
+ QObject::connect(m_server, &QLanguageServer::exit, [this]() { m_hasExited = true; });
+ }
+
+ bool hasExited() const { return m_hasExited; }
+
+ QLanguageServer *server() const { return m_server; }
+
+private:
+ QIODevice *m_device = nullptr;
+ QLanguageServer *m_server = nullptr;
+ bool m_hasExited = false;
+};
+
+class tst_QLanguageServer : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void lifecycle();
+};
+
+void tst_QLanguageServer::lifecycle()
+{
+ QIOPipe pipe;
+ pipe.open(QIODevice::ReadWrite);
+
+ QLanguageServer server([&pipe](const QByteArray &data) {
+ QMetaObject::invokeMethod(pipe.end1(), [&pipe, data]() { pipe.end1()->write(data); });
+ });
+ TestRigEventHandler handler(&server, pipe.end1());
+ QCOMPARE(server.runStatus(), QLanguageServer::RunStatus::DidSetup);
+
+ QLanguageServerJsonRpcTransport transport;
+
+ QLanguageServerProtocol protocol([&pipe](const QByteArray &data) { pipe.end2()->write(data); });
+ QObject::connect(pipe.end1(), &QIODevice::readyRead,
+ [&pipe, &protocol]() { protocol.receiveData(pipe.end2()->readAll()); });
+ QCOMPARE(server.runStatus(), QLanguageServer::RunStatus::DidSetup);
+
+ enum class RequestStatus {
+ NoResponse,
+ Success,
+ Failure
+ } requestStatus = RequestStatus::NoResponse;
+
+ protocol.requestApplyWorkspaceEdit(
+ ApplyWorkspaceEditParams(),
+ [&requestStatus](const auto &) { requestStatus = RequestStatus::Success; },
+ [&requestStatus](const ResponseError &err) {
+ QCOMPARE(err.code, int(ErrorCodes::ServerNotInitialized));
+ requestStatus = RequestStatus::Failure;
+ });
+ QTRY_VERIFY(requestStatus != RequestStatus::NoResponse);
+ QCOMPARE(requestStatus, RequestStatus::Failure);
+
+ InitializeParams clientInfo;
+ clientInfo.rootUri = nullptr;
+ clientInfo.processId = nullptr;
+ requestStatus = RequestStatus::NoResponse;
+ auto requestFailureHandler = [&requestStatus, &protocol](const ResponseError &err) {
+ protocol.defaultResponseErrorHandler(err);
+ requestStatus = RequestStatus::Failure;
+ };
+ protocol.requestInitialize(
+ clientInfo,
+ [&requestStatus](const InitializeResult &serverInfo) {
+ Q_UNUSED(serverInfo);
+ requestStatus = RequestStatus::Success;
+ },
+ requestFailureHandler);
+ QTRY_VERIFY(requestStatus != RequestStatus::NoResponse);
+ QCOMPARE(requestStatus, RequestStatus::Success);
+ QCOMPARE(server.runStatus(), QLanguageServer::RunStatus::DidInitialize);
+
+ protocol.notifyInitialized(InitializedParams());
+
+ requestStatus = RequestStatus::NoResponse;
+ protocol.requestShutdown(
+ nullptr, [&requestStatus]() { requestStatus = RequestStatus::Success; },
+ requestFailureHandler);
+ QTRY_VERIFY(requestStatus != RequestStatus::NoResponse);
+ QCOMPARE(requestStatus, RequestStatus::Success);
+
+ QVERIFY(!handler.hasExited());
+ QCOMPARE(server.runStatus(), QLanguageServer::RunStatus::Stopped);
+
+ protocol.notifyExit(nullptr);
+
+ QTRY_VERIFY(handler.hasExited());
+}
+
+QTEST_MAIN(tst_QLanguageServer)
+#include <tst_qlanguageserver.moc>
diff --git a/tests/auto/qmlls/qmlls/CMakeLists.txt b/tests/auto/qmlls/qmlls/CMakeLists.txt
new file mode 100644
index 0000000000..9487f4e7f8
--- /dev/null
+++ b/tests/auto/qmlls/qmlls/CMakeLists.txt
@@ -0,0 +1,24 @@
+file(GLOB_RECURSE test_data_glob
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ data)
+list(APPEND test_data ${test_data_glob})
+
+qt_internal_add_test(tst_qmlls
+ SOURCES
+ tst_qmlls.cpp
+ DEFINES
+ QT_DEPRECATED_WARNINGS
+ QT_QMLTEST_DATADIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}/data\\\"
+ PUBLIC_LIBRARIES
+ Qt::Core
+ Qt::QmlDomPrivate
+ Qt::LanguageServerPrivate
+ Qt::Test
+ Qt::QuickTestUtilsPrivate
+ TESTDATA ${test_data}
+)
+
+qt_internal_extend_target(tst_qmldomitem CONDITION ANDROID OR IOS
+ DEFINES
+ QT_QMLTEST_DATADIR=\\\":/domdata\\\"
+)
diff --git a/tests/auto/qmlls/qmlls/data/default/Yyy.qml b/tests/auto/qmlls/qmlls/data/default/Yyy.qml
new file mode 100644
index 0000000000..dfc89bf93c
--- /dev/null
+++ b/tests/auto/qmlls/qmlls/data/default/Yyy.qml
@@ -0,0 +1,11 @@
+import QtQuick 2.0
+
+Zzz {
+ width: zzz.height
+ Rectangle {
+ color: "green"
+ anchors.fill: parent
+ }
+
+ function lala() {}
+}
diff --git a/tests/auto/qmlls/qmlls/data/default/Zzz.qml b/tests/auto/qmlls/qmlls/data/default/Zzz.qml
new file mode 100644
index 0000000000..165ea46394
--- /dev/null
+++ b/tests/auto/qmlls/qmlls/data/default/Zzz.qml
@@ -0,0 +1,10 @@
+import QtQuick 2.11
+
+Item {
+ id: zzz
+ height: 333
+
+ Rectangle {
+ width: zzz.height
+ }
+}
diff --git a/tests/auto/qmlls/qmlls/tst_qmlls.cpp b/tests/auto/qmlls/qmlls/tst_qmlls.cpp
new file mode 100644
index 0000000000..2d0d38f352
--- /dev/null
+++ b/tests/auto/qmlls/qmlls/tst_qmlls.cpp
@@ -0,0 +1,174 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** 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 General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** 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-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include <QtJsonRpc/private/qjsonrpcprotocol_p.h>
+#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
+#include <QtQuickTestUtils/private/qmlutils_p.h>
+
+#include <QtCore/qobject.h>
+#include <QtCore/qprocess.h>
+#include <QtCore/qlibraryinfo.h>
+
+#include <QtTest/qtest.h>
+
+#include <iostream>
+
+using namespace QLspSpecification;
+
+class DiagnosticsHandler
+{
+public:
+ void handleNotification(const PublishDiagnosticsParams &params)
+ {
+ m_received.append(PublishDiagnosticsParams(params));
+ }
+
+ bool contains(const QString &uri, int line1, int column1, int line2, int column2) const
+ {
+ for (const auto &params : m_received) {
+ if (params.uri != uri)
+ continue;
+ for (const auto &diagnostic : params.diagnostics) {
+ const auto range = diagnostic.range;
+ if (range.start.line == line1 && range.start.character == column1
+ && range.end.line == line2 && range.end.character == column2) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ int numDiagnostics(const QByteArray &uri) const
+ {
+ int num = 0;
+ for (const auto &params : m_received) {
+ if (params.uri == uri)
+ num += params.diagnostics.length();
+ }
+ return num;
+ }
+
+ void clear() { m_received.clear(); }
+
+private:
+ QList<PublishDiagnosticsParams> m_received;
+};
+
+class tst_Qmlls : public QQmlDataTest
+{
+ Q_OBJECT
+public:
+ tst_Qmlls();
+private slots:
+ void initTestCase() final;
+ void didOpenTextDocument();
+ void cleanupTestCase();
+
+private:
+ QProcess m_server;
+ QLanguageServerProtocol m_protocol;
+ DiagnosticsHandler m_diagnosticsHandler;
+ QString m_qmllsPath;
+};
+
+tst_Qmlls::tst_Qmlls()
+ : QQmlDataTest(QT_QMLTEST_DATADIR),
+ m_protocol([this](const QByteArray &data) { m_server.write(data); })
+{
+ connect(&m_server, &QProcess::readyReadStandardOutput, this, [this]() {
+ QByteArray data = m_server.readAllStandardOutput();
+ m_protocol.receiveData(data);
+ });
+
+ connect(&m_server, &QProcess::readyReadStandardError, this,
+ [this]() { qWarning() << "LSPerr" << m_server.readAllStandardError(); });
+
+ m_qmllsPath = QLibraryInfo::path(QLibraryInfo::BinariesPath) + QLatin1String("/qmlls");
+#ifdef Q_OS_WIN
+ m_qmllsPath += QLatin1String(".exe");
+#endif
+ m_server.setProgram(m_qmllsPath);
+ m_protocol.registerPublishDiagnosticsNotificationHandler(
+ [this](const QByteArray &, auto params) {
+ m_diagnosticsHandler.handleNotification(params);
+ });
+}
+
+void tst_Qmlls::initTestCase()
+{
+ QQmlDataTest::initTestCase();
+ if (!QFileInfo::exists(m_qmllsPath)) {
+ QString message =
+ QStringLiteral("qmlls executable not found (looked for %0)").arg(m_qmllsPath);
+ QSKIP(qPrintable(message)); // until we add a feature for this we avoid failing here
+ }
+ m_server.start();
+ InitializeParams clientInfo;
+ clientInfo.rootUri = QUrl::fromLocalFile(dataDirectory() + "/default").toString().toUtf8();
+ TextDocumentClientCapabilities tDoc;
+ PublishDiagnosticsClientCapabilities pDiag;
+ tDoc.publishDiagnostics = pDiag;
+ pDiag.versionSupport = true;
+ clientInfo.capabilities.textDocument = tDoc;
+ bool didInit = false;
+ m_protocol.requestInitialize(clientInfo, [this, &didInit](const InitializeResult &serverInfo) {
+ Q_UNUSED(serverInfo);
+ m_protocol.notifyInitialized(InitializedParams());
+ didInit = true;
+ });
+ QTRY_COMPARE_WITH_TIMEOUT(didInit, true, 10000);
+}
+
+void tst_Qmlls::didOpenTextDocument()
+{
+ QFile file(testFile("default/Yyy.qml"));
+ QVERIFY(file.open(QIODevice::ReadOnly));
+
+ DidOpenTextDocumentParams params;
+ TextDocumentItem textDocument;
+ QByteArray uri = testFileUrl("default/Yyy.qml").toString().toUtf8();
+ textDocument.uri = uri;
+ textDocument.text = file.readAll().replace("width", "wildth");
+ params.textDocument = textDocument;
+ m_protocol.notifyDidOpenTextDocument(params);
+
+ QTRY_VERIFY_WITH_TIMEOUT(m_diagnosticsHandler.numDiagnostics(uri) != 0, 10000);
+ QVERIFY(m_diagnosticsHandler.contains(uri, 3, 4, 3, 10));
+ m_diagnosticsHandler.clear();
+}
+
+void tst_Qmlls::cleanupTestCase()
+{
+ m_server.closeWriteChannel();
+ QTRY_COMPARE(m_server.state(), QProcess::NotRunning);
+ QCOMPARE(m_server.exitStatus(), QProcess::NormalExit);
+}
+
+QTEST_MAIN(tst_Qmlls)
+
+#include <tst_qmlls.moc>
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 1b7b4641d1..f3170a9d4d 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -13,6 +13,9 @@ if(QT_FEATURE_qml_devtools)
add_subdirectory(qmlimportscanner)
add_subdirectory(qmlformat)
add_subdirectory(qmltc)
+ if (TARGET Qt::LanguageServerPrivate)
+ add_subdirectory(qmlls)
+ endif()
endif()
if(QT_FEATURE_qml_devtools AND QT_FEATURE_xmlstreamwriter)
# special case begin
diff --git a/tools/qmlls/CMakeLists.txt b/tools/qmlls/CMakeLists.txt
new file mode 100644
index 0000000000..ac42d239de
--- /dev/null
+++ b/tools/qmlls/CMakeLists.txt
@@ -0,0 +1,29 @@
+#####################################################################
+## qmlls Tool:
+#####################################################################
+
+qt_get_tool_target_name(target_name qmlls)
+qt_internal_add_tool(${target_name}
+ TARGET_DESCRIPTION "QML languageserver"
+ TOOLS_TARGET Qml # special case
+ SOURCES
+ qlanguageserver.h qlanguageserver_p.h qlanguageserver.cpp
+ qqmllanguageserver.h qqmllanguageserver.cpp
+ qmllanguageservertool.cpp
+ textblock.h textblock.cpp
+ textcursor.h textcursor.cpp
+ textcursor.cpp textcursor.h
+ textdocument.cpp textdocument.h
+ qmllintsuggestions.h qmllintsuggestions.cpp
+ textsynchronization.cpp textsynchronization.h
+ qqmlcodemodel.h qqmlcodemodel.cpp
+ ../shared/qqmltoolingsettings.h
+ ../shared/qqmltoolingsettings.cpp
+ PUBLIC_LIBRARIES
+ Qt::QmlPrivate
+ Qt::CorePrivate
+ Qt::QmlDomPrivate
+ Qt::LanguageServerPrivate
+ Qt::QmlLintPrivate
+)
+qt_internal_return_unless_building_tools()
diff --git a/tools/qmlls/qlanguageserver.cpp b/tools/qmlls/qlanguageserver.cpp
new file mode 100644
index 0000000000..c7046ec5e8
--- /dev/null
+++ b/tools/qmlls/qlanguageserver.cpp
@@ -0,0 +1,434 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qlanguageserver_p.h"
+#include <QtLanguageServer/private/qlspnotifysignals_p.h>
+#include <QtJsonRpc/private/qjsonrpcprotocol_p_p.h>
+
+QT_BEGIN_NAMESPACE
+
+using namespace QLspSpecification;
+
+Q_LOGGING_CATEGORY(lspServerLog, "qt.languageserver.server")
+
+QLanguageServerPrivate::QLanguageServerPrivate(const QJsonRpcTransport::DataHandler &h)
+ : protocol(h)
+{
+}
+
+/*!
+\internal
+\class QLanguageServer
+\brief Implements a server for the language server protocol
+
+QLanguageServer is a class that uses the QLanguageServerProtocol to
+provide a server implementation.
+It handles the lifecycle management, and can be extended via
+QLanguageServerModule subclasses.
+
+The language server keeps a strictly monotonically increasing runState that can be queried
+from any thread (and is thus mutex gated), the normal run state is DidInitialize.
+
+The language server also keeps track of the task canceled by the client (or implicitly when
+shutting down, and isRequestCanceled can be called from any thread.
+*/
+
+QLanguageServer::QLanguageServer(const QJsonRpcTransport::DataHandler &h, QObject *parent)
+ : QObject(*new QLanguageServerPrivate(h), parent)
+{
+ Q_D(QLanguageServer);
+ registerMethods(*d->protocol.typedRpc());
+ d->notifySignals.registerHandlers(&d->protocol);
+}
+
+QLanguageServer::~QLanguageServer() { }
+
+QLanguageServerProtocol *QLanguageServer::protocol()
+{
+ Q_D(QLanguageServer);
+ return &d->protocol;
+}
+
+QLanguageServer::RunStatus QLanguageServer::runStatus() const
+{
+ const Q_D(QLanguageServer);
+ QMutexLocker l(&d->mutex);
+ return d->runStatus;
+}
+
+void QLanguageServer::finishSetup()
+{
+ Q_D(QLanguageServer);
+ RunStatus rStatus;
+ {
+ QMutexLocker l(&d->mutex);
+ rStatus = d->runStatus;
+ if (rStatus == RunStatus::NotSetup)
+ d->runStatus = RunStatus::SettingUp;
+ }
+ if (rStatus != RunStatus::NotSetup) {
+ emit lifecycleError();
+ return;
+ }
+ emit runStatusChanged(RunStatus::SettingUp);
+
+ registerHandlers(&d->protocol);
+ for (auto module : d->modules)
+ module->registerHandlers(this, &d->protocol);
+
+ {
+ QMutexLocker l(&d->mutex);
+ rStatus = d->runStatus;
+ if (rStatus == RunStatus::SettingUp)
+ d->runStatus = RunStatus::DidSetup;
+ }
+ if (rStatus != RunStatus::SettingUp) {
+ emit lifecycleError();
+ return;
+ }
+ emit runStatusChanged(RunStatus::DidSetup);
+}
+
+void QLanguageServer::addServerModule(QLanguageServerModule *serverModule)
+{
+ Q_D(QLanguageServer);
+ Q_ASSERT(serverModule);
+ RunStatus rStatus;
+ {
+ QMutexLocker l(&d->mutex);
+ rStatus = d->runStatus;
+ if (rStatus == RunStatus::NotSetup) {
+ if (d->modules.contains(serverModule->name())) {
+ d->modules.insert(serverModule->name(), serverModule);
+ qCWarning(lspServerLog) << "Duplicate add of QLanguageServerModule named"
+ << serverModule->name() << ", overwriting.";
+ } else {
+ d->modules.insert(serverModule->name(), serverModule);
+ }
+ }
+ }
+ if (rStatus != RunStatus::NotSetup) {
+ qCWarning(lspServerLog) << "Called QLanguageServer::addServerModule after setup";
+ emit lifecycleError();
+ return;
+ }
+}
+
+QLanguageServerModule *QLanguageServer::moduleByName(const QString &n) const
+{
+ const Q_D(QLanguageServer);
+ QMutexLocker l(&d->mutex);
+ return d->modules.value(n);
+}
+
+QLspNotifySignals *QLanguageServer::notifySignals()
+{
+ Q_D(QLanguageServer);
+ return &d->notifySignals;
+}
+
+void QLanguageServer::registerMethods(QJsonRpc::TypedRpc &typedRpc)
+{
+ typedRpc.installMessagePreprocessor(
+ [this](const QJsonDocument &doc, const QJsonParseError &err,
+ const QJsonRpcProtocol::Handler<QJsonRpcProtocol::Response> &responder) {
+ Q_D(QLanguageServer);
+ if (!doc.isObject()) {
+ qCWarning(lspServerLog)
+ << "non object jsonrpc message" << doc << err.errorString();
+ return QJsonRpcProtocol::Processing::Stop;
+ }
+ bool sendErrorResponse = false;
+ RunStatus rState;
+ QJsonValue id = doc.object()[u"id"];
+ {
+ QMutexLocker l(&d->mutex);
+ if (d->runStatus != RunStatus::DidInitialize) {
+ if (d->runStatus == RunStatus::DidSetup && !doc.isNull()
+ && doc.object()[u"method"].toString()
+ == QString::fromUtf8(
+ QLspSpecification::Requests::InitializeMethod)) {
+ return QJsonRpcProtocol::Processing::Continue;
+ } else if (!doc.isNull()
+ && doc.object()[u"method"].toString()
+ == QString::fromUtf8(
+ QLspSpecification::Notifications::ExitMethod)) {
+ return QJsonRpcProtocol::Processing::Continue;
+ }
+ if (id.isString() || id.isDouble()) {
+ sendErrorResponse = true;
+ rState = d->runStatus;
+ } else {
+ return QJsonRpcProtocol::Processing::Stop;
+ }
+ }
+ }
+ if (!sendErrorResponse) {
+ if (id.isString() || id.isDouble()) {
+ QMutexLocker l(&d->mutex);
+ d->requestsInProgress.insert(id, QRequestInProgress {});
+ }
+ return QJsonRpcProtocol::Processing::Continue;
+ }
+ if (rState == RunStatus::NotSetup || rState == RunStatus::DidSetup)
+ responder(QJsonRpcProtocol::MessageHandler::error(
+ int(QLspSpecification::ErrorCodes::ServerNotInitialized),
+ u"Request on non initialized Language Server (runStatus %1): %2"_qs
+ .arg(int(rState))
+ .arg(QString::fromUtf8(doc.toJson()))));
+ else
+ responder(QJsonRpcProtocol::MessageHandler::error(
+ int(QLspSpecification::ErrorCodes::InvalidRequest),
+ u"Method called on stopping Language Server (runStatus %1)"_qs.arg(
+ int(rState))));
+ return QJsonRpcProtocol::Processing::Stop;
+ });
+ typedRpc.installOnCloseAction([this](QJsonRpc::TypedResponse::Status,
+ const QJsonRpc::IdType &id, QJsonRpc::TypedRpc &) {
+ Q_D(QLanguageServer);
+ QJsonValue idValue = QTypedJson::toJsonValue(id);
+ bool lastReq;
+ {
+ QMutexLocker l(&d->mutex);
+ d->requestsInProgress.remove(idValue);
+ lastReq = d->runStatus == RunStatus::WaitPending && d->requestsInProgress.size() <= 1;
+ if (lastReq)
+ d->runStatus = RunStatus::Stopping;
+ }
+ if (lastReq)
+ executeShutdown();
+ });
+}
+
+void QLanguageServer::setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
+ QLspSpecification::InitializeResult &serverInfo)
+{
+ Q_D(QLanguageServer);
+ for (auto module : qAsConst(d->modules))
+ module->setupCapabilities(clientInfo, serverInfo);
+}
+
+const QLspSpecification::InitializeParams &QLanguageServer::clientInfo() const
+{
+ const Q_D(QLanguageServer);
+ switch (d->runStatus) {
+ case RunStatus::NotSetup:
+ case RunStatus::SettingUp:
+ case RunStatus::DidSetup:
+ case RunStatus::Initializing:
+ if (int(runStatus()) < int(RunStatus::DidInitialize))
+ qCWarning(lspServerLog) << "asked for Language Server clientInfo before initialization";
+ break;
+ case RunStatus::DidInitialize:
+ case RunStatus::WaitPending:
+ case RunStatus::Stopping:
+ case RunStatus::Stopped:
+ break;
+ }
+ return d->clientInfo;
+}
+
+const QLspSpecification::InitializeResult &QLanguageServer::serverInfo() const
+{
+ const Q_D(QLanguageServer);
+ switch (d->runStatus) {
+ case RunStatus::NotSetup:
+ case RunStatus::SettingUp:
+ case RunStatus::DidSetup:
+ case RunStatus::Initializing:
+ qCWarning(lspServerLog) << "asked for Language Server serverInfo before initialization";
+ break;
+ case RunStatus::DidInitialize:
+ case RunStatus::WaitPending:
+ case RunStatus::Stopping:
+ case RunStatus::Stopped:
+ break;
+ }
+ return d->serverInfo;
+}
+
+void QLanguageServer::receiveData(const QByteArray &d)
+{
+ protocol()->receiveData(d);
+}
+
+void QLanguageServer::registerHandlers(QLanguageServerProtocol *protocol)
+{
+ QObject::connect(notifySignals(), &QLspNotifySignals::receivedCancelNotification, this,
+ [this](const QLspSpecification::Notifications::CancelParamsType &params) {
+ Q_D(QLanguageServer);
+ QJsonValue id = QTypedJson::toJsonValue(params.id);
+ QMutexLocker l(&d->mutex);
+ if (d->requestsInProgress.contains(id))
+ d->requestsInProgress[id].canceled = true;
+ else
+ qCWarning(lspServerLog)
+ << "Ignoring cancellation of non in progress request" << id;
+ });
+
+ protocol->registerInitializeRequestHandler(
+ [this](const QByteArray &,
+ const QLspSpecification::Requests::InitializeParamsType &params,
+ QLspSpecification::Responses::InitializeResponseType &&response) {
+ qCDebug(lspServerLog) << "init";
+ Q_D(QLanguageServer);
+ RunStatus rStatus;
+ {
+ QMutexLocker l(&d->mutex);
+ rStatus = d->runStatus;
+ if (rStatus == RunStatus::DidSetup)
+ d->runStatus = RunStatus::Initializing;
+ }
+ if (rStatus != RunStatus::DidSetup) {
+ if (rStatus == RunStatus::NotSetup || rStatus == RunStatus::SettingUp)
+ response.sendErrorResponse(
+ int(QLspSpecification::ErrorCodes::InvalidRequest),
+ u"Initialization request received on non setup language server"_qs
+ .toUtf8());
+ else
+ response.sendErrorResponse(
+ int(QLspSpecification::ErrorCodes::InvalidRequest),
+ u"Received multiple initialization requests"_qs.toUtf8());
+ emit lifecycleError();
+ return;
+ }
+ emit runStatusChanged(RunStatus::Initializing);
+ d->clientInfo = params;
+ setupCapabilities(d->clientInfo, d->serverInfo);
+ {
+ QMutexLocker l(&d->mutex);
+ d->runStatus = RunStatus::DidInitialize;
+ }
+ emit runStatusChanged(RunStatus::DidInitialize);
+ response.sendResponse(d->serverInfo);
+ });
+
+ QObject::connect(notifySignals(), &QLspNotifySignals::receivedInitializedNotification, this,
+ [this](const QLspSpecification::Notifications::InitializedParamsType &) {
+ Q_D(QLanguageServer);
+ {
+ QMutexLocker l(&d->mutex);
+ d->clientInitialized = true;
+ }
+ emit clientInitialized(this);
+ });
+
+ protocol->registerShutdownRequestHandler(
+ [this](const QByteArray &, const QLspSpecification::Requests::ShutdownParamsType &,
+ QLspSpecification::Responses::ShutdownResponseType &&response) {
+ Q_D(QLanguageServer);
+ RunStatus rStatus;
+ bool shouldExecuteShutdown = false;
+ {
+ QMutexLocker l(&d->mutex);
+ rStatus = d->runStatus;
+ if (rStatus == RunStatus::DidInitialize) {
+ d->shutdownResponse = std::move(response);
+ if (d->requestsInProgress.size() <= 1) {
+ d->runStatus = RunStatus::Stopping;
+ shouldExecuteShutdown = true;
+ } else {
+ d->runStatus = RunStatus::WaitPending;
+ }
+ }
+ }
+ if (rStatus != RunStatus::DidInitialize)
+ emit lifecycleError();
+ else if (shouldExecuteShutdown)
+ executeShutdown();
+ });
+
+ QObject::connect(notifySignals(), &QLspNotifySignals::receivedExitNotification, this,
+ [this](const QLspSpecification::Notifications::ExitParamsType &) {
+ if (runStatus() != RunStatus::Stopped)
+ emit lifecycleError();
+ else
+ emit exit();
+ });
+}
+
+void QLanguageServer::executeShutdown()
+{
+ RunStatus rStatus = runStatus();
+ if (rStatus != RunStatus::Stopping) {
+ emit lifecycleError();
+ return;
+ }
+ emit shutdown();
+ QLspSpecification::Responses::ShutdownResponseType shutdownResponse;
+ {
+ Q_D(QLanguageServer);
+ QMutexLocker l(&d->mutex);
+ rStatus = d->runStatus;
+ if (rStatus == RunStatus::Stopping) {
+ shutdownResponse = std::move(d->shutdownResponse);
+ d->runStatus = RunStatus::Stopped;
+ }
+ }
+ if (rStatus != RunStatus::Stopping)
+ emit lifecycleError();
+ else
+ shutdownResponse.sendResponse(nullptr);
+}
+
+bool QLanguageServer::isRequestCanceled(const QJsonRpc::IdType &id) const
+{
+ const Q_D(QLanguageServer);
+ QJsonValue idVal = QTypedJson::toJsonValue(id);
+ QMutexLocker l(&d->mutex);
+ return d->requestsInProgress.value(idVal).canceled || d->runStatus != RunStatus::DidInitialize;
+}
+
+bool QLanguageServer::isInitialized() const
+{
+ switch (runStatus()) {
+ case RunStatus::NotSetup:
+ case RunStatus::SettingUp:
+ case RunStatus::DidSetup:
+ case RunStatus::Initializing:
+ return false;
+ case RunStatus::DidInitialize:
+ case RunStatus::WaitPending:
+ case RunStatus::Stopping:
+ case RunStatus::Stopped:
+ break;
+ }
+ return true;
+}
+
+QT_END_NAMESPACE
diff --git a/tools/qmlls/qlanguageserver.h b/tools/qmlls/qlanguageserver.h
new file mode 100644
index 0000000000..218658a90d
--- /dev/null
+++ b/tools/qmlls/qlanguageserver.h
@@ -0,0 +1,128 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef QLANGUAGESERVER_P_H
+#define QLANGUAGESERVER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include <QtLanguageServer/private/qlanguageserverspec_p.h>
+#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
+#include <QtLanguageServer/private/qlspnotifysignals_p.h>
+#include <QtCore/qloggingcategory.h>
+
+QT_BEGIN_NAMESPACE
+
+class QLanguageServer;
+class QLanguageServerPrivate;
+Q_DECLARE_LOGGING_CATEGORY(lspServerLog)
+
+class QLanguageServerModule : public QObject
+{
+ Q_OBJECT
+public:
+ QLanguageServerModule(QObject *parent = nullptr) : QObject(parent) { }
+ virtual QString name() const = 0;
+ virtual void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) = 0;
+ virtual void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
+ QLspSpecification::InitializeResult &) = 0;
+};
+
+class QLanguageServer : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(RunStatus runStatus READ runStatus NOTIFY runStatusChanged)
+ Q_PROPERTY(bool isInitialized READ isInitialized)
+public:
+ QLanguageServer(const QJsonRpcTransport::DataHandler &h, QObject *parent = nullptr);
+ ~QLanguageServer();
+ enum class RunStatus {
+ NotSetup,
+ SettingUp,
+ DidSetup,
+ Initializing,
+ DidInitialize, // normal state of execution
+ WaitPending,
+ Stopping,
+ Stopped
+ };
+
+ QLanguageServerProtocol *protocol();
+ void finishSetup();
+ void registerHandlers(QLanguageServerProtocol *protocol);
+ void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
+ QLspSpecification::InitializeResult &serverInfo);
+ void addServerModule(QLanguageServerModule *serverModule);
+ QLanguageServerModule *moduleByName(const QString &n) const;
+ QLspNotifySignals *notifySignals();
+
+ // API
+ RunStatus runStatus() const;
+ bool isInitialized() const;
+ bool isRequestCanceled(const QJsonRpc::IdType &id) const;
+ const QLspSpecification::InitializeParams &clientInfo() const;
+ const QLspSpecification::InitializeResult &serverInfo() const;
+
+public slots:
+ void receiveData(const QByteArray &d);
+signals:
+ void runStatusChanged(RunStatus);
+ void clientInitialized(QLanguageServer *server);
+ void shutdown();
+ void exit();
+ void lifecycleError();
+
+private:
+ void registerMethods(QJsonRpc::TypedRpc &typedRpc);
+ void executeShutdown();
+ Q_DISABLE_COPY(QLanguageServer)
+ Q_DECLARE_PRIVATE(QLanguageServer)
+};
+
+QT_END_NAMESPACE
+
+#endif // QLANGUAGESERVER_P_H
diff --git a/tools/qmlls/qlanguageserver_p.h b/tools/qmlls/qlanguageserver_p.h
new file mode 100644
index 0000000000..2e170c9b87
--- /dev/null
+++ b/tools/qmlls/qlanguageserver_p.h
@@ -0,0 +1,88 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef QLANGUAGESERVER_P_P_H
+#define QLANGUAGESERVER_P_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include "qlanguageserver.h"
+#include <QtLanguageServer/private/qlanguageserverprotocol_p.h>
+#include <QtCore/QMutex>
+#include <QtCore/QHash>
+#include <QtCore/private/qobject_p.h>
+#include <QtLanguageServer/private/qlspnotifysignals_p.h>
+
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+
+class QRequestInProgress
+{
+public:
+ bool canceled = false;
+};
+
+class QLanguageServerPrivate : public QObjectPrivate
+{
+public:
+ QLanguageServerPrivate(const QJsonRpcTransport::DataHandler &h);
+ mutable QMutex mutex;
+ // mutex gated, monotonically increasing
+ QLanguageServer::RunStatus runStatus = QLanguageServer::RunStatus::NotSetup;
+ QHash<QJsonValue, QRequestInProgress> requestsInProgress; // mutex gated
+ bool clientInitialized = false; // mutex gated
+ QLspSpecification::InitializeParams clientInfo; // immutable after runStatus > DidInitialize
+ QLspSpecification::InitializeResult serverInfo; // immutable after runStatus > DidInitialize
+ QLspSpecification::Responses::ShutdownResponseType shutdownResponse;
+ QHash<QString, QLanguageServerModule *> modules;
+ QLanguageServerProtocol protocol;
+ QLspNotifySignals notifySignals;
+};
+
+QT_END_NAMESPACE
+#endif // QLANGUAGESERVER_P_P_H
diff --git a/tools/qmlls/qmllanguageservertool.cpp b/tools/qmlls/qmllanguageservertool.cpp
new file mode 100644
index 0000000000..93c50c8614
--- /dev/null
+++ b/tools/qmlls/qmllanguageservertool.cpp
@@ -0,0 +1,213 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qqmllanguageserver.h"
+#include <QtCore/qdebug.h>
+#include <QtCore/qfile.h>
+#include <QtCore/qdir.h>
+#include <QtCore/qfileinfo.h>
+#include <QtCore/qcoreapplication.h>
+#include "../shared/qqmltoolingsettings.h"
+#include <QtCore/qdiriterator.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qjsonarray.h>
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qmutex.h>
+#include <QtCore/QMutexLocker>
+#include <QtCore/qscopedpointer.h>
+#include <QtCore/qrunnable.h>
+#include <QtCore/qthreadpool.h>
+#include <QtCore/qtimer.h>
+
+#include <QtQmlCompiler/private/qqmljsresourcefilemapper_p.h>
+#include <QtQmlCompiler/private/qqmljscompiler_p.h>
+#include <QtQmlCompiler/private/qqmljslogger_p.h>
+#include <QtQmlCompiler/private/qqmljsscope_p.h>
+#include <QtQmlCompiler/private/qqmljsimporter_p.h>
+#if QT_CONFIG(commandlineparser)
+# include <QtCore/qcommandlineparser.h>
+#endif
+
+#ifndef QT_BOOTSTRAPPED
+# include <QtCore/qlibraryinfo.h>
+#endif
+
+#include "qlanguageserver_p.h"
+
+#include <iostream>
+#ifdef Q_OS_WIN32
+# include <fcntl.h>
+# include <io.h>
+#endif
+
+using namespace QmlLsp;
+
+QFile *logFile = nullptr;
+QBasicMutex *logFileLock = nullptr;
+
+class StdinReader : public QObject
+{
+ Q_OBJECT
+public:
+ void run()
+ {
+ auto guard = qScopeGuard([this]() { emit eof(); });
+ char data[256];
+ auto buffer = static_cast<char *>(data);
+ while (std::cin.get(buffer[0])) { // should poll/select and process events
+ const int read = std::cin.readsome(buffer + 1, 255) + 1;
+ emit receivedData(QByteArray(buffer, read));
+ }
+ }
+signals:
+ void receivedData(const QByteArray &data);
+ void eof();
+};
+
+// To debug:
+//
+// * simple logging can be redirected to a file
+// passing -l <file> to the qmlls command
+//
+// * more complex debugging can use named pipes:
+//
+// mkfifo qmllsIn
+// mkfifo qmllsOut
+//
+// this together with a qmllsEcho script that can be defined as
+//
+// #!/bin/sh
+// cat -u < ~/qmllsOut &
+// cat -u > ~/qmllsIn
+//
+// allows to use qmllsEcho as lsp server, and still easily start
+// it in a terminal
+//
+// qmlls < ~/qmllsIn > ~/qmllsOut
+//
+// * statup can be slowed down to have the time to attach via the
+// -w <nSeconds> flag.
+
+int main(int argv, char *argc[])
+{
+#ifdef Q_OS_WIN32
+ // windows does not open stdin/stdout in binary mode by default
+ int err = _setmode(_fileno(stdout), _O_BINARY);
+ if (err == -1)
+ perror("Cannot set mode for stdout");
+ err = _setmode(_fileno(stdin), _O_BINARY);
+ if (err == -1)
+ perror("Cannot set mode for stdin");
+#endif
+
+ qSetGlobalQHashSeed(0);
+ QCoreApplication app(argv, argc);
+ QCoreApplication::setApplicationName("qmllanguageserver");
+ QCoreApplication::setApplicationVersion(QT_VERSION_STR);
+#if QT_CONFIG(commandlineparser)
+ QCommandLineParser parser;
+ QQmlToolingSettings settings(QLatin1String("qmllanguageserver"));
+ parser.setApplicationDescription(QLatin1String(R"(QML languageserver
+)"));
+
+
+ QCommandLineOption waitOption(QStringList() << "w"
+ << "wait",
+ QLatin1String("Waits the given number of seconds before startup"),
+ QLatin1String("waitSeconds"));
+ parser.addOption(waitOption);
+
+ QCommandLineOption verboseOption(
+ QStringList() << "v"
+ << "verbose",
+ QLatin1String("Outputs extra information on the operations being performed"));
+ parser.addOption(verboseOption);
+
+ QCommandLineOption logFileOption(QStringList() << "l"
+ << "log-file",
+ QLatin1String("Writes logging to the given file"),
+ QLatin1String("logFile"));
+ parser.addOption(logFileOption);
+
+ parser.process(app);
+ if (parser.isSet(logFileOption)) {
+ QString fileName = parser.value(logFileOption);
+ qInfo() << "will log to" << fileName;
+ logFile = new QFile(fileName);
+ logFileLock = new QMutex;
+ logFile->open(QFile::WriteOnly | QFile::Truncate | QFile::Text);
+ qInstallMessageHandler([](QtMsgType t, const QMessageLogContext &, const QString &msg) {
+ QMutexLocker l(logFileLock);
+ logFile->write(QString::number(int(t)).toUtf8());
+ logFile->write(" ");
+ logFile->write(msg.toUtf8());
+ logFile->write("\n");
+ logFile->flush();
+ });
+ }
+ if (parser.isSet(verboseOption))
+ QLoggingCategory::setFilterRules("qt.languageserver*.debug=true\n");
+ if (parser.isSet(waitOption)) {
+ int waitSeconds = parser.value(waitOption).toInt();
+ if (waitSeconds > 0)
+ qDebug() << "waiting";
+ QThread::sleep(waitSeconds);
+ qDebug() << "starting";
+ }
+#endif
+ QMutex writeMutex;
+ QQmlLanguageServer qmlServer([&writeMutex](const QByteArray &data) {
+ QMutexLocker l(&writeMutex);
+ std::cout.write(data.constData(), data.length());
+ std::cout.flush();
+ });
+ StdinReader *r = new StdinReader;
+ QObject::connect(r, &StdinReader::receivedData, qmlServer.server(),
+ &QLanguageServer::receiveData);
+ QObject::connect(r, &StdinReader::eof, &app, []() {
+ QTimer::singleShot(100, []() {
+ QCoreApplication::processEvents();
+ QCoreApplication::exit();
+ });
+ });
+ QThreadPool::globalInstance()->start([r]() { r->run(); });
+ app.exec();
+ return qmlServer.returnValue();
+}
+
+#include "qmllanguageservertool.moc"
diff --git a/tools/qmlls/qmllintsuggestions.cpp b/tools/qmlls/qmllintsuggestions.cpp
new file mode 100644
index 0000000000..d9f68b1ee0
--- /dev/null
+++ b/tools/qmlls/qmllintsuggestions.cpp
@@ -0,0 +1,221 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qmllintsuggestions.h"
+#include <QtLanguageServer/private/qlanguageserverspec_p.h>
+#include <QtQmlLint/private/qqmllinter_p.h>
+#include <QtQmlCompiler/private/qqmljslogger_p.h>
+#include <QtCore/qlibraryinfo.h>
+#include <QtCore/qtimer.h>
+#include <QtCore/qdebug.h>
+
+using namespace QLspSpecification;
+using namespace QQmlJS::Dom;
+
+Q_LOGGING_CATEGORY(lintLog, "qt.languageserver.lint")
+
+QT_BEGIN_NAMESPACE
+namespace QmlLsp {
+
+static DiagnosticSeverity severityFromString(const QStringView &str)
+{
+ if (str.compare(u"debug", Qt::CaseInsensitive) == 0)
+ return DiagnosticSeverity::Hint;
+ else if (str.compare(u"warning", Qt::CaseInsensitive) == 0)
+ return DiagnosticSeverity::Warning;
+ else if (str.compare(u"critical", Qt::CaseInsensitive) == 0)
+ return DiagnosticSeverity::Error;
+ else if (str.compare(u"fatal", Qt::CaseInsensitive) == 0)
+ return DiagnosticSeverity::Error;
+ else if (str.compare(u"info", Qt::CaseInsensitive) == 0)
+ return DiagnosticSeverity::Information;
+ else
+ return DiagnosticSeverity::Information;
+}
+
+static DiagnosticSeverity severityFromMsgType(QtMsgType t)
+{
+ switch (t) {
+ case QtDebugMsg:
+ return DiagnosticSeverity::Hint;
+ case QtInfoMsg:
+ return DiagnosticSeverity::Information;
+ case QtWarningMsg:
+ return DiagnosticSeverity::Warning;
+ case QtCriticalMsg:
+ case QtFatalMsg:
+ break;
+ }
+ return DiagnosticSeverity::Error;
+}
+
+QmlLintSuggestions::QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel)
+ : m_server(server), m_codeModel(codeModel)
+{
+ QObject::connect(m_codeModel, &QmlLsp::QQmlCodeModel::updatedSnapshot, this,
+ &QmlLintSuggestions::diagnose, Qt::DirectConnection);
+}
+
+void QmlLintSuggestions::diagnose(const QByteArray &uri)
+{
+ const int maxInvalidMsec = 4000;
+ qCDebug(lintLog) << "diagnose start";
+ QmlLsp::OpenDocumentSnapshot snapshot = m_codeModel->snapshotByUri(uri);
+ QList<Diagnostic> diagnostics;
+ std::optional<int> version;
+ DomItem doc;
+ {
+ QMutexLocker l(&m_mutex);
+ LastLintUpdate &lastUpdate = m_lastUpdate[uri];
+ if (lastUpdate.version && *lastUpdate.version == version) {
+ qCDebug(lspServerLog) << "skipped update of " << uri << "unchanged valid doc";
+ return;
+ }
+ if (snapshot.validDocVersion
+ && (!lastUpdate.version || *snapshot.validDocVersion > *lastUpdate.version)) {
+ doc = snapshot.validDoc;
+ version = snapshot.validDocVersion;
+ } else if (snapshot.docVersion
+ && (!lastUpdate.version || *snapshot.docVersion > *lastUpdate.version)) {
+ if (!lastUpdate.version || !snapshot.validDocVersion
+ || (lastUpdate.invalidUpdatesSince
+ && lastUpdate.invalidUpdatesSince->msecsTo(QDateTime::currentDateTime())
+ > maxInvalidMsec)) {
+ doc = snapshot.doc;
+ version = snapshot.docVersion;
+ } else if (!lastUpdate.invalidUpdatesSince) {
+ lastUpdate.invalidUpdatesSince = QDateTime::currentDateTime();
+ QTimer::singleShot(maxInvalidMsec, Qt::VeryCoarseTimer, this,
+ [this, uri]() { diagnose(uri); });
+ }
+ }
+ if (doc) {
+ // update immediately, and do not keep track of sent version, thus in extreme cases sent
+ // updates could be out of sync
+ lastUpdate.version = version;
+ lastUpdate.invalidUpdatesSince = {};
+ }
+ }
+ QString fileContents;
+ if (doc) {
+ qCDebug(lintLog) << "has doc, do real lint";
+ QStringList imports;
+ imports.append(QLibraryInfo::path(QLibraryInfo::QmlImportsPath));
+ // add m_server->clientInfo().rootUri & co?
+ bool useAbsolutePath = false;
+ bool silent = true;
+ QString filename = doc.canonicalFilePath();
+ fileContents = doc.field(Fields::code).value().toString();
+ QJsonArray json;
+ QStringList qmltypesFiles;
+ QStringList resourceFiles;
+ QMap<QString, QQmlJSLogger::Option> options;
+
+ QQmlLinter linter(imports, useAbsolutePath);
+
+ linter.lintFile(filename, &fileContents, silent, &json, imports, qmltypesFiles,
+ resourceFiles, options);
+ auto addLength = [&fileContents](Position &position, int startOffset, int length) {
+ int i = startOffset;
+ int iEnd = i + length;
+ if (iEnd > int(fileContents.size()))
+ iEnd = fileContents.size();
+ while (i < iEnd) {
+ if (fileContents.at(i) == u'\n') {
+ ++position.line;
+ position.character = 0;
+ if (i + 1 < iEnd && fileContents.at(i) == u'\r')
+ ++i;
+ } else {
+ ++position.character;
+ }
+ ++i;
+ }
+ };
+
+ auto jsonToDiagnostic = [&addLength](const QJsonValue &message) {
+ Diagnostic diagnostic;
+ diagnostic.severity = severityFromString(message[u"type"].toString());
+ Range &range = diagnostic.range;
+ Position &position = range.start;
+ position.line = message[u"line"].toInt(1) - 1;
+ position.character = message[u"column"].toInt(1) - 1;
+ range.end = position;
+ addLength(range.end, message[u"charOffset"].toInt(), message[u"length"].toInt());
+ diagnostic.message = message[u"message"].toString().toUtf8();
+ diagnostic.source = QByteArray("qmllint");
+ return diagnostic;
+ };
+ doc.iterateErrors(
+ [&diagnostics, &addLength](DomItem, ErrorMessage msg) {
+ Diagnostic diagnostic;
+ diagnostic.severity = severityFromMsgType(QtMsgType(int(msg.level)));
+ // do something with msg.errorGroups ?
+ auto &location = msg.location;
+ Range &range = diagnostic.range;
+ range.start.line = location.startLine - 1;
+ range.start.character = location.startColumn - 1;
+ range.end = range.start;
+ addLength(range.end, location.offset, location.length);
+ diagnostic.code = QByteArray(msg.errorId.data(), msg.errorId.size());
+ diagnostic.source = "domParsing";
+ diagnostic.message = msg.message.toUtf8();
+ diagnostics.append(diagnostic);
+ return true;
+ },
+ true);
+ for (const auto &results : qAsConst(json)) {
+ if (results[u"filename"].toString() == filename) {
+ for (const auto &message : results[u"warnings"].toArray())
+ diagnostics.append(jsonToDiagnostic(message));
+ }
+ }
+ }
+ PublishDiagnosticsParams diagnosticParams;
+ diagnosticParams.uri = uri;
+ diagnosticParams.diagnostics = diagnostics;
+ diagnosticParams.version = version;
+
+ m_server->protocol()->notifyPublishDiagnostics(diagnosticParams);
+ qCDebug(lintLog) << "lint" << QString::fromUtf8(uri) << "found"
+ << diagnosticParams.diagnostics.size() << "issues"
+ << QTypedJson::toJsonValue(diagnosticParams);
+}
+
+} // namespace QmlLsp
+QT_END_NAMESPACE
diff --git a/tools/qmlls/qmllintsuggestions.h b/tools/qmlls/qmllintsuggestions.h
new file mode 100644
index 0000000000..b3dfbd1ae4
--- /dev/null
+++ b/tools/qmlls/qmllintsuggestions.h
@@ -0,0 +1,71 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef QMLLINTSUGGESTIONS_H
+#define QMLLINTSUGGESTIONS_H
+
+#include "qlanguageserver.h"
+#include "qqmlcodemodel.h"
+
+#include <optional>
+
+QT_BEGIN_NAMESPACE
+namespace QmlLsp {
+struct LastLintUpdate
+{
+ std::optional<int> version;
+ std::optional<QDateTime> invalidUpdatesSince;
+};
+
+class QmlLintSuggestions : public QObject
+{
+ Q_OBJECT
+public:
+ QmlLintSuggestions(QLanguageServer *server, QmlLsp::QQmlCodeModel *codeModel);
+public slots:
+ void diagnose(const QByteArray &uri);
+
+private:
+ QMutex m_mutex;
+ QHash<QByteArray, LastLintUpdate> m_lastUpdate;
+ QLanguageServer *m_server;
+ QmlLsp::QQmlCodeModel *m_codeModel;
+};
+} // namespace QmlLsp
+QT_END_NAMESPACE
+#endif // QMLLINTSUGGESTIONS_H
diff --git a/tools/qmlls/qqmlcodemodel.cpp b/tools/qmlls/qqmlcodemodel.cpp
new file mode 100644
index 0000000000..77e0298180
--- /dev/null
+++ b/tools/qmlls/qqmlcodemodel.cpp
@@ -0,0 +1,610 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qqmllanguageserver.h"
+#include "qqmlcodemodel.h"
+#include <QtCore/qfileinfo.h>
+#include <QtCore/qdir.h>
+#include <QtCore/qthreadpool.h>
+#include <QtQmlDom/private/qqmldomtop_p.h>
+#include "textdocument.h"
+
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+
+namespace QmlLsp {
+
+Q_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel")
+
+using namespace QQmlJS::Dom;
+
+/*!
+\internal
+\class QQmlCodeModel
+
+The code model offers a view of the current state of the current files, and traks open files.
+All methods are threadsafe, and generally return immutable or threadsafe objects that can be
+worked on from any thread (unless otherwise noted).
+The idea is the let all other operations be as lock free as possible, concentrating all tricky
+synchronization here.
+
+\section2 Global views
+\list
+\li currentEnv() offers a view that contains the latest version of all the loaded files
+\li validEnv() is just like current env but stores only the valid (meaning correctly parsed,
+ not necessarily without errors) version of a file, it is normally a better choice to load the
+ dependencies/symbol information from
+\endlist
+
+\section2 OpenFiles
+\list
+\li snapshotByUri() returns an OpenDocumentSnapshot of an open document. Form it you can get the
+ document, its latest valid version, scope,... and connected to a specific version of the document
+ and immutable. The signal updatedSnapshot() is called every time a snapshot changes (also for
+ every partial change: document change, validDocument change, scope change).
+\li openDocumentByUri() is a lower level and more intrusive access to OpenDocument objects. These
+ contains the current snapshot, and shared pointer to a Utils::TextDocument. This is *always* the
+ current version of the document, and has line by line support.
+ Working on it is more delicate and intrusive, because you have to explicitly acquire its mutex()
+ before *any* read or write/modification to it.
+ It has a version nuber which is supposed to always change and increase.
+ It is mainly used for highlighting/indenting, and s immediately updated when the user edits a
+ document. Its use should be avoided if possible, preferring the snapshots.
+\endlist
+
+\section2 Parallelism/Theading
+Most operations are not parallel and usially take place in the main thread (but are still thread
+safe).
+There are two main task that are executed in parallel: Indexing, and OpenDocumentUpdate.
+Indexing is meant to keep the global view up to date.
+OpenDocumentUpdate keeps the snapshots of the open documents up to date.
+
+There is always a tension between bein responsive, using all threads available, and avoid to hog too
+many resources. One can choose different parallelization strategies, we went with a flexiable
+approach.
+We have (private) functions that execute part of the work: indexSome() and openUpdateSome(). These
+do all locking needed, get some work, do it without locks, and at the end update the state of the
+code model. If there is more work, then they return true. Thus while (xxxSome()); works until there
+is no work left.
+
+addDirectoriesToIndex(), the internal addDirectory() and addOpenToUpdate() add more work to do.
+
+indexNeedsUpdate() and openNeedUpdate(), check if there is work to do, and if yes ensure that a
+worker thread (or more) that work on it exist.
+
+An initial implementation allowed one to register a callback to be called when a given open document
+had some chosen parts of the snapshot up to date. But I did not need anythign more that the
+updatedSnapshot() signal, so that has been removed, but something like that might become useful in
+the future.
+*/
+
+QQmlCodeModel::QQmlCodeModel(QObject *parent)
+ : QObject { parent },
+ m_currentEnv(std::make_shared<DomEnvironment>(QStringList(),
+ DomEnvironment::Option::SingleThreaded)),
+ m_validEnv(std::make_shared<DomEnvironment>(QStringList(),
+ DomEnvironment::Option::SingleThreaded))
+{
+}
+
+OpenDocumentSnapshot QQmlCodeModel::snapshotByUri(const QByteArray &uri)
+{
+ return openDocumentByUri(uri).snapshot;
+}
+
+int QQmlCodeModel::indexEvalProgress() const
+{
+ // should be called with acquired mutex
+ const int dirCost = 10;
+ int costToDo = 1;
+ for (const ToIndex &el : qAsConst(m_toIndex))
+ costToDo += dirCost * el.leftDepth;
+ costToDo += m_indexInProgressCost;
+ return m_indexDoneCost * 100 / (costToDo + m_indexDoneCost);
+}
+
+void QQmlCodeModel::indexStart()
+{
+ Q_ASSERT(!m_mutex.tryLock()); // should be called while locked
+ qCDebug(codeModelLog) << "indexStart";
+}
+
+void QQmlCodeModel::indexEnd()
+{
+ Q_ASSERT(!m_mutex.tryLock()); // should be called while locked
+ qCDebug(codeModelLog) << "indexEnd";
+ m_lastIndexProgress = 0;
+ m_nIndexInProgress = 0;
+ m_nUpdateInProgress = 0;
+ m_toIndex.clear();
+ m_indexInProgressCost = 0;
+ m_indexDoneCost = 0;
+}
+
+void QQmlCodeModel::indexSendProgress(int progress)
+{
+ if (progress <= m_lastIndexProgress)
+ return;
+ m_lastIndexProgress = progress;
+ // to do: send progress
+}
+
+bool QQmlCodeModel::indexCancelled()
+{
+ return false;
+}
+
+void QQmlCodeModel::indexDirectory(const QString &path, int depthLeft)
+{
+ if (indexCancelled())
+ return;
+ QDir dir(path);
+ if (depthLeft > 1) {
+ const QStringList dirs =
+ dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
+ for (const QString &child : dirs)
+ addDirectory(dir.filePath(child), --depthLeft);
+ }
+ const QStringList qmljs = dir.entryList(QStringList({ "*.qml", "*.js", "*.mjs" }), QDir::Files);
+ int progress = 0;
+ {
+ QMutexLocker l(&m_mutex);
+ m_indexInProgressCost += qmljs.size();
+ progress = indexEvalProgress();
+ }
+ indexSendProgress(progress);
+ if (qmljs.isEmpty())
+ return;
+ DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
+ int iFile = 0;
+ for (const QString &file : qmljs) {
+ if (indexCancelled())
+ return;
+ QString fPath = dir.filePath(file);
+ QFileInfo fInfo(fPath);
+ QString cPath = fInfo.canonicalFilePath();
+ if (!cPath.isEmpty()) {
+ bool isNew = false;
+ newCurrent.loadFile(cPath, fPath,
+ [&isNew](Path, DomItem &oldValue, DomItem &newValue) {
+ if (oldValue != newValue)
+ isNew = true;
+ },
+ {});
+ newCurrent.loadPendingDependencies();
+ if (isNew) {
+ newCurrent.commitToBase();
+ m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
+ }
+ }
+ ++iFile;
+ {
+ QMutexLocker l(&m_mutex);
+ ++m_indexDoneCost;
+ --m_indexInProgressCost;
+ progress = indexEvalProgress();
+ }
+ indexSendProgress(progress);
+ }
+}
+
+void QQmlCodeModel::addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server)
+{
+ Q_UNUSED(server);
+ // to do create progress, &scan in a separate instance
+ const int maxDepth = 5;
+ for (const auto &path : paths)
+ addDirectory(path, maxDepth);
+ indexNeedsUpdate();
+}
+
+void QQmlCodeModel::addDirectory(const QString &path, int depthLeft)
+{
+ if (depthLeft < 1)
+ return;
+ {
+ QMutexLocker l(&m_mutex);
+ auto it = m_toIndex.begin();
+ auto end = m_toIndex.end();
+ while (it != end) {
+ if (it->path.startsWith(path)) {
+ if (it->path.size() == path.size())
+ return;
+ if (it->path.at(path.size()) == u'/') {
+ it = m_toIndex.erase(it);
+ continue;
+ }
+ } else if (path.startsWith(it->path) && path.at(it->path.size()) == u'/')
+ return;
+ }
+ m_toIndex.append({ path, depthLeft });
+ }
+}
+
+void QQmlCodeModel::removeDirectory(const QString &path)
+{
+ {
+ QMutexLocker l(&m_mutex);
+ auto toRemove = [path](const QString &p) {
+ return p.startsWith(path) && (p.size() == path.size() || p.at(path.size()) == u'/');
+ };
+ auto it = m_toIndex.begin();
+ auto end = m_toIndex.end();
+ while (it != end) {
+ if (toRemove(it->path))
+ it = m_toIndex.erase(it);
+ else
+ ++it;
+ }
+ }
+ if (auto validEnvPtr = m_validEnv.ownerAs<DomEnvironment>())
+ validEnvPtr->removePath(path);
+ if (auto currentEnvPtr = m_currentEnv.ownerAs<DomEnvironment>())
+ currentEnvPtr->removePath(path);
+}
+
+QString QQmlCodeModel::uri2Path(const QByteArray &uri, UriLookup options)
+{
+ QString res;
+ {
+ QMutexLocker l(&m_mutex);
+ res = m_uri2path.value(uri);
+ }
+ if (!res.isEmpty() && options == UriLookup::Caching)
+ return res;
+ QUrl url(QString::fromUtf8(uri));
+ QFileInfo f(url.toLocalFile());
+ QString cPath = f.canonicalFilePath();
+ if (cPath.isEmpty())
+ cPath = f.filePath();
+ {
+ QMutexLocker l(&m_mutex);
+ if (!res.isEmpty() && res != cPath)
+ m_path2uri.remove(res);
+ m_uri2path.insert(uri, cPath);
+ m_path2uri.insert(cPath, uri);
+ }
+ return cPath;
+}
+
+void QQmlCodeModel::newOpenFile(const QByteArray &uri, int version, const QString &docText)
+{
+ {
+ QMutexLocker l(&m_mutex);
+ auto &openDoc = m_openDocuments[uri];
+ if (!openDoc.textDocument)
+ openDoc.textDocument = std::make_shared<Utils::TextDocument>();
+ QMutexLocker l2(openDoc.textDocument->mutex());
+ openDoc.textDocument->setVersion(version);
+ openDoc.textDocument->setPlainText(docText);
+ }
+ addOpenToUpdate(uri);
+ openNeedUpdate();
+}
+
+OpenDocument QQmlCodeModel::openDocumentByUri(const QByteArray &uri)
+{
+ QMutexLocker l(&m_mutex);
+ return m_openDocuments.value(uri);
+}
+
+void QQmlCodeModel::indexNeedsUpdate()
+{
+ const int maxIndexThreads = 1;
+ {
+ QMutexLocker l(&m_mutex);
+ if (m_toIndex.isEmpty() || m_nIndexInProgress >= maxIndexThreads)
+ return;
+ if (++m_nIndexInProgress == 1)
+ indexStart();
+ }
+ QThreadPool::globalInstance()->start([this]() {
+ while (indexSome()) { }
+ });
+}
+
+bool QQmlCodeModel::indexSome()
+{
+ qCDebug(codeModelLog) << "indexSome";
+ ToIndex toIndex;
+ {
+ QMutexLocker l(&m_mutex);
+ if (m_toIndex.isEmpty()) {
+ if (--m_nIndexInProgress == 0)
+ indexEnd();
+ return false;
+ }
+ toIndex = m_toIndex.last();
+ m_toIndex.removeLast();
+ }
+ bool hasMore = false;
+ auto guard = qScopeGuard([this, &hasMore]() {
+ QMutexLocker l(&m_mutex);
+ if (m_toIndex.isEmpty()) {
+ if (--m_nIndexInProgress == 0)
+ indexEnd();
+ hasMore = false;
+ } else {
+ hasMore = true;
+ }
+ });
+ indexDirectory(toIndex.path, toIndex.leftDepth);
+ return hasMore;
+}
+
+void QQmlCodeModel::openNeedUpdate()
+{
+ qCDebug(codeModelLog) << "openNeedUpdate";
+ const int maxIndexThreads = 1;
+ {
+ QMutexLocker l(&m_mutex);
+ if (m_openDocumentsToUpdate.isEmpty() || m_nUpdateInProgress >= maxIndexThreads)
+ return;
+ if (++m_nUpdateInProgress == 1)
+ openUpdateStart();
+ }
+ QThreadPool::globalInstance()->start([this]() {
+ while (openUpdateSome()) { }
+ });
+}
+
+bool QQmlCodeModel::openUpdateSome()
+{
+ qCDebug(codeModelLog) << "openUpdateSome start";
+ QByteArray toUpdate;
+ {
+ QMutexLocker l(&m_mutex);
+ if (m_openDocumentsToUpdate.isEmpty()) {
+ if (--m_nUpdateInProgress == 0)
+ openUpdateEnd();
+ return false;
+ }
+ auto it = m_openDocumentsToUpdate.find(m_lastOpenDocumentUpdated);
+ auto end = m_openDocumentsToUpdate.end();
+ if (it == end)
+ it = m_openDocumentsToUpdate.begin();
+ else if (++it == end)
+ it = m_openDocumentsToUpdate.begin();
+ toUpdate = *it;
+ m_openDocumentsToUpdate.erase(it);
+ }
+ bool hasMore = false;
+ {
+ auto guard = qScopeGuard([this, &hasMore]() {
+ QMutexLocker l(&m_mutex);
+ if (m_openDocumentsToUpdate.isEmpty()) {
+ if (--m_nUpdateInProgress == 0)
+ openUpdateEnd();
+ hasMore = false;
+ } else {
+ hasMore = true;
+ }
+ });
+ openUpdate(toUpdate);
+ }
+ return hasMore;
+}
+
+void QQmlCodeModel::openUpdateStart()
+{
+ qCDebug(codeModelLog) << "openUpdateStart";
+}
+
+void QQmlCodeModel::openUpdateEnd()
+{
+ qCDebug(codeModelLog) << "openUpdateEnd";
+}
+
+DomItem QQmlCodeModel::validDocForUpdate(DomItem &item)
+{
+ if (item.field(Fields::isValid).value().toBool(false)) {
+ if (auto envPtr = m_validEnv.ownerAs<DomEnvironment>()) {
+ switch (item.fileObject().internalKind()) {
+ case DomType::QmlFile:
+ envPtr->addQmlFile(item.fileObject().ownerAs<QmlFile>());
+ break;
+ case DomType::JsFile:
+ envPtr->addJsFile(item.fileObject().ownerAs<JsFile>());
+ break;
+ default:
+ qCWarning(lspServerLog)
+ << "Unexpected file type " << item.fileObject().internalKindStr();
+ return DomItem();
+ }
+ return m_validEnv.path(item.canonicalPath());
+ }
+ }
+ return DomItem();
+}
+
+void QQmlCodeModel::newDocForOpenFile(const QByteArray &uri, int version, const QString &docText)
+{
+ qCDebug(codeModelLog) << "updating doc" << uri << "to version" << version << "("
+ << docText.length() << "chars)";
+ DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
+ QString fPath = uri2Path(uri, UriLookup::ForceLookup);
+ Path p;
+ newCurrent.loadFile(
+ fPath, fPath, docText, QDateTime::currentDateTimeUtc(),
+ [&p](Path, DomItem &, DomItem &newValue) { p = newValue.fileObject().canonicalPath(); },
+ {});
+ newCurrent.loadPendingDependencies();
+ if (p) {
+ newCurrent.commitToBase();
+ DomItem item = m_currentEnv.path(p);
+ DomItem vDoc = validDocForUpdate(item);
+ {
+ QMutexLocker l(&m_mutex);
+ OpenDocument &doc = m_openDocuments[uri];
+ if (!doc.textDocument) {
+ qCWarning(lspServerLog)
+ << "ignoring update to closed document" << QString::fromUtf8(uri);
+ return;
+ } else {
+ QMutexLocker l(doc.textDocument->mutex());
+ if (doc.textDocument->version() && *doc.textDocument->version() > version) {
+ qCWarning(lspServerLog)
+ << "docUpdate: version" << version << "of document"
+ << QString::fromUtf8(uri) << "is not the latest anymore";
+ return;
+ }
+ }
+ if (!doc.snapshot.docVersion || *doc.snapshot.docVersion < version) {
+ doc.snapshot.docVersion = version;
+ doc.snapshot.doc = item;
+ } else {
+ qCWarning(lspServerLog) << "skippig update of current doc to obsolete version"
+ << version << "of document" << QString::fromUtf8(uri);
+ }
+ if (vDoc) {
+ if (!doc.snapshot.validDocVersion || *doc.snapshot.validDocVersion < version) {
+ doc.snapshot.validDocVersion = version;
+ doc.snapshot.validDoc = vDoc;
+ } else {
+ qCWarning(lspServerLog) << "skippig update of valid doc to obsolete version"
+ << version << "of document" << QString::fromUtf8(uri);
+ }
+ } else {
+ qCWarning(lspServerLog)
+ << "avoid update of validDoc to " << version << "of document"
+ << QString::fromUtf8(uri) << "as it is invalid";
+ }
+ }
+ }
+ if (codeModelLog().isDebugEnabled()) {
+ qCDebug(codeModelLog) << "finished update doc of " << uri << "to version" << version;
+ snapshotByUri(uri).dump(qDebug() << "postSnapshot",
+ OpenDocumentSnapshot::DumpOption::AllCode);
+ }
+ // we should update the scope in the future thus call addOpen(uri)
+ emit updatedSnapshot(uri);
+}
+
+void QQmlCodeModel::closeOpenFile(const QByteArray &uri)
+{
+ QMutexLocker l(&m_mutex);
+ m_openDocuments.remove(uri);
+}
+
+void QQmlCodeModel::openUpdate(const QByteArray &uri)
+{
+ bool updateDoc = false;
+ bool updateScope = false;
+ std::optional<int> rNow = 0;
+ QString docText;
+ DomItem validDoc;
+ std::shared_ptr<Utils::TextDocument> document;
+ {
+ QMutexLocker l(&m_mutex);
+ OpenDocument &doc = m_openDocuments[uri];
+ document = doc.textDocument;
+ if (!document)
+ return;
+ {
+ QMutexLocker l2(document->mutex());
+ rNow = document->version();
+ }
+ if (rNow && (!doc.snapshot.docVersion || *doc.snapshot.docVersion != *rNow))
+ updateDoc = true;
+ else if (doc.snapshot.validDocVersion
+ && (!doc.snapshot.scopeVersion
+ || *doc.snapshot.scopeVersion != *doc.snapshot.validDocVersion))
+ updateScope = true;
+ else
+ return;
+ if (updateDoc) {
+ QMutexLocker l2(doc.textDocument->mutex());
+ rNow = doc.textDocument->version();
+ docText = doc.textDocument->toPlainText();
+ } else {
+ validDoc = doc.snapshot.validDoc;
+ rNow = doc.snapshot.validDocVersion;
+ }
+ }
+ if (updateDoc) {
+ newDocForOpenFile(uri, *rNow, docText);
+ }
+ if (updateScope) {
+ // to do
+ }
+}
+
+void QQmlCodeModel::addOpenToUpdate(const QByteArray &uri)
+{
+ QMutexLocker l(&m_mutex);
+ m_openDocumentsToUpdate.insert(uri);
+}
+
+QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options)
+{
+ dbg.noquote().nospace() << "{";
+ dbg << " uri:" << QString::fromUtf8(uri) << "\n";
+ dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_qs) << "\n";
+ if (options & DumpOption::LatestCode) {
+ dbg << " doc: ------------\n"
+ << doc.field(Fields::code).value().toString() << "\n==========\n";
+ } else {
+ dbg << u" doc:"
+ << (doc ? u"%1chars"_qs.arg(doc.field(Fields::code).value().toString().length())
+ : u"*none*"_qs)
+ << "\n";
+ }
+ dbg << " validDocVersion:"
+ << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_qs) << "\n";
+ if (options & DumpOption::ValidCode) {
+ dbg << " validDoc: ------------\n"
+ << validDoc.field(Fields::code).value().toString() << "\n==========\n";
+ } else {
+ dbg << u" validDoc:"
+ << (validDoc ? u"%1chars"_qs.arg(
+ validDoc.field(Fields::code).value().toString().length())
+ : u"*none*"_qs)
+ << "\n";
+ }
+ dbg << " scopeVersion:" << (scopeVersion ? QString::number(*scopeVersion) : u"*none*"_qs)
+ << "\n";
+ dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n";
+ dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n";
+ dbg << "}";
+ return dbg;
+}
+
+} // namespace QmlLsp
+
+QT_END_NAMESPACE
diff --git a/tools/qmlls/qqmlcodemodel.h b/tools/qmlls/qqmlcodemodel.h
new file mode 100644
index 0000000000..43d07cec80
--- /dev/null
+++ b/tools/qmlls/qqmlcodemodel.h
@@ -0,0 +1,149 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef QQMLCODEMODEL_H
+#define QQMLCODEMODEL_H
+
+#include <QObject>
+#include <QHash>
+#include <QtQmlDom/private/qqmldomitem_p.h>
+#include <QtQmlCompiler/private/qqmljsscope_p.h>
+#include "qlanguageserver_p.h"
+#include "textdocument.h"
+
+#include <functional>
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+class TextSynchronization;
+namespace QmlLsp {
+
+class OpenDocumentSnapshot
+{
+public:
+ enum class DumpOption {
+ NoCode = 0,
+ LatestCode = 0x1,
+ ValidCode = 0x2,
+ AllCode = LatestCode | ValidCode
+ };
+ Q_DECLARE_FLAGS(DumpOptions, DumpOption)
+ QByteArray uri;
+ std::optional<int> docVersion;
+ QQmlJS::Dom::DomItem doc;
+ std::optional<int> validDocVersion;
+ QQmlJS::Dom::DomItem validDoc;
+ std::optional<int> scopeVersion;
+ QDateTime scopeDependenciesLoadTime;
+ bool scopeDependenciesChanged = false;
+ QQmlJSScope::Ptr scope = {};
+ QDebug dump(QDebug dbg, DumpOptions dump = DumpOption::NoCode);
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(OpenDocumentSnapshot::DumpOptions)
+
+class OpenDocument
+{
+public:
+ OpenDocumentSnapshot snapshot;
+ std::shared_ptr<Utils::TextDocument> textDocument;
+};
+
+struct ToIndex
+{
+ QString path;
+ int leftDepth;
+};
+
+class QQmlCodeModel : public QObject
+{
+ Q_OBJECT
+public:
+ enum class UriLookup { Caching, ForceLookup };
+
+ explicit QQmlCodeModel(QObject *parent = nullptr);
+ QQmlJS::Dom::DomItem currentEnv();
+ QQmlJS::Dom::DomItem validEnv();
+ OpenDocumentSnapshot snapshotByUri(const QByteArray &uri);
+ OpenDocument openDocumentByUri(const QByteArray &uri);
+
+ void openNeedUpdate();
+ void indexNeedsUpdate();
+ void addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server);
+ void addOpenToUpdate(const QByteArray &);
+ void removeDirectory(const QString &path);
+ // void updateDocument(const OpenDocument &doc);
+ QString uri2Path(const QByteArray &uri, UriLookup options = UriLookup::Caching);
+ void newOpenFile(const QByteArray &uri, int version, const QString &docText);
+ void newDocForOpenFile(const QByteArray &uri, int version, const QString &docText);
+ void closeOpenFile(const QByteArray &uri);
+signals:
+ void updatedSnapshot(const QByteArray &uri);
+private:
+ QQmlJS::Dom::DomItem validDocForUpdate(QQmlJS::Dom::DomItem &item);
+ void indexDirectory(const QString &path, int depthLeft);
+ int indexEvalProgress() const; // to be called in the mutex
+ void indexStart(); // to be called in the mutex
+ void indexEnd(); // to be called in the mutex
+ void indexSendProgress(int progress);
+ bool indexCancelled();
+ bool indexSome();
+ void addDirectory(const QString &path, int leftDepth);
+ bool openUpdateSome();
+ void openUpdateStart();
+ void openUpdateEnd();
+ void openUpdate(const QByteArray &);
+ QMutex m_mutex;
+ int m_lastIndexProgress = 0;
+ int m_nIndexInProgress = 0;
+ QList<ToIndex> m_toIndex;
+ int m_indexInProgressCost = 0;
+ int m_indexDoneCost = 0;
+ int m_nUpdateInProgress = 0;
+ QQmlJS::Dom::DomItem m_currentEnv;
+ QQmlJS::Dom::DomItem m_validEnv;
+ QByteArray m_lastOpenDocumentUpdated;
+ QSet<QByteArray> m_openDocumentsToUpdate;
+ QHash<QByteArray, QString> m_uri2path;
+ QHash<QString, QByteArray> m_path2uri;
+ QHash<QByteArray, OpenDocument> m_openDocuments;
+};
+
+} // namespace QmlLsp
+QT_END_NAMESPACE
+#endif // QQMLCODEMODEL_H
diff --git a/tools/qmlls/qqmllanguageserver.cpp b/tools/qmlls/qqmllanguageserver.cpp
new file mode 100644
index 0000000000..8bb9b4c830
--- /dev/null
+++ b/tools/qmlls/qqmllanguageserver.cpp
@@ -0,0 +1,145 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include "qqmllanguageserver.h"
+
+#include "textsynchronization.h"
+
+#include "qlanguageserver.h"
+#include <QtCore/qdir.h>
+
+#include <iostream>
+
+QT_BEGIN_NAMESPACE
+
+namespace QmlLsp {
+
+using namespace QLspSpecification;
+/*!
+\internal
+\class QmlLsp::QQmlLanguageServer
+\brief Class that sets up a QmlLanguageServer
+
+This class sets up a QML language server.
+It needs a function
+\code
+std::function<void(const QByteArray &)> sendData
+\endcode
+to send out its replies, and one should feed the data it receives to the server()->receive() method.
+It is expected to call this method only from a single thread, and not to block, the simplest way to
+achieve this is to avoid direct calls, and connect it as slot, while reading from another thread.
+
+The Server is build with separate QLanguageServerModule that implement a given functionality, and
+all of them are constructed and registered with the QLanguageServer in the constructor o this class.
+
+Generally all operations are expected to be done in the object thread, and handlers are always
+called from it, but they are free to delegate the response to another thread, the response handler
+is thread safe. All the methods of the server() obect are also threadsafe.
+
+The code model starts other threads to update its state, see its documentation for more information.
+*/
+QQmlLanguageServer::QQmlLanguageServer(std::function<void(const QByteArray &)> sendData)
+ : m_server(sendData),
+ m_textSynchronization(&m_codeModel),
+ m_lint(&m_server, &m_codeModel)
+{
+ m_server.addServerModule(this);
+ m_server.addServerModule(&m_textSynchronization);
+ m_server.finishSetup();
+ qCWarning(lspServerLog) << "Did Setup";
+}
+
+void QQmlLanguageServer::registerHandlers(QLanguageServer *server,
+ QLanguageServerProtocol *protocol)
+{
+ Q_UNUSED(protocol);
+ QObject::connect(server, &QLanguageServer::lifecycleError, this,
+ &QQmlLanguageServer::errorExit);
+ QObject::connect(server, &QLanguageServer::exit, this, &QQmlLanguageServer::exit);
+ QObject::connect(server, &QLanguageServer::runStatusChanged, [](QLanguageServer::RunStatus r) {
+ qCDebug(lspServerLog) << "runStatus" << int(r);
+ });
+}
+
+void QQmlLanguageServer::setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
+ QLspSpecification::InitializeResult &serverInfo)
+{
+ Q_UNUSED(clientInfo);
+ Q_UNUSED(serverInfo);
+}
+
+QString QQmlLanguageServer::name() const
+{
+ return u"QQmlLanguageServer"_qs;
+}
+
+void QQmlLanguageServer::errorExit()
+{
+ qCWarning(lspServerLog) << "Error exit";
+ fclose(stdin);
+}
+
+void QQmlLanguageServer::exit()
+{
+ m_returnValue = 0;
+ fclose(stdin);
+}
+
+int QQmlLanguageServer::returnValue() const
+{
+ return m_returnValue;
+}
+
+QLanguageServer *QQmlLanguageServer::server()
+{
+ return &m_server;
+}
+
+TextSynchronization *QQmlLanguageServer::textSynchronization()
+{
+ return &m_textSynchronization;
+}
+
+QmlLintSuggestions *QQmlLanguageServer::lint()
+{
+ return &m_lint;
+}
+
+} // namespace QmlLsp
+
+QT_END_NAMESPACE
diff --git a/tools/qmlls/qqmllanguageserver.h b/tools/qmlls/qqmllanguageserver.h
new file mode 100644
index 0000000000..0e2458ff24
--- /dev/null
+++ b/tools/qmlls/qqmllanguageserver.h
@@ -0,0 +1,82 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef QQMLLANGUAGESERVER_H
+#define QQMLLANGUAGESERVER_H
+
+#include "qlanguageserver_p.h"
+#include "qqmlcodemodel.h"
+#include "textsynchronization.h"
+#include "qmllintsuggestions.h"
+
+QT_BEGIN_NAMESPACE
+namespace QmlLsp {
+
+class QQmlLanguageServer : public QLanguageServerModule
+{
+ Q_OBJECT
+public:
+ QQmlLanguageServer(std::function<void(const QByteArray &)> sendData);
+
+ QString name() const final;
+ void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) final;
+ void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
+ QLspSpecification::InitializeResult &serverInfo) final;
+
+ int returnValue() const;
+
+ QQmlCodeModel *codeModel();
+ QLanguageServer *server();
+ TextSynchronization *textSynchronization();
+ QmlLintSuggestions *lint();
+
+public slots:
+ void exit();
+ void errorExit();
+
+private:
+ QQmlCodeModel m_codeModel;
+ QLanguageServer m_server;
+ TextSynchronization m_textSynchronization;
+ QmlLintSuggestions m_lint;
+ int m_returnValue = 1;
+};
+
+} // namespace QmlLsp
+QT_END_NAMESPACE
+#endif // QQMLLANGUAGESERVER_H
diff --git a/tools/qmlls/textblock.cpp b/tools/qmlls/textblock.cpp
new file mode 100644
index 0000000000..2ea09b9e25
--- /dev/null
+++ b/tools/qmlls/textblock.cpp
@@ -0,0 +1,137 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "textblock.h"
+#include "textdocument.h"
+
+#include <QtCore/qstring.h>
+
+namespace Utils {
+
+bool TextBlock::isValid() const
+{
+ return m_document;
+}
+
+void TextBlock::setBlockNumber(int blockNumber)
+{
+ m_blockNumber = blockNumber;
+}
+
+int TextBlock::blockNumber() const
+{
+ return m_blockNumber;
+}
+
+void TextBlock::setPosition(int position)
+{
+ m_position = position;
+}
+
+int TextBlock::position() const
+{
+ return m_position;
+}
+
+void TextBlock::setLength(int length)
+{
+ m_length = length;
+}
+
+int TextBlock::length() const
+{
+ return m_length;
+}
+
+TextBlock TextBlock::next() const
+{
+ return m_document->findBlockByNumber(m_blockNumber + 1);
+}
+
+TextBlock TextBlock::previous() const
+{
+ return m_document->findBlockByNumber(m_blockNumber - 1);
+}
+
+int TextBlock::userState() const
+{
+ return m_document->userState(m_blockNumber);
+}
+
+void TextBlock::setUserState(int state)
+{
+ m_document->setUserState(m_blockNumber, state);
+}
+
+void TextBlock::setDocument(TextDocument *document)
+{
+ m_document = document;
+}
+
+TextDocument *TextBlock::document() const
+{
+ return m_document;
+}
+
+QString TextBlock::text() const
+{
+ return document()->toPlainText().mid(position(), length());
+}
+
+int TextBlock::revision() const
+{
+ return m_revision;
+}
+
+void TextBlock::setRevision(int rev)
+{
+ m_revision = rev;
+}
+
+bool TextBlock::operator==(const TextBlock &other) const
+{
+ return document() == other.document() && blockNumber() == other.blockNumber();
+}
+
+bool TextBlock::operator!=(const TextBlock &other) const
+{
+ return !operator==(other);
+}
+
+} // namespace Utils
diff --git a/tools/qmlls/textblock.h b/tools/qmlls/textblock.h
new file mode 100644
index 0000000000..edc7e77363
--- /dev/null
+++ b/tools/qmlls/textblock.h
@@ -0,0 +1,97 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef TEXTBLOCK_H
+#define TEXTBLOCK_H
+
+#include <QtCore/qstring.h>
+
+namespace Utils {
+
+class TextDocument;
+class TextBlockUserData;
+
+class TextBlock
+{
+public:
+ bool isValid() const;
+
+ void setBlockNumber(int blockNumber);
+ int blockNumber() const;
+
+ void setPosition(int position);
+ int position() const;
+
+ void setLength(int length);
+ int length() const;
+
+ TextBlock next() const;
+ TextBlock previous() const;
+
+ int userState() const;
+ void setUserState(int state);
+
+ bool isVisible() const;
+ void setVisible(bool visible);
+
+ void setLineCount(int count);
+ int lineCount() const;
+
+ void setDocument(TextDocument *document);
+ TextDocument *document() const;
+
+ QString text() const;
+
+ int revision() const;
+ void setRevision(int rev);
+
+ bool operator==(const TextBlock &other) const;
+ bool operator!=(const TextBlock &other) const;
+
+private:
+ TextDocument *m_document = nullptr;
+ int m_revision = 0;
+
+ int m_position = 0;
+ int m_length = 0;
+ int m_blockNumber = -1;
+};
+
+} // namespace Utils
+
+#endif // TEXTBLOCK_H
diff --git a/tools/qmlls/textcursor.cpp b/tools/qmlls/textcursor.cpp
new file mode 100644
index 0000000000..d74a9bf0da
--- /dev/null
+++ b/tools/qmlls/textcursor.cpp
@@ -0,0 +1,158 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "textcursor.h"
+#include "textdocument.h"
+#include "textblock.h"
+
+namespace Utils {
+
+class TextFrame;
+class TextTable;
+class TextTableCell;
+
+TextCursor::TextCursor(TextDocument *document) : m_document(document) { }
+
+bool TextCursor::movePosition(TextCursor::MoveOperation op, TextCursor::MoveMode mode, int n)
+{
+ Q_UNUSED(n);
+ switch (op) {
+ case NoMove:
+ return true;
+ case Start:
+ m_position = 0;
+ break;
+ case PreviousCharacter:
+ while (--n >= 0) {
+ if (m_position == 0)
+ return false;
+ --m_position;
+ }
+ break;
+ case End:
+ m_position = m_document->characterCount();
+ break;
+ case NextCharacter:
+ while (--n >= 0) {
+ if (m_position == m_document->characterCount())
+ return false;
+ ++m_position;
+ }
+ break;
+ }
+
+ if (mode == MoveAnchor)
+ m_anchor = m_position;
+
+ return false;
+}
+
+int TextCursor::position() const
+{
+ return m_position;
+}
+
+void TextCursor::setPosition(int pos, Utils::TextCursor::MoveMode mode)
+{
+ m_position = pos;
+ if (mode == MoveAnchor)
+ m_anchor = pos;
+}
+
+QString TextCursor::selectedText() const
+{
+ return m_document->toPlainText().mid(qMin(m_position, m_anchor), qAbs(m_position - m_anchor));
+}
+
+void TextCursor::clearSelection()
+{
+ m_anchor = m_position;
+}
+
+TextDocument *TextCursor::document() const
+{
+ return m_document;
+}
+
+void TextCursor::insertText(const QString &text)
+{
+ const QString orig = m_document->toPlainText();
+ const QString left = orig.left(qMin(m_position, m_anchor));
+ const QString right = orig.mid(qMax(m_position, m_anchor));
+ m_document->setPlainText(left + text + right);
+}
+
+TextBlock TextCursor::block() const
+{
+ TextBlock current = m_document->firstBlock();
+ while (current.isValid()) {
+ if (current.position() <= position()
+ && current.position() + current.length() > current.position())
+ break;
+ current = current.next();
+ }
+ return current;
+}
+
+int TextCursor::positionInBlock() const
+{
+ return m_position - block().position();
+}
+
+int TextCursor::blockNumber() const
+{
+ return block().blockNumber();
+}
+
+void TextCursor::removeSelectedText()
+{
+ insertText(QString());
+}
+
+int TextCursor::selectionEnd() const
+{
+ return qMax(m_position, m_anchor);
+}
+
+bool TextCursor::isNull() const
+{
+ return m_document == nullptr;
+}
+
+}
diff --git a/tools/qmlls/textcursor.h b/tools/qmlls/textcursor.h
new file mode 100644
index 0000000000..16f8c4807b
--- /dev/null
+++ b/tools/qmlls/textcursor.h
@@ -0,0 +1,96 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef TEXTCURSOR_H
+#define TEXTCURSOR_H
+
+#include <QtCore/qstring.h>
+
+namespace Utils {
+
+class TextDocument;
+class TextBlock;
+
+class TextCursor
+{
+public:
+ enum MoveOperation {
+ NoMove,
+ Start,
+ PreviousCharacter,
+ End,
+ NextCharacter,
+ };
+
+ enum MoveMode { MoveAnchor, KeepAnchor };
+
+ enum SelectionType { Document };
+
+ TextCursor();
+ TextCursor(const TextBlock &block);
+ TextCursor(TextDocument *document);
+
+ bool movePosition(MoveOperation op, MoveMode = MoveAnchor, int n = 1);
+ int position() const;
+ void setPosition(int pos, MoveMode mode = MoveAnchor);
+ QString selectedText() const;
+ void clearSelection();
+ int anchor() const;
+ TextDocument *document() const;
+ void insertText(const QString &text);
+ TextBlock block() const;
+ int positionInBlock() const;
+ int blockNumber() const;
+
+ void select(SelectionType selection);
+
+ bool hasSelection() const;
+
+ void removeSelectedText();
+ int selectionEnd() const;
+
+ bool isNull() const;
+
+private:
+ TextDocument *m_document = nullptr;
+ int m_position = 0;
+ int m_anchor = 0;
+};
+} // namespace Utils
+
+#endif // TEXTCURSOR_H
diff --git a/tools/qmlls/textdocument.cpp b/tools/qmlls/textdocument.cpp
new file mode 100644
index 0000000000..67032a6f6e
--- /dev/null
+++ b/tools/qmlls/textdocument.cpp
@@ -0,0 +1,152 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "textdocument.h"
+#include "textblock.h"
+
+namespace Utils {
+
+TextDocument::TextDocument(const QString &text)
+{
+ setPlainText(text);
+}
+
+TextBlock TextDocument::findBlockByNumber(int blockNumber) const
+{
+ return (blockNumber >= 0 && blockNumber < m_blocks.length())
+ ? m_blocks.at(blockNumber).textBlock
+ : TextBlock();
+}
+
+TextBlock TextDocument::findBlockByLineNumber(int lineNumber) const
+{
+ return findBlockByNumber(lineNumber);
+}
+
+QChar TextDocument::characterAt(int pos) const
+{
+ return m_content.at(pos);
+}
+
+int TextDocument::characterCount() const
+{
+ return m_content.length();
+}
+
+TextBlock TextDocument::begin() const
+{
+ return m_blocks.isEmpty() ? TextBlock() : m_blocks.at(0).textBlock;
+}
+
+TextBlock TextDocument::firstBlock() const
+{
+ return begin();
+}
+
+TextBlock TextDocument::lastBlock() const
+{
+ return m_blocks.isEmpty() ? TextBlock() : m_blocks.last().textBlock;
+}
+
+std::optional<int> TextDocument::version() const
+{
+ return m_version;
+}
+
+void TextDocument::setVersion(std::optional<int> v)
+{
+ m_version = v;
+}
+
+QString TextDocument::toPlainText() const
+{
+ return m_content;
+}
+
+void TextDocument::setPlainText(const QString &text)
+{
+ m_content = text;
+ m_blocks.clear();
+
+ int blockStart = 0;
+ int blockNumber = 0;
+ while (blockStart < text.length()) {
+ Block block;
+ block.textBlock.setBlockNumber(blockNumber++);
+ block.textBlock.setPosition(blockStart);
+ block.textBlock.setDocument(this);
+
+ int blockEnd = text.indexOf('\n', blockStart) + 1;
+ if (blockEnd == 0)
+ blockEnd = text.length();
+
+ block.textBlock.setLength(blockEnd - blockStart);
+ m_blocks.append(block);
+ blockStart = blockEnd;
+ }
+}
+
+bool TextDocument::isModified() const
+{
+ return m_modified;
+}
+
+void TextDocument::setModified(bool modified)
+{
+ m_modified = modified;
+}
+
+void TextDocument::setUserState(int blockNumber, int state)
+{
+ if (blockNumber >= 0 && blockNumber < m_blocks.length())
+ m_blocks[blockNumber].userState = state;
+}
+
+int TextDocument::userState(int blockNumber) const
+{
+ return (blockNumber >= 0 && blockNumber < m_blocks.length()) ? m_blocks[blockNumber].userState
+ : -1;
+}
+
+QMutex *TextDocument::mutex() const
+{
+ return &m_mutex;
+}
+
+} // namespace Utils
diff --git a/tools/qmlls/textdocument.h b/tools/qmlls/textdocument.h
new file mode 100644
index 0000000000..dcbf937e19
--- /dev/null
+++ b/tools/qmlls/textdocument.h
@@ -0,0 +1,102 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef TEXTDOCUMENT_H
+#define TEXTDOCUMENT_H
+
+#include "textblock.h"
+
+#include <QtCore/qchar.h>
+#include <QtCore/qvector.h>
+#include <QtCore/qscopedpointer.h>
+#include <QtCore/qmutex.h>
+
+#include <optional>
+
+namespace Utils {
+
+class TextBlockUserData;
+
+class TextDocument
+{
+public:
+ TextDocument() = default;
+ explicit TextDocument(const QString &text);
+
+ TextBlock findBlockByNumber(int blockNumber) const;
+ TextBlock findBlockByLineNumber(int lineNumber) const;
+ QChar characterAt(int pos) const;
+ int characterCount() const;
+ TextBlock begin() const;
+ TextBlock firstBlock() const;
+ TextBlock lastBlock() const;
+
+ std::optional<int> version() const;
+ void setVersion(std::optional<int>);
+
+ QString toPlainText() const;
+ void setPlainText(const QString &text);
+
+ bool isModified() const;
+ void setModified(bool modified);
+
+ void setUndoRedoEnabled(bool enable);
+
+ void clear();
+
+ void setUserState(int blockNumber, int state);
+ int userState(int blockNumber) const;
+ QMutex *mutex() const;
+
+private:
+ struct Block
+ {
+ TextBlock textBlock;
+ int userState = -1;
+ };
+
+ QVector<Block> m_blocks;
+
+ QString m_content;
+ bool m_modified = false;
+ std::optional<int> m_version;
+ mutable QMutex m_mutex;
+};
+} // namespace Utils
+
+#endif // TEXTDOCUMENT_H
diff --git a/tools/qmlls/textsynchronization.cpp b/tools/qmlls/textsynchronization.cpp
new file mode 100644
index 0000000000..d23089f7ca
--- /dev/null
+++ b/tools/qmlls/textsynchronization.cpp
@@ -0,0 +1,132 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "textsynchronization.h"
+#include "qqmllanguageserver.h"
+
+#include "textdocument.h"
+
+using namespace QLspSpecification;
+QT_BEGIN_NAMESPACE
+
+TextSynchronization::TextSynchronization(QmlLsp::QQmlCodeModel *codeModel, QObject *parent)
+ : QLanguageServerModule(parent), m_codeModel(codeModel)
+{
+}
+
+void TextSynchronization::didCloseTextDocument(const DidCloseTextDocumentParams &params)
+{
+ m_codeModel->closeOpenFile(params.textDocument.uri);
+}
+
+void TextSynchronization::didOpenTextDocument(const DidOpenTextDocumentParams &params)
+{
+ const TextDocumentItem &item = params.textDocument;
+ const QString fileName = m_codeModel->uri2Path(item.uri);
+
+ m_codeModel->newOpenFile(item.uri, item.version, item.text);
+}
+
+void TextSynchronization::didDidChangeTextDocument(const DidChangeTextDocumentParams &params)
+{
+ QByteArray uri = params.textDocument.uri;
+ const QString fileName = m_codeModel->uri2Path(uri);
+ auto openDoc = m_codeModel->openDocumentByUri(uri);
+ std::shared_ptr<Utils::TextDocument> document = openDoc.textDocument;
+ if (!document) {
+ qCWarning(lspServerLog) << "Ingnoring changes to non open or closed document"
+ << QString::fromUtf8(uri);
+ return;
+ }
+ const auto &changes = params.contentChanges;
+ {
+ QMutexLocker l(document->mutex());
+ for (const auto &change : changes) {
+ if (!change.range) {
+ document->setPlainText(change.text);
+ continue;
+ }
+
+ const auto &range = *change.range;
+ const auto &rangeStart = range.start;
+ const int start =
+ document->findBlockByNumber(rangeStart.line).position() + rangeStart.character;
+ const auto &rangeEnd = range.end;
+ const int end =
+ document->findBlockByNumber(rangeEnd.line).position() + rangeEnd.character;
+
+ document->setPlainText(
+ document->toPlainText().replace(start, end - start, change.text));
+ }
+ document->setVersion(params.textDocument.version);
+ }
+ m_codeModel->addOpenToUpdate(uri);
+ m_codeModel->openNeedUpdate();
+}
+
+void TextSynchronization::registerHandlers(QLanguageServer *server, QLanguageServerProtocol *)
+{
+ QObject::connect(server->notifySignals(),
+ &QLspNotifySignals::receivedDidOpenTextDocumentNotification, this,
+ &TextSynchronization::didOpenTextDocument);
+
+ QObject::connect(server->notifySignals(),
+ &QLspNotifySignals::receivedDidChangeTextDocumentNotification, this,
+ &TextSynchronization::didDidChangeTextDocument);
+
+ QObject::connect(server->notifySignals(),
+ &QLspNotifySignals::receivedDidCloseTextDocumentNotification, this,
+ &TextSynchronization::didCloseTextDocument);
+}
+
+QString TextSynchronization::name() const
+{
+ return u"TextSynchonization"_qs;
+}
+
+void TextSynchronization::setupCapabilities(const QLspSpecification::InitializeParams &,
+ QLspSpecification::InitializeResult &serverInfo)
+{
+ TextDocumentSyncOptions syncOptions;
+ syncOptions.openClose = true;
+ syncOptions.change = TextDocumentSyncKind::Incremental;
+ serverInfo.capabilities.textDocumentSync = syncOptions;
+}
+
+QT_END_NAMESPACE
diff --git a/tools/qmlls/textsynchronization.h b/tools/qmlls/textsynchronization.h
new file mode 100644
index 0000000000..8440b2001a
--- /dev/null
+++ b/tools/qmlls/textsynchronization.h
@@ -0,0 +1,68 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#ifndef TEXTSYNCH_H
+#define TEXTSYNCH_H
+
+#include "qqmlcodemodel.h"
+
+#include "qlanguageserver_p.h"
+
+QT_BEGIN_NAMESPACE
+
+class TextSynchronization : public QLanguageServerModule
+{
+ Q_OBJECT
+public:
+ TextSynchronization(QmlLsp::QQmlCodeModel *codeModel, QObject *parent = nullptr);
+ QString name() const override;
+ void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override;
+ void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
+ QLspSpecification::InitializeResult &) override;
+
+public slots:
+ void didOpenTextDocument(const QLspSpecification::DidOpenTextDocumentParams &params);
+ void didDidChangeTextDocument(const QLspSpecification::DidChangeTextDocumentParams &params);
+ void didCloseTextDocument(const QLspSpecification::DidCloseTextDocumentParams &params);
+
+private:
+ QmlLsp::QQmlCodeModel *m_codeModel;
+};
+
+QT_END_NAMESPACE
+#endif // TEXTSYNCH_H