From 7aa24b8dad50449fc4ad99f64b9707d2716e47e9 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 5 Aug 2026 21:27:38 +0200 Subject: [PATCH 01/27] ProjectServer: Configure trusted hosts for localhost case --- CHANGELOG.md | 1 + src/serena/cli.py | 7 ++----- src/serena/project_server.py | 29 +++++++++++++++++++---------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dca327a5..98bcf0c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Status of the `main` branch. Changes prior to the next official version change w - Project activation errors are now reported to the client in Serena's system prompt, instead of failures being visible only in the log. This applies both to a failed activation of an explicitly given project and to a failed `--project-from-cwd` auto-detection (#1773). + - ProjectServer: Configure trusted hosts for localhost case * CLI: - Fix: `start-mcp-server` help text for `--project-from-cwd` falsely promised a fallback to the CWD, which was diff --git a/src/serena/cli.py b/src/serena/cli.py index f5613d14..f6471733 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -489,11 +489,8 @@ class TopLevelCommands(AutoRegisteringGroup): log.info("Starting Serena project server") log.info("Storing logs in %s", log_path) - server = ProjectServer() - run_kwargs: dict[str, Any] = {"host": host} - if port is not None: - run_kwargs["port"] = port - server.run(**run_kwargs) + server = ProjectServer(host=host, port=port) + server.run() @staticmethod @click.command( diff --git a/src/serena/project_server.py b/src/serena/project_server.py index ceec43a9..12c5e907 100644 --- a/src/serena/project_server.py +++ b/src/serena/project_server.py @@ -44,9 +44,16 @@ class ProjectServer: PORT = SerenaPorts.PROJECT_SERVER_PORT - def __init__(self) -> None: + def __init__(self, host: str = "127.0.0.1", port: int | None = None) -> None: + """ + :param host: the host address to listen on. + :param port: the port to listen on; if None, use default + """ from serena.agent import SerenaAgent + if port is None: + port = self.PORT + serena_config = SerenaConfig.from_config_file() serena_config.gui_log_window = False serena_config.web_dashboard = False @@ -54,9 +61,15 @@ class ProjectServer: self._agent = SerenaAgent(serena_config=serena_config) self._loaded_projects_by_root: dict[str, "Project"] = {} + self._port = port + self._host = host - # create the Flask application + # create the Flask application, limiting trusted hosts for the case where the server is running on localhost self._app = Flask(__name__) + local_hosts = ["localhost", "127.0.0.1"] + if self._host in local_hosts: + self._app.config["TRUSTED_HOSTS"] = local_hosts + self._setup_routes() def _setup_routes(self) -> None: @@ -97,12 +110,9 @@ class ProjectServer: params = json.loads(req.tool_params_json) return tool.apply_ex(**params) - def run(self, host: str = "127.0.0.1", port: int = PORT) -> int: - """Run the server on the given host and port. - - :param host: the host address to listen on. - :param port: the port to listen on. - :return: the port number the server is running on. + def run(self) -> None: + """ + Run the server on the given host and port. """ from flask import cli @@ -111,8 +121,7 @@ class ProjectServer: # replacement, even one with an identical signature), so the monkeypatch is suppressed here cli.show_server_banner = lambda *args, **kwargs: None # ty: ignore[invalid-assignment] - self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True) - return port + self._app.run(host=self._host, port=self._port, debug=False, use_reloader=False, threaded=True) class ProjectServerClient: From df24c3151f8f395659f685311ebf13b5a8c083f0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 5 Aug 2026 21:31:03 +0200 Subject: [PATCH 02/27] Clarify responsibilities when exposing network services --- docs/02-usage/070_security.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/02-usage/070_security.md b/docs/02-usage/070_security.md index c3a06a0a..249409cf 100644 --- a/docs/02-usage/070_security.md +++ b/docs/02-usage/070_security.md @@ -111,7 +111,9 @@ By default, these services accept connections from localhost only, which is a se These services can be reconfigured to listen on other addresses, but doing so may have security implications. If you need to allow connections from other machines, we recommend that you set up a secure networking environment -(e.g. using a VPN or SSH tunnels) and ensure that only trusted machines can connect to these services. +and ensure that only trusted machines can connect to these services. +It is the responsibility of the user to restrict access appropriately, e.g. by placing the service behind a reverse +proxy (adding authentication) or firewall. ## Supply Chain Security From c7af2c09ef45faa4367c0e2a9f770fb73a62a612 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 5 Aug 2026 21:35:18 +0200 Subject: [PATCH 03/27] SerenaDashboardTrayManager: Configure trusted hosts (local hosts only) --- CHANGELOG.md | 3 ++- src/serena/dashboard.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98bcf0c6..256dd973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ Status of the `main` branch. Changes prior to the next official version change w - Project activation errors are now reported to the client in Serena's system prompt, instead of failures being visible only in the log. This applies both to a failed activation of an explicitly given project and to a failed `--project-from-cwd` auto-detection (#1773). - - ProjectServer: Configure trusted hosts for localhost case + - ProjectServer: Configure trusted hosts (local hosts only) when listening on localhost + - SerenaDashboardTrayManager: Configure trusted hosts (local hosts only) * CLI: - Fix: `start-mcp-server` help text for `--project-from-cwd` falsely promised a fallback to the CWD, which was diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index c9873608..b92fcb51 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -207,7 +207,7 @@ class SerenaDashboardAPI: self._tool_names = tool_names self._agent = agent self._host = host - self._app = Flask(__name__) + self._app = Flask(self.__class__.__name__) if trusted_hosts: self._app.config["TRUSTED_HOSTS"] = trusted_hosts self._tool_usage_stats = tool_usage_stats @@ -966,6 +966,8 @@ class SerenaDashboardTrayManager: HOST = "127.0.0.1" """listen address (local only)""" + TRUSTED_HOSTS = ["localhost", "127.0.0.1"] + ALIVE_CHECK_INTERVAL_SECONDS = 3 """interval in seconds between alive checks of registered instances""" @@ -984,7 +986,8 @@ class SerenaDashboardTrayManager: self._lock = threading.Lock() self._tray_icon: Optional["pystray.Icon"] = None self._alive_check_use_pid = alive_check_use_pid - self._app = Flask(__name__) + self._app = Flask(self.__class__.__name__) + self._app.config["TRUSTED_HOSTS"] = self.TRUSTED_HOSTS self._setup_routes() self._use_pywebview = use_pywebview From 7a870878b88089c26e5111acfb5bad80308d7a94 Mon Sep 17 00:00:00 2001 From: shiersa <25566715+shiersa@users.noreply.github.com> Date: Thu, 30 Jul 2026 02:21:18 +0800 Subject: [PATCH 04/27] feat: add Deno language server support Add a Deno language server backed by the Deno CLI's built-in `deno lsp`, serving TypeScript/JavaScript in Deno projects. Unlike the plain typescript-language-server it understands Deno module resolution (npm:/jsr:/https: imports) and the `Deno.*` global namespace. It is experimental and must be selected explicitly via `language_servers: [deno]`; it overlaps the TypeScript server on file extensions, so it is not auto-detected. Requires the `deno` CLI on PATH. Tests run in CI: the `deno` marker joins the other-langs batch, which now installs Deno via denoland/setup-deno. Off-CI and wherever the CLI is absent they skip through the central conftest guard, following the existing pattern for toolchain-gated language servers. Hover is covered explicitly, including hover on a `Deno.*` global, which is the capability this server adds over the TypeScript one. --- .github/workflows/pytest.yml | 9 +- CHANGELOG.md | 5 + README.md | 2 +- docs/01-about/020_programming-languages.md | 6 + pyproject.toml | 1 + src/serena/resources/project.template.yml | 25 +-- .../language_servers/deno_language_server.py | 152 ++++++++++++++++++ src/solidlsp/ls_config.py | 21 +++ test/conftest.py | 3 + test/resources/repos/deno/test_repo/deno.json | 7 + test/resources/repos/deno/test_repo/main.ts | 15 ++ test/resources/repos/deno/test_repo/util.ts | 7 + test/solidlsp/deno/test_deno_basic.py | 89 ++++++++++ 13 files changed, 328 insertions(+), 14 deletions(-) create mode 100644 src/solidlsp/language_servers/deno_language_server.py create mode 100644 test/resources/repos/deno/test_repo/deno.json create mode 100644 test/resources/repos/deno/test_repo/main.ts create mode 100644 test/resources/repos/deno/test_repo/util.ts create mode 100644 test/solidlsp/deno/test_deno_basic.py diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e9b73699..a502c589 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 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" # 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" @@ -219,6 +219,13 @@ jobs: with: terraform_version: "1.5.0" terraform_wrapper: false + # Deno ships its language server with the CLI (`deno lsp`), so installing the + # runtime is all that is needed for the deno-marked tests. + - name: Install Deno + if: matrix.batch == 'other-langs' + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x # - name: Install swift # if: runner.os != 'Windows' # uses: swift-actions/setup-swift@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 256dd973..6496aa69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,11 @@ Status of the `main` branch. Changes prior to the next official version change w or `textDocument/didChange`), as this is always the intention of calling the method. If files were kept open in the language server (which the Svelte and Vue language servers did), the language server was not necessarily informed about updated contents. + - Add Deno support (experimental; language server `deno`, backed by the Deno CLI's built-in + `deno lsp`). Understands Deno module resolution (`npm:` / `jsr:` / `https:` imports) and the + `Deno.*` globals, which the plain TypeScript language server does not. Overlaps TypeScript on + file extensions, so it is not auto-detected and must be selected explicitly; requires the + `deno` CLI on PATH * JetBrains: - `jet_brains_find_symbol`: Disallow wildcard-only search, delegating to overview tool if request is for file diff --git a/README.md b/README.md index 2ff37674..70e9d4c7 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, 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, 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 00bfe33b..25ac8146 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -164,6 +164,12 @@ Some languages require additional installations or setup steps, as noted. * **TOML** (experimental; uses Taplo 0.10.0, taken from PATH if present, otherwise downloaded automatically) * **TypeScript** +* **Deno** + (experimental; requires the `deno` CLI on PATH — it bundles the language server used here; + serves Deno TypeScript/JavaScript and understands `npm:` / `jsr:` / `https:` imports and the `Deno.*` + globals, which the plain TypeScript language server does not; overlaps `typescript` on file extensions, + so it is not auto-detected and must be set as the language explicitly — do not also enable `typescript` + for the same files) * **Vue** (3.x with TypeScript; requires Node.js v18+ and npm; supports .vue Single File Components with monorepo detection) * **YAML** diff --git a/pyproject.toml b/pyproject.toml index e81b2b5d..6731feeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -328,6 +328,7 @@ markers = [ "groovy: language server running for Groovy", "rust: language server running for Rust", "typescript: language server running for TypeScript", + "deno: language server running for Deno (uses the built-in deno lsp)", "svelte: language server running for Svelte (uses svelte-language-server)", "vue: language server running for Vue (uses TypeScript LSP)", "php: language server running for PHP", diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index cb5967ff..3d22e3d6 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -4,18 +4,18 @@ project_name: "project_name" # list of language servers to start when using the LSP backend; choose from: # ada al angular ansible bash # bsl clojure cpp cpp_ccls crystal -# csharp csharp_omnisharp cue dart elixir -# elm erlang fortran fsharp gdscript -# go groovy haskell haxe hlsl -# html java json julia kotlin -# latex lean4 lua luau markdown -# matlab msl 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 yaml zig +# csharp csharp_omnisharp cue dart deno +# elixir elm erlang fortran fsharp +# gdscript go groovy haskell haxe +# hlsl html java json julia +# kotlin latex lean4 lua luau +# markdown matlab msl 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 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) @@ -25,6 +25,7 @@ project_name: "project_name" # - For JavaScript, use typescript # - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root) # - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm) +# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH) # - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three) # - For Free Pascal/Lazarus, use pascal # Special requirements: diff --git a/src/solidlsp/language_servers/deno_language_server.py b/src/solidlsp/language_servers/deno_language_server.py new file mode 100644 index 00000000..c9649ced --- /dev/null +++ b/src/solidlsp/language_servers/deno_language_server.py @@ -0,0 +1,152 @@ +""" +Provides Deno-specific instantiation of the LanguageServer class, using the +language server built into the Deno CLI (``deno lsp``). +""" + +import logging +import shutil + +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__) + + +class DenoLanguageServer(SolidLanguageServer): + """ + Deno instantiation of the LanguageServer class, backed by ``deno lsp``. + + Serves TypeScript/JavaScript in Deno projects. Unlike the plain + typescript-language-server, ``deno lsp`` understands Deno-specific module + resolution (``npm:`` / ``jsr:`` / ``https:`` imports) and the ``Deno.*`` + global namespace. + + This server overlaps the TypeScript server on file extensions and is therefore + marked experimental: it is not auto-detected and must be selected explicitly via + ``languages: [deno]`` in ``project.yml``. + """ + + @classmethod + def supports_implementation_request(cls) -> bool: + return True + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + super().__init__( + config, + repository_root_path, + None, + "typescript", + solidlsp_settings, + ) + + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # node_modules appears in Deno projects using npm compatibility; vendor/ holds + # vendored remote dependencies. Neither should be indexed as project sources. + return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "dist", "build"] + + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """Return the path to the ``deno`` executable (the language server ships with it).""" + deno_path = shutil.which("deno") + if deno_path is None: + raise FileNotFoundError( + "The 'deno' executable was not found on PATH. Install Deno " + "(https://docs.deno.com/runtime/getting_started/installation/) — it bundles " + "the language server used here — and ensure 'deno' is on your PATH." + ) + return deno_path + + def _create_launch_command(self, core_path: str) -> list[str]: + return [core_path, "lsp"] + + def _get_language_id_for_file(self, relative_file_path: str) -> str: + # deno lsp relies on the correct languageId; .tsx/.jsx in particular must not + # be sent as plain "typescript" or symbol ranges get truncated at JSX expressions. + if relative_file_path.endswith(".tsx"): + return "typescriptreact" + if relative_file_path.endswith(".jsx"): + return "javascriptreact" + if relative_file_path.endswith((".js", ".mjs", ".cjs")): + return "javascript" + return "typescript" + + def _create_base_initialize_params(self) -> dict: + return { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}}, + "rename": {"dynamicRegistration": True, "prepareSupport": True}, + "publishDiagnostics": {"relatedInformation": True}, + }, + "workspace": { + "workspaceFolders": True, + "configuration": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + "symbol": {"dynamicRegistration": True}, + }, + }, + # deno lsp reads its settings from initializationOptions; enabling the server + # and the linter mirrors the defaults of the official VS Code Deno extension. + "initializationOptions": { + "enable": True, + "lint": True, + "unstable": False, + }, + } + + def _start_server(self) -> None: + """Start the ``deno lsp`` process and drive the LSP initialize handshake.""" + + 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 + + def configuration_handler(params: dict) -> list: + # deno lsp requests workspace/configuration during startup; return an empty + # settings object per requested item so it proceeds with its defaults. + return [{} for _ in params.get("items", [])] + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_request("workspace/configuration", configuration_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) + # Deno-specific notifications emitted after config discovery; no action needed. + self.server.on_notification("deno/didRefreshDenoConfigurationTree", do_nothing) + self.server.on_notification("deno/didChangeDenoConfiguration", do_nothing) + + log.info("Starting deno lsp server process") + self.server.start() + initialize_params = self._create_initialize_params() + + log.info("Sending initialize request from LSP client to deno lsp and awaiting response") + init_response = self.server.send.initialize(initialize_params) + + assert "textDocumentSync" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + assert "documentSymbolProvider" in init_response["capabilities"] + assert "referencesProvider" in init_response["capabilities"] + + self.server.notify.initialized({}) diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 2db41106..25a9f720 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -286,6 +286,14 @@ class LanguageServerId(str, Enum): project.yml — Angular LS supersedes both for Angular projects. Must be explicitly specified in project.yml. """ + DENO = "deno" + """Deno's built-in language server (``deno lsp``) for Deno TypeScript/JavaScript projects. + Understands Deno module resolution (``npm:`` / ``jsr:`` / ``https:`` imports) and the + ``Deno.*`` globals, which the plain typescript-language-server does not. Overlaps the + TypeScript server on file extensions (.ts/.tsx/.js/.jsx/.mts/.cts/.mjs/.cjs), so it is + experimental and must be explicitly specified via ``languages: [deno]`` in project.yml; + do not also enable typescript for the same files. Requires the ``deno`` CLI on PATH. + """ @classmethod def iter_all(cls, include_experimental: bool = True, include_non_programming_languages: bool = True) -> Iterable[Self]: @@ -321,6 +329,7 @@ class LanguageServerId(str, Enum): self.HTML, self.SCSS, self.ANGULAR, + self.DENO, } def is_programming_language(self) -> bool: @@ -588,6 +597,14 @@ class LanguageServerId(str, Enum): for postfix in ["x", ""]: path_patterns.append(f".{prefix}ts{postfix}") return FilenameMatcher(*path_patterns) + case self.DENO: + # Deno serves the same TS/JS family as the TypeScript server. + path_patterns = [] + for prefix in ["c", "m", ""]: + for postfix in ["x", ""]: + for base_pattern in ["ts", "js"]: + path_patterns.append(f".{prefix}{base_pattern}{postfix}") + return FilenameMatcher(*path_patterns) case _: raise ValueError(f"Unhandled language: {self}") @@ -871,6 +888,10 @@ class LanguageServerId(str, Enum): from solidlsp.language_servers.angular_language_server import AngularLanguageServer return AngularLanguageServer + case self.DENO: + from solidlsp.language_servers.deno_language_server import DenoLanguageServer + + return DenoLanguageServer case _: raise ValueError(f"Unhandled language: {self}") diff --git a/test/conftest.py b/test/conftest.py index d973bb48..796523e4 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -286,6 +286,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[LanguageServerId, list[MarkDecorator | Mark]] = { LanguageServerId.CPP_CCLS: [pytest.mark.cpp], LanguageServerId.CUE: [pytest.mark.cue], LanguageServerId.CSHARP: [pytest.mark.csharp], + LanguageServerId.DENO: [pytest.mark.deno], LanguageServerId.FSHARP: [pytest.mark.fsharp], LanguageServerId.GO: [pytest.mark.go], LanguageServerId.HAXE: [pytest.mark.haxe], @@ -477,6 +478,8 @@ def _determine_disabled_language_servers() -> list[LanguageServerId]: result.append(LanguageServerId.OCAML) if not _is_perl_language_server_available(): # perl ships with the OS; the LS module is the real signal result.append(LanguageServerId.PERL) + if _sh.which("deno") is None: # deno bundles the language server (`deno lsp`); skip where the CLI is absent + result.append(LanguageServerId.DENO) # === 4. Enabled everywhere: every language NOT listed in this function (python, go, java, ...) === diff --git a/test/resources/repos/deno/test_repo/deno.json b/test/resources/repos/deno/test_repo/deno.json new file mode 100644 index 00000000..2f35542b --- /dev/null +++ b/test/resources/repos/deno/test_repo/deno.json @@ -0,0 +1,7 @@ +{ + "lint": { + "rules": { + "tags": ["recommended"] + } + } +} diff --git a/test/resources/repos/deno/test_repo/main.ts b/test/resources/repos/deno/test_repo/main.ts new file mode 100644 index 00000000..34493ed6 --- /dev/null +++ b/test/resources/repos/deno/test_repo/main.ts @@ -0,0 +1,15 @@ +import { add } from "./util.ts"; + +export class Calculator { + sum(a: number, b: number): number { + return add(a, b); + } +} + +// Uses the Deno global namespace, which the plain TypeScript language server does not know about. +export function describeCwd(): string { + return `cwd: ${Deno.cwd()}`; +} + +const calc = new Calculator(); +console.log(calc.sum(2, 3)); diff --git a/test/resources/repos/deno/test_repo/util.ts b/test/resources/repos/deno/test_repo/util.ts new file mode 100644 index 00000000..8559ea54 --- /dev/null +++ b/test/resources/repos/deno/test_repo/util.ts @@ -0,0 +1,7 @@ +export function add(a: number, b: number): number { + return a + b; +} + +export function subtract(a: number, b: number): number { + return a - b; +} diff --git a/test/solidlsp/deno/test_deno_basic.py b/test/solidlsp/deno/test_deno_basic.py new file mode 100644 index 00000000..3efcb0f8 --- /dev/null +++ b/test/solidlsp/deno/test_deno_basic.py @@ -0,0 +1,89 @@ +from pathlib import Path + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import LanguageServerId +from solidlsp.ls_types import Hover +from solidlsp.ls_utils import SymbolUtils +from test.conftest import language_server_tests_enabled + + +def _hover_text(hover: Hover | None) -> str: + """Flatten the hover contents (markup object, plain string or list of either) into one string.""" + assert hover is not None, "Expected hover information, got None" + contents = hover["contents"] + items = contents if isinstance(contents, list) else [contents] + return "\n".join(item if isinstance(item, str) else item["value"] for item in items) + + +@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.DENO), reason="Deno tests are disabled (deno not available)") +@pytest.mark.deno +class TestDenoLanguageServer: + @pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True) + @pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True) + def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + assert language_server.is_running() + assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve() + + @pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True) + @pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True) + def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + # main.ts line 4 (0-indexed): " return add(a, b);" — cursor on `add` (char 11). + # `add` is defined in util.ts at line 0. + definitions = language_server.request_definition(str(repo_path / "main.ts"), 4, 11) + + assert definitions, f"Expected a definition for `add`, got {definitions=}" + definition = definitions[0] + assert definition["uri"].endswith("util.ts") + assert definition["range"]["start"]["line"] == 0 + + @pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True) + @pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True) + def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + # `add` is defined in util.ts line 0 at char len("export function ") == 16. + references = language_server.request_references(str(repo_path / "util.ts"), 0, len("export function ")) + + assert references, f"Expected references for `add`, got {references=}" + locations = [(ref["uri"].split("/")[-1], ref["range"]["start"]["line"]) for ref in references] + # The usage inside Calculator.sum lives on line 4 of main.ts. + assert ("main.ts", 4) in locations, f"Expected a reference in main.ts line 4, got {locations}" + + @pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True) + @pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True) + def test_document_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + all_symbols, _roots = language_server.request_document_symbols("main.ts").get_all_symbols_and_roots() + names = [sym.get("name") for sym in all_symbols] + assert "Calculator" in names, f"Calculator not found in main.ts document symbols: {names}" + assert "sum" in names, f"sum not found in main.ts document symbols: {names}" + + @pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True) + @pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True) + def test_find_symbol(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + symbols = language_server.request_full_symbol_tree() + assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add not found in symbol tree" + + @pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True) + @pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True) + def test_hover_reports_function_signature(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + # `add` is defined in util.ts line 0 at char len("export function ") == 16. + hover = language_server.request_hover(str(repo_path / "util.ts"), 0, len("export function ")) + + text = _hover_text(hover) + assert "add" in text, f"Hover should name the function, got: {text}" + assert "(a: number, b: number): number" in text, f"Hover should carry the signature, got: {text}" + + @pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True) + @pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True) + def test_hover_resolves_deno_global(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + """The `Deno.*` namespace is what this server adds over the plain TypeScript one, so it must resolve.""" + main = repo_path / "main.ts" + lines = main.read_text(encoding="utf-8").splitlines() + line = next(i for i, content in enumerate(lines) if "Deno.cwd()" in content) + + hover = language_server.request_hover(str(main), line, lines[line].index("Deno.cwd") + len("Deno.")) + + text = _hover_text(hover) + assert "cwd" in text, f"Hover should resolve Deno.cwd, got: {text}" + assert "string" in text, f"Hover should carry Deno.cwd's return type, got: {text}" From 52c3165d1ddd54f1a5fd71fcaa711a4a8754676a Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 8 Aug 2026 13:05:00 +0200 Subject: [PATCH 05/27] Add more concrete instructions on test strategy in system prompt --- .serena/project.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.serena/project.yml b/.serena/project.yml index 0e57ea7b..44f5f231 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -85,7 +85,11 @@ initial_prompt: | For simple data storage, you use dataclasses instead of dictionaries or tuples. Testing: - You test externally observable behavior and guarantees, never implementation structure. + The key principle is to test *only* externally observable behavior and guarantees, never implementation structure. + * Litmus test: a behaviour-preserving refactoring must not break any test. A test that could break is wrong and must not be written. + * When functionality is removed, you delete its tests. You never add tests asserting the *absence* of something; + absence of an implementation detail is not a behaviour, and such tests only freeze the current implementation and burden maintenance. + * Fewer, behaviour-anchored tests are preferred; a missing test is better than an implementation-coupled one. Docstrings & Comments: * You consistently use reStructuredText. From 07d49be11082a0aac1b53ea4e3e22eaf99c8f027 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 8 Aug 2026 13:35:19 +0200 Subject: [PATCH 06/27] Clean up memories and move system prompt info to critical_info memory --- .serena/memories/conventions.md | 32 ----- .serena/memories/critical_info.md | 48 ++++++++ .serena/memories/memory_maintenance.md | 2 +- .../{core.md => project_structure.md} | 6 +- .../memories/serena_repository_structure.md | 113 ------------------ .serena/memories/suggested_commands.md | 24 ---- .serena/memories/task_completion.md | 2 +- .serena/memories/tech_stack.md | 10 -- .serena/project.yml | 33 +---- 9 files changed, 53 insertions(+), 217 deletions(-) delete mode 100644 .serena/memories/conventions.md create mode 100644 .serena/memories/critical_info.md rename .serena/memories/{core.md => project_structure.md} (82%) delete mode 100644 .serena/memories/serena_repository_structure.md delete mode 100644 .serena/memories/suggested_commands.md delete mode 100644 .serena/memories/tech_stack.md diff --git a/.serena/memories/conventions.md b/.serena/memories/conventions.md deleted file mode 100644 index c40da347..00000000 --- a/.serena/memories/conventions.md +++ /dev/null @@ -1,32 +0,0 @@ -# Conventions - -## Style (project-instructed) -- Idiomatic, object-oriented design. Non-trivial interfaces use **explicitly typed abstractions** (strategy pattern etc.) rather than bare functions/callbacks. -- Avoid low-level data structures where an OO abstraction fits. For simple data containers use **dataclasses**, not dicts/tuples. -- Structure function bodies into **functional blocks separated by blank lines**, each prefixed with a short elliptical phrase (lowercase, no leading capital) describing the block's purpose. -- **Docstrings: reStructuredText.** Param/return/raises use `:param x:`, `:return:`, `:raises X:`. -- Parameter / method / class descriptions begin with a precise elliptical phrase defining *what* the thing is; details in subsequent sentences. - -## Formatting / lint (ruff) -- Line length 140, double quotes, target `py311`. -- Many "annoying" rules are disabled — see `[tool.ruff.lint] ignore` in `pyproject.toml` before adding workarounds (e.g. `Optional[T]` is preferred over `T | None`, `Union` is allowed, relative imports forbidden, `% string formatting` allowed). -- `ruff format` runs on `src scripts test`; same set for `ruff check`. -- mccabe complexity cap: 20. - -## Typing (ty) -- Type checker is **ty** (Astral), configured under `[tool.ty]` in `pyproject.toml`. - -## Tests -- Language-server tests are pytest-marker-gated (one marker per language; see `pyproject.toml` `[tool.pytest.ini_options].markers`). Default `poe test` runs unmarked tests + whatever `PYTEST_MARKERS` selects. -- Snapshot tests use **syrupy** with custom `--snapshot-patch-pycharm-diff` plugin (auto-added via `addopts`). - -## Tool descriptions (LLM-facing) -- To change how a tool is described to the model, edit the Tool class's `apply()` **docstring** (in - `src/serena/tools/*.py`) — `make_mcp_tool` parses the docstring body + `:param:` lines into the MCP tool - description/schema. Do NOT use `tool_description_overrides` in context ymls (e.g. `claude-code.yml`); the - docstring is the single source of truth and overrides drift. Keep `:param:` lines accurate. - -## Memories -- Follow `mem:memory_maintenance` for any new/updated memory in `.serena/memories/`. -- Durable knowledge goes in memories/docs (not the assistant's auto-memory); see the monorepo top-level - `CLAUDE.md` for the cross-repo conventions (no benchmark-tuning of agent-facing strings, etc.). diff --git a/.serena/memories/critical_info.md b/.serena/memories/critical_info.md new file mode 100644 index 00000000..d7e9023e --- /dev/null +++ b/.serena/memories/critical_info.md @@ -0,0 +1,48 @@ +# Software Design + +IMPORTANT: You use an idiomatic, object-oriented style (Java-esque principles, Pythonic syntax and constructs). + +* You keep each concern in exactly one home. + A mechanism whose parts are only correct in combination is implemented as a single component/class, and its parts are private to it; + expose a minimal public surface. Java-like encapsulation: If helper functions and constants are only used by one abstraction, + make them internal to the respective class. +* You enforce invariants through structure and visibility; interfaces should not permit states the design forbids. +* For any non-trivial interfaces, you use interfaces that expect explicitly typed abstractions + rather than mere functions (i.e. use the strategy pattern, for example). + You avoid the use of low-level data structures in all cases where an object-oriented abstraction would be more appropriate. + For simple data storage, you use dataclasses instead of dictionaries or tuples. + +# Testing + +The key principle is to test *only* externally observable behavior and guarantees, never implementation structure. +* Litmus test: a behaviour-preserving refactoring must not break any test. A test that could break is wrong and must not be written. +* When functionality is removed, you delete its tests. You never add tests asserting the *absence* of something; + absence of an implementation detail is not a behaviour, and such tests only freeze the current implementation and burden maintenance. +* Fewer, behaviour-anchored tests are preferred; a missing test is better than an implementation-coupled one. + +Language-server tests are pytest-marker-gated (one marker per language; see `pyproject.toml` `[tool.pytest.ini_options].markers`). Default `poe test` runs unmarked tests + whatever `PYTEST_MARKERS` selects. +Snapshot tests use syrupy. + +# Docstrings & Comments + +* You consistently use reStructuredText. +* You structure function implementations into functional blocks that are separated by blank lines. + Atop each functional block, you write an elliptical phrase (starting with lower-case letter) that describes the purpose of the + block in a concise manner. +* When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase + clearly defines *what* it is. Any details then follow in subsequent sentences. +* Each piece of information appears exactly once, at the element that owns it: callers do not + explain callees' internals, and callees do not describe their callers. + +# Pull requests + +Read `mem:creating_pull_requests` when asked to participate in the creation of a pull request. + +# Memories + +- Follow `mem:memory_maintenance` for any new/updated memory in `.serena/memories/`. +- Durable project knowledge goes into memories or docs/ + +# Dev Tools + +Read `mem:task_completion` for tools to call upon task completion. diff --git a/.serena/memories/memory_maintenance.md b/.serena/memories/memory_maintenance.md index 6f84514d..ca2df4c5 100644 --- a/.serena/memories/memory_maintenance.md +++ b/.serena/memories/memory_maintenance.md @@ -4,7 +4,7 @@ - Core principle: progressive discovery through references, building a graph of memories. - Initially, agents are provided with the list of all memories (names only). -- Agents should read `mem:core` as the top-level entry point (graph root). +- Agents should read `mem:critical_info` as the top-level entry point (graph root). This memory should contain references to other memories covering major project domains. The referenced memories shall, in turn, shall contain references to even more specific memories, and so on. The depth of the graph shall depend on the project complexity. diff --git a/.serena/memories/core.md b/.serena/memories/project_structure.md similarity index 82% rename from .serena/memories/core.md rename to .serena/memories/project_structure.md index d4a76ff1..f4654181 100644 --- a/.serena/memories/core.md +++ b/.serena/memories/project_structure.md @@ -18,12 +18,10 @@ Serena is an MCP-based "IDE for coding agents": semantic code retrieval/editing/ - `test/serena/`, `test/solidlsp//` — pytest suites; per-language tests gated by pytest markers - `test/resources/repos//` — fixture projects used by language-server tests - `scripts/` — utilities (prompt regen, tool overview, profiling, agno agent) -- `docs/` — Jupyter Book sources; build via poe `doc-build` +- `docs/` — Jupyter Book sources; build via `poe doc-build` ## Project-wide invariants -- Package name (PyPI): `serena-agent`; import root: `serena`. Wheel includes `serena`, `interprompt`, `solidlsp`. +- Package name (PyPI): `serena-agent`; Wheel includes `serena`, `interprompt`, `solidlsp`. - Python: `>=3.11, <3.15`. Dependencies are exact-pinned in `pyproject.toml` (uvx installs from git, lockfile ignored — pin exactly). - Entry points: `serena` → `serena.cli:top_level`; `serena-hooks` → `serena.hooks:hook_commands`. -- Per-project state lives under `/.serena/` (config + `memories/` as `.md` files). -- See `mem:tech_stack`, `mem:suggested_commands`, `mem:conventions`, `mem:task_completion`. diff --git a/.serena/memories/serena_repository_structure.md b/.serena/memories/serena_repository_structure.md deleted file mode 100644 index 5fc70a97..00000000 --- a/.serena/memories/serena_repository_structure.md +++ /dev/null @@ -1,113 +0,0 @@ -# Serena Repository Structure - -## Overview -Serena is a multi-language code assistant that combines two main components: -1. **Serena Core** - The main agent framework with tools and MCP server -2. **SolidLSP** - A unified Language Server Protocol wrapper for multiple programming languages - -## Top-Level Structure - -``` -serena/ -├── src/ # Main source code -│ ├── serena/ # Serena agent framework -│ ├── solidlsp/ # LSP wrapper library -│ └── interprompt/ # Multi-language prompt templates -├── test/ # Test suites -│ ├── serena/ # Serena agent tests -│ ├── solidlsp/ # Language server tests -│ └── resources/repos/ # Test repositories for each language -├── scripts/ # Build and utility scripts -├── resources/ # Static resources and configurations -├── pyproject.toml # Python project configuration -├── README.md # Project documentation -└── CHANGELOG.md # Version history -``` - -## Source Code Organization - -### Serena Core (`src/serena/`) -- **`agent.py`** - Main SerenaAgent class that orchestrates everything -- **`tools/`** - MCP tools for file operations, symbols, memory, etc. - - `file_tools.py` - File system operations (read, write, search) - - `symbol_tools.py` - Symbol-based code operations (find, edit) - - `memory_tools.py` - Knowledge persistence and retrieval - - `config_tools.py` - Project and mode management - - `workflow_tools.py` - Onboarding and meta-operations -- **`config/`** - Configuration management - - `serena_config.py` - Main configuration classes - - `context_mode.py` - Context and mode definitions -- **`util/`** - Utility modules -- **`mcp.py`** - MCP server implementation -- **`cli.py`** - Command-line interface - -### SolidLSP (`src/solidlsp/`) -- **`ls.py`** - Main SolidLanguageServer class -- **`language_servers/`** - Language-specific implementations - - `csharp_language_server.py` - C# (Microsoft.CodeAnalysis.LanguageServer) - - `python_server.py` - Python (Pyright) - - `typescript_language_server.py` - TypeScript - - `rust_analyzer.py` - Rust - - `gopls.py` - Go - - And many more... -- **`ls_config.py`** - Language server configuration -- **`ls_types.py`** - LSP type definitions -- **`ls_utils.py`** - Utilities for working with LSP data - -### Interprompt (`src/interprompt/`) -- Multi-language prompt template system -- Jinja2-based templating with language fallbacks - -## Test Structure - -### Language Server Tests (`test/solidlsp/`) -Each language has its own test directory: -``` -test/solidlsp/ -├── csharp/ -│ └── test_csharp_basic.py -├── python/ -│ └── test_python_basic.py -├── typescript/ -│ └── test_typescript_basic.py -└── ... -``` - -### Test Resources (`test/resources/repos/`) -Contains minimal test projects for each language: -``` -test/resources/repos/ -├── csharp/test_repo/ -│ ├── serena.sln -│ ├── TestProject.csproj -│ ├── Program.cs -│ └── Models/Person.cs -├── python/test_repo/ -├── typescript/test_repo/ -└── ... -``` - -### Test Infrastructure -- **`test/conftest.py`** - Shared test fixtures and utilities -- **`create_ls()`** function - Creates language server instances for testing -- **`language_server` fixture** - Parametrized fixture for multi-language tests - -## Key Configuration Files - -- **`pyproject.toml`** - Python dependencies, build config, and tool settings -- **`.serena/`** directories - Project-specific Serena configuration and memories -- **`CLAUDE.md`** - Instructions for AI assistants working on the project - -## Dependencies Management - -The project uses modern Python tooling: -- **uv** for fast dependency resolution and virtual environments -- **pytest** for testing with language-specific markers (`@pytest.mark.csharp`) -- **ruff** for linting and formatting -- **ty** (Astral) for type checking - -## Build and Development - -- **Docker support** - Full containerized development environment -- **GitHub Actions** - CI/CD with language server testing -- **Development scripts** in `scripts/` directory \ No newline at end of file diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md deleted file mode 100644 index e4e44816..00000000 --- a/.serena/memories/suggested_commands.md +++ /dev/null @@ -1,24 +0,0 @@ -# Suggested Commands - -Run via `uv run poe ` (or `poe ` inside the activated venv). Poe executor is `simple`, so plain `poe` works without uv re-resolving. - -## Dev loop -- `poe test` — pytest on `test/` (per-language tests are marker-gated; pass `-m ` to enable). -- `poe lint` — ruff format-check + ruff check (no fixes). -- `poe format` — ruff `--fix` then `ruff format` (mutates files). -- `poe type-check` — ty on `src/serena`, `src/solidlsp`, and `test/` (test pass relaxes pytest/mock-noisy rules via `[[tool.ty.overrides]]`). -- Single test file: `uv run pytest test/path/to/test_x.py -vv`. Language-gated: add `-m python` etc. - -## Docs -- `poe doc-build` — clean + autogen + sphinx (uses `rm -rf`; needs a unix-like shell, e.g. Git Bash on Windows). - -## Entrypoints -- `uv run serena ...` — main CLI (`serena.cli:top_level`). -- `uv run serena-hooks ...` — hook helpers. -- `python scripts/gen_prompt_factory.py` — regenerate `src/serena/generated/generated_prompt_factory.py` after editing prompt templates. - -## Windows shell notes (PowerShell 7+ is the project shell) -- Use `Remove-Item -Recurse -Force ` instead of `rm -rf` (the `doc-clean` poe task uses unix `rm -rf` and requires bash). -- Env vars: `$env:NAME = 'value'` (not `export`). -- Path separator in `pyproject.toml` poe tasks uses forward slashes; PowerShell accepts them in arguments. -- `git`, `uv`, `poe`, `pytest` behave identically to unix. diff --git a/.serena/memories/task_completion.md b/.serena/memories/task_completion.md index 8d6948a9..a7addd2f 100644 --- a/.serena/memories/task_completion.md +++ b/.serena/memories/task_completion.md @@ -8,4 +8,4 @@ After any code change in `src/` or `test/`, run: If prompt templates changed: `uv run python scripts/gen_prompt_factory.py` (regenerates `src/serena/generated/generated_prompt_factory.py`; use `uv run poe format` and commit the result). -If memories were edited/renamed/split: run `uv run serena memories check` from the project root to find broken `mem:` references. +If memories were edited/renamed/split: run `uv run --no-sync serena memories check` from the project root to find broken `mem:` references. diff --git a/.serena/memories/tech_stack.md b/.serena/memories/tech_stack.md deleted file mode 100644 index ca8a3a6e..00000000 --- a/.serena/memories/tech_stack.md +++ /dev/null @@ -1,10 +0,0 @@ -# Tech Stack - -- Language: Python 3.11–3.14 (`requires-python = ">=3.11, <3.15"`). -- Package/dep manager: **uv** (uv.lock present). Exact version pins in `pyproject.toml` because `uvx` installs from git and ignores the lockfile. -- Build backend: `hatchling`. Packages: `src/serena`, `src/interprompt`, `src/solidlsp`. -- Task runner: **poethepoet** (`poe `). Poe executor is `simple` (does NOT shell out via uv) — avoids env recreation while MCP server is running. -- Key runtime deps: `mcp`, `flask` (dashboard), `pydantic`, `pygls` + `lsprotocol` (LSP), `anthropic`, `jinja2`, `ruamel.yaml`, `pywebview`/`pystray` (GUI), `pythonnet` (Windows only). -- Dev deps: `ruff` (lint+format), `ty` (Astral type checker; replaced mypy), `pytest` (+ `pytest-xdist`, `pytest-timeout`, `syrupy` snapshots), `sphinx`/`jupyter-book` for docs. -- Optional extras: `agno` (Agno agent integration), `google` (gemini). -- LSP client core lives under `src/solidlsp/`; one subdir per supported language server under `language_servers/`. diff --git a/.serena/project.yml b/.serena/project.yml index 44f5f231..7e19e987 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -71,38 +71,7 @@ included_optional_tools: [] # initial prompt for the project. It will always be given to the LLM upon activating the project # (contrary to the memories, which are loaded on demand). initial_prompt: | - IMPORTANT: You use an idiomatic, object-oriented style (Java-esque principles, Pythonic syntax and constructs). - - Design: - * You keep each concern in exactly one home. - A mechanism whose parts are only correct in combination is implemented as a single component/class, and its parts are private to it; - expose a minimal public surface. Java-like encapsulation: If helper functions and constants are only used by one abstraction, - make them internal to the respective class. - * You enforce invariants through structure and visibility; interfaces should not permit states the design forbids. - * For any non-trivial interfaces, you use interfaces that expect explicitly typed abstractions - rather than mere functions (i.e. use the strategy pattern, for example). - You avoid the use of low-level data structures in all cases where an object-oriented abstraction would be more appropriate. - For simple data storage, you use dataclasses instead of dictionaries or tuples. - - Testing: - The key principle is to test *only* externally observable behavior and guarantees, never implementation structure. - * Litmus test: a behaviour-preserving refactoring must not break any test. A test that could break is wrong and must not be written. - * When functionality is removed, you delete its tests. You never add tests asserting the *absence* of something; - absence of an implementation detail is not a behaviour, and such tests only freeze the current implementation and burden maintenance. - * Fewer, behaviour-anchored tests are preferred; a missing test is better than an implementation-coupled one. - - Docstrings & Comments: - * You consistently use reStructuredText. - * You structure function implementations into functional blocks that are separated by blank lines. - Atop each functional block, you write an elliptical phrase (starting with lower-case letter) that describes the purpose of the - block in a concise manner. - * When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase - clearly defines *what* it is. Any details then follow in subsequent sentences. - * Each piece of information appears exactly once, at the element that owns it: callers do not - explain callees' internals, and callees do not describe their callers. - - Pull requests: - Read `mem:creating_pull_requests` when asked to participate in the creation of a pull request. + ALWAYS read the `critical_info` memory before doing anything else. It contains absolutely critical information about the project. # the encoding used by text files in the project # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings From 5a06cec7d08aeb7f669e20ee76bfbe31fbfde4cd Mon Sep 17 00:00:00 2001 From: yhheolab-oss Date: Sat, 8 Aug 2026 23:15:30 +0900 Subject: [PATCH 07/27] Merge pull request #1820 from yhheolab-oss/fix/project-server-active-project-race fix(project_server): serialize access to the process-wide active project --- CHANGELOG.md | 2 +- src/serena/project_server.py | 44 +++++-- test/serena/test_project_server.py | 199 +++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 test/serena/test_project_server.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 256dd973..36d20301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. * General: - - Fix: the README, the Language Support docs page and the project template omitted several already-supported language servers + - Fix: Race conditions in ProjectServer when used by multiple clients in parallel - Fix: a tool call exceeding the timeout blocked the task executor indefinitely; the executor now recovers without user-induced cancellation - Add Grok Build support (context `grok`, setup CLI, hooks) diff --git a/src/serena/project_server.py b/src/serena/project_server.py index 12c5e907..c2cbcd65 100644 --- a/src/serena/project_server.py +++ b/src/serena/project_server.py @@ -1,5 +1,6 @@ import json import logging +import threading from typing import TYPE_CHECKING import requests as requests_lib @@ -61,6 +62,9 @@ class ProjectServer: self._agent = SerenaAgent(serena_config=serena_config) self._loaded_projects_by_root: dict[str, "Project"] = {} + self._project_load_locks_by_root: dict[str, threading.Lock] = {} + self._active_project_lock = threading.Lock() + self._loaded_projects_lock = threading.Lock() self._port = port self._host = host @@ -91,19 +95,41 @@ class ProjectServer: key = str(registered_project.project_root) - if key in self._loaded_projects_by_root: - return self._loaded_projects_by_root[key] + # find or publish the per-project load lock while holding the shared dictionaries + with self._loaded_projects_lock: + project = self._loaded_projects_by_root.get(key) + if project is not None: + return project + project_load_lock = self._project_load_locks_by_root.get(key) + if project_load_lock is None: + project_load_lock = threading.Lock() + self._project_load_locks_by_root[key] = project_load_lock - with LogTime(f"Loading project '{project_root_or_name}'"): - project = registered_project.get_project_instance(serena_config) - project.create_language_server_manager() - self._loaded_projects_by_root[key] = project - return project + # initialize only this project; another project's cached lookup or cold load can proceed + with project_load_lock: + with self._loaded_projects_lock: + project = self._loaded_projects_by_root.get(key) + if project is not None: + return project + + with LogTime(f"Loading project '{project_root_or_name}'"): + project = registered_project.get_project_instance(serena_config) + project.create_language_server_manager() + + with self._loaded_projects_lock: + self._loaded_projects_by_root[key] = project + return project def _query_project(self, req: QueryProjectRequest) -> str: - """Handle a /query_project request by invoking the agent on the specified project and tool.""" + """Handle a /query_project request by invoking the agent on the specified project and tool. + + The active project is process-wide state, whereas ``apply_ex`` runs the tool on the + agent's task executor thread. Without the lock, a second request entering + ``active_project_context`` while the first request's tool is still executing would + redirect that tool to the wrong project (and restore the wrong project afterwards). + """ project = self._get_project(req.project_name) - with self._agent.active_project_context(project): + with self._active_project_lock, self._agent.active_project_context(project): tool = self._agent.get_tool_by_name(req.tool_name) if not tool.is_readonly(): raise ValueError(f"Tool '{req.tool_name}' is not read-only and cannot be executed via the query_project route") diff --git a/test/serena/test_project_server.py b/test/serena/test_project_server.py new file mode 100644 index 00000000..1288e177 --- /dev/null +++ b/test/serena/test_project_server.py @@ -0,0 +1,199 @@ +import threading +from collections.abc import Iterator +from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import TimeoutError as FutureTimeoutError +from contextlib import contextmanager +from pathlib import Path +from typing import Any, cast +from unittest.mock import MagicMock + +import pytest + +from serena.project_server import ProjectServer, QueryProjectRequest + + +@pytest.fixture +def project_server() -> ProjectServer: + server = ProjectServer.__new__(ProjectServer) + server._agent = MagicMock() + server._loaded_projects_by_root = {} + server._project_load_locks_by_root = {} + server._active_project_lock = threading.Lock() + server._loaded_projects_lock = threading.Lock() + return server + + +def test_cached_project_lookup_is_not_blocked_by_unrelated_cold_load(project_server: ProjectServer) -> None: + cached_root = Path("/cached") + cold_root = Path("/cold") + cached_project = MagicMock() + cold_project = MagicMock() + cold_load_started = threading.Event() + allow_cold_load_to_finish = threading.Event() + + cached_registration = MagicMock(project_root=cached_root) + cold_registration = MagicMock(project_root=cold_root) + + def block_cold_load() -> None: + cold_load_started.set() + assert allow_cold_load_to_finish.wait(timeout=5) + + cold_registration.get_project_instance.return_value = cold_project + cold_project.create_language_server_manager.side_effect = block_cold_load + project_server._loaded_projects_by_root[str(cached_root)] = cached_project + serena_config = cast(Any, project_server._agent.serena_config) + serena_config.get_registered_project.side_effect = { + "cached": cached_registration, + "cold": cold_registration, + }.get + + with ThreadPoolExecutor(max_workers=2) as executor: + cold_future = executor.submit(project_server._get_project, "cold") + assert cold_load_started.wait(timeout=1) + + cached_future = executor.submit(project_server._get_project, "cached") + try: + assert cached_future.result(timeout=1) is cached_project + finally: + allow_cold_load_to_finish.set() + + assert cold_future.result(timeout=1) is cold_project + + +def test_cold_loads_for_different_projects_can_run_concurrently(project_server: ProjectServer) -> None: + first_root = Path("/first") + second_root = Path("/second") + first_project = MagicMock() + second_project = MagicMock() + first_load_started = threading.Event() + allow_first_load_to_finish = threading.Event() + + first_registration = MagicMock(project_root=first_root) + second_registration = MagicMock(project_root=second_root) + + def block_first_load() -> None: + first_load_started.set() + assert allow_first_load_to_finish.wait(timeout=5) + + first_registration.get_project_instance.return_value = first_project + first_project.create_language_server_manager.side_effect = block_first_load + second_registration.get_project_instance.return_value = second_project + serena_config = cast(Any, project_server._agent.serena_config) + serena_config.get_registered_project.side_effect = { + "first": first_registration, + "second": second_registration, + }.get + + with ThreadPoolExecutor(max_workers=2) as executor: + first_future = executor.submit(project_server._get_project, "first") + assert first_load_started.wait(timeout=1) + + second_future = executor.submit(project_server._get_project, "second") + try: + assert second_future.result(timeout=1) is second_project + finally: + allow_first_load_to_finish.set() + + assert first_future.result(timeout=1) is first_project + + +def test_concurrent_lookups_load_each_project_only_once(project_server: ProjectServer) -> None: + project_root = Path("/project") + project = MagicMock() + load_started = threading.Event() + second_lookup_started = threading.Event() + allow_load_to_finish = threading.Event() + registration = MagicMock(project_root=project_root) + lookup_count = 0 + lookup_count_lock = threading.Lock() + + def block_load() -> None: + load_started.set() + assert allow_load_to_finish.wait(timeout=5) + + registration.get_project_instance.return_value = project + project.create_language_server_manager.side_effect = block_load + serena_config = cast(Any, project_server._agent.serena_config) + + def get_registration(project_name: str) -> MagicMock: + nonlocal lookup_count + assert project_name == "project" + with lookup_count_lock: + lookup_count += 1 + if lookup_count == 2: + second_lookup_started.set() + return registration + + serena_config.get_registered_project.side_effect = get_registration + + with ThreadPoolExecutor(max_workers=2) as executor: + first_future = executor.submit(project_server._get_project, "project") + assert load_started.wait(timeout=1) + second_future = executor.submit(project_server._get_project, "project") + assert second_lookup_started.wait(timeout=1) + allow_load_to_finish.set() + + assert first_future.result(timeout=1) is project + assert second_future.result(timeout=1) is project + + registration.get_project_instance.assert_called_once_with(serena_config) + project.create_language_server_manager.assert_called_once_with() + + +def test_concurrent_queries_serialize_active_project_context(project_server: ProjectServer) -> None: + first_query_started = threading.Event() + second_lookup_finished = threading.Event() + allow_first_query_to_finish = threading.Event() + state_lock = threading.Lock() + state: dict[str, str | None] = {"active_project": None} + + def get_project(project_name: str) -> str: + if project_name == "second": + second_lookup_finished.set() + return project_name + + @contextmanager + def active_project_context(project: str) -> Iterator[None]: + with state_lock: + assert state["active_project"] is None + state["active_project"] = project + try: + yield + finally: + with state_lock: + state["active_project"] = None + + def apply_tool(hold: bool = False) -> str: + if hold: + first_query_started.set() + assert allow_first_query_to_finish.wait(timeout=5) + with state_lock: + active_project = state["active_project"] + assert active_project is not None + return active_project + + tool = MagicMock() + tool.is_readonly.return_value = True + tool.apply_ex.side_effect = apply_tool + server = cast(Any, project_server) + server._get_project = MagicMock(side_effect=get_project) + agent = cast(Any, project_server._agent) + agent.active_project_context.side_effect = active_project_context + agent.get_tool_by_name.return_value = tool + first_request = QueryProjectRequest(project_name="first", tool_name="read_file", tool_params_json='{"hold": true}') + second_request = QueryProjectRequest(project_name="second", tool_name="read_file", tool_params_json="{}") + + with ThreadPoolExecutor(max_workers=2) as executor: + first_future = executor.submit(project_server._query_project, first_request) + assert first_query_started.wait(timeout=1) + second_future = executor.submit(project_server._query_project, second_request) + assert second_lookup_finished.wait(timeout=1) + + try: + with pytest.raises(FutureTimeoutError): + second_future.result(timeout=0.1) + finally: + allow_first_query_to_finish.set() + + assert first_future.result(timeout=1) == "first" + assert second_future.result(timeout=1) == "second" From 8364fc554868fbacb580ba1441d2a518478abd30 Mon Sep 17 00:00:00 2001 From: Tyce Herrman Date: Fri, 7 Aug 2026 14:18:41 -0400 Subject: [PATCH 08/27] fix(java): remove unused IntelliCode integration --- CHANGELOG.md | 2 + docs/02-usage/050_configuration.md | 22 ++-- .../language_servers/eclipse_jdtls.py | 116 ++---------------- .../downloaded_dependency_hashes.json | 3 +- .../java/test_jdtls_path_resolution.py | 4 +- 5 files changed, 22 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d20301..312d9de5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ Status of the `main` branch. Changes prior to the next official version change w - 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 + - Java/JDTLS: stop downloading and loading the unused IntelliCode completion-ranking bundle; the retired + `intellicode_version`, `intellicode_xmx` and `intellicode_xms` settings remain accepted but are ignored #1821 - Nix/nixd: support custom `ls_path` launchers and external JSON settings through `config_path` #1737 - Fix: Nix/nixd diagnostics now use published diagnostics instead of the unsupported `textDocument/diagnostic` request, which terminated nixd #1802 diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 65b5e6cc..84facd85 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -666,8 +666,7 @@ Supported settings: Java support has two installation modes: 1. **Default vscode-java VSIX mode** (no extra config required): Serena downloads the platform-specific - vscode-java VSIX (~500 MB: JDTLS + bundled JRE 21 + Lombok + IntelliCode), Gradle distribution and - IntelliCode VSIX from public hosts on first use. + vscode-java VSIX (JDTLS + bundled JRE 21 + Lombok) and a Gradle distribution from public hosts on first use. 2. **Upstream JDTLS mode** (offline-friendly): Activated by setting both `jdtls_path` and `lombok_path`. Uses an existing JDTLS installation (~100 MB) and the system JDK 21+. Nothing is downloaded. Recommended for restricted-network/corporate environments. @@ -677,9 +676,8 @@ Java support has two installation modes: - **Default vscode-java VSIX mode** — recommended for most users. No setup required; Serena downloads everything on first use. - **Upstream JDTLS mode** — recommended when: - - you cannot reach `github.com`, `services.gradle.org` or `marketplace.visualstudio.com` - from the host (corporate proxy, air-gapped network); - - you want a smaller on-disk footprint (~100 MB vs ~500 MB); + - you cannot reach `github.com` or `services.gradle.org` from the host (corporate proxy, air-gapped network); + - you want a smaller on-disk footprint; - you already maintain a JDTLS installation (e.g. for `nvim-jdtls` or another editor); - your security policy prohibits per-project runtime downloads. @@ -702,23 +700,19 @@ The following settings are supported for the Java language server: | `runtimes` | `[]` | Extra JRE/JDK entries registered with JDT-LS via `java.configuration.runtimes`. Use this when a project's source/target level exceeds the JDK JDT-LS itself runs on (currently JDK 21 in default vscode-java VSIX mode). Each entry is a mapping with required `name` (e.g. `JavaSE-25`, matching the `JavaSE-NN` container the build tool requests) and `path` (JDK/JRE home directory; must exist), plus optional `default`, `sources`, and `javadoc` (passed through to JDT-LS). Entries extend rather than replace the bundled `JavaSE-21` runtime; an entry that reuses the `JavaSE-21` name overrides the bundled one. Changing this setting invalidates the JDTLS workspace hash so a fresh import is performed. | | `gradle_version` | `8.14.2` | (vscode-java mode only) Override the Gradle distribution version Serena downloads by default. | | `vscode_java_version` | `1.54.0-923` | (vscode-java mode only) Override the bundled `vscode-java` runtime bundle version Serena downloads by default. | -| `intellicode_version` | `1.2.30` | (vscode-java mode only) Override the IntelliCode VSIX version Serena downloads by default. | | `lombok_show_generated` | `true` | Show Lombok-generated methods (`getX/setX`, `builder()`, `equals/hashCode/toString`, `withX`, fluent accessors) in `find_symbol`, `get_symbols_overview` and the symbol-edit tools. Set to `false` to restore the previous JDTLS default and hide the synthetic methods (e.g. when `@Data` classes pollute the outline with too many getters/setters). Requires JDTLS commit `b2d8952` / `vscode-java >= 1.53.0`; the bundled default already meets this. | | `jdtls_xmx` | `3G` | Maximum heap size for the JDTLS server JVM. | | `jdtls_xms` | `100m` | Initial heap size for the JDTLS server JVM. | -| `intellicode_xmx` | `1G` | (vscode-java mode only) Maximum heap size for the IntelliCode embedded JVM. | -| `intellicode_xms` | `100m` | (vscode-java mode only) Initial heap size for the IntelliCode embedded JVM. | Notes: - When overriding `vscode_java_version`, Serena still assumes that the downloaded runtime bundle keeps the same internal directory layout and file names as the bundled default version. -- In upstream-jdtls mode, IntelliCode is not loaded (it's an ML completions ranker that is irrelevant to Serena's - symbol-tools workflow), and Serena does not ship a Gradle distribution. Maven projects work via JDTLS's bundled m2e. +- Serena does not download or load IntelliCode because its completion ranking is not used by Serena's tools. The retired + `intellicode_version`, `intellicode_xmx` and `intellicode_xms` keys remain accepted and ignored so existing + configurations continue to load; they can be removed. +- In upstream-jdtls mode Serena does not ship a Gradle distribution. Maven projects work via JDTLS's bundled m2e. Gradle projects must have `./gradlew` in the project, or rely on a system-installed Gradle through Buildship's - default discovery rules. -- In upstream-jdtls mode the `gradle_version`, `vscode_java_version`, `intellicode_version`, - `intellicode_xmx`, `intellicode_xms` settings are silently ignored — they only apply to the - vscode-java VSIX mode. + default discovery rules. The `gradle_version` and `vscode_java_version` settings are silently ignored in this mode. - Without `runtimes`, JDT-LS only knows about the bundled `JavaSE-21` JRE. Projects that request a newer container (e.g. `sourceCompatibility = JavaVersion.VERSION_25`) then fail to resolve JDK types such as `java.lang.Object`. Register the matching installed JDK via `runtimes` instead of symlinking over Serena's diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index 94531a9a..baa45de9 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -35,11 +35,6 @@ log = logging.getLogger(__name__) GRADLE_ALLOWED_HOSTS = ("services.gradle.org", "github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") VSCODE_JAVA_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") -INTELLICODE_ALLOWED_HOSTS = ( - "visualstudioexptteam.gallery.vsassets.io", - "marketplace.visualstudio.com", - "download.visualstudio.microsoft.com", -) @dataclasses.dataclass(frozen=True) @@ -77,10 +72,9 @@ class RuntimeDependencyPaths: """ Stores the paths to the runtime dependencies of EclipseJDTLS. - In the default mode (vscode-java VSIX), all paths are populated. - In the upstream-jdtls mode (when ``jdtls_path`` and ``lombok_path`` are set), - fields that have no upstream equivalent (gradle distribution, IntelliCode bundle) - are set to None. + In the default mode (vscode-java VSIX), all paths are populated. In the + upstream-jdtls mode (when ``jdtls_path`` and ``lombok_path`` are set), the + Gradle distribution path is ``None`` because Serena does not download it. """ jre_path: str @@ -89,8 +83,6 @@ class RuntimeDependencyPaths: jdtls_readonly_config_path: str lombok_jar_path: str gradle_path: str | None = None - intellicode_jar_path: str | None = None - intellisense_members_path: str | None = None @dataclass @@ -113,8 +105,8 @@ class EclipseJDTLS(SolidLanguageServer): Two installation modes are supported: 1. **Default vscode-java VSIX mode** (no extra config required) — Serena downloads the platform-specific - vscode-java VSIX bundle (~500 MB: JDTLS + bundled JRE 21 + Lombok + IntelliCode), Gradle distribution - and IntelliCode VSIX from public hosts. Suitable when public network access is available. + vscode-java VSIX bundle (JDTLS + bundled JRE 21 + Lombok) and Gradle distribution from public hosts. + Suitable when public network access is available. 2. **Upstream JDTLS mode** (activated by setting both ``jdtls_path`` and ``lombok_path``) — uses an existing JDTLS installation and the system JDK. Nothing is downloaded. Suitable for restricted-network @@ -143,8 +135,6 @@ class EclipseJDTLS(SolidLanguageServer): gradle_java_home is unset, Gradle import (default: false) - jdtls_xmx: Maximum heap size for the JDTLS server JVM (default: "3G") - jdtls_xms: Initial heap size for the JDTLS server JVM (default: "100m") - - intellicode_xmx: Maximum heap size for the IntelliCode embedded JVM (default: "1G") - - intellicode_xms: Initial heap size for the IntelliCode embedded JVM (default: "100m") - lombok_show_generated: Show Lombok-generated methods (getX/setX/builder()/...) in document symbols by sending java.symbols.includeGeneratedCode=true to JDTLS (default: true). Set to false for @Data-heavy projects where the extra getters/setters are noise. @@ -155,7 +145,6 @@ class EclipseJDTLS(SolidLanguageServer): Pinned versions: "1.54.0-923" (default) and "1.42.0-561" (legacy / initial). Other versions are not supported in default VSIX mode (the resource paths inside the archive change between releases); use upstream-jdtls mode for arbitrary versions. - - intellicode_version: Override the pinned IntelliCode VSIX version downloaded by Serena - runtimes: Additional JRE/JDK entries to register with JDT-LS's ``java.configuration.runtimes``, for projects whose source/target level exceeds the JDK JDT-LS itself runs on (currently JDK 21 in default vscode-java VSIX mode). Each entry is a mapping with: @@ -190,12 +179,9 @@ class EclipseJDTLS(SolidLanguageServer): use_system_java_home: true # set to true to use system JAVA_HOME for JDTLS and Gradle fallback jdtls_xmx: "3G" # maximum heap size for the JDTLS server JVM jdtls_xms: "100m" # initial heap size for the JDTLS server JVM - intellicode_xmx: "1G" # maximum heap size for the IntelliCode embedded JVM - intellicode_xms: "100m" # initial heap size for the IntelliCode embedded JVM lombok_show_generated: true # show Lombok-generated methods in document symbols (default true) gradle_version: "8.14.2" vscode_java_version: "1.54.0-923" # also accepts pinned legacy "1.42.0-561" - intellicode_version: "1.2.30" runtimes: # register additional JDKs for projects targeting a newer Java version - name: "JavaSE-25" path: "/home/user/Java/jdk25" @@ -220,7 +206,6 @@ class EclipseJDTLS(SolidLanguageServer): self._service_ready_event = threading.Event() self._project_ready_event = threading.Event() - self._intellicode_enable_command_available = threading.Event() def _create_dependency_provider(self) -> LanguageServerDependencyProvider: ls_resources_dir = self.ls_resources_dir(self._solidlsp_settings) @@ -258,7 +243,6 @@ class EclipseJDTLS(SolidLanguageServer): # versions used initially (which use default paths without version suffix) INITIAL_VSCODE_JAVA_VERSION = "1.42.0-561" - INITIAL_INTELLICODE_VERSION = "1.2.30" INITIAL_VSCODE_JAVA_PATHS = VsixResourcePaths( jre_version="21.0.7", lombok_jar_basename="lombok-1.18.36.jar", @@ -270,7 +254,6 @@ class EclipseJDTLS(SolidLanguageServer): # scripts/update_downloaded_dependency_hashes.py and commit the resulting changes. DEFAULT_GRADLE_VERSION = "8.14.2" DEFAULT_VSCODE_JAVA_VERSION = "1.54.0-923" - DEFAULT_INTELLICODE_VERSION = "1.2.30" DEFAULT_VSCODE_JAVA_PATHS = VsixResourcePaths( jre_version="21.0.10", lombok_jar_basename="lombok-1.18.39-4050.jar", @@ -320,19 +303,9 @@ class EclipseJDTLS(SolidLanguageServer): ) return deps - @classmethod - def _create_dep_intellicode(cls, intellicode_version: str | None = None) -> DownloadedDependency: - intellicode_version = intellicode_version or cls.DEFAULT_INTELLICODE_VERSION - return DownloadedDependency( - url=f"https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/{intellicode_version}/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage", - archive_type="zip", - allowed_hosts=INTELLICODE_ALLOWED_HOSTS, - verified=intellicode_version == cls.DEFAULT_INTELLICODE_VERSION, - ) - @classmethod def update_dep_hashes(cls) -> None: - deps = [cls._create_dep_gradle(), cls._create_dep_intellicode()] + list(cls._create_deps_vscode_java().values()) + deps = [cls._create_dep_gradle()] + list(cls._create_deps_vscode_java().values()) with DownloadedDependencyHashDatabase.get_instance().update_context() as db: for dep in deps: db.update(dep) @@ -351,8 +324,8 @@ class EclipseJDTLS(SolidLanguageServer): ``brew install jdtls`` or extracted ``jdt-language-server-*.tar.gz``) and the system JDK. Nothing is downloaded by Serena. Suitable for restricted-network/corporate environments. * **Default vscode-java VSIX mode** (activated otherwise): downloads the platform-specific - vscode-java VSIX bundle (containing JDTLS, bundled JRE 21, Lombok, IntelliCode), Gradle - distribution and IntelliCode VSIX from public hosts. Original behaviour, unchanged. + vscode-java VSIX bundle (containing JDTLS, bundled JRE 21 and Lombok) and a Gradle + distribution from public hosts. """ jdtls_path = custom_settings.get("jdtls_path") lombok_path = custom_settings.get("lombok_path") @@ -369,16 +342,12 @@ class EclipseJDTLS(SolidLanguageServer): platformId = PlatformUtils.get_platform_id() gradle_version = custom_settings.get("gradle_version", cls.DEFAULT_GRADLE_VERSION) vscode_java_version = custom_settings.get("vscode_java_version", cls.DEFAULT_VSCODE_JAVA_VERSION) - intellicode_version = custom_settings.get("intellicode_version", cls.DEFAULT_INTELLICODE_VERSION) # install-dir name per the version-pinning convention (see module-level block): # INITIAL -> legacy unversioned dir; everything else -> "{name}-{resolved}" subdir vscode_java_dirname = ( "vscode-java" if vscode_java_version == cls.INITIAL_VSCODE_JAVA_VERSION else f"vscode-java-{vscode_java_version}" ) - intellicode_dirname = ( - "intellicode" if intellicode_version == cls.INITIAL_INTELLICODE_VERSION else f"intellicode-{intellicode_version}" - ) # Resolve internal VSIX paths (JRE / Lombok / launcher filenames). For pinned versions # these are known; for any other user-supplied version we bail out — guessing would @@ -478,26 +447,6 @@ class EclipseJDTLS(SolidLanguageServer): assert os.path.exists(jdtls_launcher_jar_path) assert os.path.exists(jdtls_readonly_config_path) - intellicode_dep = cls._create_dep_intellicode(intellicode_version) - intellicode_directory_path = str(PurePath(ls_resources_dir, intellicode_dirname)) - os.makedirs(intellicode_directory_path, exist_ok=True) - intellicode_jar_path = str( - PurePath(intellicode_directory_path, "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar") - ) - intellisense_members_path = str(PurePath(intellicode_directory_path, "extension/dist/bundledModels/java_intellisense-members")) - if not all( - [ - os.path.exists(intellicode_directory_path), - os.path.exists(intellicode_jar_path), - os.path.exists(intellisense_members_path), - ] - ): - intellicode_dep.download_to(intellicode_directory_path) - - assert os.path.exists(intellicode_directory_path) - assert os.path.exists(intellicode_jar_path) - assert os.path.exists(intellisense_members_path) - return RuntimeDependencyPaths( gradle_path=gradle_path, lombok_jar_path=lombok_jar_path, @@ -505,8 +454,6 @@ class EclipseJDTLS(SolidLanguageServer): jre_home_path=jre_home_path, jdtls_launcher_jar_path=jdtls_launcher_jar_path, jdtls_readonly_config_path=jdtls_readonly_config_path, - intellicode_jar_path=intellicode_jar_path, - intellisense_members_path=intellisense_members_path, ) @staticmethod @@ -520,7 +467,7 @@ class EclipseJDTLS(SolidLanguageServer): :param jdtls_path: absolute path to the JDTLS root (containing ``plugins/`` and ``config_/``) :param lombok_path: absolute path to the Lombok jar (mandatory; agent is always attached) :param custom_settings: language-server-specific settings from ls_specific_settings.java - :return: populated RuntimeDependencyPaths with gradle/intellicode fields set to None + :return: populated RuntimeDependencyPaths with ``gradle_path`` set to ``None`` """ # validate jdtls_path structure (root + plugins dir) jdtls_root = Path(jdtls_path) @@ -569,8 +516,6 @@ class EclipseJDTLS(SolidLanguageServer): jdtls_readonly_config_path=str(config_dir), lombok_jar_path=lombok_path, gradle_path=None, - intellicode_jar_path=None, - intellisense_members_path=None, ) @staticmethod @@ -1006,10 +951,6 @@ class EclipseJDTLS(SolidLanguageServer): gradle_user_home = None log.info(f"Gradle user home not found at default location ({default_gradle_home}), will use JDTLS defaults") - # IntelliCode JVM settings (used in vmargs for the embedded JVM) - intellicode_xmx = self._custom_settings.get("intellicode_xmx", "1G") - intellicode_xms = self._custom_settings.get("intellicode_xms", "100m") - # Lombok-generated symbols (getX/setX/builder()/equals/hashCode/toString/...): JDTLS filters # these out of documentSymbol results by default. Without them, find_symbol/get_symbols_overview # return only user-written sources, which breaks navigation around @Data/@Builder/@Getter/@Setter @@ -1194,19 +1135,10 @@ class EclipseJDTLS(SolidLanguageServer): "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}}, }, "initializationOptions": { - "bundles": ["intellicode-core.jar"], + "bundles": [], "settings": { "java": { "home": None, - "jdt": { - "ls": { - "java": {"home": None}, - "vmargs": f"-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx{intellicode_xmx} -Xms{intellicode_xms} -Xlog:disable", - "lombokSupport": {"enabled": True}, - "protobufSupport": {"enabled": True}, - "androidSupport": {"enabled": True}, - } - }, "errors": {"incompleteClasspath": {"severity": "error"}}, "configuration": { "checkProjectSettingsExclusions": False, @@ -1326,14 +1258,6 @@ class EclipseJDTLS(SolidLanguageServer): initialize_params["initializationOptions"]["workspaceFolders"] = [repo_uri] - # IntelliCode bundle: only attached in default vscode-java VSIX mode. - # In upstream-jdtls mode (jdtls_path set) we don't ship IntelliCode — agentic Serena workflows - # don't use completion ranking, so the bundle would be inert dead weight. - if self.runtime_dependency_paths.intellicode_jar_path is not None: - initialize_params["initializationOptions"]["bundles"] = [self.runtime_dependency_paths.intellicode_jar_path] - else: - initialize_params["initializationOptions"]["bundles"] = [] - # merge the bundled JRE with any additional runtimes configured via ls_specific_settings.java.runtimes # (e.g. so projects targeting a newer Java version than the bundled JRE resolve their JRE container)... default_runtime = {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True} @@ -1378,9 +1302,6 @@ class EclipseJDTLS(SolidLanguageServer): "*", " ", ] - if registration["method"] == "workspace/executeCommand": - if "java.intellicode.enable" in registration["registerOptions"]["commands"]: - self._intellicode_enable_command_available.set() return def lang_status_handler(params: dict) -> None: @@ -1424,23 +1345,6 @@ class EclipseJDTLS(SolidLanguageServer): self.server.notify.workspace_did_change_configuration({"settings": initialize_params["initializationOptions"]["settings"]}) # type: ignore - # IntelliCode enablement is only relevant in the default vscode-java VSIX mode where the - # IntelliCode bundle is shipped. In upstream-jdtls mode it's absent and the - # 'java.intellicode.enable' command will never be registered, so we skip the wait/call. - if self.runtime_dependency_paths.intellicode_jar_path is not None: - self._intellicode_enable_command_available.wait() - - java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path - assert java_intellisense_members_path is not None - assert os.path.exists(java_intellisense_members_path) - intellicode_enable_result = self.server.send.execute_command( - { - "command": "java.intellicode.enable", - "arguments": [True, java_intellisense_members_path], - } - ) - assert intellicode_enable_result - if not self._service_ready_event.is_set(): log.info("Waiting for service to be ready ...") self._service_ready_event.wait() diff --git a/src/solidlsp/resources/downloaded_dependency_hashes.json b/src/solidlsp/resources/downloaded_dependency_hashes.json index 7fc0bdd9..a02ce776 100644 --- a/src/solidlsp/resources/downloaded_dependency_hashes.json +++ b/src/solidlsp/resources/downloaded_dependency_hashes.json @@ -1,9 +1,8 @@ { "https://services.gradle.org/distributions/gradle-8.14.2-bin.zip": "7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999", - "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage": "7f61a7f96d101cdf230f96821be3fddd8f890ebfefb3695d18beee43004ae251", "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-darwin-arm64-1.54.0-923.vsix": "c54c45cb0d2579d8e0a4ddeb24d4a9dd0b460d07d9366adea2b38a1da22a463c", "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-darwin-x64-1.54.0-923.vsix": "dfc98abc4e54165a78372e280242a039671729b1b03420608df3b10c6b629fb6", "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-arm64-1.54.0-923.vsix": "e2bb22c427d90da8dbb1afff72ff1e2dce38d50b76deb02d7bc313a330a1330c", "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-x64-1.54.0-923.vsix": "9d4b15da54e25a0192f9bac073f086c015397d3676623b68dbf83a5dbaf5132b", "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-win32-x64-1.54.0-923.vsix": "66f3914987edeccfee8a2558470e0fde4f8c4154232ff4baa5d73373ebc819d4" -} \ No newline at end of file +} diff --git a/test/solidlsp/java/test_jdtls_path_resolution.py b/test/solidlsp/java/test_jdtls_path_resolution.py index 3bf738b7..6ffd2ea4 100644 --- a/test/solidlsp/java/test_jdtls_path_resolution.py +++ b/test/solidlsp/java/test_jdtls_path_resolution.py @@ -455,7 +455,7 @@ class TestSetupFromExistingInstall: (jdk / "bin" / "java").touch() return jdk - def test_happy_path_returns_runtime_paths_with_no_gradle_and_no_intellicode( + def test_happy_path_returns_runtime_paths_with_no_gradle( self, tmp_path: Path, jdtls_root: Path, lombok_jar: Path, custom_settings: SolidLSPSettings.CustomLSSettings ) -> None: jdk = self._fake_jdk(tmp_path) @@ -465,8 +465,6 @@ class TestSetupFromExistingInstall: result = EclipseJDTLS.DependencyProvider._setup_from_existing_install(str(jdtls_root), str(lombok_jar), custom_settings) assert result.gradle_path is None - assert result.intellicode_jar_path is None - assert result.intellisense_members_path is None assert result.lombok_jar_path == str(lombok_jar) assert result.jre_home_path == str(jdk) assert Path(result.jdtls_launcher_jar_path).name.startswith("org.eclipse.equinox.launcher_") From 217517ecdfb299a2a1a4d680d8659fe272765718 Mon Sep 17 00:00:00 2001 From: Tyce Herrman Date: Sat, 8 Aug 2026 10:25:13 -0400 Subject: [PATCH 09/27] feat(kotlin): update language server to 262.9593.0 (#1811) * feat(kotlin): update language server to 262.9593.0 Note: download URL structure differs depending on version --------- Co-authored-by: Dominik Jain --- CHANGELOG.md | 2 + docs/02-usage/050_configuration.md | 9 +- .../update_downloaded_dependency_hashes.py | 2 + .../kotlin_language_server.py | 180 ++++++++---- .../downloaded_dependency_hashes.json | 13 +- test/solidlsp/kotlin/test_kotlin_basic.py | 2 +- .../kotlin/test_kotlin_dependency_provider.py | 275 ++++++++++++++++++ .../kotlin/test_kotlin_diagnostics.py | 2 + test/solidlsp/kotlin/test_kotlin_startup.py | 109 +++++++ 9 files changed, 537 insertions(+), 57 deletions(-) create mode 100644 test/solidlsp/kotlin/test_kotlin_dependency_provider.py create mode 100644 test/solidlsp/kotlin/test_kotlin_startup.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 312d9de5..316009cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ Status of the `main` branch. Changes prior to the next official version change w - Add `python_basedpyright` as an alternative Python language server - Java/JDTLS: stop downloading and loading the unused IntelliCode completion-ranking bundle; the retired `intellicode_version`, `intellicode_xmx` and `intellicode_xms` settings remain accepted but are ignored #1821 + - Kotlin: update the managed Kotlin LSP from `261.13587.0` to `262.9593.0`, including support for the + new platform-specific archive layout and Windows ARM64 builds - Nix/nixd: support custom `ls_path` launchers and external JSON settings through `config_path` #1737 - Fix: Nix/nixd diagnostics now use published diagnostics instead of the unsupported `textDocument/diagnostic` request, which terminated nixd #1802 diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 84facd85..659f73b8 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -759,15 +759,20 @@ Supported settings: | Setting | Default | Description | |---|---|---| | `ls_path` | managed download | Override the Kotlin Language Server executable path. | -| `kotlin_lsp_version` | `261.13587.0` | Override the Kotlin Language Server version Serena downloads when `ls_path` is not set. | +| `kotlin_lsp_version` | `262.9593.0` | Override the Kotlin Language Server version Serena downloads when `ls_path` is not set. | | `jvm_options` | `-Xmx2G` | Value assigned to `JAVA_TOOL_OPTIONS` for the Kotlin LS process. Set to `""` to disable JVM options entirely. | +The managed `262.9593.0` packages include a bundled JBR. For a custom `ls_path`, point directly to +`bin/intellij-server` (`bin/intellij-server.exe` on Windows). Serena also retains the legacy download +layout for custom Kotlin LSP versions older than `262.4739.0`. The pinned current and frozen initial +releases are checksum-verified; arbitrary custom versions are downloaded without checksum verification. + Example: ```yaml ls_specific_settings: kotlin: - kotlin_lsp_version: "261.13587.0" + kotlin_lsp_version: "262.9593.0" jvm_options: "-Xmx4G -XX:+UseG1GC" ``` diff --git a/scripts/update_downloaded_dependency_hashes.py b/scripts/update_downloaded_dependency_hashes.py index b82eab1d..02e56bda 100644 --- a/scripts/update_downloaded_dependency_hashes.py +++ b/scripts/update_downloaded_dependency_hashes.py @@ -1,7 +1,9 @@ from sensai.util import logging from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS +from solidlsp.language_servers.kotlin_language_server import KotlinLanguageServer if __name__ == "__main__": logging.configure() EclipseJDTLS.DependencyProvider.update_dep_hashes() + KotlinLanguageServer.DependencyProvider.update_dep_hashes() diff --git a/src/solidlsp/language_servers/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server.py index 940758f6..df51dfd1 100644 --- a/src/solidlsp/language_servers/kotlin_language_server.py +++ b/src/solidlsp/language_servers/kotlin_language_server.py @@ -5,8 +5,8 @@ You can configure the following options in ls_specific_settings (in serena_confi ls_specific_settings: kotlin: - ls_path: '/path/to/kotlin-lsp.sh' # Custom path to Kotlin Language Server executable - kotlin_lsp_version: '261.13587.0' # Kotlin Language Server version (default: current bundled version) + ls_path: '/path/to/bin/intellij-server' # Custom path to Kotlin Language Server executable + kotlin_lsp_version: '262.9593.0' # Kotlin Language Server version (default: current bundled version) jvm_options: '-Xmx2G' # JVM options for Kotlin Language Server (default: -Xmx2G) Example configuration for large projects: @@ -21,16 +21,18 @@ import os import pathlib import stat import threading +from dataclasses import dataclass from overrides import override +from solidlsp.dependency_provider import DownloadedDependency, DownloadedDependencyHashDatabase from solidlsp.ls import ( LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer, ) from solidlsp.ls_config import LanguageServerConfig -from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) @@ -44,34 +46,21 @@ KOTLIN_LSP_ALLOWED_HOSTS = ("download-cdn.jetbrains.com",) # Version pinning convention (see eclipse_jdtls.py for the full spec): # INITIAL_* — frozen forever; legacy unversioned install dir is reserved for it. # DEFAULT_* — bumped on upgrades; goes into a versioned subdir. +# NOTE: After changing either pinned version, run scripts/update_downloaded_dependency_hashes.py. INITIAL_KOTLIN_LSP_VERSION = "261.13587.0" -INITIAL_KOTLIN_LSP_SHA256_BY_SUFFIX = { - "win-x64": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad", - "linux-x64": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec", - "linux-aarch64": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651", - "mac-x64": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3", - "mac-aarch64": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c", -} -DEFAULT_KOTLIN_LSP_VERSION = "261.13587.0" -DEFAULT_KOTLIN_LSP_SHA256_BY_SUFFIX = { - "win-x64": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad", - "linux-x64": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec", - "linux-aarch64": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651", - "mac-x64": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3", - "mac-aarch64": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c", -} +DEFAULT_KOTLIN_LSP_VERSION = "262.9593.0" +# Versions before this one use kotlin-lsp-{version}-{platform}.zip and a kotlin-lsp script. +# Starting with 262.4739.0, JetBrains publishes kotlin-server archives with platform-specific +# formats, a bundled JBR, and bin/intellij-server as the launcher. +KOTLIN_SERVER_PACKAGING_MIN_VERSION = (262, 4739, 0) -def _kotlin_lsp_sha(version: str, kotlin_suffix: str) -> str | None: - if version == INITIAL_KOTLIN_LSP_VERSION: - return INITIAL_KOTLIN_LSP_SHA256_BY_SUFFIX.get(kotlin_suffix) - if version == DEFAULT_KOTLIN_LSP_VERSION: - return DEFAULT_KOTLIN_LSP_SHA256_BY_SUFFIX.get(kotlin_suffix) - return None +# The first modern archives remained under the legacy /kotlin-lsp CDN path. +# Starting with 262.8190.0, JetBrains moved them to /language-server/kotlin-server. +KOTLIN_SERVER_CDN_PATH_MIN_VERSION = (262, 8190, 0) - -# Platform-specific Kotlin LSP download suffixes -PLATFORM_KOTLIN_SUFFIX = { +# Platform-specific suffixes used by legacy Kotlin LSP ZIP archives. +LEGACY_PLATFORM_KOTLIN_SUFFIX = { "win-x64": "win-x64", "linux-x64": "linux-x64", "linux-arm64": "linux-aarch64", @@ -79,6 +68,22 @@ PLATFORM_KOTLIN_SUFFIX = { "osx-arm64": "mac-aarch64", } +# Modern archive filename suffix, FileUtils archive type, and launcher path within the archive. +KOTLIN_SERVER_ARTIFACT_BY_PLATFORM: dict[str, tuple[str, FileUtils.ArchiveType, tuple[str, ...]]] = { + "win-x64": (".win.zip", "zip", ("bin", "intellij-server.exe")), + "win-arm64": ("-aarch64.win.zip", "zip", ("bin", "intellij-server.exe")), + "linux-x64": (".tar.gz", "gztar", ("kotlin-server-{version}", "bin", "intellij-server")), + "linux-arm64": ("-aarch64.tar.gz", "gztar", ("kotlin-server-{version}", "bin", "intellij-server")), + "osx-x64": (".sit", "zip", ("kotlin-server-{version}", "bin", "intellij-server")), + "osx-arm64": ("-aarch64.sit", "zip", ("kotlin-server-{version}", "bin", "intellij-server")), +} + + +@dataclass(frozen=True) +class KotlinLSPArtifact: + dependency: DownloadedDependency + launcher_parts: tuple[str, ...] + class KotlinLanguageServer(SolidLanguageServer): """ @@ -102,15 +107,78 @@ class KotlinLanguageServer(SolidLanguageServer): # set again once all progress tokens have ended. self._indexing_complete = threading.Event() self._indexing_complete.set() + self._intellij_server_ready = threading.Event() self._active_progress_tokens: set[str] = set() self._progress_lock = threading.Lock() def _create_dependency_provider(self) -> LanguageServerDependencyProvider: - return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir, str(self.cache_dir)) class DependencyProvider(LanguageServerDependencyProviderSinglePath): - def __init__(self, custom_settings: SolidLSPSettings.CustomLSSettings, ls_resources_dir: str): + def __init__(self, custom_settings: SolidLSPSettings.CustomLSSettings, ls_resources_dir: str, project_cache_dir: str): super().__init__(custom_settings, ls_resources_dir) + self._project_cache_dir = project_cache_dir + + @classmethod + def _create_artifact(cls, version: str, platform_id: PlatformId) -> KotlinLSPArtifact: + """Build download and launcher metadata for one Kotlin LSP release. + + JetBrains has three publishing layouts: legacy ZIPs before 262.4739.0, + modern platform archives under ``/kotlin-lsp`` through 262.7569.0, + and modern archives under ``/language-server/kotlin-server`` from + 262.8190.0 onward. Serena verifies the frozen initial and current default + releases; arbitrary user-selected versions are unverified by design. + """ + try: + version_parts = tuple(int(part) for part in version.split(".")) + except ValueError as exc: + raise ValueError(f"Kotlin LSP version must contain only dot-separated integers: {version!r}") from exc + + verified = version in {INITIAL_KOTLIN_LSP_VERSION, DEFAULT_KOTLIN_LSP_VERSION} + if version_parts >= KOTLIN_SERVER_PACKAGING_MIN_VERSION: + artifact_config = KOTLIN_SERVER_ARTIFACT_BY_PLATFORM.get(platform_id.value) + if artifact_config is None: + raise ValueError(f"Unsupported platform for Kotlin LSP {version}: {platform_id.value}") + + asset_suffix, archive_type, launcher_parts = artifact_config + asset_name = f"kotlin-server-{version}{asset_suffix}" + cdn_path = "language-server/kotlin-server" if version_parts >= KOTLIN_SERVER_CDN_PATH_MIN_VERSION else "kotlin-lsp" + return KotlinLSPArtifact( + dependency=DownloadedDependency( + url=f"https://download-cdn.jetbrains.com/{cdn_path}/{version}/{asset_name}", + archive_type=archive_type, + allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS, + verified=verified, + ), + launcher_parts=tuple(part.format(version=version) for part in launcher_parts), + ) + + kotlin_suffix = LEGACY_PLATFORM_KOTLIN_SUFFIX.get(platform_id.value) + if kotlin_suffix is None: + raise ValueError(f"Unsupported platform for Kotlin LSP {version}: {platform_id.value}") + + return KotlinLSPArtifact( + dependency=DownloadedDependency( + url=f"https://download-cdn.jetbrains.com/kotlin-lsp/{version}/kotlin-lsp-{version}-{kotlin_suffix}.zip", + archive_type="zip", + allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS, + verified=verified, + ), + launcher_parts=("kotlin-lsp.cmd",) if platform_id.is_windows() else ("kotlin-lsp.sh",), + ) + + @classmethod + def update_dep_hashes(cls) -> None: + pinned_artifacts = [ + *(cls._create_artifact(INITIAL_KOTLIN_LSP_VERSION, PlatformId(platform)) for platform in LEGACY_PLATFORM_KOTLIN_SUFFIX), + *( + cls._create_artifact(DEFAULT_KOTLIN_LSP_VERSION, PlatformId(platform)) + for platform in KOTLIN_SERVER_ARTIFACT_BY_PLATFORM + ), + ] + with DownloadedDependencyHashDatabase.get_instance().update_context() as database: + for artifact in pinned_artifacts: + database.update(artifact.dependency) def _get_or_install_core_dependency(self) -> str: """ @@ -118,16 +186,9 @@ class KotlinLanguageServer(SolidLanguageServer): """ platform_id = PlatformUtils.get_platform_id() - # Verify platform support - assert platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-"), ( - "Only Windows, Linux and macOS platforms are supported for Kotlin in multilspy at the moment" - ) - - kotlin_suffix = PLATFORM_KOTLIN_SUFFIX.get(platform_id.value) - assert kotlin_suffix, f"Unsupported platform for Kotlin LSP: {platform_id.value}" - # Setup paths for dependencies; legacy unversioned dir reserved for INITIAL only kotlin_lsp_version = self._custom_settings.get("kotlin_lsp_version", DEFAULT_KOTLIN_LSP_VERSION) + artifact = self._create_artifact(kotlin_lsp_version, platform_id) ls_dirname = ( "kotlin_language_server" if kotlin_lsp_version == INITIAL_KOTLIN_LSP_VERSION @@ -137,22 +198,13 @@ class KotlinLanguageServer(SolidLanguageServer): os.makedirs(static_dir, exist_ok=True) # Setup Kotlin Language Server - kotlin_script_name = "kotlin-lsp.cmd" if platform_id.value.startswith("win-") else "kotlin-lsp.sh" - kotlin_script = os.path.join(static_dir, kotlin_script_name) + kotlin_script = os.path.join(static_dir, *artifact.launcher_parts) if not os.path.exists(kotlin_script): - kotlin_url = f"https://download-cdn.jetbrains.com/kotlin-lsp/{kotlin_lsp_version}/kotlin-lsp-{kotlin_lsp_version}-{kotlin_suffix}.zip" - expected_sha256 = _kotlin_lsp_sha(kotlin_lsp_version, kotlin_suffix) log.info("Downloading Kotlin Language Server...") - FileUtils.download_and_extract_archive_verified( - kotlin_url, - static_dir, - "zip", - expected_sha256=expected_sha256, - allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS, - ) + artifact.dependency.download_to(static_dir) - if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"): + if os.path.exists(kotlin_script) and not platform_id.is_windows(): os.chmod( kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH, @@ -165,10 +217,18 @@ class KotlinLanguageServer(SolidLanguageServer): return kotlin_script def _create_launch_command(self, core_path: str) -> list[str]: - return [core_path, "--stdio"] + command = [core_path, "--stdio"] + # A custom ls_path is independent of Serena's managed version. Select + # arguments from the actual launcher so existing kotlin-lsp.sh/.cmd + # configurations do not inherit IntelliJ-server-only options. + platform_id = PlatformUtils.get_platform_id() + intellij_launcher_name = "intellij-server.exe" if platform_id.is_windows() else "intellij-server" + if os.path.basename(core_path).lower() == intellij_launcher_name: + command.extend(["--system-path", os.path.join(self._project_cache_dir, "kotlin-lsp-system")]) + return command def create_launch_command_env(self) -> dict[str, str]: - """Provides JAVA_HOME and JVM options for the Kotlin Language Server process.""" + """Provides JVM options for the Kotlin Language Server process.""" env: dict[str, str] = {} # Get JVM options from settings or use default @@ -427,6 +487,7 @@ class KotlinLanguageServer(SolidLanguageServer): """ Starts the Kotlin Language Server """ + self._intellij_server_ready.clear() def execute_client_command_handler(params: dict) -> list: return [] @@ -437,6 +498,11 @@ class KotlinLanguageServer(SolidLanguageServer): def window_log_message(msg: dict) -> None: log.info(f"LSP: window/logMessage: {msg}") + def intellij_server_ready(_params: dict | None) -> None: + """Mark the modern IntelliJ-based server ready after its startup/import phase.""" + log.info("Kotlin IntelliJ language server reported that it is ready") + self._intellij_server_ready.set() + def work_done_progress_create(params: dict) -> dict: """Handle window/workDoneProgress/create: the server is about to report async progress. Clear the indexing-complete event so _start_server waits until all tokens finish. @@ -487,9 +553,12 @@ class KotlinLanguageServer(SolidLanguageServer): self.server.on_request("workspace/codeLens/refresh", do_nothing) self.server.on_notification("language/status", do_nothing) self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("window/showMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_request("workspace/diagnostic/refresh", do_nothing) self.server.on_request("window/workDoneProgress/create", work_done_progress_create) self.server.on_notification("$/progress", progress_handler) + self.server.on_notification("intellij/ready-for-test", intellij_server_ready) self.server.on_notification("$/logTrace", do_nothing) self.server.on_notification("$/cancelRequest", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) @@ -503,6 +572,8 @@ class KotlinLanguageServer(SolidLanguageServer): init_response = self.server.send.initialize(initialize_params) capabilities = init_response["capabilities"] + server_info = init_response.get("serverInfo", {}) + wait_for_intellij_ready = isinstance(server_info, dict) and server_info.get("name") == "IntelliJ Language Server by JetBrains" assert "textDocumentSync" in capabilities, "Server must support textDocumentSync" assert "hoverProvider" in capabilities, "Server must support hover" assert "completionProvider" in capabilities, "Server must support code completion" @@ -515,14 +586,17 @@ class KotlinLanguageServer(SolidLanguageServer): self.server.notify.initialized({}) - # Wait for any async indexing to complete. + # Wait for workspace import and async indexing to complete. # - Older KLS (0.253.x): indexing is synchronous inside `initialize`, no $/progress is sent, # _indexing_complete stays SET -> wait() returns immediately. # - Newer KLS (261+): server sends window/workDoneProgress/create after initialized, # which clears the event; wait() blocks until all progress tokens end. + # - IntelliJ-based KLS (262.4739+): server sends intellij/ready-for-test after + # its workspace import and indexing phase completes. _INDEXING_TIMEOUT = 120.0 log.info("Waiting for Kotlin LSP indexing to complete (if async)...") - if self._indexing_complete.wait(timeout=_INDEXING_TIMEOUT): + ready_event = self._intellij_server_ready if wait_for_intellij_ready else self._indexing_complete + if ready_event.wait(timeout=_INDEXING_TIMEOUT): log.info("Kotlin LSP ready") else: log.warning("Kotlin LSP did not signal indexing completion within %.0fs; proceeding anyway", _INDEXING_TIMEOUT) diff --git a/src/solidlsp/resources/downloaded_dependency_hashes.json b/src/solidlsp/resources/downloaded_dependency_hashes.json index a02ce776..c6602add 100644 --- a/src/solidlsp/resources/downloaded_dependency_hashes.json +++ b/src/solidlsp/resources/downloaded_dependency_hashes.json @@ -4,5 +4,16 @@ "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-darwin-x64-1.54.0-923.vsix": "dfc98abc4e54165a78372e280242a039671729b1b03420608df3b10c6b629fb6", "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-arm64-1.54.0-923.vsix": "e2bb22c427d90da8dbb1afff72ff1e2dce38d50b76deb02d7bc313a330a1330c", "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-x64-1.54.0-923.vsix": "9d4b15da54e25a0192f9bac073f086c015397d3676623b68dbf83a5dbaf5132b", - "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-win32-x64-1.54.0-923.vsix": "66f3914987edeccfee8a2558470e0fde4f8c4154232ff4baa5d73373ebc819d4" + "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-win32-x64-1.54.0-923.vsix": "66f3914987edeccfee8a2558470e0fde4f8c4154232ff4baa5d73373ebc819d4", + "https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-win-x64.zip": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad", + "https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-linux-x64.zip": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec", + "https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-linux-aarch64.zip": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651", + "https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-mac-x64.zip": "a3972f27229eba2c226060e54baea1c958c82c326dfc971bf53f72a74d0564a3", + "https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-mac-aarch64.zip": "d4ea28b22b29cf906fe16d23698a8468f11646a6a66dcb15584f306aaefbee6c", + "https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0.win.zip": "f2daaa476f26d99301b406f76de6d87c437d04dc72f06845154619d8f991c51f", + "https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0-aarch64.win.zip": "73a552a6a420158622e5ad8d96b53da8aa8ced3f88a24fded01575927a2fd8e7", + "https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0.tar.gz": "2d99d8e198fbe4aa8f4481e37799724ce94803b4ea12a60b416040e3fcd7cc5e", + "https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0-aarch64.tar.gz": "2317831c6e5607d05b7ebc1da655330125ce0e3d66fbf24517dfce442debc14e", + "https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0.sit": "17369fda97c85418ac24ab38a9df56b21522a3468dfe193832fe455c13920745", + "https://download-cdn.jetbrains.com/language-server/kotlin-server/262.9593.0/kotlin-server-262.9593.0-aarch64.sit": "6ba6021a706b21e64cef33f7e2b79f187c0910320722bb2d3ed05ad1115ec43f" } diff --git a/test/solidlsp/kotlin/test_kotlin_basic.py b/test/solidlsp/kotlin/test_kotlin_basic.py index d903a06d..7530a545 100644 --- a/test/solidlsp/kotlin/test_kotlin_basic.py +++ b/test/solidlsp/kotlin/test_kotlin_basic.py @@ -10,7 +10,7 @@ from test.conftest import language_server_tests_enabled from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols -# Kotlin LSP (IntelliJ-based, pre-alpha v261) crashes on JVM restart under CI resource constraints +# Kotlin LSP (IntelliJ-based, pre-alpha v262) crashes on JVM restart under CI resource constraints # (2 CPUs, 7GB RAM). First start succeeds but subsequent starts fail with cancelled (-32800). # Tests pass reliably on developer machines. See PR #1061 for investigation details. # (The CI quarantine lives centrally in test/conftest.py::_determine_disabled_languages.) diff --git a/test/solidlsp/kotlin/test_kotlin_dependency_provider.py b/test/solidlsp/kotlin/test_kotlin_dependency_provider.py new file mode 100644 index 00000000..707a0633 --- /dev/null +++ b/test/solidlsp/kotlin/test_kotlin_dependency_provider.py @@ -0,0 +1,275 @@ +"""Tests for Kotlin Language Server dependency resolution and installation.""" + +from contextlib import nullcontext +from pathlib import Path +from unittest.mock import patch + +import pytest + +from solidlsp.dependency_provider import DownloadedDependency, DownloadedDependencyHashDatabase +from solidlsp.language_servers.kotlin_language_server import ( + DEFAULT_KOTLIN_LSP_VERSION, + INITIAL_KOTLIN_LSP_VERSION, + KOTLIN_LSP_ALLOWED_HOSTS, + KotlinLanguageServer, +) +from solidlsp.ls_utils import PlatformId +from solidlsp.settings import SolidLSPSettings + + +def _make_provider( + tmp_path: Path, + custom_settings: dict[str, str] | None = None, +) -> KotlinLanguageServer.DependencyProvider: + return KotlinLanguageServer.DependencyProvider( + custom_settings=SolidLSPSettings.CustomLSSettings(custom_settings or {}), + ls_resources_dir=str(tmp_path), + project_cache_dir=str(tmp_path / "project-cache"), + ) + + +@pytest.mark.kotlin +class TestKotlinDependencyProvider: + @pytest.mark.parametrize( + ("platform_id", "asset_suffix", "archive_type", "launcher_parts", "sha256"), + [ + ( + PlatformId.WIN_x64, + ".win.zip", + "zip", + ("bin", "intellij-server.exe"), + "f2daaa476f26d99301b406f76de6d87c437d04dc72f06845154619d8f991c51f", + ), + ( + PlatformId.WIN_arm64, + "-aarch64.win.zip", + "zip", + ("bin", "intellij-server.exe"), + "73a552a6a420158622e5ad8d96b53da8aa8ced3f88a24fded01575927a2fd8e7", + ), + ( + PlatformId.LINUX_x64, + ".tar.gz", + "gztar", + (f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"), + "2d99d8e198fbe4aa8f4481e37799724ce94803b4ea12a60b416040e3fcd7cc5e", + ), + ( + PlatformId.LINUX_arm64, + "-aarch64.tar.gz", + "gztar", + (f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"), + "2317831c6e5607d05b7ebc1da655330125ce0e3d66fbf24517dfce442debc14e", + ), + ( + PlatformId.OSX_x64, + ".sit", + "zip", + (f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"), + "17369fda97c85418ac24ab38a9df56b21522a3468dfe193832fe455c13920745", + ), + ( + PlatformId.OSX_arm64, + "-aarch64.sit", + "zip", + (f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"), + "6ba6021a706b21e64cef33f7e2b79f187c0910320722bb2d3ed05ad1115ec43f", + ), + ], + ) + def test_default_artifacts_match_jetbrains_release_matrix( + self, + tmp_path: Path, + platform_id: PlatformId, + asset_suffix: str, + archive_type: str, + launcher_parts: tuple[str, ...], + sha256: str, + ) -> None: + artifact = KotlinLanguageServer.DependencyProvider._create_artifact(DEFAULT_KOTLIN_LSP_VERSION, platform_id) + url = ( + f"https://download-cdn.jetbrains.com/language-server/kotlin-server/{DEFAULT_KOTLIN_LSP_VERSION}/" + f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}{asset_suffix}" + ) + + with patch("solidlsp.dependency_provider.FileUtils.download_and_extract_archive_verified") as download: + artifact.dependency.download_to(tmp_path) + + download.assert_called_once_with( + url, + str(tmp_path), + archive_type=archive_type, + expected_sha256=sha256, + allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS, + ) + assert artifact.launcher_parts == launcher_parts + + @pytest.mark.parametrize( + ("version", "platform_id", "url", "launcher_parts"), + [ + ( + "262.2310.0", + PlatformId.LINUX_x64, + "https://download-cdn.jetbrains.com/kotlin-lsp/262.2310.0/kotlin-lsp-262.2310.0-linux-x64.zip", + ("kotlin-lsp.sh",), + ), + ( + "262.4739.0", + PlatformId.LINUX_x64, + "https://download-cdn.jetbrains.com/kotlin-lsp/262.4739.0/kotlin-server-262.4739.0.tar.gz", + ("kotlin-server-262.4739.0", "bin", "intellij-server"), + ), + ( + "262.8190.0", + PlatformId.OSX_arm64, + "https://download-cdn.jetbrains.com/language-server/kotlin-server/262.8190.0/kotlin-server-262.8190.0-aarch64.sit", + ("kotlin-server-262.8190.0", "bin", "intellij-server"), + ), + ], + ) + def test_version_boundaries_select_the_published_layout( + self, + version: str, + platform_id: PlatformId, + url: str, + launcher_parts: tuple[str, ...], + ) -> None: + artifact = KotlinLanguageServer.DependencyProvider._create_artifact(version, platform_id) + + assert artifact.dependency.get_url() == url + assert artifact.launcher_parts == launcher_parts + + def test_custom_versions_skip_hash_lookup_by_design(self, tmp_path: Path) -> None: + artifact = KotlinLanguageServer.DependencyProvider._create_artifact("262.8190.1", PlatformId.LINUX_x64) + + with ( + patch( + "solidlsp.dependency_provider.DownloadedDependencyHashDatabase.get_instance", + side_effect=AssertionError("custom versions must not consult the pinned hash database"), + ), + patch("solidlsp.dependency_provider.FileUtils.download_and_extract_archive_verified") as download, + ): + artifact.dependency.download_to(tmp_path) + + download.assert_called_once_with( + "https://download-cdn.jetbrains.com/language-server/kotlin-server/262.8190.1/kotlin-server-262.8190.1.tar.gz", + str(tmp_path), + archive_type="gztar", + expected_sha256=None, + allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS, + ) + + def test_hash_updater_registers_frozen_and_default_artifacts(self) -> None: + updated_asset_names: list[str] = [] + + class RecordingUpdater: + def update(self, dependency: DownloadedDependency) -> None: + updated_asset_names.append(dependency.get_url().rsplit("/", 1)[-1]) + + with patch.object(DownloadedDependencyHashDatabase, "get_instance") as get_hash_database: + get_hash_database.return_value.update_context.return_value = nullcontext(RecordingUpdater()) + KotlinLanguageServer.DependencyProvider.update_dep_hashes() + + assert set(updated_asset_names) == { + "kotlin-lsp-261.13587.0-win-x64.zip", + "kotlin-lsp-261.13587.0-linux-x64.zip", + "kotlin-lsp-261.13587.0-linux-aarch64.zip", + "kotlin-lsp-261.13587.0-mac-x64.zip", + "kotlin-lsp-261.13587.0-mac-aarch64.zip", + "kotlin-server-262.9593.0.win.zip", + "kotlin-server-262.9593.0-aarch64.win.zip", + "kotlin-server-262.9593.0.tar.gz", + "kotlin-server-262.9593.0-aarch64.tar.gz", + "kotlin-server-262.9593.0.sit", + "kotlin-server-262.9593.0-aarch64.sit", + } + + @pytest.mark.parametrize( + ("settings", "platform_id", "relative_launcher", "expected_directory"), + [ + ( + {"kotlin_lsp_version": INITIAL_KOTLIN_LSP_VERSION}, + PlatformId.LINUX_x64, + ("kotlin-lsp.sh",), + "kotlin_language_server", + ), + ( + {}, + PlatformId.LINUX_arm64, + (f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"), + f"kotlin_language_server-{DEFAULT_KOTLIN_LSP_VERSION}", + ), + ], + ) + def test_installs_pinned_versions_in_compatible_directories( + self, + tmp_path: Path, + settings: dict[str, str], + platform_id: PlatformId, + relative_launcher: tuple[str, ...], + expected_directory: str, + ) -> None: + provider = _make_provider(tmp_path, settings) + + def fake_download( + _url: str, + target_path: str, + archive_type: str, + expected_sha256: str | None = None, + allowed_hosts: tuple[str, ...] | list[str] | None = None, + ) -> None: + del archive_type, expected_sha256, allowed_hosts + launcher = Path(target_path).joinpath(*relative_launcher) + launcher.parent.mkdir(parents=True, exist_ok=True) + launcher.write_text("#!/bin/sh\n", encoding="utf-8") + + with ( + patch( + "solidlsp.language_servers.kotlin_language_server.PlatformUtils.get_platform_id", + return_value=platform_id, + ), + patch( + "solidlsp.dependency_provider.FileUtils.download_and_extract_archive_verified", + side_effect=fake_download, + ), + ): + launcher_path = provider._get_or_install_core_dependency() + + assert launcher_path == str((tmp_path / expected_directory).joinpath(*relative_launcher)) + + @pytest.mark.parametrize( + ("platform_id", "modern_launcher", "other_os_launcher"), + [ + (PlatformId.LINUX_x64, "/path/to/intellij-server", "/path/to/intellij-server.exe"), + (PlatformId.WIN_x64, "/path/to/intellij-server.exe", "/path/to/intellij-server"), + ], + ) + def test_custom_launcher_arguments_use_the_active_os_name( + self, + tmp_path: Path, + platform_id: PlatformId, + modern_launcher: str, + other_os_launcher: str, + ) -> None: + modern_provider = _make_provider(tmp_path, {"ls_path": modern_launcher}) + other_os_provider = _make_provider(tmp_path, {"ls_path": other_os_launcher}) + + with patch( + "solidlsp.language_servers.kotlin_language_server.PlatformUtils.get_platform_id", + return_value=platform_id, + ): + assert modern_provider.create_launch_command() == [ + modern_launcher, + "--stdio", + "--system-path", + str(tmp_path / "project-cache" / "kotlin-lsp-system"), + ] + assert other_os_provider.create_launch_command() == [other_os_launcher, "--stdio"] + + def test_invalid_version_is_rejected_before_download(self) -> None: + with pytest.raises(ValueError, match="dot-separated integers"): + KotlinLanguageServer.DependencyProvider._create_artifact("latest", PlatformId.OSX_arm64) + + def test_unsupported_modern_platform_is_rejected(self) -> None: + with pytest.raises(ValueError, match="Unsupported platform"): + KotlinLanguageServer.DependencyProvider._create_artifact(DEFAULT_KOTLIN_LSP_VERSION, PlatformId.LINUX_x86) diff --git a/test/solidlsp/kotlin/test_kotlin_diagnostics.py b/test/solidlsp/kotlin/test_kotlin_diagnostics.py index c981add4..0d0d824c 100644 --- a/test/solidlsp/kotlin/test_kotlin_diagnostics.py +++ b/test/solidlsp/kotlin/test_kotlin_diagnostics.py @@ -2,9 +2,11 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import LanguageServerId +from test.conftest import language_server_tests_enabled from test.solidlsp.util.diagnostics import assert_file_diagnostics +@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.KOTLIN), reason="Kotlin tests are disabled") @pytest.mark.kotlin class TestKotlinDiagnostics: @pytest.mark.parametrize("language_server", [LanguageServerId.KOTLIN], indirect=True) diff --git a/test/solidlsp/kotlin/test_kotlin_startup.py b/test/solidlsp/kotlin/test_kotlin_startup.py new file mode 100644 index 00000000..0f6a0345 --- /dev/null +++ b/test/solidlsp/kotlin/test_kotlin_startup.py @@ -0,0 +1,109 @@ +"""Regression tests for Kotlin LSP startup readiness handling.""" + +import logging +from pathlib import Path +from typing import Any, cast +from unittest.mock import Mock, patch + +import pytest + +from solidlsp.language_servers.kotlin_language_server import KotlinLanguageServer +from solidlsp.ls_config import LanguageServerConfig, LanguageServerId +from solidlsp.settings import SolidLSPSettings + +pytestmark = pytest.mark.kotlin + +_REQUIRED_CAPABILITIES = { + "textDocumentSync": 1, + "hoverProvider": True, + "completionProvider": {}, + "signatureHelpProvider": {}, + "definitionProvider": True, + "referencesProvider": True, + "documentSymbolProvider": True, + "workspaceSymbolProvider": True, + "semanticTokensProvider": {}, +} + + +def _make_server(tmp_path: Path) -> tuple[KotlinLanguageServer, Mock, Mock]: + settings = SolidLSPSettings( + solidlsp_dir=str(tmp_path / "global"), + project_data_path=str(tmp_path / "project"), + ls_specific_settings={LanguageServerId.KOTLIN: {}}, + ) + server_interface = Mock() + with patch.object(KotlinLanguageServer, "_create_language_server_interface", return_value=server_interface): + server = KotlinLanguageServer( + LanguageServerConfig(ls_id=LanguageServerId.KOTLIN), + str(tmp_path), + settings, + ) + + indexing_complete = Mock() + indexing_complete.wait.return_value = True + intellij_server_ready = Mock() + intellij_server_ready.wait.return_value = True + server_state = cast(Any, server) + server_state._indexing_complete = indexing_complete + server_state._intellij_server_ready = intellij_server_ready + return server, indexing_complete, intellij_server_ready + + +def _configure_initialize(server: KotlinLanguageServer, server_name: str | None) -> dict[str, Any]: + response: dict[str, Any] = {"capabilities": _REQUIRED_CAPABILITIES} + if server_name is not None: + response["serverInfo"] = {"name": server_name} + language_server_interface = cast(Any, server.server) + language_server_interface.send.initialize.return_value = response + return response + + +def test_modern_server_registers_and_waits_for_explicit_ready_notification(tmp_path: Path) -> None: + server, indexing_complete, intellij_server_ready = _make_server(tmp_path) + _configure_initialize(server, "IntelliJ Language Server by JetBrains") + events: list[str] = [] + notification_handlers: dict[str, Any] = {} + language_server_interface = cast(Any, server.server) + + def on_notification(method: str, handler: Any) -> None: + events.append(f"notification:{method}") + notification_handlers[method] = handler + + language_server_interface.on_notification.side_effect = on_notification + language_server_interface.start.side_effect = lambda: events.append("start") + language_server_interface.send.initialize.side_effect = lambda _params: ( + events.append("initialize") + or {"capabilities": _REQUIRED_CAPABILITIES, "serverInfo": {"name": "IntelliJ Language Server by JetBrains"}} + ) + + server._start_server() + + assert events.index("notification:intellij/ready-for-test") < events.index("start") < events.index("initialize") + intellij_server_ready.clear.assert_called_once_with() + intellij_server_ready.wait.assert_called_once_with(timeout=120.0) + indexing_complete.wait.assert_not_called() + + notification_handlers["intellij/ready-for-test"](None) + intellij_server_ready.set.assert_called_once_with() + + +def test_legacy_server_keeps_progress_event_wait(tmp_path: Path) -> None: + server, indexing_complete, intellij_server_ready = _make_server(tmp_path) + _configure_initialize(server, None) + + server._start_server() + + indexing_complete.wait.assert_called_once_with(timeout=120.0) + intellij_server_ready.wait.assert_not_called() + + +def test_modern_ready_timeout_warns_and_continues(tmp_path: Path, caplog: Any) -> None: + server, _indexing_complete, intellij_server_ready = _make_server(tmp_path) + _configure_initialize(server, "IntelliJ Language Server by JetBrains") + intellij_server_ready.wait.return_value = False + + with caplog.at_level(logging.WARNING): + server._start_server() + + assert "Kotlin LSP did not signal indexing completion within 120s; proceeding anyway" in caplog.text From 600d3b2fadd305321cba3d7fcb07251eb1e9f26f Mon Sep 17 00:00:00 2001 From: Merlin Hughes Date: Sat, 8 Aug 2026 10:28:04 -0400 Subject: [PATCH 10/27] Scala: answer Metals' build-import prompt (#1769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * scala: answer Metals' build-import prompt Metals asks, via window/showMessageRequest, whether to import a workspace it has not seen before. Serena registered no handler, so the request came back `method 'window/showMessageRequest' not handled on client` and Metals logged "Unexpected error initializing server" and gave up. No build server, no build target, and every cross-file query answered by the fallback presentation compiler — which cannot see past the file it is given. A client cannot decline the question instead: Metals' `disableShowMessageRequest` is server-side configuration, and its no-op fallback answers "Not now", which imports nothing either. Answering is the only route to a build. So answer the three prompts that lead to a build server, and dismiss anything else with `null` — a prompt we do not recognise is one whose consequences we cannot judge, and two of Metals' others offer to kill a process and to open a window. "Don't show again" is never chosen; Metals persists that in the project's own state. Since answering yes lets Metals run the project's build tool, `ls_specific_settings.scala.auto_import_build: false` declines instead. * scala: name the build-tool choice as a gap, and test the setting `Messages.ChooseBuildTool` ("Multiple build definitions found. Which would you like to use?") offers the build tools' own executable names, and precedes the import prompt wherever a workspace holds more than one kind of build. It is dismissed like anything else unrecognised, so such a workspace is still not imported — a deliberate choice, since picking one is a guess of a different order, but one the comment and the setup guide should admit to rather than claim every prompt on the path is answered. Also: cover the route from `ls_specific_settings` to `auto_import_build`, which nothing exercised, and correct the `ImportBuildChanges` message in the fixtures, which had the notification variant's trailing full stop rather than the request's own text. --- CHANGELOG.md | 7 ++ docs/01-about/020_programming-languages.md | 2 +- .../scala_setup_guide_for_serena.md | 13 ++- .../language_servers/scala_language_server.py | 56 +++++++++++ .../scala/test_scala_show_message_request.py | 94 +++++++++++++++++++ 5 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 test/solidlsp/scala/test_scala_show_message_request.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 316009cb..2861231d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,13 @@ Status of the `main` branch. Changes prior to the next official version change w - Language servers and their dependency providers now go through the `subprocess_run` helper instead of calling `subprocess.run` directly (e.g. for installation processes), so all such subprocesses get `stdin=DEVNULL` and can no longer interfere with the stdio MCP connection #1748 + - `scala`: Fix: Metals asks via `window/showMessageRequest` whether to import a workspace it has not + seen before, and Serena had no handler, so the request failed with `MethodNotFound` and Metals gave + up on the import ("Unexpected error initializing server"). No build server was ever connected and + every cross-file query fell back to the presentation compiler, which sees one file at a time, unless + the project happened to have been imported beforehand by another editor. The three prompts that lead + to a build server are now answered; anything else is dismissed, including the choice between several + build definitions in one workspace. `ls_specific_settings.scala.auto_import_build: false` opts out * Dashboard: - Fix: Serena PyPI version check triggered by callback on main thread could delay agent startup #1774 diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 00bfe33b..25d413a9 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -147,7 +147,7 @@ Some languages require additional installations or setup steps, as noted. * **Rust** (requires [rustup](https://rustup.rs/) - uses rust-analyzer from your toolchain) * **Scala** - (requires some [manual setup](../03-special-guides/scala_setup_guide_for_serena); uses Metals LSP) + (uses Metals LSP, which imports the build on first use — see the [setup guide](../03-special-guides/scala_setup_guide_for_serena)) * **SCSS / Sass / CSS** (experimental; requires Node.js + npm; uses [some-sass-language-server](https://github.com/wkillerud/some-sass) to handle `.scss`, `.sass`, and `.css`) diff --git a/docs/03-special-guides/scala_setup_guide_for_serena.md b/docs/03-special-guides/scala_setup_guide_for_serena.md index 1e4bebca..643e42ac 100644 --- a/docs/03-special-guides/scala_setup_guide_for_serena.md +++ b/docs/03-special-guides/scala_setup_guide_for_serena.md @@ -15,7 +15,16 @@ Install the following on your system and ensure they are available on `PATH`: - Serena uses `cs` if available; if only `coursier` exists, it will attempt to install `cs`. If neither is present, install Coursier first. --- -## Quick Start (Recommended: VS Code + Metals auto‑import) +## Quick Start + +Start Serena in your project root. Metals asks whether to import a workspace it has not seen before, and Serena answers that prompt for it — the build is imported (for sbt, by running `sbt bloopInstall`), `.bloop/` and `.metals/` are created, and cross-file navigation works from there. The first run therefore takes as long as your build takes to load. + +Set `auto_import_build: false` under `ls_specific_settings.scala` to decline instead; you then need to import the build yourself by one of the routes below, or cross-file queries will be served by the fallback presentation compiler and see only one file at a time. + +Serena answers three of Metals' prompts — “Import build”, “Import changes”, “Connect”. Anything else Metals asks is dismissed and logged, including “Multiple build definitions found. Which would you like to use?”, so a workspace holding more than one kind of build (say both an sbt and a Maven definition) still needs importing by one of the routes below. + +--- +## Importing the build yourself (VS Code) 1. Open your Scala project in VS Code. 2. When prompted by Metals, accept “Import build”. Wait until the import and initial compile/indexing finish. @@ -25,7 +34,7 @@ Install the following on your system and ensure they are available on `PATH`: This flow ensures the `.bloop/` and (if applicable) `.metals/` directories are created and your build is known to the build server that Metals uses. --- -## Manual Setup (No VS Code) +## Importing the build yourself (No VS Code) Follow these steps if you prefer a manual setup or you are not using VS Code: diff --git a/src/solidlsp/language_servers/scala_language_server.py b/src/solidlsp/language_servers/scala_language_server.py index 2a7d6330..771c6104 100644 --- a/src/solidlsp/language_servers/scala_language_server.py +++ b/src/solidlsp/language_servers/scala_language_server.py @@ -28,6 +28,22 @@ DEFAULT_METALS_VERSION = "1.6.4" DEFAULT_CLIENT_NAME = "Serena" DEFAULT_ON_STALE_LOCK = "auto-clean" DEFAULT_LOG_MULTI_INSTANCE_NOTICE = True +DEFAULT_AUTO_IMPORT_BUILD = True + +# The `window/showMessageRequest` actions Serena answers affirmatively: the ones standing between +# an un-imported workspace and a build server. Everything else is dismissed, since a prompt we do +# not recognise is one whose consequences we cannot judge — `Messages.OldBloopVersionRunning` +# offers to kill a process, `Messages.NewScalaProject` to open a window. "Don't show again" is +# never chosen: Metals persists that dismissal in the project's own state. +# (scala/meta/internal/metals/Messages.scala, at `ImportBuild`, `ImportBuildChanges`, +# `GenerateBspAndConnect`.) +# +# Not answered, deliberately: `Messages.ChooseBuildTool` ("Multiple build definitions found. Which +# would you like to use?"), whose actions are the build tools' own executable names. It precedes +# the import prompt in a workspace holding more than one kind of build, so such a workspace is +# still not imported — but choosing a build tool for the user is a guess of a different order, and +# Metals offers no way to say "whichever you would have picked". +BUILD_IMPORT_PROMPT_ACTIONS = ("Import build", "Import changes", "Connect") class StaleLockMode(Enum): @@ -43,6 +59,28 @@ class StaleLockMode(Enum): """Raise an error and refuse to start.""" +def choose_show_message_request_action(params: dict, auto_import_build: bool = DEFAULT_AUTO_IMPORT_BUILD) -> dict | None: + """ + Choose Serena's answer to a `window/showMessageRequest`, which Metals uses to ask whether to + import the build. + + :param params: the request's `ShowMessageRequestParams` + :param auto_import_build: whether to answer the build-import prompts affirmatively + :return: the action to select, or None to select none — which is what the LSP spec provides + for, and what leaving the request unanswered fails to say + """ + message = params.get("message", "") + actions = params.get("actions") or [] + if auto_import_build: + for action in actions: + if isinstance(action, dict) and action.get("title") in BUILD_IMPORT_PROMPT_ACTIONS: + log.info(f"Metals asked: {message!r}; answering {action['title']!r}") + return action + offered = [action.get("title") for action in actions if isinstance(action, dict)] + log.info(f"Metals asked: {message!r}; dismissing (offered: {offered})") + return None + + def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object]: """ Extract Scala-specific settings with defaults applied. @@ -52,6 +90,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object - client_name: str - on_stale_lock: StaleLockMode - log_multi_instance_notice: bool + - auto_import_build: bool """ from solidlsp.ls_config import LanguageServerId @@ -60,6 +99,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object "client_name": DEFAULT_CLIENT_NAME, "on_stale_lock": StaleLockMode.AUTO_CLEAN, "log_multi_instance_notice": DEFAULT_LOG_MULTI_INSTANCE_NOTICE, + "auto_import_build": DEFAULT_AUTO_IMPORT_BUILD, } if not solidlsp_settings.ls_specific_settings: @@ -80,6 +120,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object "client_name": scala_settings.get("client_name", DEFAULT_CLIENT_NAME), "on_stale_lock": on_stale_lock, "log_multi_instance_notice": scala_settings.get("log_multi_instance_notice", DEFAULT_LOG_MULTI_INSTANCE_NOTICE), + "auto_import_build": scala_settings.get("auto_import_build", DEFAULT_AUTO_IMPORT_BUILD), } @@ -100,6 +141,14 @@ class ScalaLanguageServer(SolidLanguageServer): metals_version: '1.6.4' # Client identifier sent to Metals (default: DEFAULT_CLIENT_NAME) client_name: 'Serena' + # Answer Metals' build-import prompts affirmatively, which lets it run the project's + # build tool (e.g. sbt bloopInstall). Set false to leave the build un-imported. + auto_import_build: true + + Build import: + Metals asks, via `window/showMessageRequest`, whether to import a workspace it has not + seen before; until that is answered it has no build server and so no build target, and + every cross-file query is served by the fallback presentation compiler. Multi-instance support: Metals uses H2 AUTO_SERVER mode (enabled by default) to support multiple @@ -116,6 +165,8 @@ class ScalaLanguageServer(SolidLanguageServer): # Check for stale locks before setting up dependencies (fail-fast) self._check_metals_db_status(repository_root_path, solidlsp_settings) + self._auto_import_build: bool = _get_scala_settings(solidlsp_settings)["auto_import_build"] # type: ignore[assignment] + scala_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings) super().__init__( config, @@ -300,10 +351,15 @@ class ScalaLanguageServer(SolidLanguageServer): } return initialize_params + def _answer_show_message_request(self, params: dict) -> dict | None: + return choose_show_message_request_action(params, auto_import_build=self._auto_import_build) + def _start_server(self) -> None: """ Starts the Scala Language Server """ + self.server.on_request("window/showMessageRequest", self._answer_show_message_request) + log.info("Starting Scala server process") self.server.start() diff --git a/test/solidlsp/scala/test_scala_show_message_request.py b/test/solidlsp/scala/test_scala_show_message_request.py new file mode 100644 index 00000000..beb336b9 --- /dev/null +++ b/test/solidlsp/scala/test_scala_show_message_request.py @@ -0,0 +1,94 @@ +""" +Unit tests for Serena's answer to Metals' `window/showMessageRequest` prompts. +""" + +import pytest + +from solidlsp.language_servers.scala_language_server import ( + _get_scala_settings, + choose_show_message_request_action, +) +from solidlsp.ls_config import LanguageServerId +from solidlsp.settings import SolidLSPSettings + +# Metals' own prompts, with the message and action titles as its `Messages` object builds them +# (scala/meta/internal/metals/Messages.scala) and `type` as lsp4j serialises `MessageType.Info`. +IMPORT_BUILD = { + "message": "New sbt workspace detected, would you like to import the build?", + "type": 3, + "actions": [{"title": "Import build"}, {"title": "Not now"}, {"title": "Don't show again"}], +} +IMPORT_CHANGES = { + "message": "sbt build needs to be re-imported", + "type": 3, + "actions": [{"title": "Import changes"}, {"title": "Not now"}, {"title": "Don't show again"}], +} +GENERATE_BSP_AND_CONNECT = { + "message": "New sbt workspace detected, would you like connect to the Bloop build server?", + "type": 3, + "actions": [{"title": "Connect"}, {"title": "Not now"}, {"title": "Don't show again"}], +} +OLD_BLOOP_VERSION_RUNNING = { + "message": "Deprecated Bloop server is still running and is taking up resources, do you want to kill the process?", + "type": 3, + "actions": [{"title": "Yes"}, {"title": "Not now"}], +} +CHOOSE_BUILD_TOOL = { + "message": "Multiple build definitions found. Which would you like to use?", + "type": 3, + "actions": [{"title": "sbt"}, {"title": "mill"}], +} + + +@pytest.mark.scala +class TestChooseShowMessageRequestAction: + @pytest.mark.parametrize("params", [IMPORT_BUILD, IMPORT_CHANGES, GENERATE_BSP_AND_CONNECT]) + def test_build_import_prompts_are_answered_affirmatively(self, params: dict) -> None: + chosen = choose_show_message_request_action(params) + assert chosen is not None + assert chosen["title"] in ("Import build", "Import changes", "Connect") + + @pytest.mark.parametrize("params", [IMPORT_BUILD, IMPORT_CHANGES, GENERATE_BSP_AND_CONNECT]) + def test_auto_import_build_can_be_turned_off(self, params: dict) -> None: + assert choose_show_message_request_action(params, auto_import_build=False) is None + + def test_dont_show_again_is_never_chosen(self) -> None: + """Metals persists that dismissal in the project's own state.""" + params = {"message": "…", "actions": [{"title": "Don't show again"}, {"title": "Not now"}]} + assert choose_show_message_request_action(params) is None + + def test_an_unrecognised_prompt_is_dismissed(self) -> None: + """Answering "Yes" here would kill a process on the user's machine.""" + assert choose_show_message_request_action(OLD_BLOOP_VERSION_RUNNING) is None + + def test_a_prompt_with_no_actions_is_dismissed(self) -> None: + assert choose_show_message_request_action({"message": "just so you know"}) is None + assert choose_show_message_request_action({"message": "just so you know", "actions": None}) is None + + def test_malformed_actions_do_not_raise(self) -> None: + params = {"message": "…", "actions": ["Import build", None, {"title": "Import build"}]} + assert choose_show_message_request_action(params) == {"title": "Import build"} + + def test_choosing_between_build_tools_is_left_alone(self) -> None: + """Naming the build tool for the user is a guess of a different order; see the comment + on BUILD_IMPORT_PROMPT_ACTIONS. A workspace with several build definitions is therefore + still not imported. + """ + assert choose_show_message_request_action(CHOOSE_BUILD_TOOL) is None + + +@pytest.mark.scala +class TestAutoImportBuildSetting: + """`auto_import_build` has to reach the handler from the project configuration.""" + + @staticmethod + def setting(**scala_settings) -> object: + settings = SolidLSPSettings(ls_specific_settings={LanguageServerId.SCALA: scala_settings}) + return _get_scala_settings(settings)["auto_import_build"] + + def test_defaults_to_true(self) -> None: + assert self.setting() is True + assert _get_scala_settings(SolidLSPSettings())["auto_import_build"] is True + + def test_can_be_disabled(self) -> None: + assert self.setting(auto_import_build=False) is False From 7acd78bde0c5ee7fa8d1df32f7684d3006bfe198 Mon Sep 17 00:00:00 2001 From: Tyce Herrman Date: Thu, 30 Jul 2026 23:32:26 -0400 Subject: [PATCH 11/27] ci: exclude JDTLS workspaces from cache --- .github/workflows/pytest.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e9b73699..4249aaa9 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -706,7 +706,11 @@ jobs: id: cache-language-servers uses: actions/cache@v3 with: - path: ~/.serena/language_servers/static + # JDTLS workspaces contain mutable Eclipse/Buildship runtime state. + # Cache the installed servers, but create each JDTLS workspace locally. + path: | + ~/.serena/language_servers/static + !~/.serena/language_servers/static/EclipseJDTLS/workspaces/** key: language-servers-${{ runner.os }}-${{ matrix.batch }}-v1 restore-keys: | language-servers-${{ runner.os }}-${{ matrix.batch }}- From 0968bae6ebbb18acd6105631c78c475cf881fc36 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 8 Aug 2026 13:52:09 +0200 Subject: [PATCH 12/27] Enclose sub-prompts in tags --- CHANGELOG.md | 1 + src/serena/agent.py | 22 ++++++++++++------- .../config/prompt_templates/system_prompt.yml | 2 -- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 256dd973..c89dea24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Status of the `main` branch. Changes prior to the next official version change w failed `--project-from-cwd` auto-detection (#1773). - ProjectServer: Configure trusted hosts (local hosts only) when listening on localhost - SerenaDashboardTrayManager: Configure trusted hosts (local hosts only) + - Enclose sub-prompts in XML-like tags to make scopes explicit * CLI: - Fix: `start-mcp-server` help text for `--project-from-cwd` falsely promised a fallback to the CWD, which was diff --git a/src/serena/agent.py b/src/serena/agent.py index 2b70389a..6dd9954c 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -956,14 +956,21 @@ class SerenaAgent: result[tool_class.get_name_from_cls()] = new_tool_class.get_name_from_cls() return result - def _format_prompt(self, prompt_template: str) -> str: + def _format_prompt(self, prompt_template: str, tag: str | None = None, name : str | None = None) -> str: template = JinjaTemplate(prompt_template) - return template.render( + text = template.render( available_tools=self._exposed_tools.tool_names, available_markers=self._exposed_tools.tool_marker_names, tool_names=self._prompt_tool_names_mapping, ) + if tag is not None: + open_tag = f"<{tag}" + (f' name="{name}"' if name is not None else "") + ">" + close_tag = f"" + text = f"{open_tag}{text}{close_tag}" + + return text + def create_connection_prompt(self) -> str: """ Returns the bootstrap prompt to be sent at MCP connection time. @@ -997,8 +1004,8 @@ class SerenaAgent: self._project_prompt_status.mark_mode_prompts_as_provided(session_id) system_prompt = self.prompt_factory.create_system_prompt( - context_system_prompt=self._format_prompt(self._context.prompt), - mode_system_prompts=[self._format_prompt(mode.prompt) for mode in relevant_modes], + context_system_prompt=self._format_prompt(self._context.prompt, tag="context"), + mode_system_prompts=[self._format_prompt(mode.prompt, tag="mode", name=mode.name) for mode in relevant_modes], available_tools=available_tools.tool_names, available_markers=available_markers, global_memories_list=global_memories_str, @@ -1007,7 +1014,7 @@ class SerenaAgent: # provide the project activation message if it hasn't yet been provided if self._active_project is not None and not self._project_prompt_status.is_project_activation_message_already_provided(session_id): - system_prompt += "\n\n" + self.get_project_activation_message(session_id) + system_prompt += "\n\n" + self.get_project_activation_message(session_id) + "" elif self._project_activation_error: system_prompt += f"\n\nNo project is active ({self._project_activation_error})." @@ -1054,14 +1061,13 @@ class SerenaAgent: # add prompts for modes that were dynamically activated by the project modes_with_prompts = self._project_prompt_status.get_modes_with_prompts_to_be_provided_for_project_activation(session_id) if modes_with_prompts: - msg += "\nNewly applicable mode instructions:" for mode in modes_with_prompts: - msg += f"\n{mode.prompt}" + msg += f'\n{mode.prompt}' self._project_prompt_status.mark_mode_prompts_as_provided(session_id) # add project-specific prompt if proj.project_config.initial_prompt: - msg += f"\nProject-specific instructions:\n {proj.project_config.initial_prompt}" + msg += f"\n{proj.project_config.initial_prompt}" self._project_prompt_status.mark_project_activation_message_as_provided(session_id) diff --git a/src/serena/resources/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml index 78a7ad18..99604bf1 100644 --- a/src/serena/resources/config/prompt_templates/system_prompt.yml +++ b/src/serena/resources/config/prompt_templates/system_prompt.yml @@ -49,10 +49,8 @@ prompts: The context and modes of operation are described below. These determine how to interact with your user and which kinds of interactions are expected of you. - Context description: {{ context_system_prompt }} - Modes descriptions: {% for prompt in mode_system_prompts %} {{ prompt }} {% endfor %} From 548aea28a620a181a28ae2283cfdf4278d4d205c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 8 Aug 2026 13:56:19 +0200 Subject: [PATCH 13/27] Allow project initial prompt & newly activated dynamic mode prompts to use templating/variables Add helper method _format_prompt_tag --- CHANGELOG.md | 1 + src/serena/agent.py | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c89dea24..a5b0966e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Status of the `main` branch. Changes prior to the next official version change w - ProjectServer: Configure trusted hosts (local hosts only) when listening on localhost - SerenaDashboardTrayManager: Configure trusted hosts (local hosts only) - Enclose sub-prompts in XML-like tags to make scopes explicit + - Allow initial project prompts and project-specific newly activated modes to use templating * CLI: - Fix: `start-mcp-server` help text for `--project-from-cwd` falsely promised a fallback to the CWD, which was diff --git a/src/serena/agent.py b/src/serena/agent.py index 6dd9954c..545bc599 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -956,7 +956,12 @@ class SerenaAgent: result[tool_class.get_name_from_cls()] = new_tool_class.get_name_from_cls() return result - def _format_prompt(self, prompt_template: str, tag: str | None = None, name : str | None = None) -> str: + def _format_prompt_tag(self, text: str, tag: str, tag_name_attr: str | None = None) -> str: + open_tag = f"<{tag}" + (f' name="{tag_name_attr}"' if tag_name_attr is not None else "") + ">" + close_tag = f"" + return f"{open_tag}\n{text.strip()}\n{close_tag}" + + def _format_prompt(self, prompt_template: str, tag: str | None = None, tag_name_attr: str | None = None) -> str: template = JinjaTemplate(prompt_template) text = template.render( available_tools=self._exposed_tools.tool_names, @@ -965,9 +970,7 @@ class SerenaAgent: ) if tag is not None: - open_tag = f"<{tag}" + (f' name="{name}"' if name is not None else "") + ">" - close_tag = f"" - text = f"{open_tag}{text}{close_tag}" + text = self._format_prompt_tag(text, tag=tag, tag_name_attr=tag_name_attr) return text @@ -1005,7 +1008,7 @@ class SerenaAgent: system_prompt = self.prompt_factory.create_system_prompt( context_system_prompt=self._format_prompt(self._context.prompt, tag="context"), - mode_system_prompts=[self._format_prompt(mode.prompt, tag="mode", name=mode.name) for mode in relevant_modes], + mode_system_prompts=[self._format_prompt(mode.prompt, tag="mode", tag_name_attr=mode.name) for mode in relevant_modes], available_tools=available_tools.tool_names, available_markers=available_markers, global_memories_list=global_memories_str, @@ -1014,11 +1017,11 @@ class SerenaAgent: # provide the project activation message if it hasn't yet been provided if self._active_project is not None and not self._project_prompt_status.is_project_activation_message_already_provided(session_id): - system_prompt += "\n\n" + self.get_project_activation_message(session_id) + "" + system_prompt += "\n\n" + self._format_prompt_tag(self.get_project_activation_message(session_id), tag="active-project") elif self._project_activation_error: system_prompt += f"\n\nNo project is active ({self._project_activation_error})." - return system_prompt + return self._format_prompt_tag(system_prompt, tag="serena") def get_project_activation_message(self, session_id: str) -> str: """ @@ -1062,12 +1065,12 @@ class SerenaAgent: modes_with_prompts = self._project_prompt_status.get_modes_with_prompts_to_be_provided_for_project_activation(session_id) if modes_with_prompts: for mode in modes_with_prompts: - msg += f'\n{mode.prompt}' + msg += self._format_prompt(mode.prompt, tag="mode", tag_name_attr=mode.name) + "\n" self._project_prompt_status.mark_mode_prompts_as_provided(session_id) # add project-specific prompt if proj.project_config.initial_prompt: - msg += f"\n{proj.project_config.initial_prompt}" + msg += "\n" + self._format_prompt(proj.project_config.initial_prompt, tag="project-instructions") self._project_prompt_status.mark_project_activation_message_as_provided(session_id) From d3a8e9b5658a9721596142cbc7f2dedabb585859 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 8 Aug 2026 14:11:41 +0200 Subject: [PATCH 14/27] Improve project activation message --- src/serena/agent.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 545bc599..b9649fcf 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -1041,13 +1041,13 @@ class SerenaAgent: # provide basic project information (name, location, languages, encoding) if proj.is_newly_created: - msg = f"Created and activated a new project with name '{proj.project_name}' at {proj.project_root}. " + msg = f"Created and activated a new project with name '{proj.project_name}' at {proj.project_root}.\n" else: - msg = f"The project with name '{proj.project_name}' at {proj.project_root} is activated." + msg = f"The project with name '{proj.project_name}' at {proj.project_root} is activated.\n" if self._language_backend == LanguageBackend.LSP: - languages_str = ", ".join([lang.value for lang in proj.project_config.language_servers]) - msg += f"\nProgramming languages: {languages_str}." - msg += f"File encoding: {proj.project_config.encoding}." + language_servers_str = ", ".join([ls.value for ls in proj.project_config.language_servers]) + msg += f"Active language servers: {language_servers_str}.\n" + msg += f"File encoding: {proj.project_config.encoding}.\n" # add list of memories (if memories are enabled) include_memories = self._active_tools.contains_tool_class(ReadMemoryTool) @@ -1055,11 +1055,11 @@ class SerenaAgent: project_memories = proj.memory_manager.list_project_memories() if project_memories: msg += ( - f"\n{json.dumps(project_memories.to_dict())}\n" - + "Use the `read_memory` tool to read these memories later if they are relevant to the task." + f"{json.dumps(project_memories.to_dict())}\n" + + f"Use the `{ReadMemoryTool.get_name_from_cls()}` tool to read these memories later if they are relevant to the task.\n" ) elif self._active_tools.contains_tool_class(OnboardingTool): - msg += "Onboarding has not been performed yet, you should call Serena's `onboarding` tool now to set up project memories." + msg += f"Onboarding has not been performed yet. Ask the user whether to perform onboarding via the `{OnboardingTool.get_name_from_cls()}` tool.\n" # add prompts for modes that were dynamically activated by the project modes_with_prompts = self._project_prompt_status.get_modes_with_prompts_to_be_provided_for_project_activation(session_id) From ed401769b514c71200bd610022fce37ad1859eb6 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 8 Aug 2026 16:57:40 +0200 Subject: [PATCH 15/27] ProjectServer: Use general headless mode config overrides --- src/serena/project_server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/serena/project_server.py b/src/serena/project_server.py index c2cbcd65..0debaac0 100644 --- a/src/serena/project_server.py +++ b/src/serena/project_server.py @@ -55,9 +55,7 @@ class ProjectServer: if port is None: port = self.PORT - serena_config = SerenaConfig.from_config_file() - serena_config.gui_log_window = False - serena_config.web_dashboard = False + serena_config = SerenaConfig.from_config_file().with_headless_mode_overrides() serena_config.language_backend = LanguageBackend.LSP self._agent = SerenaAgent(serena_config=serena_config) From bb06ed05dd4e7eb101983e31913fe15c68be4f18 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 8 Aug 2026 17:43:08 +0200 Subject: [PATCH 16/27] Support embed_memory function in prompt templates Apply it in Serena's own project prompt --- .serena/project.yml | 2 +- src/serena/agent.py | 28 +++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.serena/project.yml b/.serena/project.yml index 7e19e987..1420123a 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -71,7 +71,7 @@ included_optional_tools: [] # initial prompt for the project. It will always be given to the LLM upon activating the project # (contrary to the memories, which are loaded on demand). initial_prompt: | - ALWAYS read the `critical_info` memory before doing anything else. It contains absolutely critical information about the project. + {{ embed_memory("critical_info") }} # the encoding used by text files in the project # For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings diff --git a/src/serena/agent.py b/src/serena/agent.py index b9649fcf..82376cb4 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -962,11 +962,20 @@ class SerenaAgent: return f"{open_tag}\n{text.strip()}\n{close_tag}" def _format_prompt(self, prompt_template: str, tag: str | None = None, tag_name_attr: str | None = None) -> str: + def embed_memory(memory_name: str) -> str: + try: + memory_manager = self._get_memory_manager() + return self._format_prompt_tag(memory_manager.load_memory(memory_name), tag="memory", tag_name_attr=memory_name) + except Exception as e: + log.error("Tried to embed memory '%s' but failed to load it: %s", memory_name, e) + return "" + template = JinjaTemplate(prompt_template) text = template.render( available_tools=self._exposed_tools.tool_names, available_markers=self._exposed_tools.tool_marker_names, tool_names=self._prompt_tool_names_mapping, + embed_memory=embed_memory, ) if tag is not None: @@ -982,6 +991,21 @@ class SerenaAgent: """ return self.prompt_factory.create_connection_prompt() + def _create_global_memory_manager(self) -> MemoryManager: + """ + :return: a memory manager for global memories only (no project memories) + """ + return MemoryManager(serena_data_folder=None, read_only_memory_patterns=self.serena_config.read_only_memory_patterns) + + def _get_memory_manager(self) -> MemoryManager: + """ + :return: the memory manager for the active project (if any) or a global memory manager if no project is active + """ + if self._active_project is not None: + return self._active_project.memory_manager + else: + return self._create_global_memory_manager() + def create_system_prompt(self, session_id: str = "global") -> str: """ Returns the 'Serena Instructions Manual', i.e. Serena's system prompt. @@ -991,9 +1015,7 @@ class SerenaAgent: """ available_tools = self._active_tools available_markers = available_tools.tool_marker_names - global_memories = MemoryManager( - serena_data_folder=None, read_only_memory_patterns=self.serena_config.read_only_memory_patterns - ).list_global_memories() + global_memories = self._create_global_memory_manager().list_global_memories() global_memories_str = dict_string(global_memories.to_dict()) if len(global_memories) > 0 else "" log.info("Generating system prompt with available_tools=(see active tools), available_markers=%s", available_markers) From 69d14891ca359443c99ff85c71956ca410e5976d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 8 Aug 2026 17:46:15 +0200 Subject: [PATCH 17/27] Minor refactoring: Rename _format_prompt to _render_prompt, adding docstring --- CHANGELOG.md | 4 +++- src/serena/agent.py | 22 ++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 755801f7..75148ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ Status of the `main` branch. Changes prior to the next official version change w - ProjectServer: Configure trusted hosts (local hosts only) when listening on localhost - SerenaDashboardTrayManager: Configure trusted hosts (local hosts only) - Enclose sub-prompts in XML-like tags to make scopes explicit - - Allow initial project prompts and project-specific newly activated modes to use templating + - Prompts and prompt templates: + - Allow initial project prompts and project-specific newly activated modes to use templating + - Support function `embed_memory` in prompt templates to inline a memory's contents * CLI: - Fix: `start-mcp-server` help text for `--project-from-cwd` falsely promised a fallback to the CWD, which was diff --git a/src/serena/agent.py b/src/serena/agent.py index 82376cb4..898bfcab 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -956,12 +956,22 @@ class SerenaAgent: result[tool_class.get_name_from_cls()] = new_tool_class.get_name_from_cls() return result - def _format_prompt_tag(self, text: str, tag: str, tag_name_attr: str | None = None) -> str: + @staticmethod + def _format_prompt_tag(text: str, tag: str, tag_name_attr: str | None = None) -> str: open_tag = f"<{tag}" + (f' name="{tag_name_attr}"' if tag_name_attr is not None else "") + ">" close_tag = f"" return f"{open_tag}\n{text.strip()}\n{close_tag}" - def _format_prompt(self, prompt_template: str, tag: str | None = None, tag_name_attr: str | None = None) -> str: + def _render_prompt(self, prompt_template: str, tag: str | None = None, tag_name_attr: str | None = None) -> str: + """ + Renders the given prompt template, providing the necessary variables and functions + + :param prompt_template: the template text (jinja2) to render + :param tag: if not None, wraps the rendered prompt text in a tag + :param tag_name_attr: for the case where tag is not None, specifies the value of the "name" attribute of the tag + :return: the rendered prompt + """ + def embed_memory(memory_name: str) -> str: try: memory_manager = self._get_memory_manager() @@ -1029,8 +1039,8 @@ class SerenaAgent: self._project_prompt_status.mark_mode_prompts_as_provided(session_id) system_prompt = self.prompt_factory.create_system_prompt( - context_system_prompt=self._format_prompt(self._context.prompt, tag="context"), - mode_system_prompts=[self._format_prompt(mode.prompt, tag="mode", tag_name_attr=mode.name) for mode in relevant_modes], + context_system_prompt=self._render_prompt(self._context.prompt, tag="context"), + mode_system_prompts=[self._render_prompt(mode.prompt, tag="mode", tag_name_attr=mode.name) for mode in relevant_modes], available_tools=available_tools.tool_names, available_markers=available_markers, global_memories_list=global_memories_str, @@ -1087,12 +1097,12 @@ class SerenaAgent: modes_with_prompts = self._project_prompt_status.get_modes_with_prompts_to_be_provided_for_project_activation(session_id) if modes_with_prompts: for mode in modes_with_prompts: - msg += self._format_prompt(mode.prompt, tag="mode", tag_name_attr=mode.name) + "\n" + msg += self._render_prompt(mode.prompt, tag="mode", tag_name_attr=mode.name) + "\n" self._project_prompt_status.mark_mode_prompts_as_provided(session_id) # add project-specific prompt if proj.project_config.initial_prompt: - msg += "\n" + self._format_prompt(proj.project_config.initial_prompt, tag="project-instructions") + msg += "\n" + self._render_prompt(proj.project_config.initial_prompt, tag="project-instructions") self._project_prompt_status.mark_project_activation_message_as_provided(session_id) From c9fa15741e8d075cc4746f3faf89fc1ed6c9b375 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 8 Aug 2026 17:57:21 +0200 Subject: [PATCH 18/27] Add documentation on prompt templating in Serena --- docs/02-usage/050_configuration.md | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 659f73b8..dd3b8a8d 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -155,6 +155,43 @@ You can manage modes using the `mode` command, serena mode edit serena mode delete +(prompt-templates)= +## Prompt Templates + +All prompts that Serena provides to the LLM are [Jinja2](https://jinja.palletsprojects.com/) templates. +Templating applies to + + * **Serena's system prompt** (the "Serena Instructions Manual"), which is defined in the prompt template `system_prompt` + (see [Custom Prompts](custom-prompts) for how to override it), + * **context and mode prompts**, i.e. the `prompt` field in context and mode definition files, and + * **the project prompt**, i.e. `initial_prompt` in `project.yml`, which is provided to the LLM upon project activation. + +Templating allows prompts to adapt to the active configuration; for instance, a mode prompt can mention a tool +only if that tool is actually available in the current session. + +### Variables and Functions + +The following variables can be used in all of the above templates: + +| Variable | Description | +|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `available_tools` | the list of names of the tools that are currently exposed to the LLM. Use it to include content conditionally, e.g. `{% if 'replace_content' in available_tools %}…{% endif %}`. | +| `available_markers` | the list of names of the tool markers (tool categories, e.g. `ToolMarkerSymbolicRead`) for which at least one tool is exposed; useful for conditioning on entire groups of tools. | +| `tool_names` | a mapping from canonical tool names to effective tool names, which accounts for legacy tool renames as well as for tools being functionally replaced due to the active language backend (e.g. `find_symbol` being replaced by `jet_brains_find_symbol` when the JetBrains backend is active). Prefer `{{ tool_names['find_symbol'] }}` over hard-coded names. | + +Context, mode and project prompts (but not Serena's system prompt) additionally support the following function: + +| Function | Description | +|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `embed_memory(name)` | embeds the content of the memory with the given name, wrapped in a tag ``. Use this to inline knowledge that shall always be provided to the LLM rather than being loaded on demand. If the memory cannot be loaded, an error is logged and nothing is rendered. | + +For example, a project can inline its coding conventions from a memory into the project prompt: + +```yaml +initial_prompt: | + {{ embed_memory("coding_conventions") }} +``` + ## Advanced Configuration For advanced users, Serena's configuration can be further customized. @@ -1247,6 +1284,7 @@ Supported settings: | `yaml_language_server_version` | `1.19.2` | Override the npm package version Serena installs when `ls_path` is not set. | | `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | +(custom-prompts)= ### Custom Prompts All of Serena's prompts can be fully customized. From 436fbe4ac3428224eeba190dfb0f24a59cb4e14e Mon Sep 17 00:00:00 2001 From: Kobi Hikri Date: Sat, 8 Aug 2026 19:44:10 +0300 Subject: [PATCH 19/27] ci: attach provenance and SBOM attestations to the published image (#1775) --- .github/workflows/docker.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5513b734..2d5e17bd 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -52,6 +52,8 @@ jobs: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} + provenance: mode=max + sbom: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha From de2f1a658a2b41aa70095aabda7dc58c69549087 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:30:55 +0000 Subject: [PATCH 20/27] Bump pillow from 12.2.0 to 12.3.0 in the uv group across 1 directory Bumps the uv group with 1 update in the / directory: [pillow](https://github.com/python-pillow/Pillow). Updates `pillow` from 12.2.0 to 12.3.0 - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/12.2.0...12.3.0) --- updated-dependencies: - dependency-name: pillow dependency-version: 12.3.0 dependency-type: indirect dependency-group: uv ... Signed-off-by: dependabot[bot] --- uv.lock | 139 +++++++++++++++++++++++--------------------------------- 1 file changed, 58 insertions(+), 81 deletions(-) diff --git a/uv.lock b/uv.lock index a73af24d..536cb571 100644 --- a/uv.lock +++ b/uv.lock @@ -1650,89 +1650,66 @@ wheels = [ [[package]] name = "pillow" -version = "12.2.0" +version = "12.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] [[package]] From 76b94f327428aa9b195e95b1b09db8a47d3091f5 Mon Sep 17 00:00:00 2001 From: Evangelos Karatzas <32259775+vagkaratzas@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:58:46 +0100 Subject: [PATCH 21/27] Add Nextflow language server support (#1815) The Nextflow language server schedules a debounced (1s) AST update on every didOpen/didChange, and LanguageService.references is the only request that does not await it -- documentSymbol, codeLens, documentLink and semanticTokensFull all do. A references request issued inside that window races the recompile of the file it asks about and can return an empty list, which is what the macOS CI runner hit. Send a documentSymbol request for the same file first, which blocks server-side until the pending update has been applied. Also compare reference paths using the platform separator in the tests, since relativePath is OS-native and the hardcoded forward slashes failed on Windows. --- .github/workflows/pytest.yml | 2 +- CHANGELOG.md | 3 + README.md | 2 +- docs/01-about/020_programming-languages.md | 5 + pyproject.toml | 1 + .../update_downloaded_dependency_hashes.py | 2 + src/serena/hooks.py | 1 + src/serena/resources/project.template.yml | 15 +- .../nextflow_language_server.py | 337 ++++++++++++++++++ src/solidlsp/ls_config.py | 14 + .../downloaded_dependency_hashes.json | 1 + .../repos/nextflow/test_repo/.gitignore | 3 + .../repos/nextflow/test_repo/main.nf | 23 ++ .../nextflow/test_repo/modules/greet/main.nf | 27 ++ .../nextflow/test_repo/modules/util/main.nf | 7 + .../repos/nextflow/test_repo/nextflow.config | 8 + test/solidlsp/nextflow/test_nextflow_basic.py | 87 +++++ 17 files changed, 529 insertions(+), 9 deletions(-) create mode 100644 src/solidlsp/language_servers/nextflow_language_server.py create mode 100644 test/resources/repos/nextflow/test_repo/.gitignore create mode 100644 test/resources/repos/nextflow/test_repo/main.nf create mode 100644 test/resources/repos/nextflow/test_repo/modules/greet/main.nf create mode 100644 test/resources/repos/nextflow/test_repo/modules/util/main.nf create mode 100644 test/resources/repos/nextflow/test_repo/nextflow.config create mode 100644 test/solidlsp/nextflow/test_nextflow_basic.py diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 551dedf3..ac700fcf 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -66,7 +66,7 @@ jobs: # test can fall through to "no batch". To add/rebalance: edit a MARKERS_* var, and (for a new # group) add a case branch below + the batch name to the matrix list. env: - MARKERS_JVM: "java or csharp or fsharp or kotlin or scala or groovy or clojure or bsl" + MARKERS_JVM: "java or csharp or fsharp or kotlin or scala or groovy or clojure or bsl or nextflow" MARKERS_NATIVE: "cpp or rust or go or zig or pascal or swift" # swift is macOS-only (skips elsewhere via the guard) # 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fea982a..2cd22234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ Status of the `main` branch. Changes prior to the next official version change w * Language Servers: - 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 + [Nextflow language server](https://github.com/nextflow-io/language-server); the JAR is downloaded + automatically, a Java 17+ runtime is required - Add `python_basedpyright` as an alternative Python language server - Java/JDTLS: stop downloading and loading the unused IntelliCode completion-ranking bundle; the retired `intellicode_version`, `intellicode_xmx` and `intellicode_xms` settings remain accepted but are ignored #1821 diff --git a/README.md b/README.md index 70e9d4c7..a7941e37 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, 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, 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 884cc4b7..87742337 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -115,6 +115,11 @@ Some languages require additional installations or setup steps, as noted. * **mSL** (mIRC Scripting Language) (auto-installed; no external dependencies required — uses a custom pygls-based LSP server shipped with Serena; supports document symbols, workspace symbols, references, and go-to-definition for aliases, events, menus, dialogs, and CTCP handlers in `.mrc` files) +* **Nextflow** + (uses the official [Nextflow language server](https://github.com/nextflow-io/language-server), which is automatically + downloaded; requires a Java 17+ runtime, discovered via `ls_specific_settings.nextflow.java_home`, `JAVA_HOME` or `java` on PATH; + covers `.nf` scripts — Nextflow `.config` files are not treated as source files, since the language server reports no symbols for them; + processes, workflows and functions are reported under their declared name, e.g. `GREET` for `process GREET`) * **Nix** (requires nixd installation) * **OCaml** diff --git a/pyproject.toml b/pyproject.toml index 6731feeb..e9df07a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -345,6 +345,7 @@ markers = [ "zig: language server running for Zig", "lua: language server running for Lua", "luau: language server running for Luau", + "nextflow: language server running for Nextflow", "nix: language server running for Nix", "dart: language server running for Dart", "erlang: language server running for Erlang", diff --git a/scripts/update_downloaded_dependency_hashes.py b/scripts/update_downloaded_dependency_hashes.py index 02e56bda..8e4136f0 100644 --- a/scripts/update_downloaded_dependency_hashes.py +++ b/scripts/update_downloaded_dependency_hashes.py @@ -1,9 +1,11 @@ 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 if __name__ == "__main__": logging.configure() EclipseJDTLS.DependencyProvider.update_dep_hashes() + NextflowLanguageServer.DependencyProvider.update_dep_hashes() KotlinLanguageServer.DependencyProvider.update_dep_hashes() diff --git a/src/serena/hooks.py b/src/serena/hooks.py index 6c2aafa2..cd1fb84f 100644 --- a/src/serena/hooks.py +++ b/src/serena/hooks.py @@ -323,6 +323,7 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook): ".lua", ".m", ".matlab", + ".nf", ".php", ".proto", ".ps1", diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index 3d22e3d6..5b1d0293 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -9,13 +9,14 @@ project_name: "project_name" # gdscript go groovy haskell haxe # hlsl html java json julia # kotlin latex lean4 lua luau -# markdown matlab msl 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 yaml zig +# 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 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) diff --git a/src/solidlsp/language_servers/nextflow_language_server.py b/src/solidlsp/language_servers/nextflow_language_server.py new file mode 100644 index 00000000..d39cc834 --- /dev/null +++ b/src/solidlsp/language_servers/nextflow_language_server.py @@ -0,0 +1,337 @@ +""" +Provides Nextflow specific instantiation of the LanguageServer class, using the official +Nextflow language server (https://github.com/nextflow-io/language-server), which is +distributed as a fat JAR and requires a Java runtime. + +You can configure the following options in ls_specific_settings (in serena_config.yml): + + ls_specific_settings: + nextflow: + ls_path: '/path/to/language-server-all.jar' # use an existing JAR instead of downloading one + nextflow_ls_version: '26.04.3' # JAR version to download (default: bundled version) + java_home: '/path/to/jdk' # JDK to run the JAR with (default: JAVA_HOME or PATH) + jvm_options: ['-Xmx2G'] # JVM options for the language server process + exclude_patterns: ['work', '.nextflow'] # workspace paths the language server shall ignore +""" + +import logging +import os +import shutil +import threading +from collections.abc import Hashable +from pathlib import Path + +from overrides import override + +from solidlsp.dependency_provider import DownloadedDependency, DownloadedDependencyHashDatabase +from solidlsp.ls import ( + LanguageServerDependencyProvider, + LanguageServerDependencyProviderSinglePath, + RawDocumentSymbol, + SolidLanguageServer, +) +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_exceptions import SolidLSPException +from solidlsp.lsp_protocol_handler import lsp_types +from solidlsp.settings import SolidLSPSettings + +log = logging.getLogger(__name__) + +NEXTFLOW_LS_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") + +# NOTE: after bumping this, re-run scripts/update_downloaded_dependency_hashes.py and commit the +# updated src/solidlsp/resources/downloaded_dependency_hashes.json. +DEFAULT_NEXTFLOW_LS_VERSION = "26.04.3" + +NEXTFLOW_LS_JAR_NAME = "language-server-all.jar" + +#: the Nextflow language server is built for Java 17+ +MIN_JDK_VERSION = 17 + +#: paths that hold Nextflow's own outputs rather than pipeline sources; scanning them is pure overhead +DEFAULT_EXCLUDE_PATTERNS = ["work", ".nextflow"] + +#: the server scans the workspace only when a configuration change asks it to, and it compares the new +#: configuration against its own defaults (see LanguageServerConfiguration.defaults() upstream, where the +#: error reporting mode is "warnings"), so the settings we push must differ in at least one of the keys +#: that trigger a scan +_SCAN_TRIGGERING_ERROR_REPORTING_MODE = "errors" + +#: upper bound for the initial workspace scan, which compiles every .nf file in the project +_WORKSPACE_SCAN_TIMEOUT = 180.0 + +#: declaration keywords the server prepends to symbol names (see ScriptSymbolProvider.getSymbolName upstream) +_SYMBOL_NAME_PREFIXES = frozenset(("process", "workflow", "function", "record", "enum")) + + +class NextflowLanguageServer(SolidLanguageServer): + """ + Provides Nextflow specific instantiation of the LanguageServer class. + """ + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + """ + Creates a Nextflow Language Server instance. This class is not meant to be instantiated directly. + Use LanguageServer.create() instead. + """ + super().__init__( + config, + repository_root_path, + None, + "nextflow", + solidlsp_settings, + ) + + # The workspace scan is asynchronous and reported via $/progress under the "initialize" token; + # requests answered before it ends see an empty AST cache. + self._scan_complete = threading.Event() + self._active_progress_tokens: set[str] = set() + self._progress_lock = threading.Lock() + + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + @classmethod + def _create_dep_language_server(cls, version: str | None = None) -> DownloadedDependency: + version = version or DEFAULT_NEXTFLOW_LS_VERSION + return DownloadedDependency( + url=f"https://github.com/nextflow-io/language-server/releases/download/v{version}/{NEXTFLOW_LS_JAR_NAME}", + allowed_hosts=NEXTFLOW_LS_ALLOWED_HOSTS, + ) + + @classmethod + def update_dep_hashes(cls) -> None: + deps = [cls._create_dep_language_server()] + with DownloadedDependencyHashDatabase.get_instance().update_context() as db: + for dep in deps: + db.update(dep) + + def _get_or_install_core_dependency(self) -> str: + version = self._custom_settings.get("nextflow_ls_version", DEFAULT_NEXTFLOW_LS_VERSION) + install_dir = os.path.join(self._ls_resources_dir, f"nextflow_language_server-{version}") + jar_path = os.path.join(install_dir, NEXTFLOW_LS_JAR_NAME) + + if not os.path.exists(jar_path): + os.makedirs(install_dir, exist_ok=True) + log.info("Downloading Nextflow Language Server %s ...", version) + self._create_dep_language_server(version).download_to(jar_path) + + if not os.path.exists(jar_path): + raise FileNotFoundError(f"Nextflow Language Server JAR not found at {jar_path}") + return jar_path + + def _create_launch_command(self, core_path: str) -> list[str]: + java_exe = self._resolve_java() + jvm_options = self._custom_settings.get("jvm_options", []) + if not isinstance(jvm_options, list): + log.warning("The 'jvm_options' setting should be a list of strings. Ignoring the provided value: %s", jvm_options) + jvm_options = [] + return [java_exe, *jvm_options, "-jar", core_path] + + def _resolve_java(self) -> str: + """ + Locates a ``java`` executable (``java_home`` setting -> ``JAVA_HOME`` -> PATH) and verifies + that it is new enough to run the language server. + + :return: the path to the java executable + """ + java_exe_name = "java.exe" if os.name == "nt" else "java" + + java_exe: str | None = None + if explicit_home := self._custom_settings.get("java_home"): + candidate = str(Path(explicit_home) / "bin" / java_exe_name) + if not os.path.exists(candidate): + raise SolidLSPException( + f"java_home='{explicit_home}' is invalid: '{candidate}' does not exist. " + f"Set ls_specific_settings.nextflow.java_home to a JDK home that contains bin/{java_exe_name}." + ) + java_exe = candidate + elif env_home := os.environ.get("JAVA_HOME"): + candidate = str(Path(env_home) / "bin" / java_exe_name) + if os.path.exists(candidate): + java_exe = candidate + else: + log.warning("JAVA_HOME='%s' invalid (no '%s'), falling back to PATH.", env_home, candidate) + if java_exe is None: + java_exe = shutil.which("java") + if java_exe is None: + raise SolidLSPException( + "Could not locate a Java installation for the Nextflow language server. " + "Set ls_specific_settings.nextflow.java_home, set the JAVA_HOME environment variable, " + f"or ensure 'java' is on PATH. Required: JDK {MIN_JDK_VERSION}+." + ) + + # reuse JDTLS's JVM interrogation, which reports the real java.home even for stub launchers + from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS + + java_home, major_version = EclipseJDTLS.DependencyProvider._inspect_java(java_exe) + if major_version < MIN_JDK_VERSION: + raise SolidLSPException( + f"The Nextflow language server requires JDK {MIN_JDK_VERSION}+ but '{java_exe}' is JDK {major_version} " + f"(java.home={java_home}). Install a newer JDK and update ls_specific_settings.nextflow.java_home " + "or JAVA_HOME." + ) + log.info("Using JDK %d at %s for the Nextflow language server", major_version, java_exe) + return java_exe + + def _create_base_initialize_params(self) -> dict: + """ + Returns the initialize params for the Nextflow Language Server. + """ + return { + "capabilities": { + "textDocument": { + "synchronization": {"dynamicRegistration": True, "didSave": True}, + "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": False}}, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "hierarchicalDocumentSymbolSupport": True, + }, + "documentLink": {"dynamicRegistration": True}, + "callHierarchy": {"dynamicRegistration": True}, + "rename": {"dynamicRegistration": True, "prepareSupport": True}, + "publishDiagnostics": {"relatedInformation": True}, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + "symbol": {"dynamicRegistration": True, "symbolKind": {"valueSet": list(range(1, 27))}}, + }, + "window": {"workDoneProgress": True}, + }, + } + + def _create_configuration_settings(self) -> dict: + """ + Returns the settings pushed via workspace/didChangeConfiguration, which is what makes the server + scan the workspace (see ``_start_server``). + """ + exclude_patterns = self._custom_settings.get("exclude_patterns", DEFAULT_EXCLUDE_PATTERNS) + if not isinstance(exclude_patterns, list): + log.warning("The 'exclude_patterns' setting should be a list of strings. Ignoring the provided value: %s", exclude_patterns) + exclude_patterns = DEFAULT_EXCLUDE_PATTERNS + return { + "nextflow": { + "debug": False, + # deliberately not the server's default of "warnings": the value must differ from the + # defaults for the configuration change to trigger the workspace scan + "errorReportingMode": _SCAN_TRIGGERING_ERROR_REPORTING_MODE, + "files": {"exclude": exclude_patterns}, + } + } + + def _start_server(self) -> None: + """ + Starts the Nextflow Language Server. + """ + + def do_nothing(params: dict) -> None: + return + + def window_log_message(msg: dict) -> None: + log.info(f"LSP: window/logMessage: {msg}") + + def work_done_progress_create(params: dict) -> dict: + token = str(params.get("token", "")) + with self._progress_lock: + self._active_progress_tokens.add(token) + self._scan_complete.clear() + return {} + + def progress_handler(params: dict) -> None: + token = str(params.get("token", "")) + value = params.get("value", {}) + kind = value.get("kind") + if kind == "begin": + with self._progress_lock: + self._active_progress_tokens.add(token) + self._scan_complete.clear() + log.info("Nextflow LS progress [%s]: started - %s", token, value.get("title", "")) + elif kind == "report": + log.debug("Nextflow LS progress [%s]: %s", token, value.get("message", "")) + elif kind == "end": + with self._progress_lock: + self._active_progress_tokens.discard(token) + if not self._active_progress_tokens: + self._scan_complete.set() + log.info("Nextflow LS progress [%s]: ended", token) + + self.server.on_request("client/registerCapability", 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_handler) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + log.info("Starting Nextflow server process") + self.server.start() + + log.info("Sending initialize request from LSP client to LSP server and awaiting response") + init_response = self.server.send.initialize(self._create_initialize_params()) + + capabilities = init_response["capabilities"] + assert "textDocumentSync" in capabilities, "Server must support textDocumentSync" + assert "hoverProvider" in capabilities, "Server must support hover" + assert "completionProvider" in capabilities, "Server must support code completion" + assert "definitionProvider" in capabilities, "Server must support go to definition" + assert "referencesProvider" in capabilities, "Server must support find references" + assert "documentSymbolProvider" in capabilities, "Server must support document symbols" + assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols" + + self.server.notify.initialized({}) + + # The server does not index anything on `initialized`; it (re-)scans the workspace only when a + # configuration change arrives whose error reporting mode, exclude patterns or plugin registry + # URL differ from what it currently holds. Without this notification, every symbol request + # answers with an empty result. + self._scan_complete.clear() + self.server.notify.workspace_did_change_configuration({"settings": self._create_configuration_settings()}) + + log.info("Waiting for the Nextflow workspace scan to complete...") + if self._scan_complete.wait(timeout=_WORKSPACE_SCAN_TIMEOUT): + log.info("Nextflow LS ready") + else: + log.warning("Nextflow LS did not signal scan completion within %.0fs; proceeding anyway", _WORKSPACE_SCAN_TIMEOUT) + + @override + def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None: + """ + Flushes the server's pending recompile before asking for references. + + Every ``didOpen``/``didChange`` schedules a debounced (1s) update of the file's AST, and + ``LanguageService.references`` is the one request that does not await it (``documentSymbol``, + ``codeLens``, ``documentLink`` and ``semanticTokensFull`` all do). A references request issued + within the debounce window therefore races the recompile of the very file it asks about and can + come back empty. Sending a ``documentSymbol`` request for the same file first is a cheap way to + block until the update has been applied. + """ + self.server.send.document_symbol({"textDocument": {"uri": self._resolve_file_uri(relative_file_path)}}) + return super()._send_references_request(relative_file_path, line, column) + + @override + def _get_wait_time_for_cross_file_referencing(self) -> float: + """Small safety buffer; the actual synchronisation happens in _send_references_request.""" + return 1.0 + + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + """ + Strips the declaration keyword the language server prefixes to every symbol name + ("process GREET", "workflow SAY_HELLO", "function foo"), so that symbols can be addressed + by the name they are written with in the source. The implicit entry workflow keeps its + "" placeholder name, which is what the server calls it. + """ + name = symbol["name"] + prefix, sep, rest = name.partition(" ") + if sep and prefix in _SYMBOL_NAME_PREFIXES: + return rest + return name diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 25a9f720..d24ff2f0 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -185,6 +185,12 @@ 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 """ + 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 + it reports no symbols for them, so they are not treated as source files here). + Automatically downloads the language server JAR; requires Java 17+ (JAVA_HOME or 'java' on PATH). + """ # 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""" @@ -581,6 +587,10 @@ class LanguageServerId(str, Enum): return FilenameMatcher(".gd", ".gdscript") case self.QML: return FilenameMatcher(".qml") + 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 + return FilenameMatcher(".nf") case self.HTML: return FilenameMatcher(".html", ".htm") case self.SCSS: @@ -876,6 +886,10 @@ class LanguageServerId(str, Enum): from solidlsp.language_servers.qml_language_server import QmlLanguageServer return QmlLanguageServer + case self.NEXTFLOW: + from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer + + return NextflowLanguageServer case self.HTML: from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer diff --git a/src/solidlsp/resources/downloaded_dependency_hashes.json b/src/solidlsp/resources/downloaded_dependency_hashes.json index c6602add..d1006ff8 100644 --- a/src/solidlsp/resources/downloaded_dependency_hashes.json +++ b/src/solidlsp/resources/downloaded_dependency_hashes.json @@ -5,6 +5,7 @@ "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-arm64-1.54.0-923.vsix": "e2bb22c427d90da8dbb1afff72ff1e2dce38d50b76deb02d7bc313a330a1330c", "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-linux-x64-1.54.0-923.vsix": "9d4b15da54e25a0192f9bac073f086c015397d3676623b68dbf83a5dbaf5132b", "https://github.com/redhat-developer/vscode-java/releases/download/v1.54.0/java-win32-x64-1.54.0-923.vsix": "66f3914987edeccfee8a2558470e0fde4f8c4154232ff4baa5d73373ebc819d4", + "https://github.com/nextflow-io/language-server/releases/download/v26.04.3/language-server-all.jar": "20cfa34f6e202d6b8babd8d786202ce00e0d39b70ccec3290e2ab3fbd02bc016", "https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-win-x64.zip": "2806c2bd4810bd8e7ccc27d8c0ca4a5232a1c4f26ea1f4ba40e578b60860ccad", "https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-linux-x64.zip": "dc0ed2e70cb0d61fdabb26aefce8299b7a75c0dcfffb9413715e92caec6e83ec", "https://download-cdn.jetbrains.com/kotlin-lsp/261.13587.0/kotlin-lsp-261.13587.0-linux-aarch64.zip": "d1dceb000fe06c5e2c30b95e7f4ab01d05101bd03ed448167feeb544a9f1d651", diff --git a/test/resources/repos/nextflow/test_repo/.gitignore b/test/resources/repos/nextflow/test_repo/.gitignore new file mode 100644 index 00000000..3a54819e --- /dev/null +++ b/test/resources/repos/nextflow/test_repo/.gitignore @@ -0,0 +1,3 @@ +work/ +.nextflow/ +.nextflow.log* diff --git a/test/resources/repos/nextflow/test_repo/main.nf b/test/resources/repos/nextflow/test_repo/main.nf new file mode 100644 index 00000000..d391f217 --- /dev/null +++ b/test/resources/repos/nextflow/test_repo/main.nf @@ -0,0 +1,23 @@ +#!/usr/bin/env nextflow + +include { GREET; SHOUT } from './modules/greet/main.nf' +include { buildGreeting } from './modules/util/main.nf' + +params.names = ['Ada', 'Grace'] + +workflow SAY_HELLO { + take: + names + + main: + greetings = GREET(names) + shouted = SHOUT(greetings) + + emit: + shouted +} + +workflow { + log.info buildGreeting('World') + SAY_HELLO(Channel.fromList(params.names)) +} diff --git a/test/resources/repos/nextflow/test_repo/modules/greet/main.nf b/test/resources/repos/nextflow/test_repo/modules/greet/main.nf new file mode 100644 index 00000000..0a687436 --- /dev/null +++ b/test/resources/repos/nextflow/test_repo/modules/greet/main.nf @@ -0,0 +1,27 @@ +process GREET { + tag "$name" + + input: + val name + + output: + path "greeting.txt" + + script: + """ + echo "Hello, ${name}!" > greeting.txt + """ +} + +process SHOUT { + input: + path greeting + + output: + path "shout.txt" + + script: + """ + tr '[:lower:]' '[:upper:]' < ${greeting} > shout.txt + """ +} diff --git a/test/resources/repos/nextflow/test_repo/modules/util/main.nf b/test/resources/repos/nextflow/test_repo/modules/util/main.nf new file mode 100644 index 00000000..f9a8eab6 --- /dev/null +++ b/test/resources/repos/nextflow/test_repo/modules/util/main.nf @@ -0,0 +1,7 @@ +def normalizeName(String name) { + return name.trim().toLowerCase() +} + +def buildGreeting(String name) { + return "Hello, " + normalizeName(name) +} diff --git a/test/resources/repos/nextflow/test_repo/nextflow.config b/test/resources/repos/nextflow/test_repo/nextflow.config new file mode 100644 index 00000000..6c5f47b9 --- /dev/null +++ b/test/resources/repos/nextflow/test_repo/nextflow.config @@ -0,0 +1,8 @@ +manifest { + name = 'serena-nextflow-test-repo' + description = 'Minimal Nextflow pipeline used by the solidlsp Nextflow language server tests' +} + +process { + cpus = 1 +} diff --git a/test/solidlsp/nextflow/test_nextflow_basic.py b/test/solidlsp/nextflow/test_nextflow_basic.py new file mode 100644 index 00000000..4f7243d1 --- /dev/null +++ b/test/solidlsp/nextflow/test_nextflow_basic.py @@ -0,0 +1,87 @@ +import os +import shutil +from pathlib import Path + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import LanguageServerId +from solidlsp.ls_utils import SymbolUtils + +pytestmark = pytest.mark.skipif(shutil.which("java") is None, reason="Nextflow language server requires a Java runtime") + + +@pytest.mark.nextflow +class TestNextflowLanguageServer: + @pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True) + @pytest.mark.parametrize("repo_path", [LanguageServerId.NEXTFLOW], indirect=True) + def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + """Language server starts and attaches to the test repository.""" + assert language_server.is_running() + assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve() + + @pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True) + def test_document_symbols_workflows(self, language_server: SolidLanguageServer) -> None: + """Workflows declared in a script are reported, with the ``workflow`` keyword stripped.""" + all_symbols, _ = language_server.request_document_symbols("main.nf").get_all_symbols_and_roots() + names = [s["name"] for s in all_symbols] + assert "SAY_HELLO" in names, f"SAY_HELLO not found in main.nf symbols. Found: {names}" + # the implicit entry workflow has no name of its own; the language server calls it "" + assert "" in names, f"entry workflow not found in main.nf symbols. Found: {names}" + + @pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True) + def test_document_symbols_processes_and_functions(self, language_server: SolidLanguageServer) -> None: + """Processes and functions are reported without their declaration keyword.""" + process_symbols, _ = language_server.request_document_symbols("modules/greet/main.nf").get_all_symbols_and_roots() + process_names = [s["name"] for s in process_symbols] + assert "GREET" in process_names, f"GREET not found in modules/greet/main.nf symbols. Found: {process_names}" + assert "SHOUT" in process_names, f"SHOUT not found in modules/greet/main.nf symbols. Found: {process_names}" + + function_symbols, _ = language_server.request_document_symbols("modules/util/main.nf").get_all_symbols_and_roots() + function_names = [s["name"] for s in function_symbols] + assert "normalizeName" in function_names, f"normalizeName not found in modules/util/main.nf symbols. Found: {function_names}" + assert "buildGreeting" in function_names, f"buildGreeting not found in modules/util/main.nf symbols. Found: {function_names}" + + @pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True) + def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree() + for name in ("SAY_HELLO", "GREET", "SHOUT", "normalizeName", "buildGreeting"): + assert SymbolUtils.symbol_tree_contains_name(symbols, name), f"{name} not found in symbol tree" + + @pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True) + def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None: + """``normalizeName`` is defined and called within modules/util/main.nf only.""" + util_path = os.path.join("modules", "util", "main.nf") + symbol = self._get_symbol(language_server, util_path, "normalizeName") + start = symbol["selectionRange"]["start"] + + refs = language_server.request_references(util_path, start["line"], start["character"]) + ref_lines = self._reference_lines(refs) + # modules/util/main.nf, 0-indexed line 5: return "Hello, " + normalizeName(name) + assert (util_path, 5) in ref_lines, f"call to normalizeName inside buildGreeting not found. Found: {ref_lines}" + + @pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True) + def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None: + """The GREET process is defined in a module and used by the workflow in main.nf.""" + module_path = os.path.join("modules", "greet", "main.nf") + symbol = self._get_symbol(language_server, module_path, "GREET") + start = symbol["selectionRange"]["start"] + + refs = language_server.request_references(module_path, start["line"], start["character"]) + ref_lines = self._reference_lines(refs) + # main.nf, 0-indexed line 2: include { GREET; SHOUT } from './modules/greet/main.nf' + assert ("main.nf", 2) in ref_lines, f"include of GREET in main.nf not found. Found: {ref_lines}" + # main.nf, 0-indexed line 12: greetings = GREET(names) + assert ("main.nf", 12) in ref_lines, f"invocation of GREET in main.nf not found. Found: {ref_lines}" + + @staticmethod + def _reference_lines(references: list[dict]) -> set[tuple[str, int]]: + """(relative path, 0-indexed start line) per reference; paths use the platform separator.""" + return {(ref["relativePath"], ref["range"]["start"]["line"]) for ref in references} + + @staticmethod + def _get_symbol(language_server: SolidLanguageServer, relative_path: str, name: str) -> dict: + all_symbols, _ = language_server.request_document_symbols(relative_path).get_all_symbols_and_roots() + matches = [s for s in all_symbols if s["name"] == name] + assert matches, f"{name} not found in {relative_path}. Found: {[s['name'] for s in all_symbols]}" + return matches[0] From 88d71530abae9ceb61d39530964c51b9ede23ee6 Mon Sep 17 00:00:00 2001 From: xor <127287135+xormania@users.noreply.github.com> Date: Sat, 8 Aug 2026 13:05:31 -0400 Subject: [PATCH 22/27] Test speed optimization, svelte: remove unused dependencies (#1707) --- .../repos/svelte/test_repo/package-lock.json | 662 +++--------------- .../repos/svelte/test_repo/package.json | 4 - .../src/routes/(sverdle)/+page.svelte | 17 - .../svelte/test_repo/src/routes/layout.css | 3 - .../repos/svelte/test_repo/vite.config.ts | 3 +- 5 files changed, 104 insertions(+), 585 deletions(-) diff --git a/test/resources/repos/svelte/test_repo/package-lock.json b/test/resources/repos/svelte/test_repo/package-lock.json index 5fa3a17a..e3bcb954 100644 --- a/test/resources/repos/svelte/test_repo/package-lock.json +++ b/test/resources/repos/svelte/test_repo/package-lock.json @@ -8,16 +8,12 @@ "name": "test-repo", "version": "0.0.1", "devDependencies": { - "@fontsource/fira-mono": "^5.2.7", - "@neoconfetti/svelte": "^2.2.2", "@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/kit": "^2.57.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/vite": "^4.2.2", "@types/node": "^25.6.1", "svelte": "^5.55.2", "svelte-check": "^4.4.6", - "tailwindcss": "^4.2.2", "typescript": "^6.0.2", "vite": "^8.0.7" } @@ -56,16 +52,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@fontsource/fira-mono": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-5.2.7.tgz", - "integrity": "sha512-wYrAn6i3nH6luqQBZxtWUpl4UTUvs9AEbEeZxksPMwIqyjRRaxHTNW3c2VfM50gabS2IS7pT8lVWS2USB4ukYA==", - "dev": true, - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -135,20 +121,10 @@ "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@neoconfetti/svelte": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@neoconfetti/svelte/-/svelte-2.2.2.tgz", - "integrity": "sha512-E7xCFVEEm5Ctnj2udTJy1b9oaTvjz1zi1mYdEtE8rB5BVwq6kHisosDS+zdWN5PMfEMjtbsOV9Cl6tsNSAD1sA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" - } - }, "node_modules/@oxc-project/types": { - "version": "0.138.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", - "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", "dev": true, "license": "MIT", "funding": { @@ -163,9 +139,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", - "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", "cpu": [ "arm64" ], @@ -180,9 +156,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", - "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", "cpu": [ "arm64" ], @@ -197,9 +173,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", - "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", "cpu": [ "x64" ], @@ -214,9 +190,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", - "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", "cpu": [ "x64" ], @@ -231,9 +207,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", - "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", "cpu": [ "arm" ], @@ -248,16 +224,13 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", - "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -268,16 +241,13 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", - "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -288,16 +258,13 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", - "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", "cpu": [ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -308,16 +275,13 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", - "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", "cpu": [ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -328,16 +292,13 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", - "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -348,16 +309,13 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", - "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -368,9 +326,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", - "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", "cpu": [ "arm64" ], @@ -385,9 +343,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", - "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", "cpu": [ "wasm32" ], @@ -404,9 +362,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", - "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", "cpu": [ "arm64" ], @@ -421,9 +379,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", - "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", "cpu": [ "x64" ], @@ -452,9 +410,9 @@ "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", - "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", + "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -472,9 +430,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.69.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.69.1.tgz", - "integrity": "sha512-+nz8Fx/cElzb2ZPHC+6Ll3y3NEVIe4Na75PeplLlyTmd1dBXAjz2KR14y1ZgNjb2ThfAYzulu+PFy1UE3RCUzA==", + "version": "2.69.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.69.3.tgz", + "integrity": "sha512-cphwqMRcE19/9VkrIPr5qZhQ0SptSSDfDzRUpYHu9OJDFGuYBFyJzK+KQA27wB4YG32O/yF2QjBkDmTyo0vtCw==", "dev": true, "license": "MIT", "dependencies": { @@ -524,9 +482,9 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", - "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.2.0.tgz", + "integrity": "sha512-1SpkuMSRLfugrVX+IrKfE1RUegzo8AQzKQ6qQPfVzbcWi5IhuTPaKb5ZrLpucleFznkc4/RTeSPoRnGWFxX+EQ==", "dev": true, "license": "MIT", "dependencies": { @@ -543,356 +501,6 @@ "vite": "^8.0.0-beta.7 || ^8.0.0" } }, - "node_modules/@tailwindcss/node": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", - "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "5.21.6", - "jiti": "^2.7.0", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.3.2" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", - "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-x64": "4.3.2", - "@tailwindcss/oxide-freebsd-x64": "4.3.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-x64-musl": "4.3.2", - "@tailwindcss/oxide-wasm32-wasi": "4.3.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", - "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", - "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", - "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", - "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", - "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", - "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", - "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", - "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", - "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", - "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.11.1", - "@emnapi/runtime": "^1.11.1", - "@emnapi/wasi-threads": "^1.2.2", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", - "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", - "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", - "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.3.2", - "@tailwindcss/oxide": "4.3.2", - "tailwindcss": "4.3.2" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -919,9 +527,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", - "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -1031,20 +639,6 @@ "dev": true, "license": "MIT" }, - "node_modules/enhanced-resolve": { - "version": "5.21.6", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", - "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", @@ -1103,13 +697,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -1120,16 +707,6 @@ "@types/estree": "^1.0.6" } }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -1283,9 +860,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1307,9 +881,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1331,9 +902,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1355,9 +923,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1451,9 +1016,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -1470,9 +1035,9 @@ } }, "node_modules/obug": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", - "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -1504,9 +1069,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", "dev": true, "funding": [ { @@ -1547,13 +1112,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", - "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.138.0", + "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -1563,21 +1128,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.4", - "@rolldown/binding-darwin-arm64": "1.1.4", - "@rolldown/binding-darwin-x64": "1.1.4", - "@rolldown/binding-freebsd-x64": "1.1.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", - "@rolldown/binding-linux-arm64-gnu": "1.1.4", - "@rolldown/binding-linux-arm64-musl": "1.1.4", - "@rolldown/binding-linux-ppc64-gnu": "1.1.4", - "@rolldown/binding-linux-s390x-gnu": "1.1.4", - "@rolldown/binding-linux-x64-gnu": "1.1.4", - "@rolldown/binding-linux-x64-musl": "1.1.4", - "@rolldown/binding-openharmony-arm64": "1.1.4", - "@rolldown/binding-wasm32-wasi": "1.1.4", - "@rolldown/binding-win32-arm64-msvc": "1.1.4", - "@rolldown/binding-win32-x64-msvc": "1.1.4" + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, "node_modules/sade": { @@ -1594,9 +1159,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.1.tgz", - "integrity": "sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", "dev": true, "license": "MIT" }, @@ -1626,9 +1191,9 @@ } }, "node_modules/svelte": { - "version": "5.56.4", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz", - "integrity": "sha512-/d0QHehmRuJW8gVz395MTkPcPozxzdjBMBE8oEYGz8O3b9KTMzzQ9ZHJQLuFKOHOPQbU6kx/X4iid/EBBzH7iw==", + "version": "5.56.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.6.tgz", + "integrity": "sha512-p4HDLDogGHKRKCrgckQHNs5PEfXkju6JI5jTywueaKJI5hAdjPohEhRtQ0M1SWC/+TA73SPln+r7srr+7e4nZA==", "dev": true, "license": "MIT", "dependencies": { @@ -1654,9 +1219,9 @@ } }, "node_modules/svelte-check": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.1.tgz", - "integrity": "sha512-FGUOmAqxXdN/H9Zm8slrqO7SLtFisXRB7rfOsHNJ3MLTD2po/+Stg8XyErkpumPHbuUiYTcqrEIzxpVWKTLqtg==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", + "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", "dev": true, "license": "MIT", "dependencies": { @@ -1678,27 +1243,6 @@ "typescript": ">=5.0.0" } }, - "node_modules/tailwindcss": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", - "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -1756,16 +1300,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.16", - "rolldown": "~1.1.3", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "bin": { diff --git a/test/resources/repos/svelte/test_repo/package.json b/test/resources/repos/svelte/test_repo/package.json index b0dde71f..b41ada07 100644 --- a/test/resources/repos/svelte/test_repo/package.json +++ b/test/resources/repos/svelte/test_repo/package.json @@ -12,16 +12,12 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" }, "devDependencies": { - "@fontsource/fira-mono": "^5.2.7", - "@neoconfetti/svelte": "^2.2.2", "@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/kit": "^2.57.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tailwindcss/vite": "^4.2.2", "@types/node": "^25.6.1", "svelte": "^5.55.2", "svelte-check": "^4.4.6", - "tailwindcss": "^4.2.2", "typescript": "^6.0.2", "vite": "^8.0.7" } diff --git a/test/resources/repos/svelte/test_repo/src/routes/(sverdle)/+page.svelte b/test/resources/repos/svelte/test_repo/src/routes/(sverdle)/+page.svelte index b237ca34..4f58b11d 100644 --- a/test/resources/repos/svelte/test_repo/src/routes/(sverdle)/+page.svelte +++ b/test/resources/repos/svelte/test_repo/src/routes/(sverdle)/+page.svelte @@ -1,15 +1,11 @@