"""
Provides Lua specific instantiation of the LanguageServer class using lua-language-server.
"""
import logging
import os
import pathlib
import platform
import shutil
import tarfile
import zipfile
from pathlib import Path
import requests
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
class LuaLanguageServer(SolidLanguageServer):
"""
Provides Lua specific instantiation of the LanguageServer class using lua-language-server.
"""
@override
def is_ignored_dirname(self, dirname: str) -> bool:
# For Lua projects, we should ignore:
# - .luarocks: package manager cache
# - lua_modules: local dependencies
# - node_modules: if the project has JavaScript components
return super().is_ignored_dirname(dirname) or dirname in [".luarocks", "lua_modules", "node_modules", "build", "dist", ".cache"]
@staticmethod
def _get_lua_ls_path() -> str | None:
"""Get the path to lua-language-server executable."""
# First check if it's in PATH
lua_ls = shutil.which("lua-language-server")
if lua_ls:
return lua_ls
# Check common installation locations
home = Path.home()
possible_paths = [
home / ".local" / "bin" / "lua-language-server",
home / ".serena" / "language_servers" / "lua" / "bin" / "lua-language-server",
Path("/usr/local/bin/lua-language-server"),
Path("/opt/lua-language-server/bin/lua-language-server"),
]
# Add Windows-specific paths
if platform.system() == "Windows":
possible_paths.extend(
[
home / "AppData" / "Local" / "lua-language-server" / "bin" / "lua-language-server.exe",
home / ".serena" / "language_servers" / "lua" / "bin" / "lua-language-server.exe",
]
)
for path in possible_paths:
if path.exists():
return str(path)
return None
@staticmethod
def _download_lua_ls() -> str:
"""Download and install lua-language-server if not present."""
system = platform.system()
machine = platform.machine().lower()
lua_ls_version = "3.15.0"
# Map platform and architecture to download URL
if system == "Linux":
if machine in ["x86_64", "amd64"]:
download_name = f"lua-language-server-{lua_ls_version}-linux-x64.tar.gz"
elif machine in ["aarch64", "arm64"]:
download_name = f"lua-language-server-{lua_ls_version}-linux-arm64.tar.gz"
else:
raise RuntimeError(f"Unsupported Linux architecture: {machine}")
elif system == "Darwin":
if machine in ["x86_64", "amd64"]:
download_name = f"lua-language-server-{lua_ls_version}-darwin-x64.tar.gz"
elif machine in ["arm64", "aarch64"]:
download_name = f"lua-language-server-{lua_ls_version}-darwin-arm64.tar.gz"
else:
raise RuntimeError(f"Unsupported macOS architecture: {machine}")
elif system == "Windows":
if machine in ["amd64", "x86_64"]:
download_name = f"lua-language-server-{lua_ls_version}-win32-x64.zip"
else:
raise RuntimeError(f"Unsupported Windows architecture: {machine}")
else:
raise RuntimeError(f"Unsupported operating system: {system}")
download_url = f"https://github.com/LuaLS/lua-language-server/releases/download/{lua_ls_version}/{download_name}"
# Create installation directory
install_dir = Path.home() / ".serena" / "language_servers" / "lua"
install_dir.mkdir(parents=True, exist_ok=True)
# Download the file
print(f"Downloading lua-language-server from {download_url}...")
response = requests.get(download_url, stream=True)
response.raise_for_status()
# Save and extract
download_path = install_dir / download_name
with open(download_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Extracting lua-language-server to {install_dir}...")
if download_name.endswith(".tar.gz"):
with tarfile.open(download_path, "r:gz") as tar:
tar.extractall(install_dir)
elif download_name.endswith(".zip"):
with zipfile.ZipFile(download_path, "r") as zip_ref:
zip_ref.extractall(install_dir)
# Clean up download file
download_path.unlink()
# Make executable on Unix systems
if system != "Windows":
lua_ls_path = install_dir / "bin" / "lua-language-server"
if lua_ls_path.exists():
lua_ls_path.chmod(0o755)
return str(lua_ls_path)
else:
lua_ls_path = install_dir / "bin" / "lua-language-server.exe"
if lua_ls_path.exists():
return str(lua_ls_path)
raise RuntimeError("Failed to find lua-language-server executable after extraction")
@staticmethod
def _setup_runtime_dependency() -> str:
"""
Check if required Lua runtime dependencies are available.
Downloads lua-language-server if not present.
"""
lua_ls_path = LuaLanguageServer._get_lua_ls_path()
if not lua_ls_path:
print("lua-language-server not found. Downloading...")
lua_ls_path = LuaLanguageServer._download_lua_ls()
print(f"lua-language-server installed at: {lua_ls_path}")
return lua_ls_path
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
lua_ls_path = self._setup_runtime_dependency()
super().__init__(
config, repository_root_path, ProcessLaunchInfo(cmd=lua_ls_path, cwd=repository_root_path), "lua", solidlsp_settings
)
self.request_id = 0
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Lua 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},
},
},
},
"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),
}
],
"initializationOptions": {
# Lua Language Server specific options
"runtime": {
"version": "Lua 5.4",
"path": ["?.lua", "?/init.lua"],
},
"diagnostics": {
"enable": True,
"globals": ["vim", "describe", "it", "before_each", "after_each"], # Common globals
},
"workspace": {
"library": [], # Can be extended with project-specific libraries
"checkThirdParty": False,
"userThirdParty": [],
},
"telemetry": {
"enable": False,
},
"completion": {
"enable": True,
"callSnippet": "Both",
"keywordSnippet": "Both",
},
},
}
return initialize_params # type: ignore[return-value]
def _start_server(self) -> None:
"""Start Lua Language Server process"""
def register_capability_handler(params: dict) -> None:
return
def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")
def do_nothing(params: dict) -> None:
return
self.server.on_request("client/registerCapability", register_capability_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 Lua 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({})
# Lua Language Server is typically ready immediately after initialization
# (no need to wait for events)