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:
@@ -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"
|
||||
Reference in New Issue
Block a user