Skip to content
Draft

Plugins #1088

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- For DM types, `.Parent` is now typed with a write type of "Instance" in the new solver, preventing false-positive type
errors ([#1039](https://github.com/JohnnyMorganz/luau-lsp/issues/1039))
- Renamed command "Luau: Regenerate Rojo Sourcemap" to "Luau: Regenerate Sourcemap" as it can be configured to generate sourcemaps from non-Rojo tooling

### Fixed

Expand Down
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ target_sources(Luau.LanguageServer PRIVATE
src/Flags.cpp
src/JsonTomlSyntaxParser.cpp
src/CliConfigurationParser.cpp
src/PluginManager.cpp
src/platform/AutoImports.cpp
src/platform/LSPPlatform.cpp
src/platform/StringRequireAutoImporter.cpp
Expand Down Expand Up @@ -167,7 +168,7 @@ set(EXTERN_INCLUDES extern/json/include extern/glob/include extern/argparse/incl
target_compile_features(Luau.LanguageServer PUBLIC cxx_std_17)
target_compile_options(Luau.LanguageServer PRIVATE ${LUAU_LSP_OPTIONS})
target_include_directories(Luau.LanguageServer PUBLIC src/include ${EXTERN_INCLUDES})
target_link_libraries(Luau.LanguageServer PRIVATE Luau.Ast Luau.Analysis Luau.Compiler)
target_link_libraries(Luau.LanguageServer PRIVATE Luau.Ast Luau.Analysis Luau.Compiler Luau.VM)

set_target_properties(Luau.LanguageServer.CLI PROPERTIES OUTPUT_NAME luau-lsp)
target_compile_features(Luau.LanguageServer.CLI PUBLIC cxx_std_17)
Expand Down
14 changes: 13 additions & 1 deletion editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
},
{
"command": "luau-lsp.regenerateSourcemap",
"title": "Luau: Regenerate Rojo Sourcemap"
"title": "Luau: Regenerate Sourcemap"
},
{
"command": "luau-lsp.reloadServer",
Expand Down Expand Up @@ -656,6 +656,18 @@
"default": "Vector3",
"scope": "resource",
"markdownDescription": "The `vectorType` to use when compiling bytecode"
},
"luau-lsp.plugins.paths": {
"markdownDescription": "EXPERIMENTAL: A list of paths to plugin files to load in to the language server",
"type": "array",
"default": [],
"items": {
"type": "string"
},
"scope": "resource",
"tags": [
"experimental"
]
}
}
}
Expand Down
160 changes: 160 additions & 0 deletions src/PluginManager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#include "LSP/PluginManager.hpp"

#include "Luau/Compiler.h"

namespace Plugins
{
static void writestring(const char* s, size_t l)
{
fwrite(s, 1, l, stderr);
}

static int luaB_print(lua_State* L)
{
int n = lua_gettop(L); // number of arguments
for (int i = 1; i <= n; i++)
{
size_t l;
const char* s = luaL_tolstring(L, i, &l); // convert to string using __tostring et al
if (i > 1)
writestring("\t", 1);
writestring(s, l);
lua_pop(L, 1); // pop result
}
writestring("\n", 1);
return 0;
}

PluginManager::PluginManager(Client* client)
: client(client)
, globalState(luaL_newstate(), lua_close)
{
GL = globalState.get();
luaL_openlibs(GL);

// Redefine print to go to stderr
lua_pushcfunction(GL, luaB_print, "print");
lua_setglobal(GL, "print");

luaL_sandbox(GL);
}

static Luau::CompileOptions copts()
{
Luau::CompileOptions result = {};
result.optimizationLevel = 1;
result.debugLevel = 1;
result.typeInfoLevel = 1;
result.coverageLevel = 0;

return result;
}

void PluginManager::registerPlugin(const std::string& name)
{
client->sendTrace("plugins: loading " + name);

std::optional<std::string> source = readFile(name);
if (!source)
{
client->sendLogMessage(lsp::MessageType::Error, "Failed to register plugin '" + name + "', file not found");
return;
}

lua_State* L = lua_newthread(GL);
luaL_sandboxthread(L);

std::string chunkname = "@" + name; //+ normalizePath(name);
std::string bytecode = Luau::compile(*source, copts());
int status = 0;

if (luau_load(L, chunkname.c_str(), bytecode.data(), bytecode.size(), 0) == 0)
{
// if (codegen)
// {
// Luau::CodeGen::CompilationOptions nativeOptions;
// Luau::CodeGen::compile(L, -1, nativeOptions);
// }

status = lua_resume(L, NULL, 0);
}
else
{
status = LUA_ERRSYNTAX;
}

if (status == 0)
{
plugins.emplace_back(Plugin{L});
}
else
{
// TODO: report error properly
std::string error;

if (status == LUA_YIELD)
{
error = "thread yielded unexpectedly";
}
else if (const char* str = lua_tostring(L, -1))
{
error = str;
}

error += "\nstacktrace:\n";
error += lua_debugtrace(L);

fprintf(stderr, "%s", error.c_str());
}

lua_pop(GL, 1);
// return status == 0;
}

std::optional<std::string> PluginManager::handleReadFile(Plugins::OnReadFileContext ctx)
{
std::optional<std::string> result = std::nullopt;

// TODO: build over multiple plugins, rather than last plugin wins
for (const auto& plugin : plugins)
{
client->sendTrace("executing plugin");

lua_rawcheckstack(plugin.L, 3);

lua_getglobal(plugin.L, "OnReadFile");
if (!lua_isnil(plugin.L, -1))
{
client->sendTrace("plugin defines OnReadFile");

lua_createtable(plugin.L, 0, 3);

lua_pushstring(plugin.L, ctx.uri.toString().c_str());
lua_setfield(plugin.L, -2, "uri");

lua_pushstring(plugin.L, ctx.moduleName.c_str());
lua_setfield(plugin.L, -2, "moduleName");

lua_pushlstring(plugin.L, ctx.contents.data(), ctx.contents.size());
lua_setfield(plugin.L, -2, "contents");

if (lua_pcall(plugin.L, 1, 1, 0) != 0)
client->sendLogMessage(lsp::MessageType::Error, "plugin OnReadFile failed");

if (lua_isstring(plugin.L, -1))
result = lua_tostring(plugin.L, -1);
else if (lua_isnil(plugin.L, -1))
result = std::nullopt;
else
client->sendLogMessage(lsp::MessageType::Error, "plugin OnReadFile must return string or nil");

lua_pop(plugin.L, 1);
}
else
lua_pop(plugin.L, 1);
}

return result;
}

}
13 changes: 13 additions & 0 deletions src/Workspace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,19 @@ void WorkspaceFolder::setupWithConfiguration(const ClientConfiguration& configur
registerTypes(configuration.types.disabledGlobals);
}

if (!configuration.plugins.paths.empty())
{
client->sendTrace("workspace: registering plugins");

for (const auto& pluginFile : configuration.plugins.paths)
{
auto resolvedFilePath = resolvePath(pluginFile);
pluginManager.registerPlugin(resolvedFilePath);
}

client->sendTrace("workspace: registering plugins COMPLETED");
}

client->sendTrace("workspace: apply platform-specific configuration");

platform->setupWithConfiguration(configuration);
Expand Down
11 changes: 10 additions & 1 deletion src/WorkspaceFileResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ std::optional<Luau::SourceCode> WorkspaceFileResolver::readSource(const Luau::Mo
Luau::SourceCode::Type sourceType = Luau::SourceCode::Type::None;

std::filesystem::path realFileName = name;
Uri uri = Uri::file(name);
if (platform->isVirtualPath(name))
{
auto filePath = platform->resolveToRealPath(name);
if (!filePath)
return std::nullopt;

realFileName = *filePath;
uri = *filePath;
sourceType = platform->sourceCodeTypeFromPath(*filePath);
}
else
Expand All @@ -84,7 +86,14 @@ std::optional<Luau::SourceCode> WorkspaceFileResolver::readSource(const Luau::Mo
}

if (auto source = platform->readSourceCode(name, realFileName))
return Luau::SourceCode{*source, sourceType};
{
Plugins::OnReadFileContext ctx{uri, name, *source};

if (auto modifiedSource = pluginManager->handleReadFile(ctx))
return Luau::SourceCode{*modifiedSource, sourceType};
else
return Luau::SourceCode{*source, sourceType};
}

return std::nullopt;
}
Expand Down
10 changes: 9 additions & 1 deletion src/include/LSP/ClientConfiguration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,13 @@ struct ClientPlatformConfiguration

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientPlatformConfiguration, type);

struct ClientPluginConfiguration
{
std::vector<std::filesystem::path> paths;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientPluginConfiguration, paths);

// These are the passed configuration options by the client, prefixed with `luau-lsp.`
// Here we also define the default settings
struct ClientConfiguration
Expand All @@ -244,6 +251,7 @@ struct ClientConfiguration
ClientIndexConfiguration index{};
ClientFFlagsConfiguration fflags{};
ClientBytecodeConfiguration bytecode{};
ClientPluginConfiguration plugins{};
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientConfiguration, autocompleteEnd, ignoreGlobs, platform, sourcemap, diagnostics, types,
inlayHints, hover, completion, signatureHelp, require, index, fflags, bytecode);
inlayHints, hover, completion, signatureHelp, require, index, fflags, bytecode, plugins);
64 changes: 64 additions & 0 deletions src/include/LSP/PluginManager.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#pragma once
#include <string>

#include "../../../luau/VM/include/lua.h"
#include "../../../luau/VM/include/lualib.h"

#include "Luau/Module.h"

#include "LSP/Client.hpp"

namespace Plugins
{

struct OnReadFileContext
{
Uri uri;
Luau::ModuleName moduleName;
std::string_view contents;
};

struct Plugin
{
lua_State* L;
};

class PluginManager
{
public:
PluginManager(Client* client);

void registerPlugin(const std::string& name);

std::optional<std::string> handleReadFile(OnReadFileContext ctx);

private:
Client* client;

std::unique_ptr<lua_State, void (*)(lua_State*)> globalState;
lua_State* GL; // Shorthand alias for 'globalState'

std::vector<Plugin> plugins;
};

}


// PluginManager
// - Read luau-lsp.plugins.paths
// - Register each plugin
// Plugin API:
//
// function OnReadFile(ctx)
// - ctx.uri: source URI file
// - ctx.moduleName: resolved module name of src file
// - ctx.contents: source file contents
// end
//
// function ResolveStringRequire(ctx)
// - ctx.uri: source URI file
// - ctx.moduleName: resolved module name of src file
// Returns:
// - string: resolved module name of file to read
// - nil: no special resolution
// end
6 changes: 6 additions & 0 deletions src/include/LSP/Workspace.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "LSP/Client.hpp"
#include "LSP/WorkspaceFileResolver.hpp"
#include "LSP/LuauExt.hpp"
#include "LSP/PluginManager.hpp"

struct Reference
{
Expand All @@ -36,6 +37,9 @@ class WorkspaceFolder
bool isConfigured = false;
std::optional<nlohmann::json> definitionsFileMetadata;

private:
Plugins::PluginManager pluginManager;

public:
WorkspaceFolder(const std::shared_ptr<Client>& client, std::string name, const lsp::DocumentUri& uri, std::optional<Luau::Config> defaultConfig)
: client(client)
Expand All @@ -47,7 +51,9 @@ class WorkspaceFolder
// when calling Luau::autocomplete
, frontend(Luau::Frontend(
&fileResolver, &fileResolver, {/* retainFullTypeGraphs: */ true, /* forAutocomplete: */ false, /* runLintChecks: */ true}))
, pluginManager(client.get())
{
fileResolver.pluginManager = &pluginManager;
fileResolver.client = std::static_pointer_cast<BaseClient>(client);
fileResolver.rootUri = uri;
}
Expand Down
2 changes: 2 additions & 0 deletions src/include/LSP/WorkspaceFileResolver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "LSP/Client.hpp"
#include "LSP/Uri.hpp"
#include "LSP/TextDocument.hpp"
#include "LSP/PluginManager.hpp"
#include "Platform/LSPPlatform.hpp"


Expand Down Expand Up @@ -85,6 +86,7 @@ struct WorkspaceFileResolver
std::shared_ptr<BaseClient> client;
Uri rootUri;

Plugins::PluginManager* pluginManager = nullptr;
LSPPlatform* platform = nullptr;

// Currently opened files where content is managed by client
Expand Down
Loading