Skip to content
Draft
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Deprecated

- `luau-lsp.types.definitionFiles` has been deprecated in favour of environment support. Instead, create an environment in `luau-lsp.types.environments` and then assign a glob "\*" to your environment.

### Added

- Added support for environments. You can now configure environments with definition files attached to them, then assign a glob pattern an environment, using `luau-lsp.types.environments` and `luau-lsp.types.environmentGlobs`. For example, adding an environment "testez" pointing to a testez definitions file, then globbing "\*.spec.lua" to "testez". This deprecates global definitions configuration `luau-lsp.types.definitionFiles`

### Changed

- Sync to upstream Luau 0.541
Expand Down
19 changes: 19 additions & 0 deletions editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
},
"luau-lsp.types.definitionFiles": {
"markdownDescription": "A list of paths to definition files to load in to the type checker. Note that definition file syntax is currently unstable and may change at any time",
"markdownDeprecationMessage": "**Deprecated**: Please use `#luau-lsp.types.builtinDefinitions#` and `#luau-lsp.types.environments#` instead.",
"type": "array",
"default": [],
"items": {
Expand All @@ -167,6 +168,24 @@
"type": "boolean",
"default": true
},
"luau-lsp.types.environments": {
"markdownDescription": "A map between environment names and a path to a definition file to register. See `#luau-lsp.types.environmentGlobs#` for applying an environment to a file",
"type": "object",
"default": {},
"additionalProperties": {
"type": "string",
"description": "A path to a definitions file"
}
},
"luau-lsp.types.environmentGlobs": {
"markdownDescription": "A map between globs and the environment they are registered too. Only one environment can be applied per file: ensure globs are uniqued as the order applied is indeterminate",
"type": "object",
"default": {},
"additionalProperties": {
"type": "string",
"description": "An environment name to link to"
}
},
"luau-lsp.inlayHints.parameterNames": {
"markdownDescription": "Show inlay hints for function parameter names",
"type": "string",
Expand Down
59 changes: 43 additions & 16 deletions editors/code/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,32 +217,26 @@ const updateSourceMap = async (workspaceFolder: vscode.WorkspaceFolder) => {
);
};

const parseConfigPath = (path: string) => {
if (vscode.workspace.workspaceFolders) {
return resolveUri(vscode.workspace.workspaceFolders[0].uri, path);
} else {
return vscode.Uri.file(path);
}
};

export async function activate(context: vscode.ExtensionContext) {
console.log("Luau LSP activated");

const args = ["lsp"];

// Load roblox type definitions
const typesConfig = vscode.workspace.getConfiguration("luau-lsp.types");
if (typesConfig.get<boolean>("roblox")) {
await updateApiInfo(context);
args.push(`--definitions=${globalTypesUri(context).fsPath}`);
args.push(`--docs=${apiDocsUri(context).fsPath}`);
}

// Load extra type definitions
// DEPRECATED: Load global type definitions
const definitionFiles = typesConfig.get<string[]>("definitionFiles");
if (definitionFiles) {
for (const definitionPath of definitionFiles) {
let uri;
if (vscode.workspace.workspaceFolders) {
uri = resolveUri(
vscode.workspace.workspaceFolders[0].uri,
definitionPath
);
} else {
uri = vscode.Uri.file(definitionPath);
}
const uri = parseConfigPath(definitionPath);
if (await exists(uri)) {
args.push(`--definitions=${uri.fsPath}`);
} else {
Expand All @@ -253,6 +247,37 @@ export async function activate(context: vscode.ExtensionContext) {
}
}

// Load environments
const environments: Record<string, string> = {};

const environmentsConfig =
typesConfig.get<Record<string, string>>("environments");
if (environmentsConfig) {
for (const [name, file] of Object.entries(environmentsConfig)) {
const uri = parseConfigPath(file);
if (await exists(uri)) {
environments[name] = uri.fsPath;
} else {
vscode.window.showWarningMessage(
`Definitions file at ${file} does not exist, types will not be provided for environment ${name}`
);
}
}
}

// Load roblox type definitions
if (typesConfig.get<boolean>("roblox")) {
await updateApiInfo(context);
if (!environments["roblox"]) {
environments["roblox"] = globalTypesUri(context).fsPath;
}
args.push(`--docs=${apiDocsUri(context).fsPath}`);
}

for (const [name, builtins] of Object.entries(environments)) {
args.push(`--environment:${name}=${builtins}`);
}

// Handle FFlags
const fflags: FFlags = {};
const fflagsConfig = vscode.workspace.getConfiguration("luau-lsp.fflags");
Expand Down Expand Up @@ -292,6 +317,8 @@ export async function activate(context: vscode.ExtensionContext) {
args.push(`--flag:${name}=${value}`);
}

console.log("Starting server with: ", args);

const run: Executable = {
command: vscode.Uri.joinPath(
context.extensionUri,
Expand Down
3 changes: 1 addition & 2 deletions src/AnalyzeCli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,7 @@ int startAnalyze(int argc, char** argv)
Luau::FrontendOptions frontendOptions;
frontendOptions.retainFullTypeGraphs = annotate;

WorkspaceFileResolver fileResolver;
fileResolver.rootUri = Uri::file(std::filesystem::current_path());
WorkspaceFileResolver fileResolver(Uri::file(std::filesystem::current_path()));
Luau::Frontend frontend(&fileResolver, &fileResolver, frontendOptions);

if (sourcemapPath)
Expand Down
1 change: 0 additions & 1 deletion src/LanguageServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ using json = nlohmann::json;
using namespace json_rpc;
using Response = json;
using WorkspaceFolderPtr = std::shared_ptr<WorkspaceFolder>;
using ClientPtr = std::shared_ptr<Client>;

#define REQUIRED_PARAMS(params, method) \
!params ? throw json_rpc::JsonRpcException(lsp::ErrorCode::InvalidParams, "params not provided for " method) : params.value()
Expand Down
42 changes: 40 additions & 2 deletions src/Workspace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ void WorkspaceFolder::initialize()
Luau::registerBuiltinTypes(frontend.typeChecker);
Luau::registerBuiltinTypes(frontend.typeCheckerForAutocomplete);

if (client->definitionsFiles.empty())
if (client->definitionsFiles_DEPRECATED.empty())
{
client->sendLogMessage(lsp::MessageType::Warning, "No definitions file provided by client");
}

for (auto definitionsFile : client->definitionsFiles)
for (auto definitionsFile : client->definitionsFiles_DEPRECATED)
{
client->sendLogMessage(lsp::MessageType::Info, "Loading definitions file: " + definitionsFile.generic_string());
auto result = types::registerDefinitions(frontend.typeChecker, definitionsFile);
Expand Down Expand Up @@ -155,6 +155,44 @@ void WorkspaceFolder::initialize()
client->publishDiagnostics({uri, std::nullopt, diagnostics});
}
}

// Load environments
for (auto& [environment, definitionsFile] : client->environments)
{
auto scope = frontend.addEnvironment(environment);
if (auto definitions = readFile(definitionsFile))
{
auto uri = Uri::file(definitionsFile);
auto loadResult = Luau::loadDefinitionFile(frontend.typeChecker, scope, *definitions, "@" + name);
if (loadResult.success)
{
// Clear any set diagnostics
client->publishDiagnostics({uri, std::nullopt, {}});
}
else
{
client->sendWindowMessage(lsp::MessageType::Error,
"Failed to read definitions file for built-in definition " + name + ". Extended types will not be provided");

// Display relevant diagnostics
std::vector<lsp::Diagnostic> diagnostics;
for (auto& error : loadResult.parseResult.errors)
diagnostics.emplace_back(createParseErrorDiagnostic(error));

if (loadResult.module)
for (auto& error : loadResult.module->errors)
diagnostics.emplace_back(createTypeErrorDiagnostic(error, &fileResolver));

client->publishDiagnostics({uri, std::nullopt, diagnostics});
}
}
else
{
client->sendWindowMessage(lsp::MessageType::Error, "Could not read definitions file for built-in definition " + name + " (" +
definitionsFile.generic_string() + "). Extended types will not be provided");
}
}

Luau::freeze(frontend.typeChecker.globalTypes);
Luau::freeze(frontend.typeCheckerForAutocomplete.globalTypes);
}
Expand Down
27 changes: 27 additions & 0 deletions src/WorkspaceFileResolver.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <optional>
#include <unordered_map>
#include <iostream>
#include "glob/glob.hpp"
#include "Luau/Ast.h"
#include "LSP/WorkspaceFileResolver.hpp"
#include "LSP/Utils.hpp"
Expand Down Expand Up @@ -247,6 +248,32 @@ std::string WorkspaceFileResolver::getHumanReadableModuleName(const Luau::Module
}
}

std::optional<std::string> WorkspaceFileResolver::getEnvironmentForModule(const Luau::ModuleName& name) const
{
if (!client)
return std::nullopt;

auto path = resolveToRealPath(name);
if (!path)
return std::nullopt;

// We want to test globs against a relative path to workspace, since thats what makes most sense
auto relativePath = path->lexically_relative(rootUri.fsPath()).generic_string(); // HACK: we convert to generic string so we get '/' separators

auto config = client->getConfiguration(rootUri);
for (auto& [glob, environment] : config.types.environmentGlobs)
{
if (glob::fnmatch_case(relativePath, glob))
return environment;
}

// In roblox mode, set the default environment to "roblox"
if (config.types.roblox)
return "roblox";

return std::nullopt;
}

const Luau::Config& WorkspaceFileResolver::getConfig(const Luau::ModuleName& name) const
{
std::optional<std::filesystem::path> realPath = resolveToRealPath(name);
Expand Down
10 changes: 7 additions & 3 deletions src/include/LSP/Client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ class Client
public:
lsp::ClientCapabilities capabilities;
lsp::TraceValue traceMode = lsp::TraceValue::Off;
/// A registered definitions file passed by the client
std::vector<std::filesystem::path> definitionsFiles;
/// [DEPRECATED] A registered definitions file passed by the client
std::vector<std::filesystem::path> definitionsFiles_DEPRECATED;
std::unordered_map<std::string, std::filesystem::path> environments;

/// A registered documentation file passed by the client
std::optional<std::filesystem::path> documentationFile = std::nullopt;
/// Parsed documentation database
Expand Down Expand Up @@ -72,4 +74,6 @@ class Client

private:
void sendRawMessage(const json& message);
};
};

using ClientPtr = std::shared_ptr<Client>;
6 changes: 4 additions & 2 deletions src/include/LSP/ClientConfiguration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ struct ClientTypesConfiguration
{
/// Whether Roblox-related definitions should be supported
bool roblox = true;
/// Any definition files to load globally
/// [DEPRECATED] Any definition files to load globally
std::vector<std::filesystem::path> definitionFiles;
/// A map between globs and the environment they are registered too
std::unordered_map<std::string, std::string> environmentGlobs;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientTypesConfiguration, roblox, definitionFiles);
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ClientTypesConfiguration, roblox, definitionFiles, environmentGlobs);

enum struct InlayHintsParameterNamesConfig
{
Expand Down
8 changes: 2 additions & 6 deletions src/include/LSP/LanguageServer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ using json = nlohmann::json;
using namespace json_rpc;
using Response = json;
using WorkspaceFolderPtr = std::shared_ptr<WorkspaceFolder>;
using ClientPtr = std::shared_ptr<Client>;

class LanguageServer
{
Expand All @@ -24,12 +23,9 @@ class LanguageServer
std::vector<WorkspaceFolderPtr> workspaceFolders;
ClientPtr client;

LanguageServer(std::vector<std::filesystem::path> definitionsFiles, std::optional<std::filesystem::path> documentationFile)
: client(std::make_shared<Client>())
LanguageServer(ClientPtr client)
: client(client)
{
client->definitionsFiles = definitionsFiles;
client->documentationFile = documentationFile;
parseDocumentation(documentationFile, client->documentation, client);
nullWorkspace = std::make_shared<WorkspaceFolder>(client, "$NULL_WORKSPACE", Uri());
}

Expand Down
5 changes: 2 additions & 3 deletions src/include/LSP/Workspace.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
class WorkspaceFolder
{
public:
std::shared_ptr<Client> client;
ClientPtr client;
std::string name;
lsp::DocumentUri rootUri;
WorkspaceFileResolver fileResolver;
Expand All @@ -32,10 +32,9 @@ class WorkspaceFolder
: client(client)
, name(name)
, rootUri(uri)
, fileResolver(WorkspaceFileResolver())
, fileResolver(WorkspaceFileResolver(client, rootUri))
, frontend(Luau::Frontend(&fileResolver, &fileResolver, {true}))
{
fileResolver.rootUri = uri;
}

// Initialises the workspace folder
Expand Down
14 changes: 13 additions & 1 deletion src/include/LSP/WorkspaceFileResolver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "Luau/FileResolver.h"
#include "Luau/StringUtils.h"
#include "Luau/Config.h"
#include "LSP/Client.hpp"
#include "LSP/Uri.hpp"
#include "LSP/Protocol.hpp"
#include "LSP/Sourcemap.hpp"
Expand All @@ -14,6 +15,8 @@ struct WorkspaceFileResolver
: Luau::FileResolver
, Luau::ConfigResolver
{
/// WARNING: make sure to check that client exists - it won't exist in CLI mode
ClientPtr client;
Luau::Config defaultConfig;

// The root source node from a parsed Rojo source map
Expand All @@ -27,7 +30,14 @@ struct WorkspaceFileResolver
mutable std::unordered_map<std::string, Luau::Config> configCache;
mutable std::vector<std::pair<std::filesystem::path, std::string>> configErrors;

WorkspaceFileResolver()
WorkspaceFileResolver(lsp::DocumentUri rootUri)
: WorkspaceFileResolver(nullptr, rootUri)
{
}

WorkspaceFileResolver(ClientPtr client, lsp::DocumentUri rootUri)
: client(client)
, rootUri(rootUri)
{
defaultConfig.mode = Luau::Mode::Nonstrict;
}
Expand Down Expand Up @@ -65,6 +75,8 @@ struct WorkspaceFileResolver

std::string getHumanReadableModuleName(const Luau::ModuleName& name) const override;

std::optional<std::string> getEnvironmentForModule(const Luau::ModuleName& name) const override;

const Luau::Config& getConfig(const Luau::ModuleName& name) const override;

const Luau::Config& readConfigRec(const std::filesystem::path& path) const;
Expand Down
Loading