diff --git a/README.md b/README.md index 0473535a7..56e39fcb4 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Serena incorporates a powerful abstraction layer for the integration of language The underlying language servers are typically open-source projects or at least freely available for use. When using Serena's language server backend, we provide **support for over 40 programming languages**, including -AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. +AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, Gleam, GLSL, Go, Groovy, Haskell, Haxe, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. ### The Serena JetBrains Plugin diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 999782ee1..7ec03cd43 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -52,6 +52,8 @@ Some languages require additional installations or setup steps, as noted. (requires Elixir installation; Expert language server is downloaded automatically) * **Elm** (requires Elm compiler) +* **Gleam** + (requires [Gleam](https://gleam.run/getting-started/installing/) compiler; uses the built-in language server via `gleam lsp`) * **Erlang** (requires installation of beam and [erlang_ls](https://github.com/erlang-ls/erlang_ls); experimental, might be slow or hang) * **F#** diff --git a/pyproject.toml b/pyproject.toml index 004e130be..cd8fd5cb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -326,6 +326,7 @@ markers = [ "luau: language server running for Luau", "nix: language server running for Nix", "dart: language server running for Dart", + "gleam: language server running for Gleam", "erlang: language server running for Erlang", "ocaml: language server running for OCaml and Reason", "scala: language server running for Scala", diff --git a/src/solidlsp/language_servers/gleam_language_server.py b/src/solidlsp/language_servers/gleam_language_server.py new file mode 100644 index 000000000..a3e31e08d --- /dev/null +++ b/src/solidlsp/language_servers/gleam_language_server.py @@ -0,0 +1,135 @@ +""" +Provides Gleam specific instantiation of the LanguageServer class. +Uses the language server built into the Gleam compiler (gleam lsp). +""" + +import logging +import os +import pathlib +import shutil +import threading + +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 GleamLanguageServer(SolidLanguageServer): + """ + Provides Gleam specific instantiation of the LanguageServer class. + + Uses the language server built into the Gleam compiler, invoked via ``gleam lsp``. + Requires the Gleam compiler to be installed and available on PATH. + See https://gleam.run/getting-started/installing/ for installation instructions. + """ + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + gleam_path = shutil.which("gleam") + if gleam_path is None: + raise RuntimeError( + "Gleam is not installed or not in PATH.\n" + "Please install Gleam from https://gleam.run/getting-started/installing/\n" + "and make sure the 'gleam' binary is available on your PATH." + ) + super().__init__( + config, + repository_root_path, + ProcessLaunchInfo(cmd=[gleam_path, "lsp"], cwd=repository_root_path), + "gleam", + solidlsp_settings, + ) + self.server_ready = threading.Event() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["build", "_gleam_artefacts"] + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "completion": { + "dynamicRegistration": True, + "completionItem": { + "snippetSupport": True, + "documentationFormat": ["markdown", "plaintext"], + }, + }, + "hover": { + "dynamicRegistration": True, + "contentFormat": ["markdown", "plaintext"], + }, + "codeAction": { + "dynamicRegistration": True, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": ["quickfix", "refactor", "source"], + } + }, + }, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + "configuration": True, + }, + }, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return initialize_params # type: ignore[return-value] + + def _start_server(self) -> None: + """Start the Gleam 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 Gleam 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) + + capabilities = init_response["capabilities"] + log.info(f"Gleam language server capabilities: {list(capabilities.keys())}") + assert "textDocumentSync" in capabilities, "textDocumentSync capability missing" + + self.server.notify.initialized({}) + self.server_ready.set() diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index ff1378944..c5e71f7ea 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -60,6 +60,7 @@ class Language(str, Enum): Supports .luau files. Configure via .luaurc in the project root. """ NIX = "nix" + GLEAM = "gleam" ERLANG = "erlang" OCAML = "ocaml" AL = "al" @@ -262,6 +263,8 @@ def get_source_fn_matcher(self) -> FilenameMatcher: return FilenameMatcher("*.luau") case self.NIX: return FilenameMatcher("*.nix") + case self.GLEAM: + return FilenameMatcher("*.gleam") case self.ERLANG: return FilenameMatcher("*.erl", "*.hrl", "*.escript", "*.config", "*.app", "*.app.src") case self.OCAML: @@ -468,6 +471,10 @@ def get_ls_class(self) -> type["SolidLanguageServer"]: return LuauLanguageServer + case self.GLEAM: + from solidlsp.language_servers.gleam_language_server import GleamLanguageServer + + return GleamLanguageServer case self.ERLANG: from solidlsp.language_servers.erlang_language_server import ErlangLanguageServer