aboutsummaryrefslogtreecommitdiffstats
path: root/tests/lspclient/lspclient.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/lspclient/lspclient.cpp')
-rw-r--r--tests/lspclient/lspclient.cpp434
1 files changed, 434 insertions, 0 deletions
diff --git a/tests/lspclient/lspclient.cpp b/tests/lspclient/lspclient.cpp
new file mode 100644
index 000000000..40adc9288
--- /dev/null
+++ b/tests/lspclient/lspclient.cpp
@@ -0,0 +1,434 @@
+/****************************************************************************
+**
+** Copyright (C) 2023 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of Qbs.
+**
+** $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 <lsp/clientcapabilities.h>
+#include <lsp/completion.h>
+#include <lsp/initializemessages.h>
+#include <lsp/languagefeatures.h>
+#include <lsp/textsynchronization.h>
+
+#include <QBuffer>
+#include <QCommandLineOption>
+#include <QCommandLineParser>
+#include <QCoreApplication>
+#include <QLocalSocket>
+
+#include <cstdlib>
+#include <iostream>
+
+enum class Command {
+ GotoDefinition,
+ Completion,
+};
+
+class LspClient : public QObject
+{
+public:
+ LspClient(Command command, const QString &socketPath, const QString &codeToInsert,
+ int insertLine, int insertColumn,
+ const QString &filePath, int line, int column);
+ void start();
+
+private:
+ void finishWithError(const QString &msg);
+ void exit(int code);
+ void initiateProtocol();
+ void handleIncomingData();
+ void sendMessage(const lsp::JsonObject &msg);
+ void sendMessage(const lsp::JsonRpcMessage &msg);
+ void handleCurrentMessage();
+ void handleInitializeReply();
+ void openDocument();
+ void insertCode();
+ void sendRequest();
+ void handleResponse();
+ void sendGotoDefinitionRequest();
+ void handleGotoDefinitionResponse();
+ void sendCompletionRequest();
+ void handleCompletionResponse();
+ lsp::DocumentUri uri() const;
+ lsp::DocumentUri::PathMapper mapper() const;
+
+ const Command m_command;
+ const QString m_socketPath;
+ const QString m_codeToInsert;
+ const int m_insertLine;
+ const int m_insertColumn;
+ const QString m_filePath;
+ int m_line;
+ int m_column;
+ QLocalSocket m_socket;
+ QBuffer m_incomingData;
+ lsp::BaseMessage m_currentMessage;
+ QJsonObject m_messageObject;
+
+ enum class State { Inactive, Connecting, Initializing, RunningCommand }
+ m_state = State::Inactive;
+};
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication app(argc, argv);
+
+ const QCommandLineOption socketOption({"socket", "s"}, "The server socket to connect to.",
+ "socket");
+ const QCommandLineOption gotoDefinitionOption(
+ {"goto-def", "g"}, "Go to definition from the specified location.");
+ const QCommandLineOption completionOption(
+ {"completion", "c"}, "Request completion at the specified location.");
+ const QCommandLineOption insertCodeOption("insert-code",
+ "A piece of code to insert before doing the actual "
+ "operation.",
+ "code");
+ const QCommandLineOption insertLocationOption("insert-location",
+ "The location at which to insert the code.",
+ "<line>:<column>");
+ QCommandLineParser parser;
+ parser.addOptions(
+ {socketOption,
+ insertCodeOption,
+ insertLocationOption,
+ gotoDefinitionOption,
+ completionOption});
+ parser.addHelpOption();
+ parser.addPositionalArgument("location", "The location at which to operate.",
+ "<file>:<line>:<column>");
+ parser.process(app);
+
+ const auto complainAndExit = [&](const char *text) {
+ std::cerr << text << std::endl;
+ parser.showHelp(EXIT_FAILURE);
+ };
+
+ if (!parser.isSet(socketOption))
+ complainAndExit("Socket must be specified.");
+
+ // Initialized to suppress warning. TODO: In C++23, mark lambdas as noreturn instead.
+ Command command = Command::GotoDefinition;
+
+ if (parser.isSet(gotoDefinitionOption))
+ command = Command::GotoDefinition;
+ else if (parser.isSet(completionOption))
+ command = Command::Completion;
+ else
+ complainAndExit("Don't know what to do.");
+
+ if (parser.positionalArguments().size() != 1)
+ complainAndExit("Need location.");
+ const auto complainAboutLocationString = [&] { complainAndExit("Invalid location."); };
+ const QString loc = parser.positionalArguments().first();
+ int sep1 = loc.indexOf(':');
+ if (sep1 <= 0)
+ complainAboutLocationString();
+ if (qbs::Internal::HostOsInfo::isWindowsHost()) {
+ sep1 = loc.indexOf(':', sep1 + 1);
+ if (sep1 < 0)
+ complainAboutLocationString();
+ }
+ const int sep2 = loc.indexOf(':', sep1 + 1);
+ if (sep2 == -1 || sep2 == loc.size() - 1)
+ complainAboutLocationString();
+ const auto extractNumber = [&](const QString &s) {
+ bool ok;
+ const int n = s.toInt(&ok);
+ if (!ok || n <= 0)
+ complainAboutLocationString();
+ return n;
+ };
+ const int line = extractNumber(loc.mid(sep1 + 1, sep2 - sep1 - 1));
+ const int column = extractNumber(loc.mid(sep2 + 1));
+
+ const QString insertLoc = parser.value(insertLocationOption);
+ int insertLine = -1;
+ int insertColumn = -1;
+ if (insertLoc.isEmpty()) {
+ insertLine = line;
+ insertColumn = column;
+ } else {
+ const int sep = insertLoc.indexOf(':');
+ if (sep <= 0)
+ complainAboutLocationString();
+ insertLine = extractNumber(insertLoc.left(sep));
+ insertColumn = extractNumber(insertLoc.mid(sep + 1));
+ }
+
+ LspClient client(command, parser.value(socketOption), parser.value(insertCodeOption),
+ insertLine, insertColumn,
+ QDir::fromNativeSeparators(loc.left(sep1)), line, column);
+ QMetaObject::invokeMethod(&client, &LspClient::start, Qt::QueuedConnection);
+
+ return app.exec();
+}
+
+LspClient::LspClient(Command command, const QString &socketPath, const QString &codeToInsert,
+ int insertLine, int insertColumn,
+ const QString &filePath, int line, int column)
+ : m_command(command), m_socketPath(socketPath), m_codeToInsert(codeToInsert),
+ m_insertLine(insertLine), m_insertColumn(insertColumn),
+ m_filePath(filePath), m_line(line), m_column(column)
+{
+ connect(&m_socket, &QLocalSocket::disconnected, this, [this] {
+ finishWithError("Server disconnected unexpectedly.");
+ });
+ connect(&m_socket, &QLocalSocket::errorOccurred, this, [this] {
+ finishWithError(QString::fromLatin1("Socket error: %1").arg(m_socket.errorString()));
+ });
+ connect(&m_socket, &QLocalSocket::connected, this, &LspClient::initiateProtocol);
+ connect(&m_socket, &QLocalSocket::readyRead, this, &LspClient::handleIncomingData);
+}
+
+void LspClient::start()
+{
+ m_state = State::Connecting;
+ m_incomingData.open(QIODevice::ReadWrite | QIODevice::Append);
+ m_socket.connectToServer(m_socketPath);
+}
+
+void LspClient::finishWithError(const QString &msg)
+{
+ std::cerr << qPrintable(msg) << std::endl;
+ m_socket.disconnectFromServer();
+ exit(EXIT_FAILURE);
+}
+
+void LspClient::exit(int code)
+{
+ m_socket.disconnect(this);
+ qApp->exit(code);
+}
+
+void LspClient::initiateProtocol()
+{
+ if (m_state != State::Connecting) {
+ finishWithError(QString::fromLatin1("State should be %1, was %2.")
+ .arg(int(State::Connecting), int(m_state)));
+ return;
+ }
+ m_state = State::Initializing;
+
+ lsp::DynamicRegistrationCapabilities definitionCaps;
+ definitionCaps.setDynamicRegistration(false);
+ lsp::TextDocumentClientCapabilities docCaps;
+ docCaps.setDefinition(definitionCaps);
+ lsp::ClientCapabilities clientCaps;
+ clientCaps.setTextDocument(docCaps);
+ lsp::InitializeParams initParams;
+ initParams.setCapabilities(clientCaps);
+ sendMessage(lsp::InitializeRequest(initParams));
+}
+
+void LspClient::handleIncomingData()
+{
+ const int pos = m_incomingData.pos();
+ m_incomingData.write(m_socket.readAll());
+ m_incomingData.seek(pos);
+ QString parseError;
+ lsp::BaseMessage::parse(&m_incomingData, parseError, m_currentMessage);
+ if (!parseError.isEmpty()) {
+ return finishWithError(QString::fromLatin1("Error parsing server message: %1.")
+ .arg(parseError));
+ }
+ if (m_currentMessage.isComplete()) {
+ m_incomingData.buffer().remove(0, m_incomingData.pos());
+ m_incomingData.seek(0);
+ handleCurrentMessage();
+ m_currentMessage = {};
+ m_messageObject = {};
+ if (m_socket.state() == QLocalSocket::ConnectedState)
+ handleIncomingData();
+ }
+}
+
+void LspClient::sendMessage(const lsp::JsonObject &msg)
+{
+ sendMessage(lsp::JsonRpcMessage(msg));
+}
+
+void LspClient::sendMessage(const lsp::JsonRpcMessage &msg)
+{
+ lsp::BaseMessage baseMsg = msg.toBaseMessage();
+ m_socket.write(baseMsg.header());
+ m_socket.write(baseMsg.content);
+}
+
+void LspClient::handleCurrentMessage()
+{
+ m_messageObject = lsp::JsonRpcMessage(m_currentMessage).toJsonObject();
+ switch (m_state) {
+ case State::Inactive:
+ case State::Connecting:
+ finishWithError("Received message in non-connected state.");
+ break;
+ case State::Initializing:
+ handleInitializeReply();
+ openDocument();
+ insertCode();
+ sendRequest();
+ break;
+ case State::RunningCommand:
+ handleResponse();
+ break;
+ }
+}
+
+void LspClient::handleInitializeReply()
+{
+ lsp::ServerCapabilities serverCaps = lsp::InitializeRequest::Response(
+ m_messageObject).result().value_or(lsp::InitializeResult()).capabilities();
+ const auto defProvider = serverCaps.definitionProvider();
+ if (!defProvider)
+ return finishWithError("Expected definition provider.");
+ const bool * const defProviderValue = std::get_if<bool>(&(*defProvider));
+ if (!defProviderValue || !*defProviderValue)
+ return finishWithError("Expected definition provider.");
+ sendMessage(lsp::InitializeNotification(lsp::InitializedParams()));
+}
+
+void LspClient::openDocument()
+{
+ QFile file(m_filePath);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return finishWithError(QString::fromLatin1("Could not open '%1': %2")
+ .arg(m_filePath, file.errorString()));
+ }
+ lsp::TextDocumentItem item;
+ item.setUri(uri());
+ item.setVersion(0);
+ item.setText(QString::fromUtf8(file.readAll()));
+ item.setLanguageId("application/x-qt.qbs+qml");
+ sendMessage(lsp::DidOpenTextDocumentNotification(lsp::DidOpenTextDocumentParams(item)));
+}
+
+void LspClient::insertCode()
+{
+ if (m_codeToInsert.isEmpty())
+ return;
+
+ lsp::VersionedTextDocumentIdentifier docId;
+ docId.setUri(uri());
+ docId.setVersion(1);
+ lsp::DidChangeTextDocumentParams params(docId);
+ lsp::DidChangeTextDocumentParams::TextDocumentContentChangeEvent change;
+ const lsp::Position insertPos(m_insertLine - 1, m_insertColumn- 1);
+ change.setRange({insertPos, insertPos});
+ change.setText(m_codeToInsert);
+ params.setContentChanges({change});
+ sendMessage(lsp::DidChangeTextDocumentNotification(params));
+
+ if (m_insertLine > m_line || (m_insertLine == m_line && m_insertColumn > m_column))
+ return;
+
+ const int newlineCount = m_codeToInsert.count('\n');
+ m_line += newlineCount;
+ m_column += m_codeToInsert.size() - (newlineCount == 0 ? 0 : m_codeToInsert.lastIndexOf('\n'));
+}
+
+void LspClient::sendRequest()
+{
+ m_state = State::RunningCommand;
+ switch (m_command) {
+ case Command::GotoDefinition:
+ return sendGotoDefinitionRequest();
+ case Command::Completion:
+ return sendCompletionRequest();
+ }
+}
+
+void LspClient::handleResponse()
+{
+ const QString error = m_messageObject.value(lsp::errorKey).toObject()
+ .value("message").toString();
+ if (!error.isEmpty())
+ return finishWithError(error);
+
+ switch (m_command) {
+ case Command::GotoDefinition:
+ return handleGotoDefinitionResponse();
+ case Command::Completion:
+ return handleCompletionResponse();
+ }
+}
+
+void LspClient::sendGotoDefinitionRequest()
+{
+ const lsp::TextDocumentIdentifier doc(uri());
+ const lsp::Position pos(m_line - 1, m_column - 1);
+ sendMessage(lsp::GotoDefinitionRequest({doc, pos}));
+}
+
+void LspClient::handleGotoDefinitionResponse()
+{
+ const lsp::GotoResult result(lsp::GotoDefinitionRequest::Response(m_messageObject)
+ .result().value_or(lsp::GotoResult()));
+ QList<lsp::Utils::Link> links;
+ const auto loc2Link = [this](const lsp::Location &loc) { return loc.toLink(mapper()); };
+ if (const auto loc = std::get_if<lsp::Location>(&result)) {
+ links << loc2Link(*loc);
+ } else if (const auto locs = std::get_if<QList<lsp::Location>>(&result)) {
+ links = lsp::Utils::transform(*locs, loc2Link);
+ }
+ for (const lsp::Utils::Link &link : std::as_const(links)) {
+ std::cout << qPrintable(link.targetFilePath) << ':' << link.targetLine << ':'
+ << (link.targetColumn + 1) << std::endl;
+ }
+ exit(EXIT_SUCCESS);
+}
+
+void LspClient::sendCompletionRequest()
+{
+ const lsp::TextDocumentIdentifier doc(uri());
+ const lsp::Position pos(m_line - 1, m_column - 1);
+ sendMessage(lsp::CompletionRequest({doc, pos}));
+}
+
+void LspClient::handleCompletionResponse()
+{
+ const lsp::CompletionResult result(lsp::CompletionRequest::Response(m_messageObject)
+ .result()
+ .value_or(lsp::CompletionResult()));
+ if (const auto items = std::get_if<QList<lsp::CompletionItem>>(&result)) {
+ for (const lsp::CompletionItem &item : *items) {
+ std::cout << qPrintable(item.label());
+ if (item.detail())
+ std::cout << ' ' << qPrintable(*item.detail());
+ std::cout << std::endl;
+ }
+ }
+ exit(EXIT_SUCCESS);
+}
+
+lsp::DocumentUri LspClient::uri() const
+{
+ return lsp::DocumentUri::fromFilePath(lsp::Utils::FilePath::fromUserInput(m_filePath),
+ mapper());
+}
+
+lsp::DocumentUri::PathMapper LspClient::mapper() const
+{
+ return [](const lsp::Utils::FilePath &fp) { return fp; };
+}