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 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e9b73699..ee126e5c 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -66,12 +66,12 @@ 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 # 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 or gleam" # niche: slow, mostly-cached toolchains. ocaml + haskell are the two slowest -- kept in separate # batches (ocaml here, haskell in other-langs). nix + perl skip on Windows via the guard. MARKERS_NICHE: "julia or r or perl or lean4 or nix or ocaml" @@ -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 @@ -446,6 +453,24 @@ jobs: missing="$(ldd "$QMLLS_DIR/qmlls" | grep 'not found' || true)" [ -z "$missing" ] || { echo "ERROR: qmlls has unresolved runtime libs:"; echo "$missing"; exit 1; } echo "$QMLLS_DIR" >> "$GITHUB_PATH" + - name: Install Gleam compiler + # Gleam's LSP is bundled with the compiler (started via `gleam lsp`). Restricted to Linux like + # qmlls; Gleam skips on Windows/macOS CI via the conftest guard. The compiler is a self-contained + # Rust binary (x86_64-unknown-linux-musl static build) with no runtime deps. + if: matrix.batch == 'other-langs' && runner.os == 'Linux' + shell: bash + run: | + GLEAM_VERSION="1.17.0" + GLEAM_DIR="$HOME/.gleam" + mkdir -p "$GLEAM_DIR" + curl -fsSL -o /tmp/gleam.tar.gz "https://github.com/gleam-lang/gleam/releases/download/v${GLEAM_VERSION}/gleam-v${GLEAM_VERSION}-x86_64-unknown-linux-musl.tar.gz" + tar xzf /tmp/gleam.tar.gz -C "$GLEAM_DIR" + rm /tmp/gleam.tar.gz + chmod +x "$GLEAM_DIR/gleam" + # Fail loudly if any shared lib is unresolved (the musl build should be fully static). + missing="$(ldd "$GLEAM_DIR/gleam" | grep 'not found' || true)" + [ -z "$missing" ] || { echo "ERROR: gleam has unresolved runtime libs:"; echo "$missing"; exit 1; } + echo "$GLEAM_DIR" >> "$GITHUB_PATH" - name: Cache Free Pascal Compiler if: matrix.batch == 'native' id: cache-fpc @@ -706,7 +731,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 }}- 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 0e57ea7b..7c5257c1 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -68,37 +68,11 @@ excluded_tools: [] # Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html 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 for the project, which will be provided to the LLM upon project activation +# (or within Serena's initial instructions if the project is activated at startup). +## See: https://oraios.github.io/serena/02-usage/050_configuration.html#prompt-templates 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: - You test externally observable behavior and guarantees, never implementation structure. - - 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. + {{ 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/CHANGELOG.md b/CHANGELOG.md index 009bae11..49345c97 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) @@ -13,6 +13,12 @@ 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 (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 + - 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 - Security: Use sandboxed environment for prompt templating, preventing attackers from using custom prompts to execute commands in an uncontrolled manner @@ -36,9 +42,17 @@ Status of the `main` branch. Changes prior to the next official version change w Note that if the base path is itself ignored, ignored paths cannot be considered. * Language Servers: + - Add Gleam language server support (via the `gleam lsp` server bundled with the Gleam compiler) - Allow language server priorities to be configured in `serena_config.yml` (for auto-detection during project creation) + - **Add support for Nextflow** (language server `nextflow`), using the official + [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 + - 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 @@ -65,6 +79,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 @@ -83,11 +102,28 @@ 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 + - `scala`: Fix: in a repository whose builds live below its root, Metals was given only the repository + root as a workspace folder, and its own one-level search takes just the first build it finds — so in + a monorepo all but one build were served with no build target, silently returning no cross-file + references. The build roots are now detected and passed as workspace folders, one Metals service per + build; `ls_specific_settings.scala.project_roots` and `project_root_scan_depth` override the + detection #1766 * Dashboard: - Fix: Serena PyPI version check triggered by callback on main thread could delay agent startup #1774 - - Fix: on macOS, the `tray_manager` interface put an icon in the Dock and in the app switcher - (should only use menu bar icon) + - Improvements in `tray_manager` interface mode: + - Fix: on macOS, the `tray_manager` interface put an icon in the Dock and in the app switcher + (should only use menu bar icon) + - On Serena shutdown, message the tray manager before lengthy project shutdowns + - Fix: When dead ports are detected (Serena instance gone), explicitly update the tray menu + immediately (may not update automatically) - Use `tray_manager` interface as new default on macOS * Hooks: diff --git a/README.md b/README.md index 2ff37674..bd58d919 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, Gleam, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, LaTeX, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nextflow, Nix, OCaml, Pascal, Perl, PHP, PowerShell, Python, QML, R, Rego, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, SystemVerilog, Terraform, TOML, TypeScript, Vue, WGSL, YAML, and Zig. ### The Serena JetBrains Plugin diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 00bfe33b..62d581bb 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -78,6 +78,8 @@ Some languages require additional installations or setup steps, as noted. (requires the Godot editor to be running with its built-in LSP enabled — default on port 6008; Serena connects over TCP and does not launch Godot itself; see the [GDScript Setup Guide](../03-special-guides/godot_gdscript_setup_guide_for_serena) for details) +* **Gleam** + (requires the [Gleam compiler](https://gleam.run) on PATH; the language server is bundled with the compiler and started via `gleam lsp`) * **Go** (requires installation of `gopls`) * **Groovy** @@ -115,6 +117,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** @@ -147,7 +154,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`) @@ -164,6 +171,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/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 65b5e6cc..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. @@ -666,8 +703,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 +713,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 +737,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 @@ -765,15 +796,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" ``` @@ -1248,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. 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 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..e682827c 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: @@ -63,6 +72,21 @@ These instructions cover the setup for projects that use sbt as the build tool, Notes: - Ensure you completed the manual or auto‑import steps so that the build is compiled and indexed; otherwise, code navigation and references may be incomplete until the first successful compile. +--- +## Monorepos: builds below the repository root + +Metals serves one build per workspace folder, so what it needs is the build roots, not the repository root. Serena detects them: if the repository root is not itself a build root (no `build.sbt`, `build.mill`, `pom.xml`, `.bsp/`, …), it searches up to three levels below for directories that are, and passes those to Metals — one Metals service per build. + +Override the detection where it guesses wrong: + +```yaml +# ~/.serena/serena_config.yml or .serena/project.yml +ls_specific_settings: + scala: + project_roots: ["backend", "tooling/plugin"] # relative to the repository root + project_root_scan_depth: 3 # only applies when project_roots is unset +``` + --- ## Running Multiple Metals Instances diff --git a/pyproject.toml b/pyproject.toml index e81b2b5d..9944d840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "starlette==1.3.1", "python-multipart==0.0.31", "filelock==3.25.2", - "cryptography==48.0.1", + "cryptography==50.0.0", "regex==2026.2.28", "pythonnet==3.1.0 ; sys_platform == 'win32'", "oslex==2.0.0", @@ -94,7 +94,7 @@ dev = [ # Exact pins because uvx installs from git, ignoring the lock file. "tornado==6.5.7", "wheel==0.46.3", - "pyasn1==0.6.3", + "pyasn1==0.6.4", "PyJWT==2.12.0", ] agno = ["agno==2.6.6", "sqlalchemy==2.0.41"] # agno bumped for session state overwrite CVE @@ -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", @@ -344,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", @@ -378,6 +380,7 @@ markers = [ "angular: language server running for Angular (uses @angular/language-server + @angular/language-service tsserver plugin)", "ada: language server running for Ada / SPARK (uses AdaCore Ada Language Server)", "qml: language server running for QML (uses Qt's qmlls)", + "gleam: language server running for Gleam (uses the Gleam compiler's bundled `gleam lsp`)", ] [tool.codespell] diff --git a/scripts/update_downloaded_dependency_hashes.py b/scripts/update_downloaded_dependency_hashes.py index b82eab1d..ad25da46 100644 --- a/scripts/update_downloaded_dependency_hashes.py +++ b/scripts/update_downloaded_dependency_hashes.py @@ -1,7 +1,11 @@ from sensai.util import logging from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS +from solidlsp.language_servers.kotlin_language_server import KotlinLanguageServer +from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer 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/agent.py b/src/serena/agent.py index 2c4f310f..175f338b 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -956,14 +956,43 @@ 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: + @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 _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() + 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) - 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, + embed_memory=embed_memory, ) + if tag is not None: + text = self._format_prompt_tag(text, tag=tag, tag_name_attr=tag_name_attr) + + return text + def create_connection_prompt(self) -> str: """ Returns the bootstrap prompt to be sent at MCP connection time. @@ -972,6 +1001,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. @@ -981,9 +1025,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) @@ -997,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), - mode_system_prompts=[self._format_prompt(mode.prompt) 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, @@ -1007,11 +1049,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: """ @@ -1031,13 +1073,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) @@ -1045,27 +1087,26 @@ 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) if modes_with_prompts: - msg += "\nNewly applicable mode instructions:" for mode in modes_with_prompts: - msg += f"\n{mode.prompt}" + 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 += f"\nProject-specific instructions:\n {proj.project_config.initial_prompt}" + 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) - return self._format_prompt(msg) + return msg def _update_active_modes(self, log_message: bool = True) -> None: """ @@ -1431,10 +1472,7 @@ class SerenaAgent: Shutdown handler of the agent, freeing resources and stopping background tasks. """ log.info("SerenaAgent is shutting down ...") - if self._active_project is not None: - log.info(f"Shutting down active project '{self._active_project.project_name}' ...") - self._active_project.shutdown(timeout=timeout) - self._active_project = None + # apply shutdown depending on allocated resources, handling quick ones first (dashboard manager & GUI viewer) if self._gui_log_viewer: log.info("Stopping the GUI log window ...") self._gui_log_viewer.stop() @@ -1442,6 +1480,10 @@ class SerenaAgent: if self._dashboard_manager: self._dashboard_manager.shutdown() self._dashboard_manager = None + if self._active_project is not None: + log.info(f"Shutting down active project '{self._active_project.project_name}' ...") + self._active_project.shutdown(timeout=timeout) + self._active_project = None def shutdown(self) -> None: """ 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/dashboard.py b/src/serena/dashboard.py index c9873608..1a1718a2 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 @@ -1152,6 +1155,7 @@ class SerenaDashboardTrayManager: for port in dead_ports: self._instances.pop(port, None) log.info("Removed unreachable instance on port %d", port) + self._update_menu() # terminate if no instances remain with self._lock: 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/project_server.py b/src/serena/project_server.py index ceec43a9..0debaac0 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 @@ -44,19 +45,33 @@ 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 - serena_config = SerenaConfig.from_config_file() - serena_config.gui_log_window = False - serena_config.web_dashboard = False + if port is None: + port = self.PORT + + serena_config = SerenaConfig.from_config_file().with_headless_mode_overrides() serena_config.language_backend = LanguageBackend.LSP 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 - # 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: @@ -78,31 +93,50 @@ 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") 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 +145,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: diff --git a/src/serena/resources/config/contexts/context.template.yml b/src/serena/resources/config/contexts/context.template.yml index ba2fd963..1f8e37ab 100644 --- a/src/serena/resources/config/contexts/context.template.yml +++ b/src/serena/resources/config/contexts/context.template.yml @@ -1,11 +1,28 @@ -# See Serena's documentation for more details on concept of contexts. -description: Description of the context, not used in the code. -prompt: Prompt that will form part of the system prompt/initial instructions for agents started in this context. +# See Serena's documentation for more details on concept of contexts: +# https://oraios.github.io/serena/02-usage/050_configuration.html#contexts +description: Description of the context (meta-information only) + +# define a prompt which will be included in the system prompt when agents are started in this context. +# See: https://oraios.github.io/serena/02-usage/050_configuration.html#prompt-templates +prompt: | + This prompt will form part of the system prompt/initial instructions for agents started in this context. + Add as much content as needed. + Leave it undefined if no prompt is needed, i.e. + prompt: null + +# tools to be excluded in this context. +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html excluded_tools: [] -# several tools are excluded by default and have to be explicitly included by the user +# included tools that would otherwise be excluded (particularly optional tools that are disabled by default). +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html included_optional_tools: [] +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html +fixed_tools: [] + # mapping of tool names to an override of their descriptions (the default description is the docstring of the Tool's apply method). # Sometimes, tool descriptions are too long (e.g., for ChatGPT), or users may want to override them for another reason. tool_description_overrides: {} diff --git a/src/serena/resources/config/modes/mode.template.yml b/src/serena/resources/config/modes/mode.template.yml index d7efce0d..c62573d7 100644 --- a/src/serena/resources/config/modes/mode.template.yml +++ b/src/serena/resources/config/modes/mode.template.yml @@ -1,8 +1,25 @@ -# See Serena's documentation for more details on concept of modes. +# See Serena's documentation for more details on concept of modes: +# https://oraios.github.io/serena/02-usage/050_configuration.html#modes description: Description of the mode (meta-information only) + +# define a prompt which will be included in the system prompt when this mode is activated +# (or will be provided in the project activation message if the mode is activated dynamically). +# See: https://oraios.github.io/serena/02-usage/050_configuration.html#prompt-templates prompt: | - Provide a prompt that will form part of the instructions sent to the model when this mode is activated. -# tools that are to be excluded by this mode + This prompt will form part of the system prompt/initial instructions for agents started in this context. + Add as much content as needed. + Leave it undefined if no prompt is needed, i.e. + prompt: null + +# tools to be excluded in this context. +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html excluded_tools: [] -# several tools are excluded by default and have to be explicitly included by the user -included_optional_tools: [] \ No newline at end of file + +# included tools that would otherwise be excluded (particularly optional tools that are disabled by default). +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html +fixed_tools: [] \ No newline at end of file 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 %} diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index cb5967ff..5b1d0293 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -4,18 +4,19 @@ 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 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) @@ -25,6 +26,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/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/language_servers/gleam_language_server.py b/src/solidlsp/language_servers/gleam_language_server.py new file mode 100644 index 00000000..3c550626 --- /dev/null +++ b/src/solidlsp/language_servers/gleam_language_server.py @@ -0,0 +1,230 @@ +""" +Provides Gleam specific instantiation of the LanguageServer class. + +The Gleam language server is bundled with the Gleam compiler and is started via +``gleam lsp``. No separate language-server package is required beyond the Gleam +compiler itself (https://gleam.run). +""" + +import logging +import shutil +import subprocess +import threading + +from overrides import override + +from solidlsp.ls import ( + LanguageServerDependencyProvider, + LanguageServerDependencyProviderSinglePath, + SolidLanguageServer, +) +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.settings import SolidLSPSettings + +log = logging.getLogger(__name__) + +# Gleam LSP emits $/progress (begin/end) while resolving/downloading project +# dependencies on first start. Once dependencies are resolved, symbol, reference, +# hover and diagnostics queries are served synchronously — no further +# compile-wait or per-request retry is needed (this was verified against +# gleam 1.17 on Windows: cross-file references return immediately once the +# dependency-download progress phase ends). +_INITIAL_PROGRESS_BEGIN_TIMEOUT = 10 # seconds to wait for the first $/progress begin +_INITIAL_PROGRESS_IDLE_TIMEOUT = 180 # seconds to wait for all progress phases to end + + +class GleamLanguageServer(SolidLanguageServer): + """ + Provides Gleam specific instantiation of the LanguageServer class. + + Uses the language server bundled with the Gleam compiler (``gleam lsp``). + Requires the ``gleam`` binary to be installed and available on PATH; see + https://gleam.run/getting-started/installing/ for installation instructions. + + The Gleam compiler is a self-contained Rust binary — neither Erlang/OTP nor + a separate LSP package is required for symbol, reference, hover, definition, + or diagnostics queries. (Full project compilation to BEAM bytecode does + require Erlang, but the LSP's analysis path does not.) + """ + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + super().__init__(config, repository_root_path, None, "gleam", solidlsp_settings) + self._progress_lock = threading.Lock() + self._active_progress_tokens: set[str | int] = set() + self._first_progress_seen = threading.Event() + self._idle = threading.Event() + self._idle.set() # optimistic: assume ready until a $/progress begin arrives + + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """ + Discover the Gleam compiler executable on PATH. + + :return: path to the ``gleam`` binary + :raises FileNotFoundError: if ``gleam`` is neither on PATH nor provided via ``ls_path`` + """ + gleam_binary = shutil.which("gleam") + if gleam_binary is None: + raise FileNotFoundError( + "Gleam is not installed or not in PATH.\n" + "Please install the Gleam compiler from https://gleam.run/getting-started/installing/\n" + "and ensure the 'gleam' binary is available on your PATH.\n" + "The Gleam language server is bundled with the compiler and is started via `gleam lsp`." + ) + return gleam_binary + + def _create_launch_command(self, core_path: str) -> list[str]: + # `gleam lsp` runs the language server over stdio. + return [core_path, "lsp"] + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # Gleam writes compiled output and downloaded dependencies under build/. + return super().is_ignored_dirname(dirname) or dirname == "build" + + @override + def _supports_pull_diagnostics(self) -> bool: + # `gleam lsp` does not implement `textDocument/diagnostic` (LSP 3.17 pull diagnostics) and + # advertises no `diagnosticProvider` capability. Sending a pull request would hang (the server + # neither responds nor errors), so opt out and rely on `textDocument/publishDiagnostics` (push), + # which the base class stores via `_observe_server_notification`. + return False + + def _create_base_initialize_params(self) -> dict: + """ + Return the language-specific initialize params for the Gleam language server. + + ``processId``, ``rootPath``, ``rootUri`` and ``workspaceFolders`` are populated by the + default ``InitializeParamsBuilder`` and must not be set here. + """ + return { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "completion": { + "dynamicRegistration": True, + "completionItem": { + "snippetSupport": True, + "documentationFormat": ["markdown", "plaintext"], + }, + }, + "hover": { + "dynamicRegistration": True, + "contentFormat": ["markdown", "plaintext"], + }, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + "configuration": True, + }, + }, + } + + def _download_dependencies(self) -> None: + """Run ``gleam deps download`` so the stdlib is available before the LSP starts. + + Gleam LSP also fetches missing dependencies on start and reports progress via + ``$/progress``; pre-fetching keeps the readiness wait short and avoids first-request + races on slow CI runners. + """ + gleam_path = shutil.which("gleam") + if gleam_path is None: + return # _get_or_install_core_dependency will raise a clear error when the server starts + try: + result = subprocess.run( + [gleam_path, "deps", "download"], + cwd=self.repository_root_path, + capture_output=True, + text=True, + timeout=120, + check=False, + ) + if result.returncode != 0: + log.warning("gleam deps download failed (exit %s): %s", result.returncode, result.stderr[:200]) + else: + log.info("gleam deps download completed") + except Exception as e: + log.warning("Failed to run gleam deps download: %s", e) + + def _start_server(self) -> None: + """Start the Gleam language server process.""" + + def register_capability_handler(_params: dict) -> None: + return + + def window_log_message(msg: dict) -> None: + log.info("LSP: window/logMessage: %s", msg) + + def on_progress(params: dict) -> None: + token = params.get("token") + value = params.get("value", {}) + if not isinstance(value, dict) or token is None: + return + kind = value.get("kind") + with self._progress_lock: + if kind == "begin": + self._active_progress_tokens.add(token) + self._first_progress_seen.set() + self._idle.clear() + log.info( + "Gleam LSP: progress begin (token=%s), active=%d", + token, + len(self._active_progress_tokens), + ) + elif kind == "end": + self._active_progress_tokens.discard(token) + if not self._active_progress_tokens: + self._idle.set() + log.info("Gleam LSP: all progress phases finished") + + def do_nothing(_params: dict) -> None: + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", on_progress) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + # Pre-fetch dependencies so the LSP's own dependency-download phase is short. + self._download_dependencies() + + log.info("Starting Gleam language server (gleam lsp) process") + self.server.start() + initialize_params = self._create_initialize_params() + + log.info("Sending initialize request to Gleam LSP and awaiting response") + init_response = self.server.send.initialize(initialize_params) + + capabilities = init_response.get("capabilities", {}) if isinstance(init_response, dict) else {} + log.info("Gleam LSP capabilities: %s", list(capabilities.keys())) + + self.server.notify.initialized({}) + + # Wait for Gleam's initial dependency-download progress to finish. + # Gleam emits $/progress begin/end for dependency resolution on first start; once all + # phases end, queries are served synchronously. If no progress is reported within the + # short begin-timeout, dependencies were already resolved and the server is ready. + if not self._first_progress_seen.wait(timeout=_INITIAL_PROGRESS_BEGIN_TIMEOUT): + log.info( + "Gleam LSP: no $/progress within %ss, assuming ready (dependencies already resolved)", + _INITIAL_PROGRESS_BEGIN_TIMEOUT, + ) + elif not self._idle.wait(timeout=_INITIAL_PROGRESS_IDLE_TIMEOUT): + log.warning( + "Gleam LSP: timed out waiting for initial progress after %ss, proceeding anyway", + _INITIAL_PROGRESS_IDLE_TIMEOUT, + ) + + log.info("Gleam language server ready") diff --git a/src/solidlsp/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/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/language_servers/scala_language_server.py b/src/solidlsp/language_servers/scala_language_server.py index 2a7d6330..450627dd 100644 --- a/src/solidlsp/language_servers/scala_language_server.py +++ b/src/solidlsp/language_servers/scala_language_server.py @@ -10,6 +10,7 @@ from enum import Enum from overrides import override +from solidlsp.initialize_params import DefaultInitializeParamsBuilder, InitializeParamsBuilder from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.ls_utils import PlatformUtils @@ -28,6 +29,56 @@ 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 +DEFAULT_PROJECT_ROOT_SCAN_DEPTH = 3 + +# 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") + +# Files whose presence marks a directory as the root of a build Metals can import, following the +# per-build-tool probes in Metals' `BuildTools` (scala/meta/internal/builds/BuildTools.scala) and +# `BazelBuildTool.workspaceSupportsBsp`. Deliberately partial: the probes that need to read a file's +# contents (scala-cli's BSP scope) are left out, since missing a build root only returns us to the +# previous behaviour, whereas a false positive would hide the real builds beneath it. +BUILD_ROOT_MARKER_FILES = ( + "MODULE.bazel", + "WORKSPACE", + "build.gradle", + "build.gradle.kts", + "build.mill", + "build.mill.scala", + "build.mill.yaml", + "build.sbt", + "build.sc", + "deder.pkl", + "mill", + "mill.bat", + "pom.xml", + "project.scala", + "settings.gradle", + "settings.gradle.kts", +) + +# Directories which, when they hold a JSON file, mark an already-configured Metals project +# (`BuildTools.hasJsonFile`). An empty one is a leftover, not a build. +BUILD_ROOT_MARKER_JSON_DIRS = (".bloop", ".bsp") + +# Directories not worth descending into when scanning for build roots: build output, dependencies, +# and the directories belonging to a build we would have recognised at their parent. They are still +# probed themselves — only the descent below them is skipped. +BUILD_ROOT_SCAN_SKIP_DIRS = frozenset({"node_modules", "out", "project", "src", "target", "venv"}) class StaleLockMode(Enum): @@ -43,6 +94,150 @@ 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 _contains_json_file(path: str) -> bool: + try: + return any(entry.name.endswith(".json") for entry in os.scandir(path)) + except OSError: + return False + + +def _is_build_root(path: str) -> bool: + """ + Whether `path` is the root of a build that Metals can import. + """ + if any(os.path.isfile(os.path.join(path, name)) for name in BUILD_ROOT_MARKER_FILES): + return True + if any(_contains_json_file(os.path.join(path, name)) for name in BUILD_ROOT_MARKER_JSON_DIRS): + return True + # sbt allows the build to be defined entirely under project/, with no build.sbt + build_properties = os.path.join(path, "project", "build.properties") + if os.path.isfile(build_properties): + try: + with open(build_properties, encoding="utf-8", errors="replace") as f: + return any(line.lstrip().startswith("sbt.version") for line in f) + except OSError: + return False + return False + + +def find_build_roots(repository_root_path: str, max_depth: int = DEFAULT_PROJECT_ROOT_SCAN_DEPTH) -> list[str]: + """ + Find the roots of the builds contained in the given repository. + + Metals serves one build per workspace folder, so a repository holding several builds + (or a single build below its root) must name those directories rather than the repository root. + + :param repository_root_path: the repository root + :param max_depth: how many directory levels below the repository root to search; + the search does not descend into a directory that is itself a build root + :return: the absolute paths of the build roots found, or `[repository_root_path]` if there are none + (which leaves Metals' own behaviour unchanged) + """ + if _is_build_root(repository_root_path): + return [repository_root_path] + + roots: list[str] = [] + visited: set[str] = set() + + def scan(directory: str, depth: int) -> None: + # symlinks are followed, as Metals' own search does, so guard against cycles + real_path = os.path.realpath(directory) + if depth > max_depth or real_path in visited: + return + visited.add(real_path) + try: + entries = sorted(os.scandir(directory), key=lambda e: e.name) + except OSError: + return + for entry in entries: + if entry.name.startswith(".") or not entry.is_dir(): + continue + if _is_build_root(entry.path): + roots.append(entry.path) + elif entry.name not in BUILD_ROOT_SCAN_SKIP_DIRS: + scan(entry.path, depth + 1) + + scan(repository_root_path, 1) + return roots or [repository_root_path] + + +class ScalaInitializeParamsBuilder(DefaultInitializeParamsBuilder): + """ + Sends the repository's build roots as the workspace folders, so that Metals creates one + service per build (see `MetalsLanguageServer.initialize`), in place of `ls_workspace_folders`, + which is about what SolidLSP indexes and is shared across a project's language servers. + + `ls_additional_workspace_folders` is still honoured: those folders can lie outside the + repository and so could never be detected, which is the whole point of the setting. + """ + + def __init__(self, ls: SolidLanguageServer, build_roots: list[str]): + super().__init__(ls, set_workspace_folders=False) + self._build_roots = build_roots + + @override + def _apply_updates(self) -> None: + super()._apply_updates() + folders = list(self._build_roots) + for path in self._ls.config.get_absolute_additional_workspace_folders(self._ls.repository_root_path): + if path not in folders: + folders.append(path) + log.info("Workspace folders sent to Metals: %s", folders) + self._set("workspaceFolders", [self._create_workspace_folder_entry(path) for path in folders]) + + +def _parse_project_roots(value: object) -> list[str] | None: + """ + Validate the `project_roots` setting, returning None (i.e. detect them) if it is unusable. + """ + if value is None: + return None + if isinstance(value, str) or not isinstance(value, list) or not all(isinstance(item, str) for item in value): + log.warning(f"Invalid project_roots value {value!r}, expected a list of paths; detecting the build roots instead") + return None + roots: list[str] = [item for item in value if isinstance(item, str)] + if not roots: + log.warning("Empty project_roots; detecting the build roots instead") + return None + return roots + + +def _parse_project_root_scan_depth(value: object) -> int: + """ + Validate the `project_root_scan_depth` setting, falling back to the default if it is unusable. + """ + if value is None: + return DEFAULT_PROJECT_ROOT_SCAN_DEPTH + if isinstance(value, bool) or not isinstance(value, int) or value < 1: + log.warning( + f"Invalid project_root_scan_depth value {value!r}, expected a positive integer; using {DEFAULT_PROJECT_ROOT_SCAN_DEPTH}" + ) + return DEFAULT_PROJECT_ROOT_SCAN_DEPTH + return value + + def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object]: """ Extract Scala-specific settings with defaults applied. @@ -52,6 +247,9 @@ 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 + - project_roots: list[str] | None + - project_root_scan_depth: int """ from solidlsp.ls_config import LanguageServerId @@ -60,6 +258,9 @@ 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, + "project_roots": None, + "project_root_scan_depth": DEFAULT_PROJECT_ROOT_SCAN_DEPTH, } if not solidlsp_settings.ls_specific_settings: @@ -80,6 +281,9 @@ 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), + "project_roots": _parse_project_roots(scala_settings.get("project_roots")), + "project_root_scan_depth": _parse_project_root_scan_depth(scala_settings.get("project_root_scan_depth")), } @@ -100,6 +304,24 @@ 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 roots to serve, relative to the repository root; when unset, they are + # auto-detected (see find_build_roots) + project_roots: ['backend', 'tooling/plugin'] + # How many levels below the repository root auto-detection searches + project_root_scan_depth: 3 + + 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. + + Monorepo support: + Metals serves one build per workspace folder, so the build roots — not the repository + root — are what it must be given. They are detected automatically; `project_roots` + overrides the detection where it guesses wrong. Multi-instance support: Metals uses H2 AUTO_SERVER mode (enabled by default) to support multiple @@ -113,8 +335,14 @@ class ScalaLanguageServer(SolidLanguageServer): Creates a ScalaLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ + self._build_roots = self._resolve_build_roots(repository_root_path, solidlsp_settings) + log.info(f"Metals will be given these build roots as workspace folders: {self._build_roots}") + # Check for stale locks before setting up dependencies (fail-fast) - self._check_metals_db_status(repository_root_path, solidlsp_settings) + for build_root in self._build_roots: + self._check_metals_db_status(build_root, 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__( @@ -125,9 +353,37 @@ class ScalaLanguageServer(SolidLanguageServer): solidlsp_settings, ) - def _check_metals_db_status(self, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None: + @staticmethod + def _resolve_build_roots(repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> list[str]: """ - Check the Metals H2 database status and handle stale locks. + Determine the build roots to serve, from the `project_roots` setting if given and by + detection otherwise. + """ + settings = _get_scala_settings(solidlsp_settings) + configured_roots: list[str] | None = settings["project_roots"] # type: ignore[assignment] + if configured_roots is None: + scan_depth: int = settings["project_root_scan_depth"] # type: ignore[assignment] + return find_build_roots(repository_root_path, scan_depth) + + roots = [] + for root in configured_roots: + abs_root = os.path.abspath(os.path.join(repository_root_path, root)) + if os.path.isdir(abs_root): + roots.append(abs_root) + else: + log.warning(f"Configured Scala project root does not exist, skipping: {abs_root}") + if not roots: + log.error("No configured Scala project root exists; detecting the build roots instead") + return find_build_roots(repository_root_path, settings["project_root_scan_depth"]) # type: ignore[arg-type] + return roots + + @override + def _create_initialize_params_builder(self) -> InitializeParamsBuilder: + return ScalaInitializeParamsBuilder(self, self._build_roots) + + def _check_metals_db_status(self, build_root_path: str, solidlsp_settings: SolidLSPSettings) -> None: + """ + Check the Metals H2 database status of one build root and handle stale locks. This method is called before setting up runtime dependencies to fail-fast if there's a stale lock that the user has configured to fail on. @@ -141,7 +397,7 @@ class ScalaLanguageServer(SolidLanguageServer): cleanup_stale_lock, ) - project_path = Path(repository_root_path) + project_path = Path(build_root_path) status, lock_info = check_metals_db_status(project_path) # Get settings using the shared helper function @@ -300,10 +556,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/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 2db41106..d4f05de0 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -185,6 +185,17 @@ class LanguageServerId(str, Enum): Supports .qml files. Requires Qt 6 installation providing qmlls on PATH. See https://doc.qt.io/qt-6/qtqml-tool-qmlls.html """ + GLEAM = "gleam" + """Gleam language server bundled with the Gleam compiler (`gleam lsp`). + Supports .gleam files. Requires the `gleam` binary on PATH. + See https://gleam.run/getting-started/installing/ for installation. + """ + NEXTFLOW = "nextflow" + """Nextflow language server (https://github.com/nextflow-io/language-server), the one that backs the + official VS Code extension. Supports .nf scripts (Nextflow .config files are parsed by the server, but + 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""" @@ -286,6 +297,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 +340,7 @@ class LanguageServerId(str, Enum): self.HTML, self.SCSS, self.ANGULAR, + self.DENO, } def is_programming_language(self) -> bool: @@ -572,6 +592,12 @@ class LanguageServerId(str, Enum): return FilenameMatcher(".gd", ".gdscript") case self.QML: return FilenameMatcher(".qml") + case self.GLEAM: + return FilenameMatcher(".gleam") + case self.NEXTFLOW: + # only scripts: the language server does have a service for .config files, but it provides + # no symbols for them, so treating them as source files would only pollute the symbol index + return FilenameMatcher(".nf") case self.HTML: return FilenameMatcher(".html", ".htm") case self.SCSS: @@ -588,6 +614,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}") @@ -859,6 +893,14 @@ class LanguageServerId(str, Enum): from solidlsp.language_servers.qml_language_server import QmlLanguageServer return QmlLanguageServer + case self.GLEAM: + from solidlsp.language_servers.gleam_language_server import GleamLanguageServer + + return GleamLanguageServer + case self.NEXTFLOW: + from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer + + return NextflowLanguageServer case self.HTML: from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer @@ -871,6 +913,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/src/solidlsp/resources/downloaded_dependency_hashes.json b/src/solidlsp/resources/downloaded_dependency_hashes.json index 7fc0bdd9..d1006ff8 100644 --- a/src/solidlsp/resources/downloaded_dependency_hashes.json +++ b/src/solidlsp/resources/downloaded_dependency_hashes.json @@ -1,9 +1,20 @@ { "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 + "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", + "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/conftest.py b/test/conftest.py index d973bb48..a44be041 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], @@ -436,6 +437,11 @@ def _determine_disabled_language_servers() -> list[LanguageServerId]: # catching a CI setup regression. On Windows/macOS CI (never installed) and off-CI without the binary it skips. if (_sh.which("qmlls6") is None and _sh.which("qmlls") is None) and not (is_ci and is_linux): result.append(LanguageServerId.QML) + # gleam is installed (see pytest.yml) on the Ubuntu other-langs CI batch. Same rationale as + # qmlls: a missing binary on Linux CI is NOT skipped (fails loudly on a CI setup regression); + # Windows/macOS CI and off-CI without the binary skip. + if _sh.which("gleam") is None and not (is_ci and is_linux): + result.append(LanguageServerId.GLEAM) # === 3. Disabled wherever the precondition is missing (including on CI) === # 3a. Platform precondition: these language servers have no native Windows support. @@ -477,6 +483,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/resources/repos/gleam/test_repo/.gitignore b/test/resources/repos/gleam/test_repo/.gitignore new file mode 100644 index 00000000..567609b1 --- /dev/null +++ b/test/resources/repos/gleam/test_repo/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/test/resources/repos/gleam/test_repo/gleam.toml b/test/resources/repos/gleam/test_repo/gleam.toml new file mode 100644 index 00000000..df8730eb --- /dev/null +++ b/test/resources/repos/gleam/test_repo/gleam.toml @@ -0,0 +1,5 @@ +name = "test_repo" +version = "1.0.0" + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" diff --git a/test/resources/repos/gleam/test_repo/manifest.toml b/test/resources/repos/gleam/test_repo/manifest.toml new file mode 100644 index 00000000..6e752061 --- /dev/null +++ b/test/resources/repos/gleam/test_repo/manifest.toml @@ -0,0 +1,14 @@ +# Do not manually edit this file, it is managed by Gleam. +# +# This file locks the dependency versions used, to make your build +# deterministic and to prevent unexpected versions from being included +# in your application. +# +# You should check this file into your source control repository. + +packages = [ + { name = "gleam_stdlib", version = "1.0.3", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1F543AFBA5D33DA493E6087F4E4C4F20D899411343512686C98A8ABB2963CF22" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } diff --git a/test/resources/repos/gleam/test_repo/src/calculator.gleam b/test/resources/repos/gleam/test_repo/src/calculator.gleam new file mode 100644 index 00000000..2b60b841 --- /dev/null +++ b/test/resources/repos/gleam/test_repo/src/calculator.gleam @@ -0,0 +1,37 @@ +import utils + +/// A simple calculator type. +pub type Calculator { + Calculator(name: String) +} + +/// Adds two integers and returns the result. +pub fn add(a: Int, b: Int) -> Int { + a + b +} + +/// Subtracts b from a. +pub fn subtract(a: Int, b: Int) -> Int { + a - b +} + +/// Multiplies two integers. +pub fn multiply(a: Int, b: Int) -> Int { + a * b +} + +/// Formats the result of an operation using the utils module (cross-file reference). +pub fn format_result(label: String, value: Int) -> String { + utils.format_output(label, value) +} + +/// Returns the description of a calculator. +pub fn describe(calc: Calculator) -> String { + "Calculator: " <> calc.name +} + +/// Demonstrates calling add (same-file reference) and format_output (cross-file reference). +pub fn demo() -> String { + let sum = add(7, 8) + utils.format_output("sum", sum) +} diff --git a/test/resources/repos/gleam/test_repo/src/diagnostics_sample.gleam b/test/resources/repos/gleam/test_repo/src/diagnostics_sample.gleam new file mode 100644 index 00000000..bff8d72b --- /dev/null +++ b/test/resources/repos/gleam/test_repo/src/diagnostics_sample.gleam @@ -0,0 +1,5 @@ +// This file intentionally contains a semantic error: `undefined_symbol` is not +// defined anywhere, so the Gleam language server reports a diagnostic for it. +pub fn broken() -> Int { + undefined_symbol +} diff --git a/test/resources/repos/gleam/test_repo/src/utils.gleam b/test/resources/repos/gleam/test_repo/src/utils.gleam new file mode 100644 index 00000000..e967c675 --- /dev/null +++ b/test/resources/repos/gleam/test_repo/src/utils.gleam @@ -0,0 +1,12 @@ +import gleam/int +import gleam/string + +/// Formats a label and integer value into a human-readable string. +pub fn format_output(label: String, value: Int) -> String { + label <> ": " <> int.to_string(value) +} + +/// Checks whether a string is non-empty. +pub fn is_non_empty(s: String) -> Bool { + !string.is_empty(s) +} diff --git a/test/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/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 @@