From 42d4b46b94135c5946c2cdfef54365b6d23fbf9b Mon Sep 17 00:00:00 2001 From: weiconghe <13976098570@163.com> Date: Mon, 27 Jul 2026 18:23:37 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Gleam=20?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=94=AF=E6=8C=81=20(gleam=20lsp)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pytest.yml | 20 +- CHANGELOG.md | 1 + README.md | 2 +- docs/01-about/020_programming-languages.md | 2 + pyproject.toml | 1 + .../language_servers/gleam_language_server.py | 230 ++++++++++++++++++ src/solidlsp/ls_config.py | 11 + test/conftest.py | 5 + .../repos/gleam/test_repo/.gitignore | 1 + .../repos/gleam/test_repo/gleam.toml | 5 + .../repos/gleam/test_repo/manifest.toml | 14 ++ .../gleam/test_repo/src/calculator.gleam | 37 +++ .../test_repo/src/diagnostics_sample.gleam | 5 + .../repos/gleam/test_repo/src/utils.gleam | 12 + test/solidlsp/gleam/__init__.py | 0 test/solidlsp/gleam/test_gleam_basic.py | 118 +++++++++ 16 files changed, 462 insertions(+), 2 deletions(-) create mode 100644 src/solidlsp/language_servers/gleam_language_server.py create mode 100644 test/resources/repos/gleam/test_repo/.gitignore create mode 100644 test/resources/repos/gleam/test_repo/gleam.toml create mode 100644 test/resources/repos/gleam/test_repo/manifest.toml create mode 100644 test/resources/repos/gleam/test_repo/src/calculator.gleam create mode 100644 test/resources/repos/gleam/test_repo/src/diagnostics_sample.gleam create mode 100644 test/resources/repos/gleam/test_repo/src/utils.gleam create mode 100644 test/solidlsp/gleam/__init__.py create mode 100644 test/solidlsp/gleam/test_gleam_basic.py diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 6cbcf36c9c..b3f7372eed 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -71,7 +71,7 @@ jobs: # other-langs: grab-bag + scripting languages + haskell + qml. Haskell (GHC/HLS) and qml (qmlls) are # installed only on Linux -- too slow/expensive to build on the macOS runner (Haskell), or not worth # the cost of a cross-OS Qt pull (qmlls) -- and skip on Windows+macOS via the central conftest guard. - MARKERS_OTHER_LANGS: "ruby or php or lua or luau or bash or powershell or elixir or erlang or dart or haxe or haskell or terraform or rego or ansible or yaml or toml or markdown or latex or crystal or cue or fortran or ada or matlab or systemverilog or hlsl or msl or al or qml" + MARKERS_OTHER_LANGS: "ruby or php or lua or luau or bash or powershell or elixir or erlang or dart or haxe or haskell or terraform or rego or ansible or yaml or toml or markdown or latex or crystal or cue or fortran or ada or matlab or systemverilog or hlsl or msl or al or qml or gleam" # niche: slow, mostly-cached toolchains. ocaml + haskell are the two slowest -- kept in separate # batches (ocaml here, haskell in other-langs). nix + perl skip on Windows via the guard. MARKERS_NICHE: "julia or r or perl or lean4 or nix or ocaml" @@ -445,6 +445,24 @@ jobs: missing="$(ldd "$QMLLS_DIR/qmlls" | grep 'not found' || true)" [ -z "$missing" ] || { echo "ERROR: qmlls has unresolved runtime libs:"; echo "$missing"; exit 1; } echo "$QMLLS_DIR" >> "$GITHUB_PATH" + - name: Install Gleam compiler + # Gleam's LSP is bundled with the compiler (started via `gleam lsp`). Restricted to Linux like + # qmlls; Gleam skips on Windows/macOS CI via the conftest guard. The compiler is a self-contained + # Rust binary (x86_64-unknown-linux-musl static build) with no runtime deps. + if: matrix.batch == 'other-langs' && runner.os == 'Linux' + shell: bash + run: | + GLEAM_VERSION="1.17.0" + GLEAM_DIR="$HOME/.gleam" + mkdir -p "$GLEAM_DIR" + curl -fsSL -o /tmp/gleam.tar.gz "https://github.com/gleam-lang/gleam/releases/download/v${GLEAM_VERSION}/gleam-v${GLEAM_VERSION}-x86_64-unknown-linux-musl.tar.gz" + tar xzf /tmp/gleam.tar.gz -C "$GLEAM_DIR" + rm /tmp/gleam.tar.gz + chmod +x "$GLEAM_DIR/gleam" + # Fail loudly if any shared lib is unresolved (the musl build should be fully static). + missing="$(ldd "$GLEAM_DIR/gleam" | grep 'not found' || true)" + [ -z "$missing" ] || { echo "ERROR: gleam has unresolved runtime libs:"; echo "$missing"; exit 1; } + echo "$GLEAM_DIR" >> "$GITHUB_PATH" - name: Cache Free Pascal Compiler if: matrix.batch == 'native' id: cache-fpc diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e7e093b7..6d95d0b859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Status of the `main` branch. Changes prior to the next official version change w - Fix: glob matching bare `*` and `?` in non-`**` patterns matched across `/`, contradicting documented behaviour #1732 * Language Servers: + - Add Gleam language server support (via the `gleam lsp` server bundled with the Gleam compiler) - Allow language server priorities to be configured in `serena_config.yml` (for auto-detection during project creation) - Add `python_basedpyright` as an alternative Python language server diff --git a/README.md b/README.md index 2ff3767466..dfb3bd1b1b 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,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 -Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Elixir, Elm, Erlang, Fortran, F#, GDScript, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, LaTeX, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Pascal, Perl, PHP, PowerShell, Python, QML, R, Rego, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, SystemVerilog, Terraform, TOML, TypeScript, Vue, WGSL, YAML, and Zig. +Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Elixir, Elm, Erlang, Fortran, F#, GDScript, Gleam, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, LaTeX, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Pascal, Perl, PHP, PowerShell, Python, QML, R, Rego, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, SystemVerilog, Terraform, TOML, TypeScript, Vue, 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 46d764dbe9..60837e2f5d 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -77,6 +77,8 @@ Some languages require additional installations or setup steps, as noted. (requires the Godot editor to be running with its built-in LSP enabled — default on port 6008; Serena connects over TCP and does not launch Godot itself; see the [GDScript Setup Guide](../03-special-guides/godot_gdscript_setup_guide_for_serena) for details) +* **Gleam** + (requires the [Gleam compiler](https://gleam.run) on PATH; the language server is bundled with the compiler and started via `gleam lsp`) * **Go** (requires installation of `gopls`) * **Groovy** diff --git a/pyproject.toml b/pyproject.toml index 90d888e751..f743316e86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -379,6 +379,7 @@ markers = [ "angular: language server running for Angular (uses @angular/language-server + @angular/language-service tsserver plugin)", "ada: language server running for Ada / SPARK (uses AdaCore Ada Language Server)", "qml: language server running for QML (uses Qt's qmlls)", + "gleam: language server running for Gleam (uses the Gleam compiler's bundled `gleam lsp`)", ] [tool.codespell] 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 0000000000..3c550626d2 --- /dev/null +++ b/src/solidlsp/language_servers/gleam_language_server.py @@ -0,0 +1,230 @@ +""" +Provides Gleam specific instantiation of the LanguageServer class. + +The Gleam language server is bundled with the Gleam compiler and is started via +``gleam lsp``. No separate language-server package is required beyond the Gleam +compiler itself (https://gleam.run). +""" + +import logging +import shutil +import subprocess +import threading + +from overrides import override + +from solidlsp.ls import ( + LanguageServerDependencyProvider, + LanguageServerDependencyProviderSinglePath, + SolidLanguageServer, +) +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.settings import SolidLSPSettings + +log = logging.getLogger(__name__) + +# Gleam LSP emits $/progress (begin/end) while resolving/downloading project +# dependencies on first start. Once dependencies are resolved, symbol, reference, +# hover and diagnostics queries are served synchronously — no further +# compile-wait or per-request retry is needed (this was verified against +# gleam 1.17 on Windows: cross-file references return immediately once the +# dependency-download progress phase ends). +_INITIAL_PROGRESS_BEGIN_TIMEOUT = 10 # seconds to wait for the first $/progress begin +_INITIAL_PROGRESS_IDLE_TIMEOUT = 180 # seconds to wait for all progress phases to end + + +class GleamLanguageServer(SolidLanguageServer): + """ + Provides Gleam specific instantiation of the LanguageServer class. + + Uses the language server bundled with the Gleam compiler (``gleam lsp``). + Requires the ``gleam`` binary to be installed and available on PATH; see + https://gleam.run/getting-started/installing/ for installation instructions. + + The Gleam compiler is a self-contained Rust binary — neither Erlang/OTP nor + a separate LSP package is required for symbol, reference, hover, definition, + or diagnostics queries. (Full project compilation to BEAM bytecode does + require Erlang, but the LSP's analysis path does not.) + """ + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + super().__init__(config, repository_root_path, None, "gleam", solidlsp_settings) + self._progress_lock = threading.Lock() + self._active_progress_tokens: set[str | int] = set() + self._first_progress_seen = threading.Event() + self._idle = threading.Event() + self._idle.set() # optimistic: assume ready until a $/progress begin arrives + + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """ + Discover the Gleam compiler executable on PATH. + + :return: path to the ``gleam`` binary + :raises FileNotFoundError: if ``gleam`` is neither on PATH nor provided via ``ls_path`` + """ + gleam_binary = shutil.which("gleam") + if gleam_binary is None: + raise FileNotFoundError( + "Gleam is not installed or not in PATH.\n" + "Please install the Gleam compiler from https://gleam.run/getting-started/installing/\n" + "and ensure the 'gleam' binary is available on your PATH.\n" + "The Gleam language server is bundled with the compiler and is started via `gleam lsp`." + ) + return gleam_binary + + def _create_launch_command(self, core_path: str) -> list[str]: + # `gleam lsp` runs the language server over stdio. + return [core_path, "lsp"] + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # Gleam writes compiled output and downloaded dependencies under build/. + return super().is_ignored_dirname(dirname) or dirname == "build" + + @override + def _supports_pull_diagnostics(self) -> bool: + # `gleam lsp` does not implement `textDocument/diagnostic` (LSP 3.17 pull diagnostics) and + # advertises no `diagnosticProvider` capability. Sending a pull request would hang (the server + # neither responds nor errors), so opt out and rely on `textDocument/publishDiagnostics` (push), + # which the base class stores via `_observe_server_notification`. + return False + + def _create_base_initialize_params(self) -> dict: + """ + Return the language-specific initialize params for the Gleam language server. + + ``processId``, ``rootPath``, ``rootUri`` and ``workspaceFolders`` are populated by the + default ``InitializeParamsBuilder`` and must not be set here. + """ + return { + "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"], + }, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + "configuration": True, + }, + }, + } + + def _download_dependencies(self) -> None: + """Run ``gleam deps download`` so the stdlib is available before the LSP starts. + + Gleam LSP also fetches missing dependencies on start and reports progress via + ``$/progress``; pre-fetching keeps the readiness wait short and avoids first-request + races on slow CI runners. + """ + gleam_path = shutil.which("gleam") + if gleam_path is None: + return # _get_or_install_core_dependency will raise a clear error when the server starts + try: + result = subprocess.run( + [gleam_path, "deps", "download"], + cwd=self.repository_root_path, + capture_output=True, + text=True, + timeout=120, + check=False, + ) + if result.returncode != 0: + log.warning("gleam deps download failed (exit %s): %s", result.returncode, result.stderr[:200]) + else: + log.info("gleam deps download completed") + except Exception as e: + log.warning("Failed to run gleam deps download: %s", e) + + 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("LSP: window/logMessage: %s", msg) + + def on_progress(params: dict) -> None: + token = params.get("token") + value = params.get("value", {}) + if not isinstance(value, dict) or token is None: + return + kind = value.get("kind") + with self._progress_lock: + if kind == "begin": + self._active_progress_tokens.add(token) + self._first_progress_seen.set() + self._idle.clear() + log.info( + "Gleam LSP: progress begin (token=%s), active=%d", + token, + len(self._active_progress_tokens), + ) + elif kind == "end": + self._active_progress_tokens.discard(token) + if not self._active_progress_tokens: + self._idle.set() + log.info("Gleam LSP: all progress phases finished") + + 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", on_progress) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + # Pre-fetch dependencies so the LSP's own dependency-download phase is short. + self._download_dependencies() + + log.info("Starting Gleam language server (gleam lsp) process") + self.server.start() + initialize_params = self._create_initialize_params() + + log.info("Sending initialize request to Gleam LSP and awaiting response") + init_response = self.server.send.initialize(initialize_params) + + capabilities = init_response.get("capabilities", {}) if isinstance(init_response, dict) else {} + log.info("Gleam LSP capabilities: %s", list(capabilities.keys())) + + self.server.notify.initialized({}) + + # Wait for Gleam's initial dependency-download progress to finish. + # Gleam emits $/progress begin/end for dependency resolution on first start; once all + # phases end, queries are served synchronously. If no progress is reported within the + # short begin-timeout, dependencies were already resolved and the server is ready. + if not self._first_progress_seen.wait(timeout=_INITIAL_PROGRESS_BEGIN_TIMEOUT): + log.info( + "Gleam LSP: no $/progress within %ss, assuming ready (dependencies already resolved)", + _INITIAL_PROGRESS_BEGIN_TIMEOUT, + ) + elif not self._idle.wait(timeout=_INITIAL_PROGRESS_IDLE_TIMEOUT): + log.warning( + "Gleam LSP: timed out waiting for initial progress after %ss, proceeding anyway", + _INITIAL_PROGRESS_IDLE_TIMEOUT, + ) + + log.info("Gleam language server ready") diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 2db41106c6..3f01b5773c 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -185,6 +185,11 @@ class LanguageServerId(str, Enum): Supports .qml files. Requires Qt 6 installation providing qmlls on PATH. See https://doc.qt.io/qt-6/qtqml-tool-qmlls.html """ + GLEAM = "gleam" + """Gleam language server bundled with the Gleam compiler (`gleam lsp`). + Supports .gleam files. Requires the `gleam` binary on PATH. + See https://gleam.run/getting-started/installing/ for installation. + """ # Experimental or deprecated Language Servers TYPESCRIPT_VTS = "typescript_vts" """Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls""" @@ -572,6 +577,8 @@ def get_source_fn_matcher(self) -> FilenameMatcher: return FilenameMatcher(".gd", ".gdscript") case self.QML: return FilenameMatcher(".qml") + case self.GLEAM: + return FilenameMatcher(".gleam") case self.HTML: return FilenameMatcher(".html", ".htm") case self.SCSS: @@ -859,6 +866,10 @@ def get_ls_class(self) -> type["SolidLanguageServer"]: from solidlsp.language_servers.qml_language_server import QmlLanguageServer return QmlLanguageServer + case self.GLEAM: + from solidlsp.language_servers.gleam_language_server import GleamLanguageServer + + return GleamLanguageServer case self.HTML: from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer diff --git a/test/conftest.py b/test/conftest.py index 9110b68e2d..8cf9c32e64 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -430,6 +430,11 @@ def _determine_disabled_language_servers() -> list[LanguageServerId]: # catching a CI setup regression. On Windows/macOS CI (never installed) and off-CI without the binary it skips. if (_sh.which("qmlls6") is None and _sh.which("qmlls") is None) and not (is_ci and is_linux): result.append(LanguageServerId.QML) + # gleam is installed (see pytest.yml) on the Ubuntu other-langs CI batch. Same rationale as + # qmlls: a missing binary on Linux CI is NOT skipped (fails loudly on a CI setup regression); + # Windows/macOS CI and off-CI without the binary skip. + if _sh.which("gleam") is None and not (is_ci and is_linux): + result.append(LanguageServerId.GLEAM) # === 3. Disabled wherever the precondition is missing (including on CI) === # 3a. Platform precondition: these language servers have no native Windows support. diff --git a/test/resources/repos/gleam/test_repo/.gitignore b/test/resources/repos/gleam/test_repo/.gitignore new file mode 100644 index 0000000000..567609b123 --- /dev/null +++ b/test/resources/repos/gleam/test_repo/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/test/resources/repos/gleam/test_repo/gleam.toml b/test/resources/repos/gleam/test_repo/gleam.toml new file mode 100644 index 0000000000..df8730ebb6 --- /dev/null +++ b/test/resources/repos/gleam/test_repo/gleam.toml @@ -0,0 +1,5 @@ +name = "test_repo" +version = "1.0.0" + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" diff --git a/test/resources/repos/gleam/test_repo/manifest.toml b/test/resources/repos/gleam/test_repo/manifest.toml new file mode 100644 index 0000000000..6e7520616c --- /dev/null +++ b/test/resources/repos/gleam/test_repo/manifest.toml @@ -0,0 +1,14 @@ +# Do not manually edit this file, it is managed by Gleam. +# +# This file locks the dependency versions used, to make your build +# deterministic and to prevent unexpected versions from being included +# in your application. +# +# You should check this file into your source control repository. + +packages = [ + { name = "gleam_stdlib", version = "1.0.3", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1F543AFBA5D33DA493E6087F4E4C4F20D899411343512686C98A8ABB2963CF22" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } diff --git a/test/resources/repos/gleam/test_repo/src/calculator.gleam b/test/resources/repos/gleam/test_repo/src/calculator.gleam new file mode 100644 index 0000000000..2b60b84116 --- /dev/null +++ b/test/resources/repos/gleam/test_repo/src/calculator.gleam @@ -0,0 +1,37 @@ +import utils + +/// A simple calculator type. +pub type Calculator { + Calculator(name: String) +} + +/// Adds two integers and returns the result. +pub fn add(a: Int, b: Int) -> Int { + a + b +} + +/// Subtracts b from a. +pub fn subtract(a: Int, b: Int) -> Int { + a - b +} + +/// Multiplies two integers. +pub fn multiply(a: Int, b: Int) -> Int { + a * b +} + +/// Formats the result of an operation using the utils module (cross-file reference). +pub fn format_result(label: String, value: Int) -> String { + utils.format_output(label, value) +} + +/// Returns the description of a calculator. +pub fn describe(calc: Calculator) -> String { + "Calculator: " <> calc.name +} + +/// Demonstrates calling add (same-file reference) and format_output (cross-file reference). +pub fn demo() -> String { + let sum = add(7, 8) + utils.format_output("sum", sum) +} diff --git a/test/resources/repos/gleam/test_repo/src/diagnostics_sample.gleam b/test/resources/repos/gleam/test_repo/src/diagnostics_sample.gleam new file mode 100644 index 0000000000..bff8d72bd5 --- /dev/null +++ b/test/resources/repos/gleam/test_repo/src/diagnostics_sample.gleam @@ -0,0 +1,5 @@ +// This file intentionally contains a semantic error: `undefined_symbol` is not +// defined anywhere, so the Gleam language server reports a diagnostic for it. +pub fn broken() -> Int { + undefined_symbol +} diff --git a/test/resources/repos/gleam/test_repo/src/utils.gleam b/test/resources/repos/gleam/test_repo/src/utils.gleam new file mode 100644 index 0000000000..e967c6756e --- /dev/null +++ b/test/resources/repos/gleam/test_repo/src/utils.gleam @@ -0,0 +1,12 @@ +import gleam/int +import gleam/string + +/// Formats a label and integer value into a human-readable string. +pub fn format_output(label: String, value: Int) -> String { + label <> ": " <> int.to_string(value) +} + +/// Checks whether a string is non-empty. +pub fn is_non_empty(s: String) -> Bool { + !string.is_empty(s) +} diff --git a/test/solidlsp/gleam/__init__.py b/test/solidlsp/gleam/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/solidlsp/gleam/test_gleam_basic.py b/test/solidlsp/gleam/test_gleam_basic.py new file mode 100644 index 0000000000..107430d6d8 --- /dev/null +++ b/test/solidlsp/gleam/test_gleam_basic.py @@ -0,0 +1,118 @@ +""" +Basic integration tests for the Gleam language server. + +These tests validate startup, document symbols, same-file and cross-file +reference search, and diagnostics, using the Gleam test repository at +``test/resources/repos/gleam/test_repo``. + +Requires the ``gleam`` compiler on PATH (the language server is bundled with it +and started via ``gleam lsp``). +""" + +import os +from pathlib import Path + +import pytest + +from serena.util.text_utils import find_text_coordinates +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import LanguageServerId +from solidlsp.ls_utils import SymbolUtils +from test.conftest import language_server_tests_enabled +from test.solidlsp.conftest import read_repo_file +from test.solidlsp.util.diagnostics import assert_file_diagnostics + +pytestmark = [ + pytest.mark.gleam, + pytest.mark.skipif( + not language_server_tests_enabled(LanguageServerId.GLEAM), + reason="Gleam tests are disabled (gleam compiler not available)", + ), +] + + +class TestGleamLanguageServer: + """Test Gleam language server startup and basic features.""" + + @pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True) + def test_ls_is_running(self, language_server: SolidLanguageServer) -> None: + """The language server starts successfully.""" + assert language_server.is_running() + + @pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True) + def test_document_symbols(self, language_server: SolidLanguageServer) -> None: + """Document symbols for calculator.gleam include the public functions and the type.""" + file_path = os.path.join("src", "calculator.gleam") + doc_symbols = language_server.request_document_symbols(file_path) + all_symbols, _ = doc_symbols.get_all_symbols_and_roots() + + names = {s.get("name") for s in all_symbols if s.get("name")} + assert "add" in names, f"'add' missing from calculator.gleam symbols: {names}" + assert "subtract" in names, f"'subtract' missing: {names}" + assert "multiply" in names, f"'multiply' missing: {names}" + assert "Calculator" in names, f"'Calculator' type missing: {names}" + assert "demo" in names, f"'demo' missing: {names}" + + @pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True) + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: + """The full project symbol tree includes symbols from both modules. + + ``find_symbol`` (serena's most-used tool) is backed by ``request_full_symbol_tree``; + verify it sees public symbols from calculator.gleam and utils.gleam across files. + """ + symbols = language_server.request_full_symbol_tree() + # calculator.gleam symbols + assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "calculator 'add' missing from symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator type missing from symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "demo"), "calculator 'demo' missing from symbol tree" + # utils.gleam symbols (cross-file) + assert SymbolUtils.symbol_tree_contains_name(symbols, "format_output"), "utils 'format_output' missing from symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "is_non_empty"), "utils 'is_non_empty' missing from symbol tree" + + @pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True) + @pytest.mark.parametrize("repo_path", [LanguageServerId.GLEAM], indirect=True) + def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + """References to ``add`` include its call site in ``demo`` (same file). + + ``add`` is defined in calculator.gleam and called once in ``demo``; both the + declaration and the call site should be returned by ``request_references``. + """ + rel = os.path.join("src", "calculator.gleam") + file_path = str(repo_path / rel) + content = read_repo_file(language_server, rel) + + coords = find_text_coordinates(content, r"pub fn (add)\(") + assert coords is not None, "Could not locate `pub fn add` definition in calculator.gleam" + + references = language_server.request_references(file_path, coords.line, coords.col + 1) + assert references, f"Expected non-empty references for add, got {references=}" + + ref_files = {loc["uri"].split("/")[-1] for loc in references} + assert "calculator.gleam" in ref_files, f"Expected reference in calculator.gleam, got {ref_files}" + + @pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True) + @pytest.mark.parametrize("repo_path", [LanguageServerId.GLEAM], indirect=True) + def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + """Cross-file references: ``format_output`` (defined in utils.gleam) is called in calculator.gleam.""" + rel = os.path.join("src", "utils.gleam") + file_path = str(repo_path / rel) + content = read_repo_file(language_server, rel) + + coords = find_text_coordinates(content, r"pub fn (format_output)\(") + assert coords is not None, "Could not locate `pub fn format_output` definition in utils.gleam" + + references = language_server.request_references(file_path, coords.line, coords.col + 1) + assert references, f"Expected non-empty references for format_output, got {references=}" + + ref_files = {loc["uri"].split("/")[-1] for loc in references} + assert "calculator.gleam" in ref_files, f"Expected cross-file reference in calculator.gleam, got {ref_files}" + + @pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + """diagnostics_sample.gleam references an undefined symbol; the LSP should report it.""" + assert_file_diagnostics( + language_server, + os.path.join("src", "diagnostics_sample.gleam"), + (), + min_count=1, + ) From 5584e75a18a4ed9c669bc5c7afd8ff430f3c8c82 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 8 Aug 2026 19:42:38 +0200 Subject: [PATCH 2/2] formating --- scripts/update_downloaded_dependency_hashes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update_downloaded_dependency_hashes.py b/scripts/update_downloaded_dependency_hashes.py index 8e4136f066..ad25da464e 100644 --- a/scripts/update_downloaded_dependency_hashes.py +++ b/scripts/update_downloaded_dependency_hashes.py @@ -1,8 +1,8 @@ from sensai.util import logging from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS -from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer from solidlsp.language_servers.kotlin_language_server import KotlinLanguageServer +from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer if __name__ == "__main__": logging.configure()