aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarcus Tillmanns <marcus.tillmanns@qt.io>2024-04-12 14:41:35 +0200
committerMarcus Tillmanns <marcus.tillmanns@qt.io>2024-04-19 13:54:26 +0000
commit26993a274e075de52318663f6e8547f494f18872 (patch)
tree3d86c3cd8ab5db9aa06af2b55ae9037a0577560c
parentf91d071c66ac60e0339626a0ceb41b4b4f8f1fcc (diff)
Lua: Add lsp support
Change-Id: I47a1f73a1e1191e116c7cf3b06db5af9e7548fc0 Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
-rw-r--r--src/plugins/CMakeLists.txt1
-rw-r--r--src/plugins/languageclient/CMakeLists.txt2
-rw-r--r--src/plugins/languageclient/languageclientsettings.cpp21
-rw-r--r--src/plugins/languageclient/languageclientsettings.h2
-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
-rw-r--r--src/plugins/lua/bindings/utils.cpp2
-rw-r--r--src/plugins/lua/meta/lsp.lua3
-rw-r--r--src/plugins/lua/meta/utils.lua4
-rw-r--r--src/plugins/lualsp/CMakeLists.txt4
-rw-r--r--src/plugins/lualsp/lualsp/init.lua160
-rw-r--r--src/plugins/lualsp/lualsp/lualsp.lua30
13 files changed, 755 insertions, 1 deletions
diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt
index 3e32757ff8..6fb0010079 100644
--- a/src/plugins/CMakeLists.txt
+++ b/src/plugins/CMakeLists.txt
@@ -119,3 +119,4 @@ add_subdirectory(qnx)
add_subdirectory(mcusupport)
add_subdirectory(qtapplicationmanager)
add_subdirectory(tellajoke)
+add_subdirectory(lualsp)
diff --git a/src/plugins/languageclient/CMakeLists.txt b/src/plugins/languageclient/CMakeLists.txt
index e098168a1c..b9a36e9c50 100644
--- a/src/plugins/languageclient/CMakeLists.txt
+++ b/src/plugins/languageclient/CMakeLists.txt
@@ -36,3 +36,5 @@ add_qtc_plugin(LanguageClient
semantichighlightsupport.cpp semantichighlightsupport.h
snippet.cpp snippet.h
)
+
+add_subdirectory(lualanguageclient)
diff --git a/src/plugins/languageclient/languageclientsettings.cpp b/src/plugins/languageclient/languageclientsettings.cpp
index 4e26641dd6..b994da6096 100644
--- a/src/plugins/languageclient/languageclientsettings.cpp
+++ b/src/plugins/languageclient/languageclientsettings.cpp
@@ -606,6 +606,27 @@ void LanguageClientSettings::init()
LanguageClientManager::applySettings();
}
+QList<Utils::Store> LanguageClientSettings::storesBySettingsType(Utils::Id settingsTypeId)
+{
+ QList<Utils::Store> result;
+
+ QtcSettings *settingsIn = Core::ICore::settings();
+ settingsIn->beginGroup(settingsGroupKey);
+
+ for (const QVariantList &varList :
+ {settingsIn->value(clientsKey).toList(), settingsIn->value(typedClientsKey).toList()}) {
+ for (const QVariant &var : varList) {
+ const Store store = storeFromVariant(var);
+ if (settingsTypeId == Id::fromSetting(store.value(typeIdKey)))
+ result << store;
+ }
+ }
+
+ settingsIn->endGroup();
+
+ return result;
+}
+
QList<BaseSettings *> LanguageClientSettings::fromSettings(QtcSettings *settingsIn)
{
settingsIn->beginGroup(settingsGroupKey);
diff --git a/src/plugins/languageclient/languageclientsettings.h b/src/plugins/languageclient/languageclientsettings.h
index eb56ea9a60..ad2f01ac74 100644
--- a/src/plugins/languageclient/languageclientsettings.h
+++ b/src/plugins/languageclient/languageclientsettings.h
@@ -140,6 +140,8 @@ public:
static QList<BaseSettings *> pageSettings();
static QList<BaseSettings *> changedSettings();
+ static QList<Utils::Store> storesBySettingsType(Utils::Id settingsTypeId);
+
/**
* must be called before the delayed initialize phase
* otherwise the settings are not loaded correctly
diff --git a/src/plugins/languageclient/lualanguageclient/CMakeLists.txt b/src/plugins/languageclient/lualanguageclient/CMakeLists.txt
new file mode 100644
index 0000000000..3bccee0808
--- /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 0000000000..a74e87f34c
--- /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 0000000000..8f869af4a3
--- /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"
diff --git a/src/plugins/lua/bindings/utils.cpp b/src/plugins/lua/bindings/utils.cpp
index 370fb53fed..c014690a23 100644
--- a/src/plugins/lua/bindings/utils.cpp
+++ b/src/plugins/lua/bindings/utils.cpp
@@ -70,6 +70,8 @@ return {
[](const FilePath &self) { return self.searchInPath(); },
"exists",
&FilePath::exists,
+ "resolveSymlinks",
+ &FilePath::resolveSymlinks,
"isExecutableFile",
&FilePath::isExecutableFile,
"dirEntries",
diff --git a/src/plugins/lua/meta/lsp.lua b/src/plugins/lua/meta/lsp.lua
index 0ea0a4ebc5..0b9aec41e5 100644
--- a/src/plugins/lua/meta/lsp.lua
+++ b/src/plugins/lua/meta/lsp.lua
@@ -4,8 +4,9 @@ local lsp = {}
---@class ClientOptions
---@field name string The name under which to register the language server.
----@field cmd string[] The command to start the language server
+---@field cmd function|string[] The command to start the language server, or a function returning a string[].
---@field transport? "stdio"|"localsocket" Defaults to stdio
+---@field serverName? string The socket path when transport == "localsocket"
---@field languageFilter LanguageFilter The language filter deciding which files to open with the language server
---@field startBehavior? "AlwaysOn"|"RequiresFile"|"RequiresProject"
---@field initializationOptions? table|string The initialization options to pass to the language server, either a json string, or a table
diff --git a/src/plugins/lua/meta/utils.lua b/src/plugins/lua/meta/utils.lua
index 61f6c13877..c3a0c30159 100644
--- a/src/plugins/lua/meta/utils.lua
+++ b/src/plugins/lua/meta/utils.lua
@@ -66,4 +66,8 @@ function utils.FilePath:resolvePath(tail) end
---@return FilePath
function utils.FilePath:parentDir() end
+---If the path targets a symlink, this function returns the target of the symlink
+---@return FilePath The resolved path
+function utils.FilePath:resolveSymlinks() end
+
return utils
diff --git a/src/plugins/lualsp/CMakeLists.txt b/src/plugins/lualsp/CMakeLists.txt
new file mode 100644
index 0000000000..bd794c48b1
--- /dev/null
+++ b/src/plugins/lualsp/CMakeLists.txt
@@ -0,0 +1,4 @@
+add_qtc_lua_plugin(lualsp
+ SOURCES lualsp/lualsp.lua
+ lualsp/init.lua
+)
diff --git a/src/plugins/lualsp/lualsp/init.lua b/src/plugins/lualsp/lualsp/init.lua
new file mode 100644
index 0000000000..8581254310
--- /dev/null
+++ b/src/plugins/lualsp/lualsp/init.lua
@@ -0,0 +1,160 @@
+-- Copyright (C) 2024 The Qt Company Ltd.
+-- SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+local LSP = require('LSP')
+local mm = require('MessageManager')
+local Utils = require('Utils')
+local Process = require('Process')
+local S = require('Settings')
+local Layout = require('Layout')
+local a = require('async')
+
+Settings = {}
+
+local function createCommand()
+ local cmd = { Settings.binary.expandedValue:nativePath() }
+ if Settings.showNode.value then
+ table.insert(cmd, '--shownode=true')
+ end
+ if Settings.showSource.value then
+ table.insert(cmd, '--showsource=true')
+ end
+ if Settings.developMode.value then
+ table.insert(cmd, '--develop=true')
+ end
+
+ return cmd
+end
+local function setupClient()
+ Client = LSP.Client.create({
+ name = 'Lua Language Server',
+ cmd = createCommand,
+ transport = 'stdio',
+ languageFilter = {
+ patterns = { '*.lua' },
+ mimeTypes = { 'text/x-lua' }
+ },
+ settings = Settings,
+ startBehavior = "RequiresFile",
+ })
+
+ Client.on_instance_start = function()
+ print("Instance has started")
+ end
+
+ Client:registerMessage("$/status/report", function(params)
+ mm.writeFlashing(params.params.text .. ": " .. params.params.tooltip);
+ end)
+end
+
+local function installServer()
+ print("Lua Language Server not found, installing ...")
+ local cmds = {
+ mac = "brew install lua-language-server",
+ windows = "winget install lua-language-server",
+ linux = "sudo apt install lua-language-server"
+ }
+ if a.wait(Process.runInTerminal(cmds[Utils.HostOsInfo.os])) == 0 then
+ print("Lua Language Server installed!")
+ Settings.binary.defaultPath = Utils.FilePath.fromUserInput("lua-language-server"):resolveSymlinks()
+ Settings:apply()
+ return true
+ end
+
+ print("Lua Language Server installation failed!")
+ return false
+end
+
+local function using(tbl)
+ local result = _G
+ for k, v in pairs(tbl) do result[k] = v end
+ return result
+end
+local function layoutSettings()
+ --- "using namespace Layout"
+ local _ENV = using(Layout)
+
+ local installButton = {}
+
+ if Settings.binary.expandedValue:isExecutableFile() == false then
+ installButton = {
+ "Language server not found:",
+ Row {
+ PushButton {
+ text("Try to install lua language server"),
+ onClicked(function() a.sync(installServer)() end),
+ br,
+ },
+ st
+ }
+ }
+ end
+ local layout = Form {
+ Settings.binary, br,
+ Settings.developMode, br,
+ Settings.showSource, br,
+ Settings.showNode, br,
+ table.unpack(installButton)
+ }
+
+ return layout
+end
+
+local function setupAspect()
+ ---@class Settings: AspectContainer
+ Settings = S.AspectContainer.create({
+ autoApply = false,
+ layouter = layoutSettings,
+ });
+
+ Settings.binary = S.FilePathAspect.create({
+ settingsKey = "LuaCopilot.Binary",
+ displayName = "Binary",
+ labelText = "Binary:",
+ toolTip = "The path to the lua-language-server binary.",
+ expectedKind = S.Kind.ExistingCommand,
+ defaultPath = Utils.FilePath.fromUserInput("lua-language-server"):resolveSymlinks(),
+ })
+ Settings.developMode = S.BoolAspect.create({
+ settingsKey = "LuaCopilot.DevelopMode",
+ displayName = "Enable Develop Mode",
+ labelText = "Enable Develop Mode:",
+ toolTip = "Turns on the develop mode of the language server.",
+ defaultValue = false,
+ labelPlacement = S.LabelPlacement.InExtraLabel,
+ })
+
+ Settings.showSource = S.BoolAspect.create({
+ settingsKey = "LuaCopilot.ShowSource",
+ displayName = "Show Source",
+ labelText = "Show Source:",
+ toolTip = "Display the internal data of the hovering token.",
+ defaultValue = false,
+ labelPlacement = S.LabelPlacement.InExtraLabel,
+ })
+
+ Settings.showNode = S.BoolAspect.create({
+ settingsKey = "LuaCopilot.ShowNode",
+ displayName = "Show Node",
+ labelText = "Show Node:",
+ toolTip = "Display the internal data of the hovering token.",
+ defaultValue = false,
+ labelPlacement = S.LabelPlacement.InExtraLabel,
+ })
+ return Settings
+end
+local function setup(parameters)
+ print("Setting up Lua Language Server ...")
+ setupAspect()
+ local serverPath = Utils.FilePath.fromUserInput("lua-language-server")
+ local absolute = serverPath:searchInPath():resolveSymlinks()
+ if absolute:isExecutableFile() == true then
+ Settings.binary.defaultPath = absolute
+ else
+ a.sync(installServer)()
+ end
+ setupClient()
+end
+
+return {
+ setup = setup,
+}
diff --git a/src/plugins/lualsp/lualsp/lualsp.lua b/src/plugins/lualsp/lualsp/lualsp.lua
new file mode 100644
index 0000000000..ba2de87414
--- /dev/null
+++ b/src/plugins/lualsp/lualsp/lualsp.lua
@@ -0,0 +1,30 @@
+-- Copyright (C) 2024 The Qt Company Ltd.
+-- SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+return {
+ Name = "LuaLanguageServer",
+ Version = "1.0.0",
+ CompatVersion = "1.0.0",
+ Vendor = "The Qt Company",
+ Category = "Language Client",
+ Description = "The Lua Language Server",
+ Experimental = false,
+ DisabledByDefault = false,
+ LongDescription = [[
+This plugin provides the Lua Language Server.
+It will try to install it if it is not found.
+ ]],
+ Dependencies = {
+ { Name = "Core", Version = "13.0.82", Required = true },
+ { Name = "Lua", Version = "13.0.82", Required = true },
+ { Name = "LuaLanguageClient", Version = "13.0.82", Required = true }
+ },
+ setup = function()
+ require 'init'.setup()
+ end,
+ hooks = {
+ editors = {
+ documentOpened = function(doc) print("documentOpened", doc) end,
+ documentClosed = function(doc) print("documentClosed", doc) end,
+ }
+ }
+} --[[@as QtcPlugin]]