feat(solidlsp): add Fatou support

Refs #1931
This commit is contained in:
Johan Larsson authored and Dr. Dominik Jain committed 2026-09-03 14:43:25 +02:00
1 parent e771adedb5
commit 25631db556
11 files changed
+190 -11

No files matched your search

+1
View File
@@ -20,6 +20,7 @@ Status of the `main` branch. Changes prior to the next official version change w
Serena's own tools to close the gap (#1852)
* Language Servers:
- Add Fatou support as an alternative Julia language server (`julia_fatou`)
- Fix: Nextflow's `_flush_deferred_workspace_scan` marked the workspace scan flushed even when both
of its `completion` probes failed, permanently skipping the flush (and silencing retries) for the
rest of the session (#1871)
+4 -1
View File
@@ -98,7 +98,10 @@ Some languages require additional installations or setup steps, as noted.
* **Java**
* **JavaScript**
(supported via the TypeScript language server, i.e. use language `typescript` for both JavaScript and TypeScript)
* **Julia**
* **Julia**
(by default, uses LanguageServer.jl (language `julia`); the
[Fatou](https://github.com/jolars/fatou) alternative (language `julia_fatou`) is installed
automatically and requires `uv`/`uvx` in PATH)
* **Kotlin**
(uses the pre-alpha [official kotlin LS](https://github.com/Kotlin/kotlin-lsp), some issues may appear)
* **LaTeX**
+9 -9
View File
@@ -8,15 +8,15 @@ project_name: "project_name"
# elixir elm erlang fortran fsharp
# gdscript gleam go groovy haskell
# haxe hlsl html java json
# julia kotlin latex lean4 lua
# luau markdown matlab msl nextflow
# nix ocaml pascal perl php
# php_phpactor php_phpantom powershell python python_basedpyright
# python_jedi python_pyrefly python_ty qml r
# rego ruby ruby_solargraph rust scala
# scss solidity svelte swift systemverilog
# terraform toml typescript typescript_vts vue
# wolfram yaml zig
# julia julia_fatou kotlin latex lean4
# lua luau markdown matlab msl
# nextflow nix ocaml pascal perl
# php php_phpactor php_phpantom powershell python
# python_basedpyright python_jedi python_pyrefly python_ty qml
# r rego ruby ruby_solargraph rust
# scala scss solidity svelte swift
# systemverilog terraform toml typescript typescript_vts
# vue wolfram yaml zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# For the current list, see values of the LanguageServerId enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
@@ -0,0 +1,119 @@
"""Julia language server integration using Fatou."""
import logging
import threading
from typing_extensions import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderUvx, SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
FATOU_VERSION = "0.18.0"
DEFAULT_INDEXING_TIMEOUT = 60.0
class FatouLanguageServer(SolidLanguageServer):
"""Provide Julia language support through Fatou.
Optional ``ls_specific_settings.julia_fatou`` values:
- ``ls_path``: Path to a preinstalled Fatou executable.
- ``fatou_version``: Version installed and run through ``uvx``.
- ``indexing_timeout``: Seconds to wait for the initial workspace index.
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
self._indexing_complete = threading.Event()
super().__init__(config, repository_root_path, None, "julia", solidlsp_settings)
self._indexing_timeout = float(self._custom_settings.get("indexing_timeout", DEFAULT_INDEXING_TIMEOUT))
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return LanguageServerDependencyProviderUvx(
self._custom_settings,
self._ls_resources_dir,
package="fatou",
entrypoint="fatou",
default_version=FATOU_VERSION,
version_setting_key="fatou_version",
extra_args=("lsp",),
)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in [".julia", "build", "dist"]
def _create_base_initialize_params(self) -> dict:
return {
"locale": "en",
"capabilities": {
"general": {"positionEncodings": ["utf-16"]},
"window": {"workDoneProgress": True},
"workspace": {
"applyEdit": True,
"workspaceEdit": {"documentChanges": True, "resourceOperations": ["create", "rename", "delete"]},
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"didChangeWatchedFiles": {"dynamicRegistration": True},
"diagnostic": {"refreshSupport": True},
"symbol": {"dynamicRegistration": True},
},
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"diagnostic": {"dynamicRegistration": False, "relatedDocumentSupport": True},
},
},
}
def _start_server(self) -> None:
def do_nothing(_params: object) -> None:
return
def work_done_progress_create(_params: object) -> dict:
self._indexing_complete.clear()
return {}
def progress(params: dict) -> None:
value = params.get("value", {})
log.info("Fatou indexing: %s", value.get("message") or value.get("title") or value.get("kind", ""))
if value.get("kind") == "end":
self._indexing_complete.set()
def window_log_message(params: dict) -> None:
log.info("Fatou: %s", params.get("message", ""))
self.server.on_request("client/registerCapability", do_nothing)
self.server.on_request("workspace/diagnostic/refresh", do_nothing)
self.server.on_request("window/workDoneProgress/create", work_done_progress_create)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", progress)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
log.info("Starting Fatou language server process")
self.server.start()
init_response = self.server.send.initialize(self._create_initialize_params())
capabilities = init_response["capabilities"]
assert "documentSymbolProvider" in capabilities
assert "definitionProvider" in capabilities
assert "referencesProvider" in capabilities
self.server.notify.initialized({})
@override
def _wait_for_cross_file_references_if_needed(self) -> None:
if self._has_waited_for_cross_file_references:
return
if not self._indexing_complete.wait(timeout=self._indexing_timeout):
log.warning("Fatou indexing did not finish within %.1f seconds", self._indexing_timeout)
self._has_waited_for_cross_file_references = True
+8 -1
View File
@@ -228,6 +228,8 @@ class LanguageServerId(str, Enum):
"""PHPantom language server for PHP (instead of Intelephense, which is the default).
Uses the open-source Rust-based phpantom_lsp binary and can be auto-downloaded.
"""
JULIA_FATOU = "julia_fatou"
"""Fatou language server for Julia (instead of LanguageServer.jl, which is the default)."""
MARKDOWN = "markdown"
"""Marksman language server for Markdown (experimental).
Must be explicitly specified as the main language, not auto-detected.
@@ -334,6 +336,7 @@ class LanguageServerId(str, Enum):
self.RUBY_SOLARGRAPH,
self.PHP_PHPACTOR,
self.PHP_PHPANTOM,
self.JULIA_FATOU,
self.MARKDOWN,
self.LATEX,
self.YAML,
@@ -532,7 +535,7 @@ class LanguageServerId(str, Enum):
return FilenameMatcher(".tex", ".bib", ".sty", ".cls")
case self.SCALA:
return FilenameMatcher(".scala", ".sbt")
case self.JULIA:
case self.JULIA | self.JULIA_FATOU:
return FilenameMatcher(".jl")
case self.FORTRAN:
return FilenameMatcher(".f90", ".f95", ".f03", ".f08", ".f", ".for", ".fpp", case_sensitive=False)
@@ -828,6 +831,10 @@ class LanguageServerId(str, Enum):
from solidlsp.language_servers.julia_server import JuliaLanguageServer
return JuliaLanguageServer
case self.JULIA_FATOU:
from solidlsp.language_servers.fatou_language_server import FatouLanguageServer
return FatouLanguageServer
case self.FORTRAN:
from solidlsp.language_servers.fortran_language_server import FortranLanguageServer
+2
View File
@@ -52,6 +52,7 @@ _LANGUAGE_REPO_ALIASES: dict[LanguageServerId, LanguageServerId] = {
LanguageServerId.CPP_CCLS: LanguageServerId.CPP,
LanguageServerId.PHP_PHPACTOR: LanguageServerId.PHP,
LanguageServerId.PHP_PHPANTOM: LanguageServerId.PHP,
LanguageServerId.JULIA_FATOU: LanguageServerId.JULIA,
LanguageServerId.PYTHON_JEDI: LanguageServerId.PYTHON,
LanguageServerId.PYTHON_BASEDPYRIGHT: LanguageServerId.PYTHON,
LanguageServerId.PYTHON_TY: LanguageServerId.PYTHON,
@@ -292,6 +293,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[LanguageServerId, list[MarkDecorator | Mark]] = {
LanguageServerId.HAXE: [pytest.mark.haxe],
LanguageServerId.JAVA: [pytest.mark.java],
LanguageServerId.KOTLIN: [pytest.mark.kotlin],
LanguageServerId.JULIA_FATOU: [pytest.mark.julia],
LanguageServerId.LEAN4: [pytest.mark.lean4],
LanguageServerId.LATEX: [pytest.mark.latex],
LanguageServerId.MSL: [pytest.mark.msl],
@@ -0,0 +1,3 @@
name = "SerenaFixture"
uuid = "f5236692-6df0-4c52-843a-f986515aef82"
version = "0.1.0"
@@ -0,0 +1,6 @@
module SerenaFixture
include("fatou_a.jl")
include("fatou_b.jl")
end
@@ -0,0 +1,2 @@
fatou_greet(value) = value
fatou_greet(1)
@@ -0,0 +1 @@
call_fatou_greet() = fatou_greet(2)
+35
View File
@@ -0,0 +1,35 @@
"""Integration tests for the Fatou Julia language server."""
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import LanguageServerId
@pytest.mark.julia
class TestFatouLanguageServer:
@pytest.mark.parametrize("language_server", [LanguageServerId.JULIA_FATOU], indirect=True)
def test_document_symbols(self, language_server: SolidLanguageServer) -> None:
symbols, _ = language_server.request_document_symbols("main.jl").get_all_symbols_and_roots()
assert {symbol["name"] for symbol in symbols} >= {"calculate_sum", "main"}
@pytest.mark.parametrize("language_server", [LanguageServerId.JULIA_FATOU], indirect=True)
def test_within_file_references(self, language_server: SolidLanguageServer) -> None:
references = language_server.request_references("main.jl", line=2, column=10)
locations = {(reference["relativePath"], reference["range"]["start"]["line"]) for reference in references}
assert ("main.jl", 7) in locations
@pytest.mark.parametrize("language_server", [LanguageServerId.JULIA_FATOU], indirect=True)
def test_cross_file_references(self, language_server: SolidLanguageServer) -> None:
references = language_server.request_references("src/fatou_a.jl", line=0, column=2)
locations = {(reference["relativePath"].replace("\\", "/"), reference["range"]["start"]["line"]) for reference in references}
assert locations >= {("src/fatou_a.jl", 1), ("src/fatou_b.jl", 0)}
def test_file_matching(self) -> None:
matcher = LanguageServerId.JULIA_FATOU.get_source_fn_matcher()
assert matcher.is_relevant_filename("analysis.jl")
assert not matcher.is_relevant_filename("analysis.R")