Add Luau language support (#1152)

This commit is contained in:
StayBlue authored and GitHub committed 2026-03-12 11:58:45 +01:00
1 parent 09c007d1e1
commit b3a6045f7e
13 files changed
+767 -2

No files matched your search

+1 -1
View File
@@ -82,7 +82,7 @@ that implement the language server protocol (LSP).
The underlying language servers are typically open-source projects (like Serena) or at least freely available for use.
With Serena's LSP library, we provide **support for over 30 programming languages**, including
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lean 4, Lua, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, GLSL, Go, Groovy (partial support), Haskell, HLSL, Java, Javascript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
> [!IMPORTANT]
> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details.
@@ -68,6 +68,7 @@ Some languages require additional installations or setup steps, as noted.
(requires `lean` and `lake` installed via [elan](https://github.com/leanprover/elan); uses the built-in Lean 4 LSP;
the project must be a Lake project with `lake build` run before use)
* **Lua**
* **Luau**
* **Markdown**
(must explicitly enable language `markdown`, primarily useful for documentation-heavy projects)
* **Nix**
+23
View File
@@ -352,6 +352,29 @@ ls_specific_settings:
jvm_options: "-Xmx4G -XX:+UseG1GC" # JVM options (default: -Xmx2G). Set to "" to disable.
```
#### Luau
Serena uses [`luau-lsp`](https://github.com/JohnnyMorganz/luau-lsp) for Luau support.
**Runtime Requirements:**
- `luau-lsp` is used from PATH if available.
- Otherwise, Serena downloads the pinned `luau-lsp` release for the current platform.
**Configuration:**
```yaml
ls_specific_settings:
luau:
ls_path: "/path/to/luau-lsp" # Optional: override the language server executable
platform: "roblox" # "roblox" (default) or "standard"
roblox_security_level: "PluginSecurity" # Roblox only: None, PluginSecurity, LocalUserSecurity, RobloxScriptSecurity
```
Notes:
- In `roblox` mode, Serena downloads Roblox definitions and Roblox API docs and passes them to `luau-lsp`.
- In `standard` mode, Serena skips Roblox definitions and only downloads the standard Luau docs bundle.
#### Pascal (`pasls`)
Serena uses [pasls](https://github.com/genericptr/pascal-language-server) (Pascal Language Server) for Pascal/Free Pascal support.
+1
View File
@@ -307,6 +307,7 @@ markers = [
"ruby: language server running for Ruby (uses ruby-lsp)",
"zig: language server running for Zig",
"lua: language server running for Lua",
"luau: language server running for Luau",
"nix: language server running for Nix",
"dart: language server running for Dart",
"erlang: language server running for Erlang",
+1 -1
View File
@@ -337,7 +337,7 @@ class Project(ToStringMixin):
return self._serena_data_folder
def path_to_project_yml(self) -> str:
return self._serena_config.get_project_yml_location(self.project_root)
return self.serena_config.get_project_yml_location(self.project_root)
def get_activation_message(self) -> str:
"""
+363
View File
@@ -0,0 +1,363 @@
"""
Provides Luau specific instantiation of the LanguageServer class using luau-lsp.
Luau is the programming language used by Roblox, derived from Lua 5.1 with
additional features like type annotations, string interpolation, and more.
This uses JohnnyMorganz/luau-lsp as the language server backend.
Requirements:
- luau-lsp binary must be installed and available in PATH,
or it will be automatically downloaded from GitHub releases.
Advanced settings via ls_specific_settings["luau"]:
- platform: "roblox" (default) or "standard"
- roblox_security_level: "None", "PluginSecurity" (default),
"LocalUserSecurity", or "RobloxScriptSecurity"
See: https://github.com/JohnnyMorganz/luau-lsp
"""
import logging
import os
import pathlib
import platform
import shutil
import threading
import zipfile
from pathlib import Path
import requests
from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
# Pin to a known stable release
LUAU_LSP_VERSION = "1.63.0"
# Luau built-in docs CDN
LUAU_DOCS_URL = "https://luau-lsp.pages.dev/api-docs/luau-en-us.json"
# Roblox type definitions and API docs CDN
ROBLOX_DOCS_URL = "https://luau-lsp.pages.dev/api-docs/en-us.json"
SUPPORTED_PLATFORMS = {"roblox", "standard"}
SUPPORTED_ROBLOX_SECURITY_LEVELS = {
"None",
"PluginSecurity",
"LocalUserSecurity",
"RobloxScriptSecurity",
}
class LuauLanguageServer(SolidLanguageServer):
"""
Provides Luau specific instantiation of the LanguageServer class using luau-lsp.
Luau is the programming language used by Roblox (a typed superset of Lua 5.1).
"""
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in [
"node_modules",
"Packages", # Wally packages
"DevPackages", # Wally dev packages
"roblox_packages", # Some Rojo projects
"build",
"dist",
".cache",
]
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def _get_or_install_core_dependency(self) -> str:
luau_lsp_path = shutil.which("luau-lsp")
if luau_lsp_path is not None:
return luau_lsp_path
return self._download_luau_lsp()
def _create_launch_command(self, core_path: str) -> list[str]:
definitions_path, docs_path = self._resolve_support_files()
cmd = [core_path, "lsp"]
if definitions_path is not None:
cmd.append(f"--definitions:@roblox={definitions_path}")
if docs_path is not None:
cmd.append(f"--docs={docs_path}")
return cmd
def _download_luau_lsp(self) -> str:
install_dir = Path(self._ls_resources_dir)
install_dir.mkdir(parents=True, exist_ok=True)
binary_path = self._find_existing_binary(install_dir)
if binary_path is not None:
return binary_path
asset_name = self._get_luau_lsp_asset_name()
download_url = f"https://github.com/JohnnyMorganz/luau-lsp/releases/download/{LUAU_LSP_VERSION}/{asset_name}"
download_path = install_dir / asset_name
log.info("Downloading luau-lsp %s from %s", LUAU_LSP_VERSION, download_url)
with requests.get(download_url, stream=True, timeout=60) as response:
response.raise_for_status()
with open(download_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
log.info("Extracting luau-lsp to %s", install_dir)
with zipfile.ZipFile(download_path, "r") as zip_ref:
zip_ref.extractall(install_dir)
if download_path.exists():
download_path.unlink()
binary_path = self._find_existing_binary(install_dir)
if binary_path is None:
raise RuntimeError("Failed to find luau-lsp executable after extraction")
return binary_path
def _resolve_support_files(self) -> tuple[str | None, str | None]:
platform_type = LuauLanguageServer._get_platform_type(self._custom_settings)
if platform_type == "standard":
return None, self._download_standard_docs()
security_level = LuauLanguageServer._get_roblox_security_level(self._custom_settings)
return self._download_roblox_support_files(security_level)
def _download_standard_docs(self) -> str | None:
install_dir = Path(self._ls_resources_dir)
install_dir.mkdir(parents=True, exist_ok=True)
return self._download_auxiliary_file(
install_dir / "luau-en-us.json",
LUAU_DOCS_URL,
"Luau API docs",
)
def _download_roblox_support_files(self, security_level: str) -> tuple[str | None, str | None]:
install_dir = Path(self._ls_resources_dir)
install_dir.mkdir(parents=True, exist_ok=True)
definitions_filename = f"globalTypes.{security_level}.d.luau"
definitions_path = self._download_auxiliary_file(
install_dir / definitions_filename,
f"https://luau-lsp.pages.dev/type-definitions/{definitions_filename}",
"Roblox type definitions",
)
docs_path = self._download_auxiliary_file(
install_dir / "en-us.json",
ROBLOX_DOCS_URL,
"Roblox API docs",
)
return definitions_path, docs_path
@staticmethod
def _download_auxiliary_file(path: Path, url: str, description: str) -> str | None:
if path.exists():
return str(path)
try:
log.info("Downloading %s from %s", description, url)
response = requests.get(url, timeout=30)
response.raise_for_status()
path.write_bytes(response.content)
return str(path)
except Exception as exc:
log.warning("Failed to download %s: %s", description, exc)
return None
@classmethod
def _find_existing_binary(cls, install_dir: Path) -> str | None:
binary_name = cls._get_binary_name()
direct_path = install_dir / binary_name
if direct_path.exists():
cls._ensure_executable_bit(direct_path)
return str(direct_path)
for candidate in install_dir.rglob(binary_name):
if candidate.is_file():
cls._ensure_executable_bit(candidate)
return str(candidate)
return None
@staticmethod
def _ensure_executable_bit(binary_path: Path) -> None:
if platform.system() != "Windows":
binary_path.chmod(0o755)
@staticmethod
def _get_binary_name() -> str:
return "luau-lsp.exe" if platform.system() == "Windows" else "luau-lsp"
@staticmethod
def _get_luau_lsp_asset_name() -> str:
system = platform.system()
machine = platform.machine().lower()
if system == "Linux":
if machine in ["x86_64", "amd64"]:
return "luau-lsp-linux-x86_64.zip"
if machine in ["aarch64", "arm64"]:
return "luau-lsp-linux-arm64.zip"
raise RuntimeError(
f"Unsupported Linux architecture: {machine}. "
"luau-lsp only provides linux-x86_64 and linux-arm64 binaries. "
"Please build from source: https://github.com/JohnnyMorganz/luau-lsp"
)
if system == "Darwin":
return "luau-lsp-macos.zip"
if system == "Windows":
return "luau-lsp-win64.zip"
raise RuntimeError(f"Unsupported operating system: {system}")
@staticmethod
def _get_platform_type(custom_settings: SolidLSPSettings.CustomLSSettings) -> str:
platform_type = custom_settings.get("platform", "roblox")
if platform_type not in SUPPORTED_PLATFORMS:
raise ValueError(f"Unsupported Luau platform: {platform_type}. Expected one of: {', '.join(sorted(SUPPORTED_PLATFORMS))}")
return platform_type
@staticmethod
def _get_roblox_security_level(custom_settings: SolidLSPSettings.CustomLSSettings) -> str:
security_level = custom_settings.get("roblox_security_level", "PluginSecurity")
if security_level not in SUPPORTED_ROBLOX_SECURITY_LEVELS:
raise ValueError(
f"Unsupported Luau Roblox security level: {security_level}. "
f"Expected one of: {', '.join(sorted(SUPPORTED_ROBLOX_SECURITY_LEVELS))}"
)
return security_level
@classmethod
def _get_workspace_configuration(cls, custom_settings: SolidLSPSettings.CustomLSSettings) -> dict[str, dict[str, str]]:
return {"platform": {"type": cls._get_platform_type(custom_settings)}}
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
super().__init__(config, repository_root_path, None, "luau", solidlsp_settings)
self.server_ready = threading.Event()
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Luau Language Server.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
initialize_params = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"completion": {
"dynamicRegistration": True,
"completionItem": {
"snippetSupport": True,
"commitCharactersSupport": True,
"documentationFormat": ["markdown", "plaintext"],
"deprecatedSupport": True,
"preselectSupport": True,
},
},
"hover": {
"dynamicRegistration": True,
"contentFormat": ["markdown", "plaintext"],
},
"signatureHelp": {
"dynamicRegistration": True,
"signatureInformation": {
"documentationFormat": ["markdown", "plaintext"],
"parameterInformation": {"labelOffsetSupport": True},
},
},
"rename": {"dynamicRegistration": True, "prepareSupport": True},
"callHierarchy": {"dynamicRegistration": True},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"configuration": True,
"symbol": {
"dynamicRegistration": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
# luau-lsp initialization options
# These can be overridden via .luaurc in the project root
"initializationOptions": {},
}
return initialize_params # type: ignore[return-value]
def _start_server(self) -> None:
"""Start Luau Language Server process"""
def register_capability_handler(params: dict) -> None:
return
def workspace_configuration_handler(params: dict) -> list:
items = params.get("items", [])
config = self._get_workspace_configuration(self._custom_settings)
return [config for _ in items]
def window_log_message(msg: dict) -> None:
message_text = msg.get("message", "")
log.info("LSP: window/logMessage: %s", message_text)
if "workspace ready" in message_text.lower() or "initialized" in message_text.lower():
log.info("Luau language server signaled readiness")
self.server_ready.set()
def do_nothing(params: dict) -> None:
return
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_request("workspace/configuration", workspace_configuration_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
log.info("Starting Luau Language Server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
# Verify server capabilities
assert "textDocumentSync" in init_response["capabilities"]
assert "definitionProvider" in init_response["capabilities"]
assert "documentSymbolProvider" in init_response["capabilities"]
assert "referencesProvider" in init_response["capabilities"]
self.server.notify.initialized({})
# Wait for luau-lsp to complete initial setup
log.info("Waiting for Luau language server to become ready...")
if self.server_ready.wait(timeout=5.0):
log.info("Luau language server ready")
else:
log.warning("Timeout waiting for Luau language server readiness, proceeding anyway")
self.server_ready.set()
+13
View File
@@ -53,6 +53,11 @@ class Language(str, Enum):
BASH = "bash"
ZIG = "zig"
LUA = "lua"
LUAU = "luau"
"""Luau Language Server for Roblox's Luau language (typed Lua 5.1 superset).
Uses luau-lsp by JohnnyMorganz. Automatically downloads the binary if not found.
Supports .luau files. Configure via .luaurc in the project root.
"""
NIX = "nix"
ERLANG = "erlang"
OCAML = "ocaml"
@@ -221,6 +226,8 @@ class Language(str, Enum):
return FilenameMatcher("*.zig", "*.zon")
case self.LUA:
return FilenameMatcher("*.lua")
case self.LUAU:
return FilenameMatcher("*.luau")
case self.NIX:
return FilenameMatcher("*.nix")
case self.ERLANG:
@@ -407,6 +414,12 @@ class Language(str, Enum):
from solidlsp.language_servers.lua_ls import LuaLanguageServer
return LuaLanguageServer
case self.LUAU:
from solidlsp.language_servers.luau_lsp import LuauLanguageServer
return LuauLanguageServer
case self.ERLANG:
from solidlsp.language_servers.erlang_language_server import ErlangLanguageServer
@@ -0,0 +1,9 @@
{
"languageMode": "strict",
"lint": {
"*": true
},
"aliases": {
"Packages": "Packages/"
}
}
@@ -0,0 +1,26 @@
local module = require("./module")
export type Config = {
name: string,
value: number,
enabled: boolean,
}
local function createConfig(name: string, value: number): Config
return {
name = name,
value = value,
enabled = true,
}
end
local function main()
local config = createConfig("test", 42)
local result = module.process(config)
print(result)
end
return {
createConfig = createConfig,
main = main,
}
@@ -0,0 +1,12 @@
local function process(data: { name: string, value: number }): string
return `Processing {data.name} with value {data.value}`
end
local function helper(x: number, y: number): number
return x + y
end
return {
process = process,
helper = helper,
}
View File
Whitespace-only changes.
+153
View File
@@ -0,0 +1,153 @@
"""
Tests for the Luau language server implementation.
These tests validate symbol finding, within-file references,
and cross-file reference capabilities for Luau modules and functions.
"""
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
@pytest.mark.luau
class TestLuauLanguageServer:
"""Test Luau language server symbol finding and cross-file references."""
@pytest.mark.parametrize("language_server", [Language.LUAU], indirect=True)
def test_find_symbols_in_init(self, language_server: SolidLanguageServer) -> None:
"""Test finding specific functions in init.luau."""
symbols = language_server.request_document_symbols("src/init.luau").get_all_symbols_and_roots()
assert symbols is not None
assert len(symbols) > 0
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
symbol_names = set()
for symbol in symbol_list:
if isinstance(symbol, dict):
name = symbol.get("name", "")
symbol_names.add(name)
assert "createConfig" in symbol_names, f"createConfig not found in symbols: {symbol_names}"
assert "main" in symbol_names, f"main not found in symbols: {symbol_names}"
@pytest.mark.parametrize("language_server", [Language.LUAU], indirect=True)
def test_find_symbols_in_module(self, language_server: SolidLanguageServer) -> None:
"""Test finding specific functions in module.luau."""
symbols = language_server.request_document_symbols("src/module.luau").get_all_symbols_and_roots()
assert symbols is not None
assert len(symbols) > 0
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
symbol_names = set()
for symbol in symbol_list:
if isinstance(symbol, dict):
name = symbol.get("name", "")
symbol_names.add(name)
assert "process" in symbol_names, f"process not found in symbols: {symbol_names}"
assert "helper" in symbol_names, f"helper not found in symbols: {symbol_names}"
@pytest.mark.parametrize("language_server", [Language.LUAU], indirect=True)
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
"""Test finding within-file references to createConfig in init.luau.
createConfig is defined at line 8 (0-indexed) and referenced at lines 17 and 23.
"""
symbols = language_server.request_document_symbols("src/init.luau").get_all_symbols_and_roots()
assert symbols is not None
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
# Find the createConfig function symbol
create_config_symbol = None
for sym in symbol_list:
if isinstance(sym, dict) and sym.get("name") == "createConfig":
create_config_symbol = sym
break
assert create_config_symbol is not None, "createConfig function not found in init.luau"
range_info = create_config_symbol.get("selectionRange", create_config_symbol.get("range"))
assert range_info is not None, "createConfig has no range information"
range_start = range_info["start"]
refs = language_server.request_references("src/init.luau", range_start["line"], range_start["character"])
assert refs is not None
assert isinstance(refs, list)
# createConfig appears multiple times within init.luau:
# definition (line 8), usage in main (line 17), and return table (line 23)
assert len(refs) >= 2, f"Should find at least 2 references to createConfig, found {len(refs)}"
# Verify that references are in init.luau
ref_files = set()
for ref in refs:
filename = ref.get("uri", "").split("/")[-1]
ref_files.add(filename)
assert "init.luau" in ref_files, f"Expected references in init.luau, found in: {ref_files}"
@pytest.mark.parametrize("language_server", [Language.LUAU], indirect=True)
def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
"""Test finding cross-file references to process function.
process is defined in module.luau and used in init.luau via module.process().
"""
symbols = language_server.request_document_symbols("src/module.luau").get_all_symbols_and_roots()
assert symbols is not None
symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
# Find the process function symbol
process_symbol = None
for sym in symbol_list:
if isinstance(sym, dict) and sym.get("name") == "process":
process_symbol = sym
break
assert process_symbol is not None, "process function not found in module.luau"
range_info = process_symbol.get("selectionRange", process_symbol.get("range"))
assert range_info is not None, "process function has no range information"
range_start = range_info["start"]
refs = language_server.request_references("src/module.luau", range_start["line"], range_start["character"])
assert refs is not None
assert isinstance(refs, list)
assert len(refs) >= 1, f"Should find at least 1 reference to process, found {len(refs)}"
# Collect reference files and lines
ref_info: dict[str, list[int]] = {}
for ref in refs:
filename = ref.get("uri", "").split("/")[-1]
if filename not in ref_info:
ref_info[filename] = []
ref_info[filename].append(ref["range"]["start"]["line"])
# The definition in module.luau may or may not be included
# We expect at least the reference in module.luau return table (line 9)
assert "module.luau" in ref_info, f"Expected references in module.luau, found in: {set(ref_info.keys())}"
@pytest.mark.parametrize("language_server", [Language.LUAU], indirect=True)
def test_find_definition(self, language_server: SolidLanguageServer) -> None:
"""Test finding definition of createConfig from its usage in main().
createConfig is used at line 17, column 20 (0-indexed) in init.luau.
Its definition should be at line 8 in init.luau.
"""
# Line 17 (0-indexed): ` local config = createConfig("test", 42)`
# createConfig starts at column 20
definition_locations = language_server.request_definition("src/init.luau", 17, 20)
assert definition_locations, f"Expected non-empty definition list but got {definition_locations}"
assert len(definition_locations) >= 1
definition = definition_locations[0]
assert definition["uri"].endswith("init.luau"), f"Definition should be in init.luau, got: {definition['uri']}"
# createConfig is defined at line 8 (0-indexed): `local function createConfig(...)`
assert definition["range"]["start"]["line"] == 8, f"Definition should be at line 8, got line {definition['range']['start']['line']}"
@@ -0,0 +1,164 @@
"""Tests for the Luau language server dependency provider."""
import io
import zipfile
from pathlib import Path
from unittest.mock import patch
import pytest
from solidlsp.language_servers.luau_lsp import LuauLanguageServer
from solidlsp.settings import SolidLSPSettings
def _make_provider(
tmp_path: Path,
custom_settings: dict[str, str] | None = None,
) -> LuauLanguageServer.DependencyProvider:
return LuauLanguageServer.DependencyProvider(
custom_settings=SolidLSPSettings.CustomLSSettings(custom_settings or {}),
ls_resources_dir=str(tmp_path),
)
class _FakeResponse:
def __init__(self, content: bytes) -> None:
self.content = content
def raise_for_status(self) -> None:
return
def iter_content(self, chunk_size: int = 8192):
yield self.content
def __enter__(self) -> "_FakeResponse":
return self
def __exit__(self, exc_type, exc, tb) -> None:
return None
@pytest.mark.luau
class TestLuauDependencyProvider:
def test_create_launch_command_uses_ls_path_override_and_adds_assets(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path, {"ls_path": "/custom/luau-lsp"})
with patch.object(
provider,
"_get_or_install_core_dependency",
side_effect=AssertionError("_get_or_install_core_dependency should not be called when ls_path is set"),
):
with patch.object(
provider,
"_resolve_support_files",
return_value=("/tmp/globalTypes.d.luau", "/tmp/en-us.json"),
):
assert provider.create_launch_command() == [
"/custom/luau-lsp",
"lsp",
"--definitions:@roblox=/tmp/globalTypes.d.luau",
"--docs=/tmp/en-us.json",
]
def test_resolve_support_files_defaults_to_roblox_mode(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path)
with patch.object(
provider,
"_download_roblox_support_files",
return_value=("/tmp/globalTypes.PluginSecurity.d.luau", "/tmp/en-us.json"),
) as download_roblox_support_files:
with patch.object(
provider,
"_download_standard_docs",
side_effect=AssertionError("_download_standard_docs should not be called in roblox mode"),
):
assert provider._resolve_support_files() == (
"/tmp/globalTypes.PluginSecurity.d.luau",
"/tmp/en-us.json",
)
download_roblox_support_files.assert_called_once_with("PluginSecurity")
def test_resolve_support_files_uses_standard_mode_docs_only(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path, {"platform": "standard"})
with patch.object(provider, "_download_standard_docs", return_value="/tmp/luau-en-us.json") as download_standard_docs:
with patch.object(
provider,
"_download_roblox_support_files",
side_effect=AssertionError("_download_roblox_support_files should not be called in standard mode"),
):
assert provider._resolve_support_files() == (
None,
"/tmp/luau-en-us.json",
)
download_standard_docs.assert_called_once_with()
def test_get_or_install_core_dependency_uses_system_binary(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path)
with patch("solidlsp.language_servers.luau_lsp.shutil.which", return_value="/usr/bin/luau-lsp"):
with patch.object(
provider,
"_download_luau_lsp",
side_effect=AssertionError("_download_luau_lsp should not be called when luau-lsp is on PATH"),
):
assert provider._get_or_install_core_dependency() == "/usr/bin/luau-lsp"
def test_download_luau_lsp_extracts_binary_into_ls_resources_dir(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path)
archive = io.BytesIO()
with zipfile.ZipFile(archive, "w") as zip_file:
zip_file.writestr("nested/luau-lsp", "#!/bin/sh\n")
with patch("solidlsp.language_servers.luau_lsp.platform.system", return_value="Linux"):
with patch("solidlsp.language_servers.luau_lsp.platform.machine", return_value="aarch64"):
with patch("solidlsp.language_servers.luau_lsp.requests.get", return_value=_FakeResponse(archive.getvalue())):
binary_path = provider._download_luau_lsp()
resolved_binary = Path(binary_path)
assert resolved_binary.exists()
assert resolved_binary.name == "luau-lsp"
assert str(resolved_binary).startswith(str(tmp_path))
def test_download_roblox_support_files_writes_into_ls_resources_dir(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path)
with patch(
"solidlsp.language_servers.luau_lsp.requests.get",
side_effect=[_FakeResponse(b"types"), _FakeResponse(b"docs")],
):
definitions_path, docs_path = provider._download_roblox_support_files("LocalUserSecurity")
assert definitions_path == str(tmp_path / "globalTypes.LocalUserSecurity.d.luau")
assert docs_path == str(tmp_path / "en-us.json")
assert (tmp_path / "globalTypes.LocalUserSecurity.d.luau").read_bytes() == b"types"
assert (tmp_path / "en-us.json").read_bytes() == b"docs"
def test_download_standard_docs_writes_into_ls_resources_dir(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path, {"platform": "standard"})
with patch("solidlsp.language_servers.luau_lsp.requests.get", return_value=_FakeResponse(b"docs")):
docs_path = provider._download_standard_docs()
assert docs_path == str(tmp_path / "luau-en-us.json")
assert (tmp_path / "luau-en-us.json").read_bytes() == b"docs"
def test_workspace_configuration_uses_configured_platform(self) -> None:
config = LuauLanguageServer._get_workspace_configuration(SolidLSPSettings.CustomLSSettings({"platform": "standard"}))
assert config == {"platform": {"type": "standard"}}
def test_invalid_platform_raises(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path, {"platform": "invalid"})
with pytest.raises(ValueError, match="Unsupported Luau platform"):
provider._resolve_support_files()
def test_invalid_roblox_security_level_raises(self, tmp_path: Path) -> None:
provider = _make_provider(tmp_path, {"roblox_security_level": "invalid"})
with pytest.raises(ValueError, match="Unsupported Luau Roblox security level"):
provider._resolve_support_files()