aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/languageclient/lualanguageclient
diff options
context:
space:
mode:
authorMarcus Tillmanns <[email protected]>2024-04-12 14:41:35 +0200
committerMarcus Tillmanns <[email protected]>2024-04-19 13:54:26 +0000
commit26993a274e075de52318663f6e8547f494f18872 (patch)
tree3d86c3cd8ab5db9aa06af2b55ae9037a0577560c /src/plugins/languageclient/lualanguageclient
parentf91d071c66ac60e0339626a0ceb41b4b4f8f1fcc (diff)
Lua: Add lsp support
Change-Id: I47a1f73a1e1191e116c7cf3b06db5af9e7548fc0 Reviewed-by: Marcus Tillmanns <[email protected]>
Diffstat (limited to 'src/plugins/languageclient/lualanguageclient')
-rw-r--r--src/plugins/languageclient/lualanguageclient/CMakeLists.txt6
-rw-r--r--src/plugins/languageclient/lualanguageclient/LuaLanguageClient.json.in21
-rw-r--r--src/plugins/languageclient/lualanguageclient/lualanguageclient.cpp500
3 files changed, 527 insertions, 0 deletions
diff --git a/src/plugins/languageclient/lualanguageclient/CMakeLists.txt b/src/plugins/languageclient/lualanguageclient/CMakeLists.txt
new file mode 100644
index 00000000000..3bccee08086
--- /dev/null
+++ b/src/plugins/languageclient/lualanguageclient/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_qtc_plugin(LuaLanguageClient
+ CONDITION TARGET Lua
+ PLUGIN_DEPENDS LanguageClient Lua
+ SOURCES
+ lualanguageclient.cpp
+)
diff --git a/src/plugins/languageclient/lualanguageclient/LuaLanguageClient.json.in b/src/plugins/languageclient/lualanguageclient/LuaLanguageClient.json.in
new file mode 100644
index 00000000000..a74e87f34c5
--- /dev/null
+++ b/src/plugins/languageclient/lualanguageclient/LuaLanguageClient.json.in
@@ -0,0 +1,21 @@
+{
+ "Name" : "LuaLanguageClient",
+ "Version" : "${IDE_VERSION}",
+ "DisabledByDefault" : true,
+ "SoftLoadable" : true,
+ "CompatVersion" : "${IDE_VERSION_COMPAT}",
+ "Vendor" : "The Qt Company Ltd",
+ "Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
+ "License" : [ "Commercial Usage",
+ "",
+ "Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt 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.",
+ "",
+ "GNU General Public License Usage",
+ "",
+ "Alternatively, this plugin 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 plugin. 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."
+ ],
+ "Category" : "Scripting",
+ "Description" : "Lua Language Client scripting support",
+ "Url" : "http://www.qt.io",
+ ${IDE_PLUGIN_DEPENDENCIES}
+}
diff --git a/src/plugins/languageclient/lualanguageclient/lualanguageclient.cpp b/src/plugins/languageclient/lualanguageclient/lualanguageclient.cpp
new file mode 100644
index 00000000000..8f869af4a32
--- /dev/null
+++ b/src/plugins/languageclient/lualanguageclient/lualanguageclient.cpp
@@ -0,0 +1,500 @@
+// Copyright (C) 2024 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include <languageclient/languageclientinterface.h>
+#include <languageclient/languageclientmanager.h>
+#include <languageclient/languageclientsettings.h>
+
+#include <lua/bindings/inheritance.h>
+#include <lua/luaengine.h>
+
+#include <extensionsystem/iplugin.h>
+#include <extensionsystem/pluginmanager.h>
+
+#include <projectexplorer/project.h>
+
+#include <utils/commandline.h>
+#include <utils/layoutbuilder.h>
+
+#include <QJsonDocument>
+
+using namespace Utils;
+using namespace Core;
+using namespace TextEditor;
+using namespace ProjectExplorer;
+
+namespace LanguageClient::Lua {
+
+static void registerLuaApi();
+
+class LuaLanguageClientPlugin final : public ExtensionSystem::IPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "LuaLanguageClient.json")
+
+public:
+ LuaLanguageClientPlugin() {}
+
+private:
+ void initialize() final { registerLuaApi(); }
+};
+
+class LuaLocalSocketClientInterface : public LocalSocketClientInterface
+{
+public:
+ LuaLocalSocketClientInterface(const CommandLine &cmd, const QString &serverName)
+ : LocalSocketClientInterface(serverName)
+ , m_cmd(cmd)
+ , m_logFile("lua-lspclient.XXXXXX.log")
+
+ {}
+
+ void startImpl() override
+ {
+ if (m_process) {
+ QTC_CHECK(!m_process->isRunning());
+ delete m_process;
+ }
+ m_process = new Process;
+ m_process->setProcessMode(ProcessMode::Writer);
+ connect(m_process,
+ &Process::readyReadStandardError,
+ this,
+ &LuaLocalSocketClientInterface::readError);
+ connect(m_process,
+ &Process::readyReadStandardOutput,
+ this,
+ &LuaLocalSocketClientInterface::readOutput);
+ connect(m_process, &Process::started, this, [this]() {
+ this->LocalSocketClientInterface::startImpl();
+ emit started();
+ });
+ connect(m_process, &Process::done, this, [this] {
+ if (m_process->result() != ProcessResult::FinishedWithSuccess)
+ emit error(QString("%1 (see logs in \"%2\")")
+ .arg(m_process->exitMessage())
+ .arg(m_logFile.fileName()));
+ emit finished();
+ });
+ m_logFile.write(
+ QString("Starting server: %1\nOutput:\n\n").arg(m_cmd.toUserOutput()).toUtf8());
+ m_process->setCommand(m_cmd);
+ m_process->setWorkingDirectory(m_workingDirectory);
+ if (m_env.hasChanges())
+ m_process->setEnvironment(m_env);
+ m_process->start();
+ }
+
+ void setWorkingDirectory(const FilePath &workingDirectory)
+ {
+ m_workingDirectory = workingDirectory;
+ }
+
+ FilePath serverDeviceTemplate() const override { return m_cmd.executable(); }
+
+ void readError()
+ {
+ QTC_ASSERT(m_process, return);
+
+ const QByteArray stdErr = m_process->readAllRawStandardError();
+ m_logFile.write(stdErr);
+ }
+
+ void readOutput()
+ {
+ QTC_ASSERT(m_process, return);
+ const QByteArray &out = m_process->readAllRawStandardOutput();
+ parseData(out);
+ }
+
+private:
+ Utils::CommandLine m_cmd;
+ Utils::FilePath m_workingDirectory;
+ Utils::Process *m_process = nullptr;
+ Utils::Environment m_env;
+ QTemporaryFile m_logFile;
+};
+
+class LuaClientWrapper;
+
+class LuaClientSettings : public BaseSettings
+{
+ std::weak_ptr<LuaClientWrapper> m_wrapper;
+
+public:
+ LuaClientSettings(const std::weak_ptr<LuaClientWrapper> &wrapper);
+ ~LuaClientSettings() override = default;
+
+ bool applyFromSettingsWidget(QWidget *widget) override;
+
+ Utils::Store toMap() const override;
+ void fromMap(const Utils::Store &map) override;
+
+ QWidget *createSettingsWidget(QWidget *parent = nullptr) const override;
+
+ BaseSettings *copy() const override { return new LuaClientSettings(*this); }
+
+protected:
+ BaseClientInterface *createInterface(ProjectExplorer::Project *project) const override;
+};
+enum class TransportType { StdIO, LocalSocket };
+
+class LuaClientWrapper : public QObject
+{
+public:
+ TransportType m_transportType{TransportType::StdIO};
+ std::function<expected_str<void>(CommandLine &)> m_cmdLineCallback;
+ AspectContainer *m_aspects{nullptr};
+ QString m_name;
+ Utils::Id m_settingsTypeId;
+ QString m_initializationOptions;
+ CommandLine m_cmdLine;
+ QString m_serverName;
+ LanguageFilter m_languageFilter;
+ BaseSettings::StartBehavior m_startBehavior = BaseSettings::RequiresFile;
+
+ std::optional<sol::protected_function> m_onInstanceStart;
+ QMap<QString, sol::protected_function> m_messageCallbacks;
+
+ QList<Client *> m_clients;
+
+public:
+ static BaseSettings::StartBehavior startBehaviorFromString(const QString &str)
+ {
+ if (str == "RequiresProject")
+ return BaseSettings::RequiresProject;
+ if (str == "RequiresFile")
+ return BaseSettings::RequiresFile;
+ if (str == "AlwaysOn")
+ return BaseSettings::AlwaysOn;
+
+ throw sol::error("Unknown start behavior: " + str.toStdString());
+ }
+
+ LuaClientWrapper(const sol::table &options)
+ {
+ m_cmdLineCallback = addValue<CommandLine>(
+ options,
+ "cmd",
+ m_cmdLine,
+ [](const sol::protected_function_result &res) -> expected_str<CommandLine> {
+ if (res.get_type(0) != sol::type::table)
+ return make_unexpected(QString("cmd callback did not return a table"));
+ return cmdFromTable(res.get<sol::table>());
+ });
+
+ m_name = options.get<QString>("name");
+ m_settingsTypeId = Utils::Id::fromString(QString("Lua_%1").arg(m_name));
+ m_serverName = options.get_or<QString>("serverName", "");
+
+ m_startBehavior = startBehaviorFromString(
+ options.get_or<QString>("startBehavior", "AlwaysOn"));
+
+ QString transportType = options.get_or<QString>("transport", "stdio");
+ if (transportType == "stdio")
+ m_transportType = TransportType::StdIO;
+ else if (transportType == "localsocket")
+ m_transportType = TransportType::LocalSocket;
+ else
+ qWarning() << "Unknown transport type:" << transportType;
+
+ auto languageFilter = options.get<std::optional<sol::table>>("languageFilter");
+ if (languageFilter) {
+ auto patterns = languageFilter->get<std::optional<sol::table>>("patterns");
+ auto mimeTypes = languageFilter->get<std::optional<sol::table>>("mimeTypes");
+
+ if (patterns)
+ for (auto [_, v] : *patterns)
+ m_languageFilter.filePattern.push_back(v.as<QString>());
+
+ if (mimeTypes)
+ for (auto [_, v] : *mimeTypes)
+ m_languageFilter.mimeTypes.push_back(v.as<QString>());
+ }
+
+ auto initOptionsTable = options.get<sol::optional<sol::table>>("initializationOptions");
+ if (initOptionsTable) {
+ QJsonValue json = ::Lua::LuaEngine::toJson(*initOptionsTable);
+ QJsonDocument doc;
+ if (json.isArray()) {
+ doc.setArray(json.toArray());
+ m_initializationOptions = QString::fromUtf8(doc.toJson());
+ } else if (json.isObject()) {
+ doc.setObject(json.toObject());
+ m_initializationOptions = QString::fromUtf8(doc.toJson());
+ }
+ }
+ auto initOptionsString = options.get<sol::optional<QString>>("initializationOptions");
+ if (initOptionsString)
+ m_initializationOptions = *initOptionsString;
+
+ // get<sol::optional<>> because on MSVC, get_or(..., nullptr) fails to compile
+ m_aspects = options.get<sol::optional<AspectContainer *>>("settings").value_or(nullptr);
+
+ connect(
+ LanguageClientManager::instance(),
+ &LanguageClientManager::clientInitialized,
+ this,
+ [this](Client *c) {
+ if (m_onInstanceStart) {
+ if (auto settings = LanguageClientManager::settingForClient(c)) {
+ if (settings->m_settingsTypeId == m_settingsTypeId) {
+ auto result = m_onInstanceStart->call();
+
+ if (!result.valid()) {
+ qWarning() << "Error calling instance start callback:"
+ << (result.get<sol::error>().what());
+ }
+
+ m_clients.push_back(c);
+ updateMessageCallbacks();
+ }
+ }
+ }
+ });
+ connect(
+ LanguageClientManager::instance(),
+ &LanguageClientManager::clientRemoved,
+ this,
+ [this](Client *c) {
+ if (m_clients.contains(c))
+ m_clients.removeOne(c);
+ });
+ }
+
+ // TODO: Unregister Client settings from LanguageClientManager
+ ~LuaClientWrapper() = default;
+
+ TransportType transportType() { return m_transportType; }
+
+ void applySettings()
+ {
+ if (m_aspects)
+ m_aspects->apply();
+
+ updateOptions();
+ }
+
+ void fromMap(const Utils::Store &map)
+ {
+ if (m_aspects)
+ m_aspects->fromMap(map);
+ updateOptions();
+ }
+
+ void toMap(Utils::Store &map) const
+ {
+ if (m_aspects)
+ m_aspects->toMap(map);
+ }
+
+ std::optional<Layouting::LayoutItem> settingsLayout()
+ {
+ if (m_aspects && m_aspects->layouter())
+ return m_aspects->layouter()();
+ return {};
+ }
+
+ void registerMessageCallback(const QString &msg, const sol::function &callback)
+ {
+ m_messageCallbacks.insert(msg, callback);
+ updateMessageCallbacks();
+ }
+
+ void updateMessageCallbacks()
+ {
+ for (Client *c : m_clients) {
+ for (const auto &[msg, func] : m_messageCallbacks.asKeyValueRange()) {
+ c->registerCustomMethod(
+ msg, [name = msg, f = func](const LanguageServerProtocol::JsonRpcMessage &m) {
+ auto table = ::Lua::LuaEngine::toTable(f.lua_state(), m.toJsonObject());
+ auto result = f.call(table);
+ if (!result.valid()) {
+ qWarning() << "Error calling message callback for:" << name << ":"
+ << (result.get<sol::error>().what());
+ }
+ });
+ }
+ }
+ }
+
+ void updateOptions()
+ {
+ if (m_cmdLineCallback) {
+ auto result = m_cmdLineCallback(m_cmdLine);
+ if (!result)
+ qWarning() << "Error applying option callback:" << result.error();
+ }
+ }
+
+ static CommandLine cmdFromTable(const sol::table &tbl)
+ {
+ CommandLine cmdLine;
+ cmdLine.setExecutable(FilePath::fromUserInput(tbl.get<QString>(1)));
+
+ for (size_t i = 2; i < tbl.size() + 1; i++)
+ cmdLine.addArg(tbl.get<QString>(i));
+
+ return cmdLine;
+ }
+
+ template<typename T>
+ std::function<expected_str<void>(T &)> addValue(
+ const sol::table &options,
+ const char *fieldName,
+ T &dest,
+ std::function<expected_str<T>(const sol::protected_function_result &)> transform)
+ {
+ auto fixed = options.get<sol::optional<sol::table>>(fieldName);
+ auto cb = options.get<sol::optional<sol::protected_function>>(fieldName);
+
+ if (fixed) {
+ dest = fixed.value().get<T>(1);
+ } else if (cb) {
+ std::function<expected_str<void>(T &)> callback =
+ [cb, transform](T &dest) -> expected_str<void> {
+ auto res = cb.value().call();
+ if (!res.valid()) {
+ sol::error err = res;
+ return Utils::make_unexpected(QString::fromLocal8Bit(err.what()));
+ }
+
+ expected_str<T> trResult = transform(res);
+ if (!trResult)
+ return make_unexpected(trResult.error());
+
+ dest = *trResult;
+ return {};
+ };
+
+ QTC_CHECK_EXPECTED(callback(dest));
+ return callback;
+ }
+ return {};
+ }
+
+ BaseClientInterface *createInterface(ProjectExplorer::Project *project)
+ {
+ if (m_transportType == TransportType::StdIO) {
+ auto interface = new StdIOClientInterface;
+ interface->setCommandLine(m_cmdLine);
+ if (project)
+ interface->setWorkingDirectory(project->projectDirectory());
+ return interface;
+ } else if (m_transportType == TransportType::LocalSocket) {
+ if (m_serverName.isEmpty())
+ return nullptr;
+
+ auto interface = new LuaLocalSocketClientInterface(m_cmdLine, m_serverName);
+ if (project)
+ interface->setWorkingDirectory(project->projectDirectory());
+ return interface;
+ }
+ return nullptr;
+ }
+};
+
+LuaClientSettings::LuaClientSettings(const std::weak_ptr<LuaClientWrapper> &wrapper)
+ : m_wrapper(wrapper)
+{
+ if (auto w = m_wrapper.lock()) {
+ m_name = w->m_name;
+ m_settingsTypeId = w->m_settingsTypeId;
+ m_languageFilter = w->m_languageFilter;
+ m_initializationOptions = w->m_initializationOptions;
+ m_startBehavior = w->m_startBehavior;
+ }
+}
+
+bool LuaClientSettings::applyFromSettingsWidget(QWidget *widget)
+{
+ BaseSettings::applyFromSettingsWidget(widget);
+
+ if (auto w = m_wrapper.lock())
+ w->applySettings();
+
+ return true;
+}
+
+Utils::Store LuaClientSettings::toMap() const
+{
+ auto store = BaseSettings::toMap();
+ if (auto w = m_wrapper.lock())
+ w->toMap(store);
+ return store;
+}
+
+void LuaClientSettings::fromMap(const Utils::Store &map)
+{
+ BaseSettings::fromMap(map);
+ if (auto w = m_wrapper.lock()) {
+ w->m_name = m_name;
+ w->m_initializationOptions = m_initializationOptions;
+ w->m_languageFilter = m_languageFilter;
+ w->m_startBehavior = m_startBehavior;
+ w->fromMap(map);
+ }
+}
+
+QWidget *LuaClientSettings::createSettingsWidget(QWidget *parent) const
+{
+ using namespace Layouting;
+
+ if (auto w = m_wrapper.lock())
+ if (std::optional<LayoutItem> layout = w->settingsLayout())
+ return new BaseSettingsWidget(this, parent, layout->subItems);
+
+ return new BaseSettingsWidget(this, parent);
+}
+
+BaseClientInterface *LuaClientSettings::createInterface(ProjectExplorer::Project *project) const
+{
+ if (auto w = m_wrapper.lock())
+ return w->createInterface(project);
+
+ return nullptr;
+}
+
+static void registerLuaApi()
+{
+ ::Lua::LuaEngine::registerProvider("LSP", [](sol::state_view lua) -> sol::object {
+ sol::table result = lua.create_table();
+
+ auto wrapperClass = result.new_usertype<LuaClientWrapper>(
+ "Client",
+ "on_instance_start",
+ sol::property(
+ [](const LuaClientWrapper *c) -> sol::function {
+ if (!c->m_onInstanceStart)
+ return sol::lua_nil;
+ return c->m_onInstanceStart.value();
+ },
+ [](LuaClientWrapper *c, const sol::function &f) { c->m_onInstanceStart = f; }),
+ "registerMessage",
+ &LuaClientWrapper::registerMessageCallback,
+ "create",
+ [](const sol::table &options) -> std::shared_ptr<LuaClientWrapper> {
+ auto luaClient = std::make_shared<LuaClientWrapper>(options);
+ auto client = new LuaClientSettings(luaClient);
+
+ // The order is important!
+ // First restore the settings ...
+ const QList<Utils::Store> savedSettings
+ = LanguageClientSettings::storesBySettingsType(luaClient->m_settingsTypeId);
+
+ if (!savedSettings.isEmpty())
+ client->fromMap(savedSettings.first());
+
+ // ... then register the settings.
+ LanguageClientManager::registerClientSettings(client);
+
+ return luaClient;
+ });
+
+ return result;
+ });
+}
+
+} // namespace LanguageClient::Lua
+
+#include "lualanguageclient.moc"