forked from qt-creator/qt-creator
Lua: Add lsp support
Change-Id: I47a1f73a1e1191e116c7cf3b06db5af9e7548fc0 Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
This commit is contained in:
@@ -119,3 +119,4 @@ add_subdirectory(qnx)
|
||||
add_subdirectory(mcusupport)
|
||||
add_subdirectory(qtapplicationmanager)
|
||||
add_subdirectory(tellajoke)
|
||||
add_subdirectory(lualsp)
|
||||
|
||||
@@ -36,3 +36,5 @@ add_qtc_plugin(LanguageClient
|
||||
semantichighlightsupport.cpp semantichighlightsupport.h
|
||||
snippet.cpp snippet.h
|
||||
)
|
||||
|
||||
add_subdirectory(lualanguageclient)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
add_qtc_plugin(LuaLanguageClient
|
||||
CONDITION TARGET Lua
|
||||
PLUGIN_DEPENDS LanguageClient Lua
|
||||
SOURCES
|
||||
lualanguageclient.cpp
|
||||
)
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -70,6 +70,8 @@ return {
|
||||
[](const FilePath &self) { return self.searchInPath(); },
|
||||
"exists",
|
||||
&FilePath::exists,
|
||||
"resolveSymlinks",
|
||||
&FilePath::resolveSymlinks,
|
||||
"isExecutableFile",
|
||||
&FilePath::isExecutableFile,
|
||||
"dirEntries",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
4
src/plugins/lualsp/CMakeLists.txt
Normal file
4
src/plugins/lualsp/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
add_qtc_lua_plugin(lualsp
|
||||
SOURCES lualsp/lualsp.lua
|
||||
lualsp/init.lua
|
||||
)
|
||||
160
src/plugins/lualsp/lualsp/init.lua
Normal file
160
src/plugins/lualsp/lualsp/init.lua
Normal file
@@ -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,
|
||||
}
|
||||
30
src/plugins/lualsp/lualsp/lualsp.lua
Normal file
30
src/plugins/lualsp/lualsp/lualsp.lua
Normal file
@@ -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]]
|
||||
Reference in New Issue
Block a user