Merge remote-tracking branch 'origin/main' into release-1.7

Conflicts:
  CHANGELOG.md
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-08-09 13:53:19 +02:00
commit d3441d3e23
69 files changed
+3201 -1259

No files matched your search

+2
View File
@@ -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
+32 -3
View File
@@ -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 }}-
-32
View File
@@ -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.).
+48
View File
@@ -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.
+1 -1
View File
@@ -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.
@@ -18,12 +18,10 @@ Serena is an MCP-based "IDE for coding agents": semantic code retrieval/editing/
- `test/serena/`, `test/solidlsp/<lang>/` — pytest suites; per-language tests gated by pytest markers
- `test/resources/repos/<lang>/` — 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 `<project>/.serena/` (config + `memories/` as `.md` files).
- See `mem:tech_stack`, `mem:suggested_commands`, `mem:conventions`, `mem:task_completion`.
@@ -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
-24
View File
@@ -1,24 +0,0 @@
# Suggested Commands
Run via `uv run poe <task>` (or `poe <task>` 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 <marker>` 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 <path>` 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.
+1 -1
View File
@@ -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.
-10
View File
@@ -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 <task>`). 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/`.
+4 -30
View File
@@ -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
+39 -3
View File
@@ -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:
+1 -1
View File
@@ -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
+14 -1
View File
@@ -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**
+53 -16
View File
@@ -155,6 +155,43 @@ You can manage modes using the `mode` command,
serena mode edit <mode-name>
serena mode delete <mode-name>
(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 `<memory name="...">`. 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.
+3 -1
View File
@@ -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
@@ -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
+5 -2
View File
@@ -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]
@@ -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()
+67 -25
View File
@@ -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"</{tag}>"
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:
"""
+2 -5
View File
@@ -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(
+6 -2
View File
@@ -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:
+1
View File
@@ -323,6 +323,7 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook):
".lua",
".m",
".matlab",
".nf",
".php",
".proto",
".ps1",
+55 -22
View File
@@ -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:
@@ -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: {}
@@ -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: []
# 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: []
@@ -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 %}
+14 -12
View File
@@ -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:
@@ -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({})
+10 -106
View File
@@ -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_<platform>/``)
: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()
@@ -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")
@@ -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)
@@ -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
"<entry>" 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
@@ -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()
+46
View File
@@ -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}")
@@ -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"
}
"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"
}
+8
View File
@@ -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, ...) ===
@@ -0,0 +1,7 @@
{
"lint": {
"rules": {
"tags": ["recommended"]
}
}
}
@@ -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));
@@ -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;
}
@@ -0,0 +1 @@
build/
@@ -0,0 +1,5 @@
name = "test_repo"
version = "1.0.0"
[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
@@ -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" }
@@ -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)
}
@@ -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
}
@@ -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)
}
@@ -0,0 +1,3 @@
work/
.nextflow/
.nextflow.log*
@@ -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))
}
@@ -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
"""
}
@@ -0,0 +1,7 @@
def normalizeName(String name) {
return name.trim().toLowerCase()
}
def buildGreeting(String name) {
return "Hello, " + normalizeName(name)
}
@@ -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
}
+103 -559
View File
@@ -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": {
@@ -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"
}
@@ -1,15 +1,11 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { resolve } from '$app/paths';
import { confetti } from '@neoconfetti/svelte';
import { MediaQuery } from 'svelte/reactivity';
import { words } from '$lib/components/Words.svelte';
import { GAME_VERSION } from '$lib/game';
let { data } = $props();
const reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');
let shake = $state(false);
let won = $derived(data.answers.at(-1) === 'xxxxx');
@@ -166,19 +162,6 @@
</div>
</form>
{#if won}
<div
style="position: absolute; left: 50%; top: 30%"
use:confetti={{
particleCount: reducedMotion.current ? 0 : undefined,
force: 0.7,
stageWidth: window.innerWidth,
stageHeight: window.innerHeight,
colors: ['#ff3e00', '#40b3ff', '#676778']
}}
></div>
{/if}
<style>
form {
width: 100%;
@@ -1,6 +1,3 @@
@import 'tailwindcss';
@import '@fontsource/fira-mono';
:root {
--font-body: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
@@ -1,5 +1,4 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });
export default defineConfig({ plugins: [sveltekit()] });
+199
View File
@@ -0,0 +1,199 @@
import threading
from collections.abc import Iterator
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import TimeoutError as FutureTimeoutError
from contextlib import contextmanager
from pathlib import Path
from typing import Any, cast
from unittest.mock import MagicMock
import pytest
from serena.project_server import ProjectServer, QueryProjectRequest
@pytest.fixture
def project_server() -> ProjectServer:
server = ProjectServer.__new__(ProjectServer)
server._agent = MagicMock()
server._loaded_projects_by_root = {}
server._project_load_locks_by_root = {}
server._active_project_lock = threading.Lock()
server._loaded_projects_lock = threading.Lock()
return server
def test_cached_project_lookup_is_not_blocked_by_unrelated_cold_load(project_server: ProjectServer) -> None:
cached_root = Path("/cached")
cold_root = Path("/cold")
cached_project = MagicMock()
cold_project = MagicMock()
cold_load_started = threading.Event()
allow_cold_load_to_finish = threading.Event()
cached_registration = MagicMock(project_root=cached_root)
cold_registration = MagicMock(project_root=cold_root)
def block_cold_load() -> None:
cold_load_started.set()
assert allow_cold_load_to_finish.wait(timeout=5)
cold_registration.get_project_instance.return_value = cold_project
cold_project.create_language_server_manager.side_effect = block_cold_load
project_server._loaded_projects_by_root[str(cached_root)] = cached_project
serena_config = cast(Any, project_server._agent.serena_config)
serena_config.get_registered_project.side_effect = {
"cached": cached_registration,
"cold": cold_registration,
}.get
with ThreadPoolExecutor(max_workers=2) as executor:
cold_future = executor.submit(project_server._get_project, "cold")
assert cold_load_started.wait(timeout=1)
cached_future = executor.submit(project_server._get_project, "cached")
try:
assert cached_future.result(timeout=1) is cached_project
finally:
allow_cold_load_to_finish.set()
assert cold_future.result(timeout=1) is cold_project
def test_cold_loads_for_different_projects_can_run_concurrently(project_server: ProjectServer) -> None:
first_root = Path("/first")
second_root = Path("/second")
first_project = MagicMock()
second_project = MagicMock()
first_load_started = threading.Event()
allow_first_load_to_finish = threading.Event()
first_registration = MagicMock(project_root=first_root)
second_registration = MagicMock(project_root=second_root)
def block_first_load() -> None:
first_load_started.set()
assert allow_first_load_to_finish.wait(timeout=5)
first_registration.get_project_instance.return_value = first_project
first_project.create_language_server_manager.side_effect = block_first_load
second_registration.get_project_instance.return_value = second_project
serena_config = cast(Any, project_server._agent.serena_config)
serena_config.get_registered_project.side_effect = {
"first": first_registration,
"second": second_registration,
}.get
with ThreadPoolExecutor(max_workers=2) as executor:
first_future = executor.submit(project_server._get_project, "first")
assert first_load_started.wait(timeout=1)
second_future = executor.submit(project_server._get_project, "second")
try:
assert second_future.result(timeout=1) is second_project
finally:
allow_first_load_to_finish.set()
assert first_future.result(timeout=1) is first_project
def test_concurrent_lookups_load_each_project_only_once(project_server: ProjectServer) -> None:
project_root = Path("/project")
project = MagicMock()
load_started = threading.Event()
second_lookup_started = threading.Event()
allow_load_to_finish = threading.Event()
registration = MagicMock(project_root=project_root)
lookup_count = 0
lookup_count_lock = threading.Lock()
def block_load() -> None:
load_started.set()
assert allow_load_to_finish.wait(timeout=5)
registration.get_project_instance.return_value = project
project.create_language_server_manager.side_effect = block_load
serena_config = cast(Any, project_server._agent.serena_config)
def get_registration(project_name: str) -> MagicMock:
nonlocal lookup_count
assert project_name == "project"
with lookup_count_lock:
lookup_count += 1
if lookup_count == 2:
second_lookup_started.set()
return registration
serena_config.get_registered_project.side_effect = get_registration
with ThreadPoolExecutor(max_workers=2) as executor:
first_future = executor.submit(project_server._get_project, "project")
assert load_started.wait(timeout=1)
second_future = executor.submit(project_server._get_project, "project")
assert second_lookup_started.wait(timeout=1)
allow_load_to_finish.set()
assert first_future.result(timeout=1) is project
assert second_future.result(timeout=1) is project
registration.get_project_instance.assert_called_once_with(serena_config)
project.create_language_server_manager.assert_called_once_with()
def test_concurrent_queries_serialize_active_project_context(project_server: ProjectServer) -> None:
first_query_started = threading.Event()
second_lookup_finished = threading.Event()
allow_first_query_to_finish = threading.Event()
state_lock = threading.Lock()
state: dict[str, str | None] = {"active_project": None}
def get_project(project_name: str) -> str:
if project_name == "second":
second_lookup_finished.set()
return project_name
@contextmanager
def active_project_context(project: str) -> Iterator[None]:
with state_lock:
assert state["active_project"] is None
state["active_project"] = project
try:
yield
finally:
with state_lock:
state["active_project"] = None
def apply_tool(hold: bool = False) -> str:
if hold:
first_query_started.set()
assert allow_first_query_to_finish.wait(timeout=5)
with state_lock:
active_project = state["active_project"]
assert active_project is not None
return active_project
tool = MagicMock()
tool.is_readonly.return_value = True
tool.apply_ex.side_effect = apply_tool
server = cast(Any, project_server)
server._get_project = MagicMock(side_effect=get_project)
agent = cast(Any, project_server._agent)
agent.active_project_context.side_effect = active_project_context
agent.get_tool_by_name.return_value = tool
first_request = QueryProjectRequest(project_name="first", tool_name="read_file", tool_params_json='{"hold": true}')
second_request = QueryProjectRequest(project_name="second", tool_name="read_file", tool_params_json="{}")
with ThreadPoolExecutor(max_workers=2) as executor:
first_future = executor.submit(project_server._query_project, first_request)
assert first_query_started.wait(timeout=1)
second_future = executor.submit(project_server._query_project, second_request)
assert second_lookup_finished.wait(timeout=1)
try:
with pytest.raises(FutureTimeoutError):
second_future.result(timeout=0.1)
finally:
allow_first_query_to_finish.set()
assert first_future.result(timeout=1) == "first"
assert second_future.result(timeout=1) == "second"
+89
View File
@@ -0,0 +1,89 @@
from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import Hover
from solidlsp.ls_utils import SymbolUtils
from test.conftest import language_server_tests_enabled
def _hover_text(hover: Hover | None) -> str:
"""Flatten the hover contents (markup object, plain string or list of either) into one string."""
assert hover is not None, "Expected hover information, got None"
contents = hover["contents"]
items = contents if isinstance(contents, list) else [contents]
return "\n".join(item if isinstance(item, str) else item["value"] for item in items)
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.DENO), reason="Deno tests are disabled (deno not available)")
@pytest.mark.deno
class TestDenoLanguageServer:
@pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True)
def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
# main.ts line 4 (0-indexed): " return add(a, b);" — cursor on `add` (char 11).
# `add` is defined in util.ts at line 0.
definitions = language_server.request_definition(str(repo_path / "main.ts"), 4, 11)
assert definitions, f"Expected a definition for `add`, got {definitions=}"
definition = definitions[0]
assert definition["uri"].endswith("util.ts")
assert definition["range"]["start"]["line"] == 0
@pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True)
def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
# `add` is defined in util.ts line 0 at char len("export function ") == 16.
references = language_server.request_references(str(repo_path / "util.ts"), 0, len("export function "))
assert references, f"Expected references for `add`, got {references=}"
locations = [(ref["uri"].split("/")[-1], ref["range"]["start"]["line"]) for ref in references]
# The usage inside Calculator.sum lives on line 4 of main.ts.
assert ("main.ts", 4) in locations, f"Expected a reference in main.ts line 4, got {locations}"
@pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True)
def test_document_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
all_symbols, _roots = language_server.request_document_symbols("main.ts").get_all_symbols_and_roots()
names = [sym.get("name") for sym in all_symbols]
assert "Calculator" in names, f"Calculator not found in main.ts document symbols: {names}"
assert "sum" in names, f"sum not found in main.ts document symbols: {names}"
@pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
symbols = language_server.request_full_symbol_tree()
assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add not found in symbol tree"
@pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True)
def test_hover_reports_function_signature(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
# `add` is defined in util.ts line 0 at char len("export function ") == 16.
hover = language_server.request_hover(str(repo_path / "util.ts"), 0, len("export function "))
text = _hover_text(hover)
assert "add" in text, f"Hover should name the function, got: {text}"
assert "(a: number, b: number): number" in text, f"Hover should carry the signature, got: {text}"
@pytest.mark.parametrize("language_server", [LanguageServerId.DENO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DENO], indirect=True)
def test_hover_resolves_deno_global(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""The `Deno.*` namespace is what this server adds over the plain TypeScript one, so it must resolve."""
main = repo_path / "main.ts"
lines = main.read_text(encoding="utf-8").splitlines()
line = next(i for i, content in enumerate(lines) if "Deno.cwd()" in content)
hover = language_server.request_hover(str(main), line, lines[line].index("Deno.cwd") + len("Deno."))
text = _hover_text(hover)
assert "cwd" in text, f"Hover should resolve Deno.cwd, got: {text}"
assert "string" in text, f"Hover should carry Deno.cwd's return type, got: {text}"
View File
Whitespace-only changes.
+118
View File
@@ -0,0 +1,118 @@
"""
Basic integration tests for the Gleam language server.
These tests validate startup, document symbols, same-file and cross-file
reference search, and diagnostics, using the Gleam test repository at
``test/resources/repos/gleam/test_repo``.
Requires the ``gleam`` compiler on PATH (the language server is bundled with it
and started via ``gleam lsp``).
"""
import os
from pathlib import Path
import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_utils import SymbolUtils
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import read_repo_file
from test.solidlsp.util.diagnostics import assert_file_diagnostics
pytestmark = [
pytest.mark.gleam,
pytest.mark.skipif(
not language_server_tests_enabled(LanguageServerId.GLEAM),
reason="Gleam tests are disabled (gleam compiler not available)",
),
]
class TestGleamLanguageServer:
"""Test Gleam language server startup and basic features."""
@pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer) -> None:
"""The language server starts successfully."""
assert language_server.is_running()
@pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True)
def test_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""Document symbols for calculator.gleam include the public functions and the type."""
file_path = os.path.join("src", "calculator.gleam")
doc_symbols = language_server.request_document_symbols(file_path)
all_symbols, _ = doc_symbols.get_all_symbols_and_roots()
names = {s.get("name") for s in all_symbols if s.get("name")}
assert "add" in names, f"'add' missing from calculator.gleam symbols: {names}"
assert "subtract" in names, f"'subtract' missing: {names}"
assert "multiply" in names, f"'multiply' missing: {names}"
assert "Calculator" in names, f"'Calculator' type missing: {names}"
assert "demo" in names, f"'demo' missing: {names}"
@pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
"""The full project symbol tree includes symbols from both modules.
``find_symbol`` (serena's most-used tool) is backed by ``request_full_symbol_tree``;
verify it sees public symbols from calculator.gleam and utils.gleam across files.
"""
symbols = language_server.request_full_symbol_tree()
# calculator.gleam symbols
assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "calculator 'add' missing from symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator type missing from symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "demo"), "calculator 'demo' missing from symbol tree"
# utils.gleam symbols (cross-file)
assert SymbolUtils.symbol_tree_contains_name(symbols, "format_output"), "utils 'format_output' missing from symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "is_non_empty"), "utils 'is_non_empty' missing from symbol tree"
@pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.GLEAM], indirect=True)
def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""References to ``add`` include its call site in ``demo`` (same file).
``add`` is defined in calculator.gleam and called once in ``demo``; both the
declaration and the call site should be returned by ``request_references``.
"""
rel = os.path.join("src", "calculator.gleam")
file_path = str(repo_path / rel)
content = read_repo_file(language_server, rel)
coords = find_text_coordinates(content, r"pub fn (add)\(")
assert coords is not None, "Could not locate `pub fn add` definition in calculator.gleam"
references = language_server.request_references(file_path, coords.line, coords.col + 1)
assert references, f"Expected non-empty references for add, got {references=}"
ref_files = {loc["uri"].split("/")[-1] for loc in references}
assert "calculator.gleam" in ref_files, f"Expected reference in calculator.gleam, got {ref_files}"
@pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.GLEAM], indirect=True)
def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Cross-file references: ``format_output`` (defined in utils.gleam) is called in calculator.gleam."""
rel = os.path.join("src", "utils.gleam")
file_path = str(repo_path / rel)
content = read_repo_file(language_server, rel)
coords = find_text_coordinates(content, r"pub fn (format_output)\(")
assert coords is not None, "Could not locate `pub fn format_output` definition in utils.gleam"
references = language_server.request_references(file_path, coords.line, coords.col + 1)
assert references, f"Expected non-empty references for format_output, got {references=}"
ref_files = {loc["uri"].split("/")[-1] for loc in references}
assert "calculator.gleam" in ref_files, f"Expected cross-file reference in calculator.gleam, got {ref_files}"
@pytest.mark.parametrize("language_server", [LanguageServerId.GLEAM], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
"""diagnostics_sample.gleam references an undefined symbol; the LSP should report it."""
assert_file_diagnostics(
language_server,
os.path.join("src", "diagnostics_sample.gleam"),
(),
min_count=1,
)
@@ -455,7 +455,7 @@ class TestSetupFromExistingInstall:
(jdk / "bin" / "java").touch()
return jdk
def test_happy_path_returns_runtime_paths_with_no_gradle_and_no_intellicode(
def test_happy_path_returns_runtime_paths_with_no_gradle(
self, tmp_path: Path, jdtls_root: Path, lombok_jar: Path, custom_settings: SolidLSPSettings.CustomLSSettings
) -> None:
jdk = self._fake_jdk(tmp_path)
@@ -465,8 +465,6 @@ class TestSetupFromExistingInstall:
result = EclipseJDTLS.DependencyProvider._setup_from_existing_install(str(jdtls_root), str(lombok_jar), custom_settings)
assert result.gradle_path is None
assert result.intellicode_jar_path is None
assert result.intellisense_members_path is None
assert result.lombok_jar_path == str(lombok_jar)
assert result.jre_home_path == str(jdk)
assert Path(result.jdtls_launcher_jar_path).name.startswith("org.eclipse.equinox.launcher_")
+1 -1
View File
@@ -10,7 +10,7 @@ from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
# Kotlin LSP (IntelliJ-based, pre-alpha v261) crashes on JVM restart under CI resource constraints
# Kotlin LSP (IntelliJ-based, pre-alpha v262) crashes on JVM restart under CI resource constraints
# (2 CPUs, 7GB RAM). First start succeeds but subsequent starts fail with cancelled (-32800).
# Tests pass reliably on developer machines. See PR #1061 for investigation details.
# (The CI quarantine lives centrally in test/conftest.py::_determine_disabled_languages.)
@@ -0,0 +1,275 @@
"""Tests for Kotlin Language Server dependency resolution and installation."""
from contextlib import nullcontext
from pathlib import Path
from unittest.mock import patch
import pytest
from solidlsp.dependency_provider import DownloadedDependency, DownloadedDependencyHashDatabase
from solidlsp.language_servers.kotlin_language_server import (
DEFAULT_KOTLIN_LSP_VERSION,
INITIAL_KOTLIN_LSP_VERSION,
KOTLIN_LSP_ALLOWED_HOSTS,
KotlinLanguageServer,
)
from solidlsp.ls_utils import PlatformId
from solidlsp.settings import SolidLSPSettings
def _make_provider(
tmp_path: Path,
custom_settings: dict[str, str] | None = None,
) -> KotlinLanguageServer.DependencyProvider:
return KotlinLanguageServer.DependencyProvider(
custom_settings=SolidLSPSettings.CustomLSSettings(custom_settings or {}),
ls_resources_dir=str(tmp_path),
project_cache_dir=str(tmp_path / "project-cache"),
)
@pytest.mark.kotlin
class TestKotlinDependencyProvider:
@pytest.mark.parametrize(
("platform_id", "asset_suffix", "archive_type", "launcher_parts", "sha256"),
[
(
PlatformId.WIN_x64,
".win.zip",
"zip",
("bin", "intellij-server.exe"),
"f2daaa476f26d99301b406f76de6d87c437d04dc72f06845154619d8f991c51f",
),
(
PlatformId.WIN_arm64,
"-aarch64.win.zip",
"zip",
("bin", "intellij-server.exe"),
"73a552a6a420158622e5ad8d96b53da8aa8ced3f88a24fded01575927a2fd8e7",
),
(
PlatformId.LINUX_x64,
".tar.gz",
"gztar",
(f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"),
"2d99d8e198fbe4aa8f4481e37799724ce94803b4ea12a60b416040e3fcd7cc5e",
),
(
PlatformId.LINUX_arm64,
"-aarch64.tar.gz",
"gztar",
(f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"),
"2317831c6e5607d05b7ebc1da655330125ce0e3d66fbf24517dfce442debc14e",
),
(
PlatformId.OSX_x64,
".sit",
"zip",
(f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"),
"17369fda97c85418ac24ab38a9df56b21522a3468dfe193832fe455c13920745",
),
(
PlatformId.OSX_arm64,
"-aarch64.sit",
"zip",
(f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"),
"6ba6021a706b21e64cef33f7e2b79f187c0910320722bb2d3ed05ad1115ec43f",
),
],
)
def test_default_artifacts_match_jetbrains_release_matrix(
self,
tmp_path: Path,
platform_id: PlatformId,
asset_suffix: str,
archive_type: str,
launcher_parts: tuple[str, ...],
sha256: str,
) -> None:
artifact = KotlinLanguageServer.DependencyProvider._create_artifact(DEFAULT_KOTLIN_LSP_VERSION, platform_id)
url = (
f"https://download-cdn.jetbrains.com/language-server/kotlin-server/{DEFAULT_KOTLIN_LSP_VERSION}/"
f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}{asset_suffix}"
)
with patch("solidlsp.dependency_provider.FileUtils.download_and_extract_archive_verified") as download:
artifact.dependency.download_to(tmp_path)
download.assert_called_once_with(
url,
str(tmp_path),
archive_type=archive_type,
expected_sha256=sha256,
allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS,
)
assert artifact.launcher_parts == launcher_parts
@pytest.mark.parametrize(
("version", "platform_id", "url", "launcher_parts"),
[
(
"262.2310.0",
PlatformId.LINUX_x64,
"https://download-cdn.jetbrains.com/kotlin-lsp/262.2310.0/kotlin-lsp-262.2310.0-linux-x64.zip",
("kotlin-lsp.sh",),
),
(
"262.4739.0",
PlatformId.LINUX_x64,
"https://download-cdn.jetbrains.com/kotlin-lsp/262.4739.0/kotlin-server-262.4739.0.tar.gz",
("kotlin-server-262.4739.0", "bin", "intellij-server"),
),
(
"262.8190.0",
PlatformId.OSX_arm64,
"https://download-cdn.jetbrains.com/language-server/kotlin-server/262.8190.0/kotlin-server-262.8190.0-aarch64.sit",
("kotlin-server-262.8190.0", "bin", "intellij-server"),
),
],
)
def test_version_boundaries_select_the_published_layout(
self,
version: str,
platform_id: PlatformId,
url: str,
launcher_parts: tuple[str, ...],
) -> None:
artifact = KotlinLanguageServer.DependencyProvider._create_artifact(version, platform_id)
assert artifact.dependency.get_url() == url
assert artifact.launcher_parts == launcher_parts
def test_custom_versions_skip_hash_lookup_by_design(self, tmp_path: Path) -> None:
artifact = KotlinLanguageServer.DependencyProvider._create_artifact("262.8190.1", PlatformId.LINUX_x64)
with (
patch(
"solidlsp.dependency_provider.DownloadedDependencyHashDatabase.get_instance",
side_effect=AssertionError("custom versions must not consult the pinned hash database"),
),
patch("solidlsp.dependency_provider.FileUtils.download_and_extract_archive_verified") as download,
):
artifact.dependency.download_to(tmp_path)
download.assert_called_once_with(
"https://download-cdn.jetbrains.com/language-server/kotlin-server/262.8190.1/kotlin-server-262.8190.1.tar.gz",
str(tmp_path),
archive_type="gztar",
expected_sha256=None,
allowed_hosts=KOTLIN_LSP_ALLOWED_HOSTS,
)
def test_hash_updater_registers_frozen_and_default_artifacts(self) -> None:
updated_asset_names: list[str] = []
class RecordingUpdater:
def update(self, dependency: DownloadedDependency) -> None:
updated_asset_names.append(dependency.get_url().rsplit("/", 1)[-1])
with patch.object(DownloadedDependencyHashDatabase, "get_instance") as get_hash_database:
get_hash_database.return_value.update_context.return_value = nullcontext(RecordingUpdater())
KotlinLanguageServer.DependencyProvider.update_dep_hashes()
assert set(updated_asset_names) == {
"kotlin-lsp-261.13587.0-win-x64.zip",
"kotlin-lsp-261.13587.0-linux-x64.zip",
"kotlin-lsp-261.13587.0-linux-aarch64.zip",
"kotlin-lsp-261.13587.0-mac-x64.zip",
"kotlin-lsp-261.13587.0-mac-aarch64.zip",
"kotlin-server-262.9593.0.win.zip",
"kotlin-server-262.9593.0-aarch64.win.zip",
"kotlin-server-262.9593.0.tar.gz",
"kotlin-server-262.9593.0-aarch64.tar.gz",
"kotlin-server-262.9593.0.sit",
"kotlin-server-262.9593.0-aarch64.sit",
}
@pytest.mark.parametrize(
("settings", "platform_id", "relative_launcher", "expected_directory"),
[
(
{"kotlin_lsp_version": INITIAL_KOTLIN_LSP_VERSION},
PlatformId.LINUX_x64,
("kotlin-lsp.sh",),
"kotlin_language_server",
),
(
{},
PlatformId.LINUX_arm64,
(f"kotlin-server-{DEFAULT_KOTLIN_LSP_VERSION}", "bin", "intellij-server"),
f"kotlin_language_server-{DEFAULT_KOTLIN_LSP_VERSION}",
),
],
)
def test_installs_pinned_versions_in_compatible_directories(
self,
tmp_path: Path,
settings: dict[str, str],
platform_id: PlatformId,
relative_launcher: tuple[str, ...],
expected_directory: str,
) -> None:
provider = _make_provider(tmp_path, settings)
def fake_download(
_url: str,
target_path: str,
archive_type: str,
expected_sha256: str | None = None,
allowed_hosts: tuple[str, ...] | list[str] | None = None,
) -> None:
del archive_type, expected_sha256, allowed_hosts
launcher = Path(target_path).joinpath(*relative_launcher)
launcher.parent.mkdir(parents=True, exist_ok=True)
launcher.write_text("#!/bin/sh\n", encoding="utf-8")
with (
patch(
"solidlsp.language_servers.kotlin_language_server.PlatformUtils.get_platform_id",
return_value=platform_id,
),
patch(
"solidlsp.dependency_provider.FileUtils.download_and_extract_archive_verified",
side_effect=fake_download,
),
):
launcher_path = provider._get_or_install_core_dependency()
assert launcher_path == str((tmp_path / expected_directory).joinpath(*relative_launcher))
@pytest.mark.parametrize(
("platform_id", "modern_launcher", "other_os_launcher"),
[
(PlatformId.LINUX_x64, "/path/to/intellij-server", "/path/to/intellij-server.exe"),
(PlatformId.WIN_x64, "/path/to/intellij-server.exe", "/path/to/intellij-server"),
],
)
def test_custom_launcher_arguments_use_the_active_os_name(
self,
tmp_path: Path,
platform_id: PlatformId,
modern_launcher: str,
other_os_launcher: str,
) -> None:
modern_provider = _make_provider(tmp_path, {"ls_path": modern_launcher})
other_os_provider = _make_provider(tmp_path, {"ls_path": other_os_launcher})
with patch(
"solidlsp.language_servers.kotlin_language_server.PlatformUtils.get_platform_id",
return_value=platform_id,
):
assert modern_provider.create_launch_command() == [
modern_launcher,
"--stdio",
"--system-path",
str(tmp_path / "project-cache" / "kotlin-lsp-system"),
]
assert other_os_provider.create_launch_command() == [other_os_launcher, "--stdio"]
def test_invalid_version_is_rejected_before_download(self) -> None:
with pytest.raises(ValueError, match="dot-separated integers"):
KotlinLanguageServer.DependencyProvider._create_artifact("latest", PlatformId.OSX_arm64)
def test_unsupported_modern_platform_is_rejected(self) -> None:
with pytest.raises(ValueError, match="Unsupported platform"):
KotlinLanguageServer.DependencyProvider._create_artifact(DEFAULT_KOTLIN_LSP_VERSION, PlatformId.LINUX_x86)
@@ -2,9 +2,11 @@ import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.KOTLIN), reason="Kotlin tests are disabled")
@pytest.mark.kotlin
class TestKotlinDiagnostics:
@pytest.mark.parametrize("language_server", [LanguageServerId.KOTLIN], indirect=True)
+109
View File
@@ -0,0 +1,109 @@
"""Regression tests for Kotlin LSP startup readiness handling."""
import logging
from pathlib import Path
from typing import Any, cast
from unittest.mock import Mock, patch
import pytest
from solidlsp.language_servers.kotlin_language_server import KotlinLanguageServer
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.settings import SolidLSPSettings
pytestmark = pytest.mark.kotlin
_REQUIRED_CAPABILITIES = {
"textDocumentSync": 1,
"hoverProvider": True,
"completionProvider": {},
"signatureHelpProvider": {},
"definitionProvider": True,
"referencesProvider": True,
"documentSymbolProvider": True,
"workspaceSymbolProvider": True,
"semanticTokensProvider": {},
}
def _make_server(tmp_path: Path) -> tuple[KotlinLanguageServer, Mock, Mock]:
settings = SolidLSPSettings(
solidlsp_dir=str(tmp_path / "global"),
project_data_path=str(tmp_path / "project"),
ls_specific_settings={LanguageServerId.KOTLIN: {}},
)
server_interface = Mock()
with patch.object(KotlinLanguageServer, "_create_language_server_interface", return_value=server_interface):
server = KotlinLanguageServer(
LanguageServerConfig(ls_id=LanguageServerId.KOTLIN),
str(tmp_path),
settings,
)
indexing_complete = Mock()
indexing_complete.wait.return_value = True
intellij_server_ready = Mock()
intellij_server_ready.wait.return_value = True
server_state = cast(Any, server)
server_state._indexing_complete = indexing_complete
server_state._intellij_server_ready = intellij_server_ready
return server, indexing_complete, intellij_server_ready
def _configure_initialize(server: KotlinLanguageServer, server_name: str | None) -> dict[str, Any]:
response: dict[str, Any] = {"capabilities": _REQUIRED_CAPABILITIES}
if server_name is not None:
response["serverInfo"] = {"name": server_name}
language_server_interface = cast(Any, server.server)
language_server_interface.send.initialize.return_value = response
return response
def test_modern_server_registers_and_waits_for_explicit_ready_notification(tmp_path: Path) -> None:
server, indexing_complete, intellij_server_ready = _make_server(tmp_path)
_configure_initialize(server, "IntelliJ Language Server by JetBrains")
events: list[str] = []
notification_handlers: dict[str, Any] = {}
language_server_interface = cast(Any, server.server)
def on_notification(method: str, handler: Any) -> None:
events.append(f"notification:{method}")
notification_handlers[method] = handler
language_server_interface.on_notification.side_effect = on_notification
language_server_interface.start.side_effect = lambda: events.append("start")
language_server_interface.send.initialize.side_effect = lambda _params: (
events.append("initialize")
or {"capabilities": _REQUIRED_CAPABILITIES, "serverInfo": {"name": "IntelliJ Language Server by JetBrains"}}
)
server._start_server()
assert events.index("notification:intellij/ready-for-test") < events.index("start") < events.index("initialize")
intellij_server_ready.clear.assert_called_once_with()
intellij_server_ready.wait.assert_called_once_with(timeout=120.0)
indexing_complete.wait.assert_not_called()
notification_handlers["intellij/ready-for-test"](None)
intellij_server_ready.set.assert_called_once_with()
def test_legacy_server_keeps_progress_event_wait(tmp_path: Path) -> None:
server, indexing_complete, intellij_server_ready = _make_server(tmp_path)
_configure_initialize(server, None)
server._start_server()
indexing_complete.wait.assert_called_once_with(timeout=120.0)
intellij_server_ready.wait.assert_not_called()
def test_modern_ready_timeout_warns_and_continues(tmp_path: Path, caplog: Any) -> None:
server, _indexing_complete, intellij_server_ready = _make_server(tmp_path)
_configure_initialize(server, "IntelliJ Language Server by JetBrains")
intellij_server_ready.wait.return_value = False
with caplog.at_level(logging.WARNING):
server._start_server()
assert "Kotlin LSP did not signal indexing completion within 120s; proceeding anyway" in caplog.text
@@ -0,0 +1,87 @@
import os
import shutil
from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_utils import SymbolUtils
pytestmark = pytest.mark.skipif(shutil.which("java") is None, reason="Nextflow language server requires a Java runtime")
@pytest.mark.nextflow
class TestNextflowLanguageServer:
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.NEXTFLOW], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Language server starts and attaches to the test repository."""
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
def test_document_symbols_workflows(self, language_server: SolidLanguageServer) -> None:
"""Workflows declared in a script are reported, with the ``workflow`` keyword stripped."""
all_symbols, _ = language_server.request_document_symbols("main.nf").get_all_symbols_and_roots()
names = [s["name"] for s in all_symbols]
assert "SAY_HELLO" in names, f"SAY_HELLO not found in main.nf symbols. Found: {names}"
# the implicit entry workflow has no name of its own; the language server calls it "<entry>"
assert "<entry>" in names, f"entry workflow not found in main.nf symbols. Found: {names}"
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
def test_document_symbols_processes_and_functions(self, language_server: SolidLanguageServer) -> None:
"""Processes and functions are reported without their declaration keyword."""
process_symbols, _ = language_server.request_document_symbols("modules/greet/main.nf").get_all_symbols_and_roots()
process_names = [s["name"] for s in process_symbols]
assert "GREET" in process_names, f"GREET not found in modules/greet/main.nf symbols. Found: {process_names}"
assert "SHOUT" in process_names, f"SHOUT not found in modules/greet/main.nf symbols. Found: {process_names}"
function_symbols, _ = language_server.request_document_symbols("modules/util/main.nf").get_all_symbols_and_roots()
function_names = [s["name"] for s in function_symbols]
assert "normalizeName" in function_names, f"normalizeName not found in modules/util/main.nf symbols. Found: {function_names}"
assert "buildGreeting" in function_names, f"buildGreeting not found in modules/util/main.nf symbols. Found: {function_names}"
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None:
symbols = language_server.request_full_symbol_tree()
for name in ("SAY_HELLO", "GREET", "SHOUT", "normalizeName", "buildGreeting"):
assert SymbolUtils.symbol_tree_contains_name(symbols, name), f"{name} not found in symbol tree"
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
"""``normalizeName`` is defined and called within modules/util/main.nf only."""
util_path = os.path.join("modules", "util", "main.nf")
symbol = self._get_symbol(language_server, util_path, "normalizeName")
start = symbol["selectionRange"]["start"]
refs = language_server.request_references(util_path, start["line"], start["character"])
ref_lines = self._reference_lines(refs)
# modules/util/main.nf, 0-indexed line 5: return "Hello, " + normalizeName(name)
assert (util_path, 5) in ref_lines, f"call to normalizeName inside buildGreeting not found. Found: {ref_lines}"
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
"""The GREET process is defined in a module and used by the workflow in main.nf."""
module_path = os.path.join("modules", "greet", "main.nf")
symbol = self._get_symbol(language_server, module_path, "GREET")
start = symbol["selectionRange"]["start"]
refs = language_server.request_references(module_path, start["line"], start["character"])
ref_lines = self._reference_lines(refs)
# main.nf, 0-indexed line 2: include { GREET; SHOUT } from './modules/greet/main.nf'
assert ("main.nf", 2) in ref_lines, f"include of GREET in main.nf not found. Found: {ref_lines}"
# main.nf, 0-indexed line 12: greetings = GREET(names)
assert ("main.nf", 12) in ref_lines, f"invocation of GREET in main.nf not found. Found: {ref_lines}"
@staticmethod
def _reference_lines(references: list[dict]) -> set[tuple[str, int]]:
"""(relative path, 0-indexed start line) per reference; paths use the platform separator."""
return {(ref["relativePath"], ref["range"]["start"]["line"]) for ref in references}
@staticmethod
def _get_symbol(language_server: SolidLanguageServer, relative_path: str, name: str) -> dict:
all_symbols, _ = language_server.request_document_symbols(relative_path).get_all_symbols_and_roots()
matches = [s for s in all_symbols if s["name"] == name]
assert matches, f"{name} not found in {relative_path}. Found: {[s['name'] for s in all_symbols]}"
return matches[0]
@@ -0,0 +1,182 @@
"""
Unit tests for the detection of Scala build roots, which Metals is given as workspace folders.
"""
from pathlib import Path
from types import SimpleNamespace
from typing import cast
from urllib.parse import urlparse
from urllib.request import url2pathname
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.language_servers.scala_language_server import (
DEFAULT_PROJECT_ROOT_SCAN_DEPTH,
ScalaInitializeParamsBuilder,
ScalaLanguageServer,
find_build_roots,
)
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.settings import SolidLSPSettings
def make_sbt_build(path: Path) -> Path:
path.mkdir(parents=True, exist_ok=True)
(path / "build.sbt").write_text('ThisBuild / scalaVersion := "3.3.6"\n')
(path / "src" / "main" / "scala").mkdir(parents=True)
return path
@pytest.mark.scala
class TestFindBuildRoots:
def test_repository_root_is_the_build_root(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path)
assert find_build_roots(str(tmp_path)) == [str(tmp_path)]
def test_single_build_below_the_root(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path / "backend")
assert find_build_roots(str(tmp_path)) == [str(tmp_path / "backend")]
def test_several_builds_below_the_root(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path / "backend")
make_sbt_build(tmp_path / "tooling")
assert find_build_roots(str(tmp_path)) == [str(tmp_path / "backend"), str(tmp_path / "tooling")]
def test_nested_build(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path / "scala" / "backend")
assert find_build_roots(str(tmp_path)) == [str(tmp_path / "scala" / "backend")]
def test_scan_depth_is_bounded(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path / "a" / "b" / "c")
assert find_build_roots(str(tmp_path), max_depth=2) == [str(tmp_path)]
assert find_build_roots(str(tmp_path), max_depth=3) == [str(tmp_path / "a" / "b" / "c")]
def test_does_not_descend_into_a_build_root(self, tmp_path: Path) -> None:
"""A subproject of an sbt build is not a build root of its own."""
make_sbt_build(tmp_path / "backend")
make_sbt_build(tmp_path / "backend" / "module")
assert find_build_roots(str(tmp_path)) == [str(tmp_path / "backend")]
def test_falls_back_to_the_repository_root(self, tmp_path: Path) -> None:
"""With nothing to find, Metals' own behaviour is left unchanged."""
(tmp_path / "docs").mkdir()
assert find_build_roots(str(tmp_path)) == [str(tmp_path)]
def test_hidden_and_uninteresting_directories_are_skipped(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path / ".git" / "backend")
make_sbt_build(tmp_path / "node_modules" / "backend")
assert find_build_roots(str(tmp_path)) == [str(tmp_path)]
def test_bsp_connection_file_marks_a_build_root(self, tmp_path: Path) -> None:
(tmp_path / "backend" / ".bsp").mkdir(parents=True)
(tmp_path / "backend" / ".bsp" / "sbt.json").write_text("{}")
assert find_build_roots(str(tmp_path)) == [str(tmp_path / "backend")]
def test_sbt_build_defined_only_under_project(self, tmp_path: Path) -> None:
(tmp_path / "backend" / "project").mkdir(parents=True)
(tmp_path / "backend" / "project" / "build.properties").write_text("sbt.version=1.11.7\n")
assert find_build_roots(str(tmp_path)) == [str(tmp_path / "backend")]
def test_build_properties_without_a_version_is_not_a_build_root(self, tmp_path: Path) -> None:
(tmp_path / "backend" / "project").mkdir(parents=True)
(tmp_path / "backend" / "project" / "build.properties").write_text("# nothing to see here\n")
assert find_build_roots(str(tmp_path)) == [str(tmp_path)]
def test_an_empty_bsp_directory_is_not_a_build_root(self, tmp_path: Path) -> None:
"""Metals requires a connection file in there, not merely the directory."""
(tmp_path / "backend" / ".bsp").mkdir(parents=True)
make_sbt_build(tmp_path / "backend" / "module")
assert find_build_roots(str(tmp_path)) == [str(tmp_path / "backend" / "module")]
def test_a_skipped_directory_is_still_probed(self, tmp_path: Path) -> None:
"""`src` and friends are not descended into, but may themselves be a build root."""
make_sbt_build(tmp_path / "src")
assert find_build_roots(str(tmp_path)) == [str(tmp_path / "src")]
def test_a_skipped_directory_is_not_descended_into(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path / "node_modules" / "backend")
assert find_build_roots(str(tmp_path)) == [str(tmp_path)]
def test_bazel_workspace_marker(self, tmp_path: Path) -> None:
(tmp_path / "backend").mkdir()
(tmp_path / "backend" / "MODULE.bazel").write_text("")
assert find_build_roots(str(tmp_path)) == [str(tmp_path / "backend")]
def test_symlinked_build_is_found_without_looping(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path / "elsewhere" / "backend")
(tmp_path / "repo").mkdir()
(tmp_path / "repo" / "link").symlink_to(tmp_path / "elsewhere", target_is_directory=True)
(tmp_path / "repo" / "loop").symlink_to(tmp_path / "repo", target_is_directory=True)
assert find_build_roots(str(tmp_path / "repo")) == [str(tmp_path / "repo" / "link" / "backend")]
@pytest.mark.scala
class TestResolveBuildRoots:
"""The `project_roots` / `project_root_scan_depth` settings, which override the detection."""
@staticmethod
def resolve(root: Path, **scala_settings) -> list[str]:
settings = SolidLSPSettings(ls_specific_settings={LanguageServerId.SCALA: scala_settings})
return ScalaLanguageServer._resolve_build_roots(str(root), settings)
def test_detection_is_used_when_unset(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path / "backend")
assert self.resolve(tmp_path) == [str(tmp_path / "backend")]
def test_configured_roots_are_resolved_against_the_repository_root(self, tmp_path: Path) -> None:
(tmp_path / "a" / "b").mkdir(parents=True)
assert self.resolve(tmp_path, project_roots=["a/b"]) == [str(tmp_path / "a" / "b")]
def test_configured_roots_need_not_look_like_builds(self, tmp_path: Path) -> None:
"""The setting exists precisely for where the detection is wrong."""
(tmp_path / "odd").mkdir()
assert self.resolve(tmp_path, project_roots=["odd"]) == [str(tmp_path / "odd")]
def test_a_missing_configured_root_is_skipped(self, tmp_path: Path) -> None:
(tmp_path / "here").mkdir()
assert self.resolve(tmp_path, project_roots=["here", "gone"]) == [str(tmp_path / "here")]
def test_detection_takes_over_when_no_configured_root_exists(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path / "backend")
assert self.resolve(tmp_path, project_roots=["gone"]) == [str(tmp_path / "backend")]
@pytest.mark.parametrize("bad", ["backend", 42, [1, 2], []])
def test_a_malformed_project_roots_falls_back_to_detection(self, tmp_path: Path, bad: object) -> None:
make_sbt_build(tmp_path / "backend")
assert self.resolve(tmp_path, project_roots=bad) == [str(tmp_path / "backend")]
@pytest.mark.parametrize("bad", [None, "3", 0, -1, True])
def test_a_malformed_scan_depth_falls_back_to_the_default(self, tmp_path: Path, bad: object) -> None:
make_sbt_build(tmp_path / "a" / "b" / "c")
assert self.resolve(tmp_path, project_root_scan_depth=bad) == [str(tmp_path / "a" / "b" / "c")]
assert DEFAULT_PROJECT_ROOT_SCAN_DEPTH == 3
def test_scan_depth_is_honoured(self, tmp_path: Path) -> None:
make_sbt_build(tmp_path / "a" / "b" / "c")
assert self.resolve(tmp_path, project_root_scan_depth=2) == [str(tmp_path)]
@pytest.mark.scala
class TestScalaInitializeParamsBuilder:
@staticmethod
def workspace_folder_paths(tmp_path: Path, build_roots: list[str], additional: list[str]) -> list[str]:
ls = SimpleNamespace(
repository_root_path=str(tmp_path),
custom_settings={},
config=LanguageServerConfig(ls_id=LanguageServerId.SCALA, additional_workspace_folders=additional),
)
params = ScalaInitializeParamsBuilder(cast(SolidLanguageServer, ls), build_roots).build()
folders = params["workspaceFolders"] or []
return [url2pathname(urlparse(folder["uri"]).path) for folder in folders]
def test_the_build_roots_become_the_workspace_folders(self, tmp_path: Path) -> None:
roots = [str(tmp_path / "alpha"), str(tmp_path / "beta")]
assert self.workspace_folder_paths(tmp_path, roots, additional=[]) == roots
def test_additional_workspace_folders_are_kept(self, tmp_path: Path) -> None:
"""They can lie outside the repository, so detection could never recover them."""
outside = tmp_path.parent / "shared-lib"
outside.mkdir(exist_ok=True)
roots = [str(tmp_path / "alpha")]
assert self.workspace_folder_paths(tmp_path, roots, additional=[str(outside)]) == [*roots, str(outside)]
@@ -0,0 +1,94 @@
"""
Unit tests for Serena's answer to Metals' `window/showMessageRequest` prompts.
"""
import pytest
from solidlsp.language_servers.scala_language_server import (
_get_scala_settings,
choose_show_message_request_action,
)
from solidlsp.ls_config import LanguageServerId
from solidlsp.settings import SolidLSPSettings
# Metals' own prompts, with the message and action titles as its `Messages` object builds them
# (scala/meta/internal/metals/Messages.scala) and `type` as lsp4j serialises `MessageType.Info`.
IMPORT_BUILD = {
"message": "New sbt workspace detected, would you like to import the build?",
"type": 3,
"actions": [{"title": "Import build"}, {"title": "Not now"}, {"title": "Don't show again"}],
}
IMPORT_CHANGES = {
"message": "sbt build needs to be re-imported",
"type": 3,
"actions": [{"title": "Import changes"}, {"title": "Not now"}, {"title": "Don't show again"}],
}
GENERATE_BSP_AND_CONNECT = {
"message": "New sbt workspace detected, would you like connect to the Bloop build server?",
"type": 3,
"actions": [{"title": "Connect"}, {"title": "Not now"}, {"title": "Don't show again"}],
}
OLD_BLOOP_VERSION_RUNNING = {
"message": "Deprecated Bloop server is still running and is taking up resources, do you want to kill the process?",
"type": 3,
"actions": [{"title": "Yes"}, {"title": "Not now"}],
}
CHOOSE_BUILD_TOOL = {
"message": "Multiple build definitions found. Which would you like to use?",
"type": 3,
"actions": [{"title": "sbt"}, {"title": "mill"}],
}
@pytest.mark.scala
class TestChooseShowMessageRequestAction:
@pytest.mark.parametrize("params", [IMPORT_BUILD, IMPORT_CHANGES, GENERATE_BSP_AND_CONNECT])
def test_build_import_prompts_are_answered_affirmatively(self, params: dict) -> None:
chosen = choose_show_message_request_action(params)
assert chosen is not None
assert chosen["title"] in ("Import build", "Import changes", "Connect")
@pytest.mark.parametrize("params", [IMPORT_BUILD, IMPORT_CHANGES, GENERATE_BSP_AND_CONNECT])
def test_auto_import_build_can_be_turned_off(self, params: dict) -> None:
assert choose_show_message_request_action(params, auto_import_build=False) is None
def test_dont_show_again_is_never_chosen(self) -> None:
"""Metals persists that dismissal in the project's own state."""
params = {"message": "…", "actions": [{"title": "Don't show again"}, {"title": "Not now"}]}
assert choose_show_message_request_action(params) is None
def test_an_unrecognised_prompt_is_dismissed(self) -> None:
"""Answering "Yes" here would kill a process on the user's machine."""
assert choose_show_message_request_action(OLD_BLOOP_VERSION_RUNNING) is None
def test_a_prompt_with_no_actions_is_dismissed(self) -> None:
assert choose_show_message_request_action({"message": "just so you know"}) is None
assert choose_show_message_request_action({"message": "just so you know", "actions": None}) is None
def test_malformed_actions_do_not_raise(self) -> None:
params = {"message": "…", "actions": ["Import build", None, {"title": "Import build"}]}
assert choose_show_message_request_action(params) == {"title": "Import build"}
def test_choosing_between_build_tools_is_left_alone(self) -> None:
"""Naming the build tool for the user is a guess of a different order; see the comment
on BUILD_IMPORT_PROMPT_ACTIONS. A workspace with several build definitions is therefore
still not imported.
"""
assert choose_show_message_request_action(CHOOSE_BUILD_TOOL) is None
@pytest.mark.scala
class TestAutoImportBuildSetting:
"""`auto_import_build` has to reach the handler from the project configuration."""
@staticmethod
def setting(**scala_settings) -> object:
settings = SolidLSPSettings(ls_specific_settings={LanguageServerId.SCALA: scala_settings})
return _get_scala_settings(settings)["auto_import_build"]
def test_defaults_to_true(self) -> None:
assert self.setting() is True
assert _get_scala_settings(SolidLSPSettings())["auto_import_build"] is True
def test_can_be_disabled(self) -> None:
assert self.setting(auto_import_build=False) is False
Generated
+157 -183
View File
@@ -428,7 +428,7 @@ name = "clr-loader"
version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "python_full_version < '3.13' or sys_platform == 'win32'" },
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/46/7eea92b6aa2d68af78e049cbecec5f757f1aad44ecdecdc16bbad7eead51/clr_loader-0.3.1.tar.gz", hash = "sha256:2e073e9aaf49d1ae2f56ecba27987ad5fb68be4bcd9dd34a5bed8f0e4e128366", size = 86805, upload-time = "2026-04-18T17:49:44.287Z" }
wheels = [
@@ -455,61 +455,58 @@ wheels = [
[[package]]
name = "cryptography"
version = "48.0.1"
version = "50.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" }
sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" },
{ url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" },
{ url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" },
{ url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" },
{ url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" },
{ url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" },
{ url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" },
{ url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" },
{ url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" },
{ url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" },
{ url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" },
{ url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
{ url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" },
{ url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" },
{ url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" },
{ url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" },
{ url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" },
{ url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" },
{ url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" },
{ url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" },
{ url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" },
{ url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" },
{ url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" },
{ url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" },
{ url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" },
{ url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" },
{ url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" },
{ url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" },
{ url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
{ url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
{ url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" },
{ url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" },
{ url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" },
{ url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" },
{ url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" },
{ url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" },
{ url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" },
{ url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" },
{ url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" },
{ url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" },
{ url = "https://files.pythonhosted.org/packages/a9/d3/eb4e394e587341fdad09a09101fa76478ead3a78b0ad63e55c22f0d75c02/cryptography-48.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:08a597acce1ff37f347400087776599e2348a3a8bc53b44120e463cd274efe4a", size = 3951747, upload-time = "2026-06-09T22:31:23.871Z" },
{ url = "https://files.pythonhosted.org/packages/e0/4a/3f43451b4f858bfceaaaffc649e6e787e8d4fb332a1d443af39ab02cc8f1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:735824ec41b7f74a7c45fb1591349333e4c696cb6c044e5f46356e560143e4cd", size = 4641226, upload-time = "2026-06-09T22:31:02.532Z" },
{ url = "https://files.pythonhosted.org/packages/73/4e/855584c2c23b09e4ce2d3b9c30e983e679cd60b068c513c6bbdb91e11782/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:92a46e1d638daa264ba2971c0b0489c9409787943efae4d60ffda3d091ef832c", size = 4668958, upload-time = "2026-06-09T22:32:06.213Z" },
{ url = "https://files.pythonhosted.org/packages/42/3b/d35750e41d803d1e516fd6d6011f065424924da7af1748cef4cc9cb3ede1/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7e234ac052af99f2700826a5c29ea99d9c1b1f80341cde62d11c8154dc8e0bd9", size = 4640793, upload-time = "2026-06-09T22:32:26.331Z" },
{ url = "https://files.pythonhosted.org/packages/ca/aa/cdb7181fe865285e87e96825aaab239400f1de0c3bfba9bd9769b79f1a92/cryptography-48.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:33842cf0888951cef5bc7ac724ab844a42044c1727b967b7f8997289a0464f92", size = 4668505, upload-time = "2026-06-09T22:31:27.534Z" },
{ url = "https://files.pythonhosted.org/packages/5d/8c/ce3823c06c2804f194f9e64f0d67fa3f4094a39f2bb1a990cd03603af8fc/cryptography-48.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6184ca7b174f28d7c703f1290d4b297217c45355f77a98f67e9b7f14549ac54a", size = 3742204, upload-time = "2026-06-09T22:31:34.773Z" },
{ url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
{ url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
{ url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
{ url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
{ url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
{ url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
{ url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
{ url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
{ url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
{ url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
{ url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
{ url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
{ url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
{ url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
{ url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
{ url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
{ url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
{ url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
{ url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
{ url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
{ url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
{ url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
{ url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
{ url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
{ url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
{ url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
{ url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
{ url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
{ url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
{ url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
{ url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
{ url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
{ url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
{ url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
{ url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
{ url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
{ url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
{ url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" },
{ url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" },
{ url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" },
{ url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" },
{ url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" },
{ url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" },
]
[[package]]
@@ -689,14 +686,14 @@ wheels = [
[[package]]
name = "gitpython"
version = "3.1.46"
version = "3.1.58"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "gitdb" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" }
sdist = { url = "https://files.pythonhosted.org/packages/26/d6/5f358ff283325580c2003a6d953aea18cfe10ae87b46f5ebc80fa3a386dc/gitpython-3.1.58.tar.gz", hash = "sha256:621416df10ef3fd0e19fabf9172ddeed0fa704d353d04f194eec56a625a95b22", size = 228498, upload-time = "2026-08-04T15:05:49.47Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" },
{ url = "https://files.pythonhosted.org/packages/ec/0c/9d8752098bc442f0726e64aa6135940b3a96809915d1aa4206c1bb97881d/gitpython-3.1.58-py3-none-any.whl", hash = "sha256:d331e722577f0fd7fc1f857419b3ecc07af66282b933d2a4d95f84a042fdd50f", size = 220183, upload-time = "2026-08-04T15:05:48.025Z" },
]
[[package]]
@@ -789,24 +786,24 @@ wheels = [
[[package]]
name = "h2"
version = "4.3.0"
version = "4.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "hpack" },
{ name = "hyperframe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
{ url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" },
]
[[package]]
name = "hpack"
version = "4.1.0"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
{ url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" },
]
[[package]]
@@ -945,17 +942,17 @@ resolution-markers = [
"python_full_version < '3.12'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" },
{ name = "decorator", marker = "python_full_version < '3.12'" },
{ name = "ipython-pygments-lexers", marker = "python_full_version < '3.12'" },
{ name = "jedi", marker = "python_full_version < '3.12'" },
{ name = "matplotlib-inline", marker = "python_full_version < '3.12'" },
{ name = "pexpect", marker = "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "prompt-toolkit", marker = "python_full_version < '3.12'" },
{ name = "pygments", marker = "python_full_version < '3.12'" },
{ name = "stack-data", marker = "python_full_version < '3.12'" },
{ name = "traitlets", marker = "python_full_version < '3.12'" },
{ name = "typing-extensions", marker = "python_full_version < '3.12'" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "decorator" },
{ name = "ipython-pygments-lexers" },
{ name = "jedi" },
{ name = "matplotlib-inline" },
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "prompt-toolkit" },
{ name = "pygments" },
{ name = "stack-data" },
{ name = "traitlets" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/25/daae0e764047b0a2480c7bbb25d48f4f509b5818636562eeac145d06dfee/ipython-9.10.1.tar.gz", hash = "sha256:e170e9b2a44312484415bdb750492699bf329233b03f2557a9692cce6466ada4", size = 4426663, upload-time = "2026-03-27T09:53:26.244Z" }
wheels = [
@@ -974,16 +971,16 @@ resolution-markers = [
"python_full_version == '3.12.*'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" },
{ name = "decorator", marker = "python_full_version >= '3.12'" },
{ name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" },
{ name = "jedi", marker = "python_full_version >= '3.12'" },
{ name = "matplotlib-inline", marker = "python_full_version >= '3.12'" },
{ name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "prompt-toolkit", marker = "python_full_version >= '3.12'" },
{ name = "pygments", marker = "python_full_version >= '3.12'" },
{ name = "stack-data", marker = "python_full_version >= '3.12'" },
{ name = "traitlets", marker = "python_full_version >= '3.12'" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "decorator" },
{ name = "ipython-pygments-lexers" },
{ name = "jedi" },
{ name = "matplotlib-inline" },
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "prompt-toolkit" },
{ name = "pygments" },
{ name = "stack-data" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" }
wheels = [
@@ -1641,7 +1638,7 @@ name = "pexpect"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ptyprocess", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "ptyprocess" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
wheels = [
@@ -1650,89 +1647,66 @@ wheels = [
[[package]]
name = "pillow"
version = "12.2.0"
version = "12.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" },
{ url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" },
{ url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" },
{ url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" },
{ url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" },
{ url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" },
{ url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" },
{ url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" },
{ url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" },
{ url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" },
{ url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" },
{ url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
{ url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
{ url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
{ url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
{ url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
{ url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
{ url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
{ url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" },
{ url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" },
{ url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" },
{ url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" },
{ url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" },
{ url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" },
{ url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
{ url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
{ url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
{ url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
{ url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
{ url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
{ url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
{ url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
{ url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
{ url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
{ url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
{ url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
{ url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
{ url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
{ url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
{ url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
{ url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
{ url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
{ url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
{ url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
{ url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
{ url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
{ url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
{ url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
{ url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
{ url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
{ url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
{ url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
{ url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
{ url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
{ url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
{ url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
{ url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
{ url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
{ url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
{ url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
{ url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
{ url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
{ url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
{ url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
{ url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
{ url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
{ url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
{ url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
{ url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
{ url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
{ url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
{ url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
{ url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
{ url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
]
[[package]]
@@ -1819,11 +1793,11 @@ wheels = [
[[package]]
name = "pyasn1"
version = "0.6.3"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" },
{ url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" },
]
[[package]]
@@ -2122,7 +2096,7 @@ name = "pyobjc-framework-cocoa"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "pyobjc-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" }
wheels = [
@@ -2139,8 +2113,8 @@ name = "pyobjc-framework-quartz"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "pyobjc-framework-cocoa", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" }
wheels = [
@@ -2157,8 +2131,8 @@ name = "pyobjc-framework-security"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "pyobjc-framework-cocoa", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/80/aa/796e09a3e3d5cee32ebeebb7dcf421b48ea86e28c387924608a05e3f668b/pyobjc_framework_security-12.1.tar.gz", hash = "sha256:7fecb982bd2f7c4354513faf90ba4c53c190b7e88167984c2d0da99741de6da9", size = 168044, upload-time = "2025-11-14T10:22:06.334Z" }
wheels = [
@@ -2175,8 +2149,8 @@ name = "pyobjc-framework-uniformtypeidentifiers"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "pyobjc-framework-cocoa", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/65/b8/dd9d2a94509a6c16d965a7b0155e78edf520056313a80f0cd352413f0d0b/pyobjc_framework_uniformtypeidentifiers-12.1.tar.gz", hash = "sha256:64510a6df78336579e9c39b873cfcd03371c4b4be2cec8af75a8a3d07dff607d", size = 17030, upload-time = "2025-11-14T10:23:02.222Z" }
wheels = [
@@ -2188,8 +2162,8 @@ name = "pyobjc-framework-webkit"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "pyobjc-framework-cocoa", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/14/10/110a50e8e6670765d25190ca7f7bfeecc47ec4a8c018cb928f4f82c56e04/pyobjc_framework_webkit-12.1.tar.gz", hash = "sha256:97a54dd05ab5266bd4f614e41add517ae62cdd5a30328eabb06792474b37d82a", size = 284531, upload-time = "2025-11-14T10:23:40.287Z" }
wheels = [
@@ -2291,7 +2265,7 @@ name = "python-xlib"
version = "0.33"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" }
wheels = [
@@ -2303,7 +2277,7 @@ name = "pythonnet"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "clr-loader", marker = "python_full_version < '3.13' or sys_platform == 'win32'" },
{ name = "clr-loader" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/57/da1992e44663b71365c6e842c8d7fa453d4ec45fb99a68cfee5b7e944d3c/pythonnet-3.1.0.tar.gz", hash = "sha256:7b34c382905d10a371509ffafd64cae0416305c28817738a9cd138336f4e9991", size = 250599, upload-time = "2026-05-23T20:30:21.578Z" }
wheels = [
@@ -2450,7 +2424,7 @@ name = "qtpy"
version = "2.4.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging", marker = "python_full_version < '3.13' or sys_platform != 'win32'" },
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" }
wheels = [
@@ -2885,7 +2859,7 @@ requires-dist = [
{ name = "agno", marker = "extra == 'agno'", specifier = "==2.6.6" },
{ name = "anthropic", specifier = "==0.117.0" },
{ name = "beautifulsoup4", specifier = "==4.14.2" },
{ name = "cryptography", specifier = "==48.0.1" },
{ name = "cryptography", specifier = "==50.0.0" },
{ name = "docstring-parser", specifier = "==0.17.0" },
{ name = "dotenv", specifier = "==0.9.9" },
{ name = "filelock", specifier = "==3.25.2" },
@@ -2902,7 +2876,7 @@ requires-dist = [
{ name = "pathspec", specifier = "==0.12.1" },
{ name = "poethepoet", marker = "extra == 'dev'", specifier = "==0.36.0" },
{ name = "psutil", specifier = "==7.0.0" },
{ name = "pyasn1", marker = "extra == 'dev'", specifier = "==0.6.3" },
{ name = "pyasn1", marker = "extra == 'dev'", specifier = "==0.6.4" },
{ name = "pydantic", specifier = "==2.12.5" },
{ name = "pygls", specifier = "==2.1.1" },
{ name = "pyinstrument", marker = "extra == 'dev'", specifier = "==5.1.1" },
@@ -2944,11 +2918,11 @@ provides-extras = ["dev", "agno", "google"]
[[package]]
name = "setuptools"
version = "82.0.1"
version = "83.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
{ url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" },
]
[[package]]