qmlls: workspace support

Add support for workspace (project files, changes detection) to qmlls

Change-Id: Ife6b40a1ace7339d2185f790bce3291e7c680438
Reviewed-by: Maximilian Goldstein <max.goldstein@qt.io>
This commit is contained in:
Fawzi Mohamed 2022-01-11 14:15:42 +01:00
parent 9290a86ac4
commit 2717985373
6 changed files with 304 additions and 1 deletions

View File

@ -98,6 +98,7 @@ public:
private slots:
void initTestCase() final;
void didOpenTextDocument();
void testWorkspace();
void cleanupTestCase();
private:
@ -105,6 +106,7 @@ private:
QLanguageServerProtocol m_protocol;
DiagnosticsHandler m_diagnosticsHandler;
QString m_qmllsPath;
QList<RegistrationParams> m_registrations;
};
tst_Qmlls::tst_Qmlls()
@ -146,7 +148,16 @@ void tst_Qmlls::initTestCase()
tDoc.publishDiagnostics = pDiag;
pDiag.versionSupport = true;
clientInfo.capabilities.textDocument = tDoc;
QJsonObject workspace({ { u"didChangeWatchedFiles"_qs,
QJsonObject({ { u"dynamicRegistration"_qs, true } }) } });
clientInfo.capabilities.workspace = workspace;
bool didInit = false;
m_protocol.registerRegistrationRequestHandler([this](const QByteArray &,
const RegistrationParams &params,
LSPResponse<std::nullptr_t> &&response) {
m_registrations.append(params);
response.sendResponse();
});
m_protocol.requestInitialize(clientInfo, [this, &didInit](const InitializeResult &serverInfo) {
Q_UNUSED(serverInfo);
m_protocol.notifyInitialized(InitializedParams());
@ -261,6 +272,41 @@ void tst_Qmlls::didOpenTextDocument()
m_diagnosticsHandler.clear();
}
void tst_Qmlls::testWorkspace()
{
QTRY_VERIFY_WITH_TIMEOUT(!m_registrations.isEmpty(), 10000);
QByteArray uri = testFileUrl("default/Zzz.qml").toString().toUtf8();
DidChangeWatchedFilesParams fChanges;
FileEvent fEvent;
fEvent.uri = uri;
fEvent.type = int(FileChangeType::Changed);
fChanges.changes.append(fEvent);
m_protocol.notifyDidChangeWatchedFiles(fChanges);
DidChangeWorkspaceFoldersParams dChanges;
WorkspaceFolder dir;
dir.name = "default";
dir.uri = uri.mid(uri.lastIndexOf('/'));
dChanges.event.added.append(dir);
dChanges.event.removed.append(dir);
m_protocol.notifyDidChangeWorkspaceFolders(dChanges);
DidOpenTextDocumentParams oParams;
TextDocumentItem textDocument;
textDocument.uri = uri;
QFile file(testFile("default/Yyy.qml"));
QVERIFY(file.open(QIODevice::ReadOnly));
textDocument.text = file.readAll().replace("width", "wildth");
oParams.textDocument = textDocument;
m_protocol.notifyDidOpenTextDocument(oParams);
QTRY_VERIFY_WITH_TIMEOUT(m_diagnosticsHandler.numDiagnostics(uri) != 0, 30000);
m_diagnosticsHandler.clear();
DidCloseTextDocumentParams closeP;
closeP.textDocument.uri = uri;
m_protocol.notifyDidCloseTextDocument(closeP);
}
void tst_Qmlls::cleanupTestCase()
{
m_server.closeWriteChannel();

View File

@ -10,6 +10,7 @@ qt_internal_add_tool(${target_name}
qlanguageserver.h qlanguageserver_p.h qlanguageserver.cpp
qqmllanguageserver.h qqmllanguageserver.cpp
qmllanguageservertool.cpp
workspace.cpp workspace.h
textblock.h textblock.cpp
textcursor.h textcursor.cpp
textcursor.cpp textcursor.h

View File

@ -68,11 +68,13 @@ QQmlLanguageServer::QQmlLanguageServer(std::function<void(const QByteArray &)> s
: m_codeModel(nullptr, settings),
m_server(sendData),
m_textSynchronization(&m_codeModel),
m_lint(&m_server, &m_codeModel)
m_lint(&m_server, &m_codeModel),
m_workspace(&m_codeModel)
{
m_server.addServerModule(this);
m_server.addServerModule(&m_textSynchronization);
m_server.addServerModule(&m_lint);
m_server.addServerModule(&m_workspace);
m_server.finishSetup();
qCWarning(lspServerLog) << "Did Setup";
}
@ -138,6 +140,11 @@ QmlLintSuggestions *QQmlLanguageServer::lint()
return &m_lint;
}
WorkspaceHandlers *QQmlLanguageServer::worspace()
{
return &m_workspace;
}
} // namespace QmlLsp
QT_END_NAMESPACE

View File

@ -32,6 +32,7 @@
#include "qqmlcodemodel.h"
#include "textsynchronization.h"
#include "qmllintsuggestions.h"
#include "workspace.h"
#include "../shared/qqmltoolingsettings.h"
QT_BEGIN_NAMESPACE
@ -68,6 +69,7 @@ public:
QLanguageServer *server();
TextSynchronization *textSynchronization();
QmlLintSuggestions *lint();
WorkspaceHandlers *worspace();
public slots:
void exit();
@ -78,6 +80,7 @@ private:
QLanguageServer m_server;
TextSynchronization m_textSynchronization;
QmlLintSuggestions m_lint;
WorkspaceHandlers m_workspace;
int m_returnValue = 1;
};

189
tools/qmlls/workspace.cpp Normal file
View File

@ -0,0 +1,189 @@
/****************************************************************************
**
** 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: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 "workspace.h"
#include "qqmllanguageserver.h"
#include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
#include <QtLanguageServer/private/qlspnotifysignals_p.h>
#include <QtCore/qfile.h>
#include <variant>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
using namespace QLspSpecification;
void WorkspaceHandlers::registerHandlers(QLanguageServer *server, QLanguageServerProtocol *)
{
QObject::connect(server->notifySignals(),
&QLspNotifySignals::receivedDidChangeWorkspaceFoldersNotification, this,
[server, this](const DidChangeWorkspaceFoldersParams &params) {
const WorkspaceFoldersChangeEvent &event = params.event;
const QList<WorkspaceFolder> &removed = event.removed;
QList<QByteArray> toRemove;
for (const WorkspaceFolder &folder : removed) {
toRemove.append(QmlLsp::lspUriToQmlUrl(folder.uri));
m_codeModel->removeDirectory(
m_codeModel->url2Path(QmlLsp::lspUriToQmlUrl(folder.uri)));
}
m_codeModel->removeRootUrls(toRemove);
const QList<WorkspaceFolder> &added = event.added;
QList<QByteArray> toAdd;
QStringList pathsToAdd;
for (const WorkspaceFolder &folder : added) {
toAdd.append(QmlLsp::lspUriToQmlUrl(folder.uri));
pathsToAdd.append(
m_codeModel->url2Path(QmlLsp::lspUriToQmlUrl(folder.uri)));
}
m_codeModel->addRootUrls(toAdd);
m_codeModel->addDirectoriesToIndex(pathsToAdd, server);
});
QObject::connect(server->notifySignals(),
&QLspNotifySignals::receivedDidChangeWatchedFilesNotification, this,
[this](const DidChangeWatchedFilesParams &params) {
const QList<FileEvent> &changes = params.changes;
for (const FileEvent &change : changes) {
const QString filename =
m_codeModel->url2Path(QmlLsp::lspUriToQmlUrl(change.uri));
switch (FileChangeType(change.type)) {
case FileChangeType::Created:
// m_codeModel->addFile(filename);
break;
case FileChangeType::Changed: {
QFile file(filename);
if (file.open(QIODevice::ReadOnly))
// m_modelManager->setFileContents(filename, file.readAll());
break;
}
case FileChangeType::Deleted:
// m_modelManager->removeFile(filename);
break;
}
}
// update due to dep changes...
});
QObject::connect(server, &QLanguageServer::clientInitialized, this,
&WorkspaceHandlers::clientInitialized);
}
QString WorkspaceHandlers::name() const
{
return u"Workspace"_s;
}
void WorkspaceHandlers::setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
QLspSpecification::InitializeResult &serverInfo)
{
if (!clientInfo.capabilities.workspace
|| !clientInfo.capabilities.workspace->value("workspaceFolders").toBool(false))
return;
WorkspaceFoldersServerCapabilities folders;
folders.supported = true;
folders.changeNotifications = true;
if (!serverInfo.capabilities.workspace)
serverInfo.capabilities.workspace = QJsonObject();
serverInfo.capabilities.workspace->insert("workspaceFolders", QTypedJson::toJsonValue(folders));
}
void WorkspaceHandlers::clientInitialized(QLanguageServer *server)
{
QLanguageServerProtocol *protocol = server->protocol();
const auto clientInfo = server->clientInfo();
QList<Registration> registrations;
if (clientInfo.capabilities.workspace
&& clientInfo.capabilities.workspace->value("didChangeWatchedFiles")["dynamicRegistration"]
.toBool(false)) {
const int watchAll =
int(WatchKind::Create) | int(WatchKind::Change) | int(WatchKind::Delete);
DidChangeWatchedFilesRegistrationOptions watchedFilesParams;
FileSystemWatcher qmlWatcher;
qmlWatcher.globPattern = QByteArray("*.{qml,js,mjs}");
qmlWatcher.kind = watchAll;
FileSystemWatcher qmldirWatcher;
qmldirWatcher.globPattern = "qmldir";
qmldirWatcher.kind = watchAll;
FileSystemWatcher qmltypesWatcher;
qmltypesWatcher.globPattern = QByteArray("*.qmltypes");
qmltypesWatcher.kind = watchAll;
watchedFilesParams.watchers =
QList<FileSystemWatcher>({ qmlWatcher, qmldirWatcher, qmltypesWatcher });
registrations.append(Registration {
// use ClientCapabilitiesInfo::WorkspaceDidChangeWatchedFiles as id too
ClientCapabilitiesInfo::WorkspaceDidChangeWatchedFiles,
ClientCapabilitiesInfo::WorkspaceDidChangeWatchedFiles,
QTypedJson::toJsonValue(watchedFilesParams) });
}
if (!registrations.isEmpty()) {
RegistrationParams params;
params.registrations = registrations;
protocol->requestRegistration(
params,
[]() {
// successful registration
},
[protocol](const ResponseError &err) {
LogMessageParams msg;
msg.message = QByteArray("registration of file udates failed, will miss file "
"changes done outside the editor due to error ");
msg.message.append(QString::number(err.code).toUtf8());
if (!err.message.isEmpty())
msg.message.append(" ");
msg.message.append(err.message);
msg.type = MessageType::Warning;
qCWarning(lspServerLog) << QString::fromUtf8(msg.message);
protocol->notifyLogMessage(msg);
});
}
QSet<QString> rootPaths;
if (std::holds_alternative<QByteArray>(clientInfo.rootUri)) {
QString path = m_codeModel->url2Path(
QmlLsp::lspUriToQmlUrl(std::get<QByteArray>(clientInfo.rootUri)));
rootPaths.insert(path);
} else if (clientInfo.rootPath && std::holds_alternative<QByteArray>(*clientInfo.rootPath)) {
QString path = QString::fromUtf8(std::get<QByteArray>(*clientInfo.rootPath));
rootPaths.insert(path);
}
if (clientInfo.workspaceFolders
&& std::holds_alternative<QList<WorkspaceFolder>>(*clientInfo.workspaceFolders)) {
for (const WorkspaceFolder &workspace :
qAsConst(std::get<QList<WorkspaceFolder>>(*clientInfo.workspaceFolders))) {
const QUrl workspaceUrl(QString::fromUtf8(QmlLsp::lspUriToQmlUrl(workspace.uri)));
rootPaths.insert(workspaceUrl.toLocalFile());
}
}
if (m_status == Status::Indexing)
m_codeModel->addDirectoriesToIndex(QStringList(rootPaths.begin(), rootPaths.end()), server);
}
QT_END_NAMESPACE

57
tools/qmlls/workspace.h Normal file
View File

@ -0,0 +1,57 @@
/****************************************************************************
**
** 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: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$
**
****************************************************************************/
#ifndef WORKSPACE_H
#define WORKSPACE_H
#include "qqmlcodemodel.h"
#include "qlanguageserver.h"
QT_BEGIN_NAMESPACE
class WorkspaceHandlers : public QLanguageServerModule
{
Q_OBJECT
public:
enum class Status { NoIndex, Indexing };
WorkspaceHandlers(QmlLsp::QQmlCodeModel *codeModel) : m_codeModel(codeModel) { }
QString name() const override;
void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override;
void setupCapabilities(const QLspSpecification::InitializeParams &clientInfo,
QLspSpecification::InitializeResult &) override;
public slots:
void clientInitialized(QLanguageServer *);
private:
QmlLsp::QQmlCodeModel *m_codeModel = nullptr;
Status m_status = Status::NoIndex;
};
QT_END_NAMESPACE
#endif // WORKSPACE_H