Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions docs/01-about/020_programming-languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
230 changes: 230 additions & 0 deletions src/solidlsp/language_servers/gleam_language_server.py
Original file line number Diff line number Diff line change
@@ -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")
11 changes: 11 additions & 0 deletions src/solidlsp/ls_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions test/resources/repos/gleam/test_repo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
5 changes: 5 additions & 0 deletions test/resources/repos/gleam/test_repo/gleam.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name = "test_repo"
version = "1.0.0"

[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
14 changes: 14 additions & 0 deletions test/resources/repos/gleam/test_repo/manifest.toml
Original file line number Diff line number Diff line change
@@ -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" }
Loading
Loading