aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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