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:
parent
9290a86ac4
commit
2717985373
|
@ -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 ¶ms,
|
||||
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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 ¶ms) {
|
||||
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 ¶ms) {
|
||||
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
|
|
@ -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
|
Loading…
Reference in New Issue