diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ac700fcf5..ee126e5c6 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 deno 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 deno 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" @@ -453,6 +453,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 68f9e60ac..c7927ce29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Status of the `main` branch. Changes prior to the next official version change w Note that if the base path is itself ignored, ignored paths cannot be considered. * 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 support for Nextflow** (language server `nextflow`), using the official diff --git a/README.md b/README.md index a7941e374..bd58d9195 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, Deno, 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, Nextflow, 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, Deno, 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, Nextflow, 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 877423372..62d581bb9 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -78,6 +78,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 e9df07a34..953347866 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -380,6 +380,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/scripts/update_downloaded_dependency_hashes.py b/scripts/update_downloaded_dependency_hashes.py index 8e4136f06..ad25da464 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() 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..3c550626d --- /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 d24ff2f07..d4f05de08 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. + """ NEXTFLOW = "nextflow" """Nextflow language server (https://github.com/nextflow-io/language-server), the one that backs the official VS Code extension. Supports .nf scripts (Nextflow .config files are parsed by the server, but @@ -587,6 +592,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.NEXTFLOW: # only scripts: the language server does have a service for .config files, but it provides # no symbols for them, so treating them as source files would only pollute the symbol index @@ -886,6 +893,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.NEXTFLOW: from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer diff --git a/test/conftest.py b/test/conftest.py index 796523e4a..a44be0411 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -437,6 +437,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 000000000..567609b12 --- /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 000000000..df8730ebb --- /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 000000000..6e7520616 --- /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 000000000..2b60b8411 --- /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 000000000..bff8d72bd --- /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 000000000..e967c6756 --- /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 000000000..e69de29bb diff --git a/test/solidlsp/gleam/test_gleam_basic.py b/test/solidlsp/gleam/test_gleam_basic.py new file mode 100644 index 000000000..107430d6d --- /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, + )