Merge remote-tracking branch 'origin/main' into feature/rename-memory-references

Conflicts:
  src/serena/tools/file_tools.py
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-05-16 15:28:32 +02:00
commit a59e12adaa
41 files changed
+2608 -41

No files matched your search

+2
View File
@@ -81,6 +81,8 @@ eggs/
lib/
!test/resources/repos/dart/test_repo/lib/
!test/resources/repos/dart/test_repo/lib/diagnostics_sample.dart
!test/resources/repos/svelte/test_repo/src/lib/
!test/resources/repos/svelte/test_repo/src/lib/**
lib64/
parts/
sdist/
+5
View File
@@ -2,8 +2,12 @@
Status of the `main` branch. Changes prior to the next official version change will appear here.
* General:
- Make tool descriptions more amenable to tool search mechanisms as now used in several clients (e.g. avoid referencing other tools' names, etc.)
* Language Servers:
- No longer store temporary files (e.g. downloads) in `~/solidlsp_tmp`; instead, use OS-specific temporary directories
- Add **GDScript** (Godot Engine) support. Serena connects over TCP to the Godot editor's built-in LSP server (port 6008, same for Godot 3 and 4) — no separate language server process to install. Godot major version is auto-detected from `config_version` in `project.godot`. Note: Godot's LSP does not implement `workspace/symbol`; first workspace-wide scans fall back to per-file requests and can be slow for large projects (results are cached to disk). See the [GDScript Setup Guide](https://oraios.github.io/serena/03-special-guides/godot_gdscript_setup_guide_for_serena.html) for details. Closes #1446.
* Dashboard:
- UI polish: switch UI font to Inter (with system fallbacks) and use JetBrains Mono only for code/logs/paths/identifiers; refine the light/dark palette with softer borders, clearer text hierarchy, and a more nuanced shadow/elevation system; introduce a consistent spacing scale; keep the orange accent.
@@ -51,6 +55,7 @@ Status of the `main` branch. Changes prior to the next official version change w
- `get_diagnostics_for_symbol`: Retrieves diagnostics pertaining to a specific symbol
* Language Servers:
- Add **Svelte** support via `svelte-language-server@0.18.0`, installed with npm into Serena-managed language-server resources. The `svelte` language handles `.svelte` Single File Components plus TypeScript/JavaScript files for Svelte projects; use it instead of also enabling `typescript` unless intentionally running multiple language servers.
- Elixir (`elixir-tools/next-ls`): Fix deadlock in monorepo projects where `mix.exs` lives in a subdirectory. The server now searches immediate subdirectories when no `mix.exs` is found at the repository root. #1444
- Java (`eclipse.jdt.ls`): Add upstream JDTLS mode for offline / restricted-network use. Setting both `jdtls_path` and `lombok_path` in `ls_specific_settings.java` makes Serena use an existing upstream JDTLS installation (e.g. `brew install jdtls`) and the system JDK 21+, skipping the ~500 MB vscode-java VSIX, Gradle, and IntelliCode downloads. New related setting `java_home` lets the user override the JDK used to launch JDTLS. Default behavior unchanged — the JDTLS workspace hash is preserved bit-for-bit for users on the default route, so existing project caches are reused without a one-time reindex; the launcher path is mixed into the hash only when `jdtls_path` is set, isolating upstream installations from the default workspace. #1415
- Java (eclipse.jdt.ls): Lombok-generated methods (getters/setters, builder(), equals/hashCode/toString, etc.) are now included in symbol-based tools (find_symbol, get_symbols_overview, edits). Added lombok_show_generated setting (default: on) to toggle this. Updated bundled vscode-java to 1.54.0-923. Issue #1432.
+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, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GDScript, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
### The Serena JetBrains Plugin
@@ -71,6 +71,10 @@ Some languages require additional installations or setup steps, as noted.
(requires [.NET v8.0+](https://dotnet.microsoft.com/en-us/download/dotnet); uses FsAutoComplete/Ionide, which is auto-installed; for Homebrew .NET on macOS, set DOTNET_ROOT in your environment)
* **Fortran**
(requires installation of fortls: `pip install fortls`)
* **GDScript** (Godot Engine)
(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)
* **Go**
(requires installation of `gopls`)
* **Groovy**
@@ -128,6 +132,8 @@ Some languages require additional installations or setup steps, as noted.
* **Solidity**
(experimental; requires Node.js and npm; automatically installs `@nomicfoundation/solidity-language-server`;
works best with a `foundry.toml` or `hardhat.config.js` in the project root)
* **Svelte**
(requires Node.js v18+ and npm; supports `.svelte` Single File Components plus TypeScript/JavaScript files via `svelte-language-server`; a companion `typescript-language-server` + `typescript-svelte-plugin` is spawned automatically for cross-file rename, go-to-definition, and references across `.ts`/`.js` and `.svelte` files; use language `svelte` for Svelte projects instead of also enabling `typescript`)
* **Swift**
* **TypeScript**
* **Vue**
+1 -1
View File
@@ -197,7 +197,7 @@ Claude Code settings file (`.claude/settings.json` in your project directory, or
]
}
],
"Stop": [
"SessionEnd": [
{
"matcher": "",
"hooks": [
+41
View File
@@ -487,6 +487,27 @@ Supported settings:
| `fsautocomplete_version` | `0.83.0` | Override the FsAutoComplete version Serena installs as a .NET tool. |
#### GDScript (Godot Engine)
Serena connects to the Godot editor's built-in LSP server over TCP. No separate process is launched.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `port` | `6008` | TCP port the running Godot editor listens on for LSP connections. |
| `request_timeout` | `30.0` | Seconds to wait for a response from the Godot LSP server. |
Example:
```yaml
ls_specific_settings:
gdscript:
port: 6008
request_timeout: 60.0
```
#### Go (`gopls`)
Serena forwards `ls_specific_settings.go.gopls_settings` to `gopls` as LSP `initializationOptions` when the Go language server is started.
@@ -971,6 +992,26 @@ Supported settings:
TypeScript supports [additional workspace folders](additional-workspace-folders) for cross-package
reference discovery. Configure `additional_workspace_folders` in `project.yml` to enable this feature.
#### Svelte
Serena uses `svelte-language-server` for the `svelte` language key. Use `svelte` for Svelte projects instead of also listing `typescript`, unless you intentionally want multiple language servers active for the same files.
A companion TypeScript language server (`typescript-language-server` + `typescript-svelte-plugin`) is spawned automatically alongside the Svelte LSP. The plugin makes the TypeScript program `.svelte`-aware so that cross-file operations — rename, go-to-definition, and find-references from `.ts`/`.js` files — correctly include `.svelte` consumers. Serena merges and deduplicates reference results from both servers automatically.
Supported settings:
| Setting | Default | Description |
|---|---|---|
| `ls_path` | managed install | Override the `svelteserver` executable path. |
| `svelte_language_server_version` | `0.18.0` | Override the `svelte-language-server` npm package version Serena installs. |
| `typescript_version` | `6.0.3` (falls back to `ls_specific_settings.typescript.typescript_version`) | Override the `typescript` npm package version used as the shared tsdk. |
| `typescript_language_server_version` | `5.1.3` (falls back to `ls_specific_settings.typescript.typescript_language_server_version`) | Override the `typescript-language-server` npm package version for the companion server. |
| `typescript_svelte_plugin_version` | `0.3.52` | Override the `typescript-svelte-plugin` npm package version used for `.svelte`-aware TS resolution. |
| `npm_registry` | `null` | Override the npm registry Serena uses for all managed installs. |
| `initialization_options_configuration` | `{}` | Deep-merge overrides for any of the ten plugin configuration sections (`svelte`, `prettier`, `emmet`, `typescript`, `javascript`, `js/ts`, `css`, `less`, `scss`, `html`). |
All four packages are tracked via a version file; changing any version setting triggers a clean reinstall.
#### TypeScript via `vtsls`
The actual configuration key for vtsls is `typescript_vts`, not `vts`.
+1
View File
@@ -150,6 +150,7 @@ Only the language servers listed below download or install additional dependenci
- **PHP (`intelephense`)**: `intelephense`
- **SCSS / Sass / CSS**: `some-sass-language-server`
- **Solidity**: `@nomicfoundation/solidity-language-server`
- **Svelte**: `svelte-language-server`
- **TypeScript**: `typescript` and `typescript-language-server`
- **Vue**: `@vue/language-server`, plus `typescript` and `typescript-language-server`
- **VTSLS**: `@vtsls/language-server`
@@ -0,0 +1,94 @@
# GDScript (Godot Engine) Setup Guide for Serena
This guide explains how to prepare a Godot project so that Serena can provide code intelligence for GDScript (`.gd`) files via Godot's built-in Language Server Protocol (LSP) support.
Unlike most language servers, Serena does **not** launch a separate LSP process for GDScript. Instead, Serena connects over TCP to the LSP server that the Godot editor itself runs while it is open. This means the Godot editor must already be running with your project loaded before you start Serena.
---
## Prerequisites
- Godot Engine 3.x or 4.x installed and available on your system.
- A Godot project with a `project.godot` file at the project root.
No additional language server needs to be installed — Godot ships with a built-in LSP server that is enabled by default.
---
## How It Works
When a Godot project is open in the editor, the editor listens for LSP connections on **TCP port 6008** (the same default for both Godot 3 and Godot 4). Serena connects to this port and communicates using standard LSP Content-Length framing.
Serena automatically detects which major version of Godot your project targets by reading the `config_version` field from `project.godot`:
- `config_version` = 5 → Godot 4
- `config_version` = 4 → Godot 3
If `config_version` is not recognized (e.g. from a future Godot release), Serena logs a warning and still connects — both Godot 3 and Godot 4 use the same port.
No additional configuration is needed for version detection.
---
## Setup Steps
1. Open your Godot project in the Godot editor and allow it to finish loading.
2. Verify that the built-in LSP is enabled (it is on by default):
- Go to **Editor → Editor Settings → Network → Language Server**
- Confirm **"Use Language Server"** is checked.
- Confirm the port is **6008** (the default).
3. Add `gdscript` to the `languages` list in your project's Serena configuration:
```yaml
# .serena/project.yml
languages:
- gdscript
```
4. Start Serena in your project root. Serena will connect to the already-running Godot editor automatically.
---
## Using Serena with GDScript
- Serena recognizes `.gd` and `.gdscript` files. The language identifier used in LSP communication is `gdscript`.
- The Godot editor must remain open for the entire Serena session. If the editor is closed, Serena will lose its connection and will need to be restarted once the editor is open again.
- On first use, you may see a brief delay while Serena establishes the TCP connection and the editor indexes your project files — this is expected.
---
## Performance Note
Godot's built-in LSP does not implement the `workspace/symbol` request (global symbol search across the whole project). As a result, when Serena performs a workspace-wide symbol search — for example, calling `find_symbol` without a `relative_path` — it must fall back to sending a `textDocument/documentSymbol` request for **every `.gd` file** in the project individually.
For large projects, this initial full-project scan can take **30–60 seconds or more**.
To mitigate this:
- **Pass `relative_path` whenever possible.** Narrowing a search to a specific file or subdirectory avoids scanning the entire workspace.
- **Let the first full scan complete.** Serena caches the results to disk after the initial scan. Subsequent Serena sessions will use the on-disk cache and start instantly.
---
## Known Limitations
| Limitation | Detail |
|---|---|
| No `workspace/symbol` support | Godot's LSP does not implement this request. All symbol lookups fall back to per-file `documentSymbol` requests, making the first full-project scan slow. |
| Editor must stay open | Serena requires the Godot editor to be running throughout the session. Closing the editor breaks the TCP connection; restart Serena after reopening the editor. |
| Slower workspace operations | `find_symbol` and `find_references` across the whole workspace are slower than for languages whose LSP servers support native workspace-wide queries. |
---
## Advanced Configuration
You can customize Serena's GDScript connection via `ls_specific_settings` in your `serena_config.yml` or `project.yml`:
```yaml
ls_specific_settings:
gdscript:
port: 6008 # TCP port the Godot editor listens on (default: 6008)
request_timeout: 30.0 # seconds to wait for an LSP response (default: 30.0)
```
These override the defaults only if explicitly set.
---
## Reference
- Godot LSP documentation: [https://docs.godotengine.org/en/stable/tutorials/editor/external_editor.html](https://docs.godotengine.org/en/stable/tutorials/editor/external_editor.html)
- GDScript language reference: [https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/)
+1
View File
@@ -310,6 +310,7 @@ markers = [
"groovy: language server running for Groovy",
"rust: language server running for Rust",
"typescript: language server running for TypeScript",
"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",
"perl: language server running for Perl",
+3 -2
View File
@@ -13,8 +13,8 @@ project_name: "project_name"
# perl php php_phpactor powershell python
# python_jedi python_ty r rego ruby
# ruby_solargraph rust scala scss solidity
# swift systemverilog terraform toml typescript
# typescript_vts vue yaml zig
# svelte swift systemverilog terraform toml
# typescript typescript_vts vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
@@ -22,6 +22,7 @@ project_name: "project_name"
# - For C, use cpp
# - 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 SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
+4 -5
View File
@@ -24,8 +24,7 @@ class ReadFileTool(Tool):
def apply(self, relative_path: str, start_line: int = 0, end_line: int | None = None, max_answer_chars: int = -1) -> str:
"""
Reads the given file or a chunk of it. Generally, symbolic operations
like find_symbol or find_referencing_symbols should be preferred if you know which symbols you are looking for.
Reads the given file or a chunk of it.
:param relative_path: the relative path to the file to read
:param start_line: the 0-based index of the first line to be retrieved.
@@ -337,17 +336,17 @@ class SearchForPatternTool(Tool):
) -> str:
"""
Searches for a regex pattern across project files, returning whole matched lines (plus optional context).
Prefer `find_symbol`/`find_referencing_symbols` when looking for known symbols.
Prefer symbolic operations if you know which symbols you are looking for!
:param substring_pattern: regular expression to search for.
:param context_lines_before: number of context lines to include before each match.
:param context_lines_after: number of context lines to include after each match.
:param paths_include_glob: optional glob (relative to project root, e.g. ``"src/**/*.ts"``) restricting which files are searched.
:param paths_exclude_glob: optional glob to exclude files; takes precedence over `paths_include_glob`.
:param relative_path: restricts the search to this file or subdirectory of the project root. Must exist.
:param relative_path: restricts the search to this file or subdirectory of the project root
:param restrict_search_to_code_files: whether to search only files containing analyzable code symbols
(useful when looking for class/method definitions); otherwise also search non-code files.
:param multiline: whether to apply multi-line matching, enabling the flags re.DOTALL and re.MULTILINE
:param multiline: whether to apply multi-line matching (default: True), enabling the flags re.DOTALL and re.MULTILINE
:param max_answer_chars: if the output exceeds this many characters, a progressively shortened summary is returned instead.
``-1`` uses the configured default.
:return: A mapping from file paths to matched consecutive lines (0-based line numbers).
+24 -30
View File
@@ -167,25 +167,20 @@ class FindSymbolTool(Tool, ToolMarkerSymbolicRead):
:param depth: depth up to which descendants shall be retrieved (e.g. use 1 to also retrieve immediate children;
for the case where the symbol is a class, this will return its methods).
Ignored if `include_body=True`. Default 0.
:param relative_path: Optional. Restrict search to this file or directory. If None, searches entire codebase.
:param relative_path: (optional) restrict search to this file or directory. If None, searches entire codebase.
If a directory is passed, the search will be restricted to the files in that directory.
If a file is passed, the search will be restricted to that file.
If you have some knowledge about the codebase, you should use this parameter, as it will significantly
speed up the search as well as reduce the number of results.
:param include_body: whether to include the symbol's source code. Use judiciously.
:param include_info: whether to include additional info (hover-like, typically including docstring and signature),
about the symbol (ignored if include_body is True). Info is never included for child symbols.
Note: Depending on the language, this can be slow (e.g., C/C++).
:param include_kinds: List of LSP symbol kind integers to include.
If not provided, all kinds are included.
:param exclude_kinds: Optional. List of LSP symbol kind integers to exclude. Takes precedence over `include_kinds`.
If not provided, no kinds are excluded.
:param include_kinds: (optional) limits results to the given LSP symbol kinds (integers)
:param exclude_kinds: (optional) list of LSP symbol kinds (integers) to exclude.
:param substring_matching: If True, use substring matching for the last element of the pattern, such that
"Foo/get" would match "Foo/getValue" and "Foo/getData".
:param max_matches: Maximum number of permitted matches. If exceeded, a shortened result is returned
:param max_matches: maximum number of permitted matches. If exceeded, a shortened result is returned
which allows refining the search. -1 (default) means no limit. Set to 1 if you search for a single symbol.
:param max_answer_chars: Max characters for the JSON result. If exceeded, no content is returned.
-1 means the default value from the config will be used.
:param max_answer_chars: max result length; -1 for default
:return: symbols (with locations) matching the name.
"""
if include_body:
@@ -260,13 +255,13 @@ class FindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead):
Finds references to the symbol at the given `name_path`. The result will contain metadata about the referencing symbols
as well as a short code snippet around the reference.
:param name_path: for finding the symbol to find references for, same logic as in the `find_symbol` tool.
:param name_path: name path of the symbol
:param relative_path: the relative path to the file containing the symbol for which to find references.
Note: for external dependencies, this must be an identifier starting with `<ext` that you have received
earlier (don't try to guess!).
:param include_kinds: same as in the `find_symbol` tool.
:param exclude_kinds: same as in the `find_symbol` tool.
:param max_answer_chars: same as in the `find_symbol` tool.
:param include_kinds: (optional) limits results to the given LSP symbol kinds (integers)
:param exclude_kinds: optional list of LSP symbol kinds (integers) to exclude.
:param max_answer_chars: max result length; -1 for default
:return: a list of JSON objects with the symbols referencing the requested symbol
"""
include_body = False # It is probably never a good idea to include the body of the referencing symbols
@@ -346,14 +341,14 @@ class FindImplementationsTool(Tool, ToolMarkerSymbolicRead):
"""
Finds implementations of the symbol at the given `name_path`.
:param name_path: for finding the symbol to find implementations for, same logic as in the `find_symbol` tool.
:param name_path: the symbol's name path
:param relative_path: the relative path to the file containing the symbol for which to find implementations.
Note that here you can't pass a directory but must pass a file.
:param include_info: whether to include additional info (hover-like, typically including docstring and signature),
about the implementing symbols.
:param include_kinds: same as in the `find_symbol` tool.
:param exclude_kinds: same as in the `find_symbol` tool.
:param max_answer_chars: same as in the `find_symbol` tool.
:param include_kinds: (optional) limits results to the given LSP symbol kinds (integers)
:param exclude_kinds: (optional) list of LSP symbol kinds (integers) to exclude.
:param max_answer_chars: max result length; -1 for default
:return: a list of JSON objects with the symbols implementing the requested symbol
"""
include_body = False
@@ -488,7 +483,7 @@ class GetDiagnosticsForFileTool(Tool, ToolMarkerSymbolicRead):
:param end_line: the last 0-based line to include. Defaults to -1, which means until the end of the file.
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
Diagnostics with lower-or-equal numeric severity are returned.
:param max_answer_chars: same as in the `find_symbol` tool.
:param max_answer_chars: max result length; -1 for default
:return: grouped diagnostics for the requested file.
"""
symbol_retriever = self.create_language_server_symbol_retriever()
@@ -539,7 +534,7 @@ class GetDiagnosticsForSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOption
:param check_symbol_references: whether to additionally collect diagnostics for symbols that reference the symbol.
:param min_severity: minimum LSP severity to include, where 1=Error, 2=Warning, 3=Information, 4=Hint.
Diagnostics with lower-or-equal numeric severity are returned.
:param max_answer_chars: same as in the `find_symbol` tool.
:param max_answer_chars: max result length; -1 for default
:return: grouped diagnostics for the requested symbol and, optionally, its referencing symbols.
"""
symbol_retriever = self.create_language_server_symbol_retriever()
@@ -575,17 +570,16 @@ class ReplaceSymbolBodyTool(EditingToolWithDiagnostics):
body: str,
) -> str:
r"""
Replaces the body of the symbol with the given `name_path`.
Replaces the body of the given symbol.
The tool shall be used to replace symbol bodies that have been previously retrieved
(e.g. via `find_symbol`).
IMPORTANT: Do not use this tool if you do not know what exactly constitutes the body of the symbol.
IMPORTANT: Only replace symbol bodies if you have previously made a retrieval with include_body=True and thus know what
constitutes the body!
:param name_path: for finding the symbol to replace, same logic as in the `find_symbol` tool.
:param name_path: name path of the symbol whose body to replace
:param relative_path: the relative path to the file containing the symbol
:param body: the new symbol body. The symbol body is the definition of a symbol
in the programming language, including e.g. the signature line for functions.
IMPORTANT: The body does NOT include any preceding docstrings/comments or imports, in particular.
Depending on the language, it may or may not include a preceding docstring or other preceding annotations.
"""
with self.DiagnosticsContext(self, relative_path) as diagnostics_context:
code_editor = self.create_code_editor()
@@ -612,7 +606,7 @@ class InsertAfterSymbolTool(EditingToolWithDiagnostics):
Use this to insert code after a class/method/function definition.
Don't use to insert after assignments (constants, fields).
:param name_path: name path of the symbol after which to insert content (definitions in the `find_symbol` tool apply)
:param name_path: name path of the symbol after which to insert content
:param relative_path: the relative path to the file containing the symbol
:param body: the body/content to be inserted. The inserted code shall begin with the next line after
the symbol.
@@ -639,7 +633,7 @@ class InsertBeforeSymbolTool(EditingToolWithDiagnostics):
A typical use case is to insert a new class, function, method, field or variable assignment; or
a new import statement before the first symbol in the file.
:param name_path: name path of the symbol before which to insert content (definitions in the `find_symbol` tool apply)
:param name_path: name path of the symbol before which to insert content
:param relative_path: the relative path to the file containing the symbol
:param body: the body/content to be inserted before the line in which the referenced symbol is defined
"""
@@ -666,7 +660,7 @@ class RenameSymbolTool(Tool, ToolMarkerSymbolicEdit):
Note: for languages with method overloading, like Java, name_path may have to include a method's
signature to uniquely identify a method.
:param name_path: name path of the symbol to rename (definitions in the `find_symbol` tool apply)
:param name_path: name path of the symbol to rename
:param relative_path: the relative path to the file containing the symbol to rename
:param new_name: the new name for the symbol
:return: result summary indicating success or failure
@@ -686,7 +680,7 @@ class SafeDeleteSymbol(Tool, ToolMarkerSymbolicEdit):
Deletes the symbol if it is safe to do so (i.e., if there are no references to it)
or returns a list of references to it.
:param name_path_pattern: name path of the symbol to delete (definitions in the `find_symbol` tool apply)
:param name_path_pattern: name path of the symbol to delete
:param relative_path: the relative path to the file containing the symbol to delete
"""
ls_symbol_retriever = self.create_language_server_symbol_retriever()
@@ -0,0 +1,141 @@
"""GDScript language server for Godot Engine projects.
Connects to an already-running Godot editor via TCP on port 6008.
Both Godot 3 and Godot 4 (tested through 4.6.x) use this port.
The editor must be open with its built-in language server enabled (default).
"""
import logging
import os
import pathlib
from collections.abc import Callable
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_process import LanguageServerInterface, TCPConnectionInfo, TCPLanguageServer
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo, StringDict
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
# Maps config_version in project.godot to the Godot major version
_CONFIG_VERSION_TO_GODOT_MAJOR: dict[int, int] = {4: 3, 5: 4}
DEFAULT_GODOT_LS_PORT = 6008
DEFAULT_GODOT_REQUEST_TIMEOUT = 30.0
class GodotLanguageServer(SolidLanguageServer):
"""GDScript language server that connects to a running Godot editor.
Both Godot 3 and Godot 4 expose an LSP server on TCP port 6008.
The Godot editor must already be running — this class connects to it
rather than launching it.
ls_specific_settings for ``gdscript``:
- ``port`` (int): TCP port the Godot editor's LSP listens on (default: 6008).
- ``request_timeout`` (float): seconds to wait for an LSP response (default: 30.0).
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None:
self._godot_version = self._detect_godot_version(repository_root_path)
if self._godot_version is not None:
log.info("Detected Godot version %d for project at %s", self._godot_version, repository_root_path)
else:
log.warning("Could not detect Godot version for project at %s", repository_root_path)
# Dummy ProcessLaunchInfo — _create_language_server_interface() ignores it
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=""), "gdscript", solidlsp_settings)
@staticmethod
def _detect_godot_version(repo_path: str) -> int | None:
"""Read project.godot to determine the major Godot version.
Returns None if detection fails or the config_version is unrecognized.
"""
project_file = os.path.join(repo_path, "project.godot")
try:
with open(project_file, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("config_version="):
config_version = int(line.split("=", 1)[1])
return _CONFIG_VERSION_TO_GODOT_MAJOR.get(config_version)
except (FileNotFoundError, ValueError, OSError):
pass
return None
def _create_language_server_interface(
self,
process_launch_info: ProcessLaunchInfo,
logging_fn: Callable[[str, str, StringDict | str], None] | None,
) -> LanguageServerInterface:
settings: dict = self._custom_settings or {}
port = settings.get("port", DEFAULT_GODOT_LS_PORT)
request_timeout = settings.get("request_timeout", DEFAULT_GODOT_REQUEST_TIMEOUT)
self._conn_info = TCPConnectionInfo(host="127.0.0.1", port=port)
return TCPLanguageServer(
connection_info=self._conn_info,
language=self.language,
determine_log_level=self._determine_log_level,
logger=logging_fn,
request_timeout=request_timeout,
)
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
root_uri = pathlib.Path(repository_absolute_path).as_uri()
params = {
"processId": os.getpid(),
"rootUri": root_uri,
"rootPath": repository_absolute_path,
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"definition": {"dynamicRegistration": True},
"declaration": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"completion": {
"dynamicRegistration": True,
"completionItem": {"snippetSupport": True},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"publishDiagnostics": {"relatedInformation": True},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
},
},
}
return params # type: ignore
def _start_server(self) -> None:
def do_nothing(params: dict) -> None:
return
# Godot sends this notification immediately on connect to report the open project path.
self.server.on_notification("gdscript_client/changeWorkspace", do_nothing)
# Godot-specific capability advertisement (not standard LSP).
self.server.on_notification("gdscript/capabilities", do_nothing)
self.server.on_notification("window/logMessage", lambda msg: log.info("LSP: window/logMessage: %s", msg))
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
self.server.on_request("client/registerCapability", lambda params: None)
log.info("Connecting to Godot LSP at %s:%d", self._conn_info.host, self._conn_info.port)
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
log.info("Sending LSP initialize request to Godot")
self.server.send.initialize(initialize_params)
self.server.notify.initialized({})
log.info("Godot LSP initialized")
@@ -0,0 +1,661 @@
"""
Provides Svelte-specific instantiation of the LanguageServer class using
``svelte-language-server`` from Svelte Language Tools.
"""
from __future__ import annotations
import logging
import os
import pathlib
import shutil
from typing import Any, cast
from overrides import override
from solidlsp import ls_types
from solidlsp.language_servers.common import (
RuntimeDependency,
RuntimeDependencyCollection,
build_npm_install_command,
)
from solidlsp.language_servers.typescript_language_server import TypeScriptLanguageServer
from solidlsp.ls import (
LanguageServerDependencyProvider,
LanguageServerDependencyProviderSinglePath,
SolidLanguageServer,
)
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
TS_EXT = frozenset({".ts", ".tsx", ".mts", ".cts"})
JS_EXT = frozenset({".js", ".jsx", ".mjs", ".cjs"})
SVELTE_EXT = frozenset({".svelte"})
def _is_ts_file(uri: str) -> bool:
return uri.lower().endswith(tuple(TS_EXT | JS_EXT))
def _is_svelte_file(uri: str) -> bool:
return uri.lower().endswith(tuple(SVELTE_EXT))
class SvelteTypeScriptServer(TypeScriptLanguageServer):
"""Companion TypeScript language server for Svelte projects.
Loads ``typescript-svelte-plugin`` so the TS graph becomes .svelte-aware:
cross-file rename, find-references, and go-to-definition from .ts/.js files
into .svelte consumers all work correctly through this companion.
Spawned and owned by :class:`SvelteLanguageServer`; not instantiated directly.
"""
class DependencyProvider(TypeScriptLanguageServer.DependencyProvider):
"""Returns the pre-installed typescript-language-server binary.
The binary is installed by ``SvelteLanguageServer.DependencyProvider``;
this provider just resolves the pre-known path without a separate install.
"""
def __init__(
self,
custom_settings: SolidLSPSettings.CustomLSSettings,
ls_resources_dir: str,
explicit_executable_path: str,
) -> None:
super().__init__(custom_settings, ls_resources_dir)
self._explicit_executable_path = explicit_executable_path
@override
def _get_or_install_core_dependency(self) -> str:
return self._explicit_executable_path
def __init__(
self,
config: LanguageServerConfig,
repository_root_path: str,
solidlsp_settings: SolidLSPSettings,
svelte_plugin_path: str,
tsdk_path: str,
ts_ls_executable_path: str,
) -> None:
self._svelte_plugin_path = svelte_plugin_path
self._custom_tsdk_path = tsdk_path
# store as instance state, not class attr, to avoid races across parallel instantiations
self._explicit_ts_ls_executable = ts_ls_executable_path
super().__init__(config, repository_root_path, solidlsp_settings)
@classmethod
@override
def get_language_enum_instance(cls) -> Language:
"""Return TYPESCRIPT; companion uses the TypeScript LS infrastructure."""
return Language.TYPESCRIPT
@override
def get_source_fn_matcher(self) -> FilenameMatcher:
# include .svelte so references returned by the plugin are not filtered out
return Language.SVELTE.get_source_fn_matcher()
@override
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(
self._custom_settings,
self._ls_resources_dir,
self._explicit_ts_ls_executable,
)
@override
def _get_language_id_for_file(self, relative_file_path: str) -> str:
""".svelte files map to 'svelte' to activate the plugin; TS/JS as normal."""
ext = os.path.splitext(relative_file_path)[1].lower()
if ext in SVELTE_EXT:
return "svelte"
if ext in JS_EXT:
return "javascript"
return "typescript"
@override
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
params = super()._get_initialize_params(repository_absolute_path)
params["initializationOptions"] = {
"plugins": [
{
"name": "typescript-svelte-plugin",
"location": self._svelte_plugin_path,
"languages": ["svelte"],
}
],
"tsserver": {"path": self._custom_tsdk_path},
}
return params
@override
def _start_server(self) -> None:
def workspace_configuration_handler(params: dict) -> list:
items = params.get("items", [])
return [{} for _ in items]
self.server.on_request("workspace/configuration", workspace_configuration_handler)
super()._start_server()
class SvelteLanguageServer(SolidLanguageServer):
"""
Svelte language server using ``svelte-language-server``.
``ls_specific_settings["svelte"]`` keys:
* ``svelte_language_server_version``: version of ``svelte-language-server``
to install (default: ``0.18.0``).
* ``npm_registry``: optional alternative npm-compatible registry URL.
* ``initialization_options_configuration``: optional dict merged into
``initializeParams.initializationOptions.configuration`` (same top-level keys as in
Svelte Language Tools: ``svelte``, ``prettier``, ``typescript``, …).
"""
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
def __init__(
self,
custom_settings: SolidLSPSettings.CustomLSSettings,
ls_resources_dir: str,
ts_settings: SolidLSPSettings.CustomLSSettings,
) -> None:
super().__init__(custom_settings, ls_resources_dir)
self._ts_settings = ts_settings
def _get_or_install_core_dependency(self) -> str:
assert shutil.which("node") is not None, "node is not installed or isn't in PATH. Please install NodeJS and try again."
assert shutil.which("npm") is not None, "npm is not installed or isn't in PATH. Please install npm and try again."
package_version = self._custom_settings.get("svelte_language_server_version", "0.18.0")
npm_registry = self._custom_settings.get("npm_registry", self._ts_settings.get("npm_registry"))
typescript_version = self._custom_settings.get("typescript_version", self._ts_settings.get("typescript_version", "6.0.3"))
typescript_language_server_version = self._custom_settings.get(
"typescript_language_server_version",
self._ts_settings.get("typescript_language_server_version", "5.1.3"),
)
typescript_svelte_plugin_version = self._custom_settings.get("typescript_svelte_plugin_version", "0.3.52")
# versioned install dir avoids silently reusing stale language-server binaries
install_dir = os.path.join(self._ls_resources_dir, f"svelte-lsp-{package_version}")
executable_path = os.path.join(install_dir, "node_modules", ".bin", "svelteserver")
if os.name == "nt":
executable_path += ".cmd"
# version file encodes all four component versions; mismatch triggers reinstall
version_file = os.path.join(install_dir, ".installed_version")
expected_version = (
f"{package_version}_{typescript_version}_{typescript_language_server_version}_{typescript_svelte_plugin_version}"
)
needs_install = not os.path.exists(executable_path)
if not needs_install:
if os.path.exists(version_file):
with open(version_file) as fv:
if fv.read().strip() != expected_version:
needs_install = True
else:
# absent version file → old install that predates companion deps
needs_install = True
if needs_install:
log.info(
"Installing svelte-language-server@%s + typescript@%s + typescript-language-server@%s + typescript-svelte-plugin@%s ...",
package_version,
typescript_version,
typescript_language_server_version,
typescript_svelte_plugin_version,
)
runtime_deps = [
RuntimeDependency(
id="svelte-language-server",
description="Svelte language server",
command=build_npm_install_command("svelte-language-server", package_version, npm_registry),
platform_id="any",
),
RuntimeDependency(
id="typescript",
description="TypeScript language service",
command=build_npm_install_command("typescript", typescript_version, npm_registry),
platform_id="any",
),
RuntimeDependency(
id="typescript-language-server",
description="TypeScript language server (companion)",
command=build_npm_install_command("typescript-language-server", typescript_language_server_version, npm_registry),
platform_id="any",
),
RuntimeDependency(
id="typescript-svelte-plugin",
description="TypeScript plugin for Svelte cross-file awareness",
command=build_npm_install_command("typescript-svelte-plugin", typescript_svelte_plugin_version, npm_registry),
platform_id="any",
),
]
RuntimeDependencyCollection(runtime_deps).install(install_dir)
with open(version_file, "w") as fv:
fv.write(expected_version)
if not os.path.exists(executable_path):
raise FileNotFoundError(
f"executable not found at {executable_path}; "
f"npm install of svelte-language-server@{package_version} did not produce the expected binary."
)
return executable_path
def _create_launch_command(self, core_path: str) -> list[str]:
# stdio suits SolidLSP's subprocess RPC; other hosts may use a different transport.
return [core_path, "--stdio"]
@override
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
ts_settings = self._solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT)
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir, ts_settings)
def __init__(self, config: LanguageServerConfig, repo_path: str, solidlsp_settings: SolidLSPSettings):
resolved_root = os.path.abspath(repo_path)
super().__init__(
config,
resolved_root,
None,
"svelte",
solidlsp_settings,
)
self.repo_path: str = resolved_root
self.tsdk_path = self._get_tsdk_path()
self._lsp_configuration: dict[str, Any] = {}
self._ts_server: SvelteTypeScriptServer | None = None
self._ts_server_started: bool = False
self._svelte_files_indexed: bool = False
self._indexed_svelte_file_uris: list[str] = []
def _get_tsdk_path(self) -> str:
"""
Compute the local typescript/lib path for the Svelte language server.
Asserts if not found, since DependencyProvider guarantees install.
"""
package_version = self._custom_settings.get("svelte_language_server_version", "0.18.0")
install_dir = os.path.join(self._ls_resources_dir, f"svelte-lsp-{package_version}")
tsdk_candidate = os.path.join(install_dir, "node_modules", "typescript", "lib")
assert os.path.isdir(tsdk_candidate), (
f"TypeScript SDK not found at expected path: {tsdk_candidate}. Installation via DependencyProvider failed or version mismatch."
)
return tsdk_candidate
def _get_install_dir(self) -> str:
""":return: versioned install directory for svelte-language-server and companion deps."""
version = self._custom_settings.get("svelte_language_server_version", "0.18.0")
return os.path.join(self._ls_resources_dir, f"svelte-lsp-{version}")
def _get_ts_ls_executable(self) -> str:
""":return: path to the typescript-language-server binary installed alongside the svelte LS."""
path = os.path.join(self._get_install_dir(), "node_modules", ".bin", "typescript-language-server")
if os.name == "nt":
path += ".cmd"
return path
def _get_svelte_ts_plugin_path(self) -> str:
""":return: path to the ``typescript-svelte-plugin`` package directory."""
return os.path.join(self._get_install_dir(), "node_modules", "typescript-svelte-plugin")
def _find_all_svelte_files(self) -> list[str]:
""":return: relative paths of all .svelte files in the repo (excluding node_modules and dot-dirs)."""
svelte_files = []
repo = pathlib.Path(self.repo_path)
for svelte_file in repo.rglob("*.svelte"):
try:
relative = str(svelte_file.relative_to(repo))
if "node_modules" not in relative and not relative.startswith("."):
svelte_files.append(relative)
except Exception as exc:
log.debug("Error processing svelte file %s: %s", svelte_file, exc)
return svelte_files
def _ensure_svelte_files_indexed_on_ts_server(self) -> None:
"""Open all .svelte files on the companion TS server so the plugin includes them in the TS program.
The ``typescript-svelte-plugin``'s ``getExternalFiles`` is called by tsserver when a project
is set up, but only after the first file in that project is opened. Opening each .svelte file
with languageId ``"svelte"`` causes tsserver to invoke the plugin's ``getScriptSnapshot``
for those files, adding them to the project graph so cross-file rename and references work.
"""
if self._svelte_files_indexed:
return
assert self._ts_server is not None
log.info("Indexing .svelte files on companion TypeScript server for cross-file awareness")
svelte_files = self._find_all_svelte_files()
log.debug("Found %d .svelte files to index", len(svelte_files))
# prepare progress tracking BEFORE opening files to avoid a race
self._ts_server.expect_indexing()
for svelte_file in svelte_files:
try:
with self._ts_server.open_file(svelte_file) as file_buffer:
file_buffer.ref_count += 1
self._indexed_svelte_file_uris.append(file_buffer.uri)
except Exception as exc:
log.debug("Failed to open %s on companion TS server: %s", svelte_file, exc)
self._svelte_files_indexed = True
log.info("Svelte file indexing complete; waiting for companion TS server to finish processing")
timeout = TypeScriptLanguageServer.INDEXING_PROGRESS_TIMEOUT
if self._ts_server.wait_for_indexing(timeout=timeout):
log.info("Companion TypeScript server finished indexing .svelte files")
else:
log.warning("Timeout (%ss) waiting for companion TS server to index .svelte files; proceeding anyway", timeout)
def _cleanup_indexed_svelte_files(self) -> None:
"""Decrement ref-counts for all .svelte files opened during indexing."""
if not self._indexed_svelte_file_uris or self._ts_server is None:
return
log.debug("Cleaning up %d indexed .svelte files", len(self._indexed_svelte_file_uris))
for uri in self._indexed_svelte_file_uris:
try:
if uri in self._ts_server.open_file_buffers:
file_buffer = self._ts_server.open_file_buffers[uri]
file_buffer.ref_count -= 1
if file_buffer.ref_count == 0:
self._ts_server.server.notify.did_close_text_document({"textDocument": {"uri": uri}})
del self._ts_server.open_file_buffers[uri]
except Exception as exc:
log.debug("Error closing indexed svelte file %s: %s", uri, exc)
self._indexed_svelte_file_uris.clear()
def _start_typescript_server(self) -> None:
"""Spawn the companion :class:`SvelteTypeScriptServer`, wait for ready, then index .svelte files."""
try:
ts_config = LanguageServerConfig(
code_language=Language.TYPESCRIPT,
trace_lsp_communication=False,
)
log.info("Creating companion SvelteTypeScriptServer")
self._ts_server = SvelteTypeScriptServer(
config=ts_config,
repository_root_path=self.repo_path,
solidlsp_settings=self._solidlsp_settings,
svelte_plugin_path=self._get_svelte_ts_plugin_path(),
tsdk_path=self.tsdk_path,
ts_ls_executable_path=self._get_ts_ls_executable(),
)
log.info("Starting companion SvelteTypeScriptServer")
self._ts_server.start()
log.info("Waiting for companion SvelteTypeScriptServer to be ready ...")
if not self._ts_server.server_ready.wait(timeout=30.0):
log.warning("Timeout waiting for companion SvelteTypeScriptServer; proceeding anyway")
self._ts_server.server_ready.set()
self._ts_server_started = True
log.info("Companion SvelteTypeScriptServer ready")
self._ensure_svelte_files_indexed_on_ts_server()
except Exception:
log.exception("Error starting companion SvelteTypeScriptServer; TS-side operations degrade to svelte LS")
self._ts_server = None
self._ts_server_started = False
def _stop_typescript_server(self) -> None:
"""Shut down the companion TypeScript server if running."""
if self._ts_server is not None:
self._cleanup_indexed_svelte_files()
try:
log.info("Stopping companion SvelteTypeScriptServer")
self._ts_server.stop()
except Exception as exc:
log.warning("Error stopping companion SvelteTypeScriptServer: %s", exc)
finally:
self._ts_server = None
self._ts_server_started = False
def _wrap_notify_send_for_ts_js_mirror(self) -> None:
"""Mirror TS/JS didChange via ``$/onDidChangeTsOrJsFile`` so the server updates TS snapshots.
Unlike upstream ``svelte-vscode`` (svelte-only documentSelector), Serena must also open and
query TS/JS files directly through the same server instance. Standard sync notifications are
therefore kept; ``$/onDidChangeTsOrJsFile`` is sent additionally for didChange so the svelte
LS keeps its internal TS snapshot in sync with the open-buffer content.
"""
_orig_notify_send = self.server.notify.send_notification
def send_notification_wrapped(method: str, params: dict | None = None) -> None:
_orig_notify_send(method, params)
if method != "textDocument/didChange" or not params:
return
text_document = params.get("textDocument")
if not text_document:
return
uri = text_document.get("uri")
if not uri:
return
fb = self.open_file_buffers.get(uri)
if fb is None or fb.language_id not in ("typescript", "javascript"):
return
changes = params.get("contentChanges")
if changes is None:
return
_orig_notify_send("$/onDidChangeTsOrJsFile", {"uri": uri, "changes": changes})
self.server.notify.send_notification = send_notification_wrapped # type: ignore[method-assign] # type: ignore[method-assign] # type: ignore[method-assign]
def _get_initialize_params(self) -> InitializeParams:
"""
Returns the initialize params for the Svelte Language Server.
Builds the full ``initializationOptions.configuration`` section mirroring all
keys expected by ``svelte-language-server`` plugins (svelte, prettier, emmet,
typescript, javascript, js/ts, css, less, scss, html). Caller-supplied overrides
from ``initialization_options_configuration`` are deep-merged on top.
The resulting dict is also stored as :attr:`_lsp_configuration` so
``workspace/configuration`` requests can be answered with real values.
"""
root_uri = pathlib.Path(self.repo_path).as_uri()
# base configuration mirroring all plugin-sections from svelte-vscode initializationOptions
lsp_config: dict[str, Any] = {
"svelte": {},
"prettier": {},
"emmet": {},
"javascript": {"tsdk": self.tsdk_path},
"typescript": {"tsdk": self.tsdk_path},
"js/ts": {"tsdk": self.tsdk_path},
"css": {},
"less": {},
"scss": {},
"html": {},
}
# apply caller-supplied overrides (same top-level keys)
for key, val in self._custom_settings.get("initialization_options_configuration", {}).items():
if key in lsp_config and isinstance(lsp_config[key], dict) and isinstance(val, dict):
lsp_config[key] = {**lsp_config[key], **val}
else:
lsp_config[key] = val
self._lsp_configuration = lsp_config
initialize_params: dict = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
"definition": {"dynamicRegistration": True, "linkSupport": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"signatureHelp": {"dynamicRegistration": True},
"codeAction": {"dynamicRegistration": True},
"rename": {"dynamicRegistration": True, "prepareSupport": True},
"implementation": {"dynamicRegistration": True},
"typeDefinition": {"dynamicRegistration": True},
"diagnostic": {"dynamicRegistration": True},
"publishDiagnostics": {"relatedInformation": True},
},
"workspace": {
"applyEdit": True,
"configuration": True,
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True},
"symbol": {"dynamicRegistration": True},
"diagnostics": {"refreshSupport": True},
"fileOperations": {"didRename": True},
},
},
"initializationOptions": {
"isTrusted": True,
"dontFilterIncompleteCompletions": True,
"configuration": lsp_config,
},
"processId": os.getpid(),
"rootPath": self.repo_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(self.repo_path),
}
],
}
return cast(InitializeParams, initialize_params)
def _start_server(self) -> None:
def window_log_message(msg: dict) -> None:
log.info("LSP: window/logMessage: %s", msg)
def register_capability_handler(params: dict) -> None:
assert "registrations" in params
def configuration_handler(params: dict) -> list:
items = params.get("items", [])
result = []
for item in items:
section = item.get("section", "") if isinstance(item, dict) else ""
result.append(self._lsp_configuration.get(section, {}))
return result
def workspace_apply_edit_handler(_params: dict) -> dict[str, Any]:
return {"applied": False}
def work_done_progress_create(_params: dict) -> dict:
return {}
def do_nothing(_params: dict) -> None:
pass
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_request("window/workDoneProgress/create", work_done_progress_create)
self.server.on_request("workspace/applyEdit", workspace_apply_edit_handler)
self.server.on_request("workspace/configuration", configuration_handler)
self.server.on_request("workspace/diagnostic/refresh", do_nothing)
self.server.on_request("workspace/inlayHints/refresh", do_nothing)
self.server.on_request("workspace/semanticTokens/refresh", do_nothing)
self._wrap_notify_send_for_ts_js_mirror()
self.server.start()
init_params = self._get_initialize_params()
init_response = self.server.send.initialize(init_params)
assert "documentSymbolProvider" in init_response["capabilities"], "Svelte LSP did not advertise documentSymbolProvider"
assert "definitionProvider" in init_response["capabilities"], "Svelte LSP did not advertise definitionProvider"
self.server.notify.initialized({})
self._start_typescript_server()
@staticmethod
def _deduplicate_reference_locations(a: list[ls_types.Location], b: list[ls_types.Location]) -> list[ls_types.Location]:
seen = set()
for loc in a:
start = loc["range"]["start"]
seen.add((loc["uri"], start["line"], start["character"]))
deduped_refs = list(a)
for loc in b:
start = loc["range"]["start"]
key = (loc["uri"], start["line"], start["character"])
if key not in seen:
seen.add(key)
deduped_refs.append(loc)
return deduped_refs
@override
def stop(self, shutdown_timeout: float = 5.0) -> None:
self._stop_typescript_server()
super().stop(shutdown_timeout)
@override
def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
"""Combine references from svelte LS and companion TS server.
For .svelte files: svelte LS + ``$/getComponentReferences``.
For .ts/.js files: companion TS server (svelte-plugin-aware) merged with
svelte LS ``$/getFileReferences`` to maximise cross-file coverage.
Falls back to svelte-LS-only behaviour when companion is unavailable.
"""
symbol_refs = super().request_references(relative_file_path, line, column)
normalize_helper = self.ReferencesLocationRequest(self, relative_file_path, line, column)
if _is_ts_file(relative_file_path):
# augment with svelte LS file-level references
raw = self.server.send_request("$/getFileReferences", cast(Any, self._resolve_file_uri(relative_file_path)))
file_refs = normalize_helper.normalize_response(raw if isinstance(raw, list) else [])
symbol_refs = self._deduplicate_reference_locations(symbol_refs, file_refs)
# augment with companion TS server (typescript-svelte-plugin gives .svelte awareness)
if self._ts_server is not None:
with self._ts_server.open_file(relative_file_path):
ts_refs = self._ts_server.request_references(relative_file_path, line, column)
symbol_refs = self._deduplicate_reference_locations(symbol_refs, ts_refs)
elif _is_svelte_file(relative_file_path):
raw = self.server.send_request("$/getComponentReferences", cast(Any, self._resolve_file_uri(relative_file_path)))
comp_refs = normalize_helper.normalize_response(raw if isinstance(raw, list) else [])
symbol_refs = self._deduplicate_reference_locations(symbol_refs, comp_refs)
return symbol_refs
@override
def request_rename_symbol_edit(self, relative_file_path: str, line: int, column: int, new_name: str) -> ls_types.WorkspaceEdit | None:
"""Delegate TS/JS renames to the companion so the svelte plugin handles cross-file edits.
Falls back to the svelte LS when the companion is unavailable or when the file is .svelte.
"""
if _is_ts_file(relative_file_path) and self._ts_server is not None:
with self._ts_server.open_file(relative_file_path):
return self._ts_server.request_rename_symbol_edit(relative_file_path, line, column, new_name)
return super().request_rename_symbol_edit(relative_file_path, line, column, new_name)
@override
def request_definition(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
"""Delegate TS/JS go-to-definition to the companion for .svelte-aware resolution.
Falls back to the svelte LS when the companion is unavailable or when the file is .svelte.
"""
if _is_ts_file(relative_file_path) and self._ts_server is not None:
with self._ts_server.open_file(relative_file_path):
return self._ts_server.request_definition(relative_file_path, line, column)
return super().request_definition(relative_file_path, line, column)
@override
def _get_language_id_for_file(self, relative_file_path: str) -> str:
ext = os.path.splitext(relative_file_path)[1].lower()
if ext in TS_EXT:
return "typescript"
if ext in JS_EXT:
return "javascript"
return self.language_id
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in ["dist", "build", "coverage"]
+29 -2
View File
@@ -93,6 +93,11 @@ class Language(str, Enum):
LEAN4 = "lean4"
GROOVY = "groovy"
VUE = "vue"
SVELTE = "svelte"
"""Svelte language server using svelte-language-server.
Supports .svelte Single File Components plus TypeScript and JavaScript
files in Svelte projects. Requires Node.js v18+ and npm.
"""
POWERSHELL = "powershell"
PASCAL = "pascal"
"""Pascal Language Server (pasls) for Free Pascal and Lazarus projects.
@@ -123,6 +128,12 @@ class Language(str, Enum):
by the same server, since SPARK is distinguished by pragmas/aspects in
source rather than by file extension.
"""
GDSCRIPT = "gdscript"
"""GDScript language server for Godot Engine projects (Godot 3 and 4).
Connects to the Godot editor's built-in LSP server over TCP (port 6008).
The editor must already be running with its built-in LSP enabled (default).
Supports .gd and .gdscript files.
"""
# 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"""
@@ -266,8 +277,8 @@ class Language(str, Enum):
# We assign lower priority to languages that are supersets of others, such that
# the "larger" language is only chosen when it matches more strongly
match self:
# languages that are supersets of others (Vue is superset of TypeScript/JavaScript)
case self.VUE:
# languages that are supersets of others (Vue/Svelte are supersets of TypeScript/JavaScript)
case self.VUE | self.SVELTE:
return 1
# regular languages
case _:
@@ -434,6 +445,12 @@ class Language(str, Enum):
for base_pattern in ["ts", "js"]:
path_patterns.append(f".{prefix}{base_pattern}{postfix}")
return FilenameMatcher(*path_patterns)
case self.SVELTE:
path_patterns = [".svelte"]
for prefix in ["c", "m", ""]:
for base_pattern in ["ts", "js"]:
path_patterns.append(f".{prefix}{base_pattern}")
return FilenameMatcher(*path_patterns)
case self.POWERSHELL:
return FilenameMatcher(".ps1", ".psm1", ".psd1")
case self.PASCAL:
@@ -472,6 +489,8 @@ class Language(str, Enum):
return FilenameMatcher(".bsl", ".os")
case self.ADA:
return FilenameMatcher(".ads", ".adb", ".ada", case_sensitive=False)
case self.GDSCRIPT:
return FilenameMatcher(".gd", ".gdscript")
case self.HTML:
return FilenameMatcher(".html", ".htm")
case self.SCSS:
@@ -537,6 +556,10 @@ class Language(str, Enum):
from solidlsp.language_servers.vue_language_server import VueLanguageServer
return VueLanguageServer
case self.SVELTE:
from solidlsp.language_servers.svelte_language_server import SvelteLanguageServer
return SvelteLanguageServer
case self.GO:
from solidlsp.language_servers.gopls import Gopls
@@ -727,6 +750,10 @@ class Language(str, Enum):
from solidlsp.language_servers.ada_language_server import AdaLanguageServer
return AdaLanguageServer
case self.GDSCRIPT:
from solidlsp.language_servers.godot_language_server import GodotLanguageServer
return GodotLanguageServer
case self.HTML:
from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer
+155
View File
@@ -3,6 +3,7 @@ import json
import logging
import os
import platform
import socket
import subprocess
import threading
import time
@@ -620,3 +621,157 @@ class StdioLanguageServer(LanguageServerInterface):
# Log the error but don't raise to prevent cascading failures
log.error(f"Failed to write to stdin: {e}")
return
@dataclass
class TCPConnectionInfo:
"""Connection parameters for an LSP server reachable over a TCP socket."""
host: str = "127.0.0.1"
port: int = 6008
connection_timeout: float = 30.0
retry_interval: float = 1.0
class TCPLanguageServer(LanguageServerInterface):
"""LanguageServerInterface that connects to an already-running LSP server over TCP.
Unlike :class:`StdioLanguageServer`, this class does not own the server process — it
connects to an externally-managed server (e.g. the Godot editor) and simply closes
the socket on stop. No LSP ``shutdown``/``exit`` sequence is sent, because the remote
server is expected to keep running after Serena disconnects.
"""
def __init__(
self,
connection_info: TCPConnectionInfo,
language: Language,
determine_log_level: Callable[[str], int],
logger: Callable[[str, str, StringDict | str], None] | None = None,
request_timeout: float | None = None,
) -> None:
super().__init__(language, determine_log_level, logger, request_timeout)
self._connection_info = connection_info
self._sock: socket.socket | None = None
self._file: Any = None # socket.makefile("rb") - buffered reader
self._write_lock = threading.Lock()
def is_running(self) -> bool:
return self._sock is not None
def _start(self) -> None:
deadline = time.monotonic() + self._connection_info.connection_timeout
last_exc: Exception | None = None
while True:
try:
sock = socket.create_connection((self._connection_info.host, self._connection_info.port), timeout=5.0)
sock.settimeout(None)
self._sock = sock
self._file = sock.makefile("rb")
log.info("TCPLanguageServer connected to %s:%d", self._connection_info.host, self._connection_info.port)
break
except OSError as exc:
last_exc = exc
remaining = deadline - time.monotonic()
if remaining <= 0:
raise RuntimeError(
f"Could not connect to {self._connection_info.host}:{self._connection_info.port} "
f"within {self._connection_info.connection_timeout}s"
) from last_exc
log.debug(
"TCPLanguageServer: connection failed (%s), retrying in %.1fs (%.0fs left)",
exc,
self._connection_info.retry_interval,
remaining,
)
time.sleep(min(self._connection_info.retry_interval, remaining))
threading.Thread(
target=self._read_loop,
name=f"LSP-tcp-reader:{self.language.value}",
daemon=True,
).start()
def _stop(self, timeout: float) -> None:
# Close the socket first — this unblocks any readline() in the reader thread.
# BufferedReader.close() acquires an internal lock that readline() holds while
# blocked waiting for data; closing the socket first causes readline() to return
# with an error, releasing the lock before we call f.close().
sock = self._sock
self._sock = None
f = self._file
self._file = None
if sock:
try:
sock.shutdown(socket.SHUT_RDWR)
except Exception:
pass
try:
sock.close()
except Exception:
pass
if f:
try:
f.close()
except Exception:
pass
def _send_payload(self, payload: StringDict) -> None:
sock = self._sock
if sock is None:
return
self._trace("solidlsp", "ls", payload)
data = b"".join(create_message(payload))
with self._write_lock:
try:
sock.sendall(data)
except OSError as e:
log.error("Failed to write to TCP language server: %s", e)
def _read_loop(self) -> None:
"""Read Content-Length-framed LSP messages from the TCP socket and dispatch them."""
exception: Exception | None = None
try:
while self._sock is not None:
f = self._file
if f is None:
break
try:
line = f.readline()
except OSError as exc:
if not self._is_stopping:
exception = LanguageServerTerminatedException("TCP read error", self.language, cause=exc)
break
if not line:
break
try:
num_bytes = content_length(line)
except ValueError:
continue
if num_bytes is None:
continue
while line and line.strip():
try:
line = f.readline()
except OSError:
line = b""
break
if not line:
continue
try:
body = f.read(num_bytes)
except OSError as exc:
if not self._is_stopping:
exception = LanguageServerTerminatedException("TCP read error", self.language, cause=exc)
break
if len(body) < num_bytes:
break
self._handle_body(body)
except Exception as exc:
exception = LanguageServerTerminatedException("Unexpected error in TCP language server read loop", self.language, cause=exc)
log.info("TCP language server read loop has terminated")
if not self._is_stopping:
if exception is None:
exception = LanguageServerTerminatedException("TCP language server read loop terminated unexpectedly", self.language)
log.error(str(exception))
self._cancel_pending_requests(exception)
+1
View File
@@ -268,6 +268,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = {
pytest.mark.bsl,
pytest.mark.skipif(_sh.which("java") is None, reason="Java is not installed"),
],
Language.SVELTE: [pytest.mark.svelte],
Language.ANGULAR: [pytest.mark.angular],
Language.HTML: [pytest.mark.html],
Language.SCSS: [pytest.mark.scss],
@@ -0,0 +1,28 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Lock files
package-lock.json
pnpm-lock.yaml
yarn.lock
@@ -0,0 +1,28 @@
{
"name": "test-repo",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"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"
}
}
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
@@ -0,0 +1,106 @@
<script lang="ts" module>
import { Spring } from 'svelte/motion';
export const count = new Spring(0);
</script>
<script lang="ts">
import { Game } from '$lib/game.js';
let game = new Game();
$inspect(game.guesses).with(console.log);
const offset = $derived(modulo(count.current, 1));
function modulo(n: number, m: number) {
return ((n % m) + m) % m;
}
</script>
<div class="counter">
<button onclick={() => (count.target -= 1)} aria-label="Decrease the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5" />
</svg>
</button>
<div class="counter-viewport">
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
<strong class="hidden" aria-hidden="true">{Math.floor(count.current + 1)}</strong>
<strong>{Math.floor(count.current)}</strong>
</div>
</div>
<button onclick={() => (count.target += 1)} aria-label="Increase the counter by one">
<svg aria-hidden="true" viewBox="0 0 1 1">
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
</svg>
</button>
</div>
<style>
.counter {
display: flex;
border-top: 1px solid rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 1rem 0;
}
.counter button {
width: 2em;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border: 0;
background-color: transparent;
touch-action: manipulation;
font-size: 2rem;
}
.counter button:hover {
background-color: var(--color-bg-1);
}
svg {
width: 25%;
height: 25%;
}
path {
vector-effect: non-scaling-stroke;
stroke-width: 2px;
stroke: #444;
}
.counter-viewport {
width: 8em;
height: 4em;
overflow: hidden;
text-align: center;
position: relative;
}
.counter-viewport strong {
position: absolute;
display: flex;
width: 100%;
height: 100%;
font-weight: 400;
color: var(--color-theme-1);
font-size: 4rem;
align-items: center;
justify-content: center;
}
.counter-digits {
position: absolute;
width: 100%;
height: 100%;
}
.hidden {
top: -100%;
user-select: none;
}
</style>
@@ -0,0 +1,121 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { count } from '$lib/components/Counter.svelte';
let countValue = $derived(count.current);
$inspect(countValue).with(console.log);
</script>
<header>
<div class="corner">
</div>
<nav>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
</svg>
<ul>
<li aria-current={page.url.pathname.startsWith('/') ? 'page' : undefined}>
<a href={resolve('/')}>Sverdle</a>
</li>
</ul>
<svg viewBox="0 0 2 3" aria-hidden="true">
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
</svg>
</nav>
<div class="corner">
</div>
</header>
<style>
header {
display: flex;
justify-content: space-between;
}
.corner {
width: 3em;
height: 3em;
}
.corner a {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.corner img {
width: 2em;
height: 2em;
object-fit: contain;
}
nav {
display: flex;
justify-content: center;
--background: rgba(255, 255, 255, 0.7);
}
svg {
width: 2em;
height: 3em;
display: block;
}
path {
fill: var(--background);
}
ul {
position: relative;
padding: 0;
margin: 0;
height: 3em;
display: flex;
justify-content: center;
align-items: center;
list-style: none;
background: var(--background);
background-size: contain;
}
li {
position: relative;
height: 100%;
}
li[aria-current='page']::before {
--size: 6px;
content: '';
width: 0;
height: 0;
position: absolute;
top: 0;
left: calc(50% - var(--size));
border: var(--size) solid transparent;
border-top: var(--size) solid var(--color-theme-1);
}
nav a {
display: flex;
height: 100%;
align-items: center;
padding: 0 0.5rem;
color: var(--color-text);
font-weight: 700;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.1em;
text-decoration: none;
transition: color 0.2s linear;
}
a:hover {
color: var(--color-theme-1);
}
</style>
@@ -0,0 +1,21 @@
<script lang="ts" module>
export const words = [
"aback",
"abase",
"abate",
"abbey",
"abbot",
"abhor",
"abide",
"abled",
"abode",
"abort",
"about",
"above",
"abuse",
"abyss",
"acorn",
"acrid",
"actor",
];
</script>
@@ -0,0 +1,5 @@
<script lang="ts">
const count: number = "not a number";
</script>
<p>{count}</p>
@@ -0,0 +1,8 @@
export function brokenFactory(): string {
return missingGreeting;
}
export function brokenConsumer(): void {
const value = brokenFactory();
console.log(missingConsumerValue);
}
@@ -0,0 +1,64 @@
import { allowed } from "../routes/(sverdle)/words.server.ts";
import { words } from "$lib/components/Words.svelte";
export const GAME_VERSION = "1.0";
export class Game {
index: number;
guesses: string[];
answers: string[];
answer: string;
constructor(serialized: string | undefined = undefined) {
if (serialized) {
const [index, guesses, answers] = serialized.split("-");
this.index = +index;
this.guesses = guesses ? guesses.split(" ") : [];
this.answers = answers ? answers.split(" ") : [];
} else {
this.index = Math.floor(Math.random() * words.length);
this.guesses = ["", "", "", "", "", ""];
this.answers = [];
}
this.answer = words[this.index];
}
enter(letters: string[]) {
const word = letters.join("");
const valid = allowed.has(word);
if (!valid) return false;
this.guesses[this.answers.length] = word;
const available = Array.from(this.answer);
const answer = Array(5).fill("_");
for (let i = 0; i < 5; i += 1) {
if (letters[i] === available[i]) {
answer[i] = "x";
available[i] = " ";
}
}
for (let i = 0; i < 5; i += 1) {
if (answer[i] === "_") {
const index = available.indexOf(letters[i]);
if (index !== -1) {
answer[i] = "c";
available[index] = " ";
}
}
}
this.answers.push(answer.join(""));
return true;
}
toString() {
return `${this.index}-${this.guesses.join(" ")}-${this.answers.join(" ")}`;
}
}
@@ -0,0 +1,55 @@
import { fail } from "@sveltejs/kit";
import { Game, GAME_VERSION } from "$lib/game.js";
console.log(GAME_VERSION);
import { count } from "$lib/components/Counter.svelte";
export const load = ({ cookies }) => {
const game = new Game(cookies.get("sverdle"));
return {
startingCount: count.current,
guesses: game.guesses,
answers: game.answers,
answer: game.answers.length >= 6 ? game.answer : null,
};
};
export const actions = {
update: async ({ request, cookies }) => {
const game = new Game(cookies.get("sverdle"));
const data = await request.formData();
const key = data.get("key");
const i = game.answers.length;
if (key === "backspace") {
game.guesses[i] = game.guesses[i].slice(0, -1);
} else {
game.guesses[i] += key;
}
cookies.set("sverdle", game.toString(), { path: "/" });
},
enter: async ({ request, cookies }) => {
const game = new Game(cookies.get("sverdle"));
const data = await request.formData();
const guess = data.getAll("guess") as string[];
if (!game.enter(guess)) {
return fail(400, { badGuess: true });
}
cookies.set("sverdle", game.toString(), { path: "/" });
},
restart: async ({ cookies }) => {
cookies.delete("sverdle", { path: "/" });
},
};
@@ -0,0 +1,393 @@
<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');
let i = $derived(won ? -1 : data.answers.length);
let currentGuess = $derived(data.guesses[i] || '');
let submittable = $derived(currentGuess.length === 5);
const { classnames, description } = $derived.by(() => {
let classnames: Record<string, 'exact' | 'close' | 'missing'> = {};
let description: Record<string, string> = {};
data.answers.forEach((answer, i) => {
const guess = data.guesses[i];
for (let i = 0; i < 5; i += 1) {
const letter = guess[i];
if (answer[i] === 'x') {
classnames[letter] = 'exact';
description[letter] = 'correct';
} else if (!classnames[letter]) {
classnames[letter] = answer[i] === 'c' ? 'close' : 'missing';
description[letter] = answer[i] === 'c' ? 'present' : 'absent';
}
}
});
return { classnames, description };
});
function update(event: MouseEvent) {
event.preventDefault();
const key = (event.target as HTMLButtonElement).getAttribute(
'data-key'
);
if (key === 'backspace') {
currentGuess = currentGuess.slice(0, -1);
shake = false;
} else if (currentGuess.length < 5) {
currentGuess += key;
}
}
function keydown(event: KeyboardEvent) {
if (event.metaKey) return;
if (event.key === 'Enter' && !submittable) return;
document
.querySelector(`[data-key="${event.key}" i]`)
?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));
}
console.log(GAME_VERSION, words);
</script>
<svelte:window onkeydown={keydown} />
<svelte:head>
<title>Sverdle</title>
<meta name="description" content="A Wordle clone written in SvelteKit" />
</svelte:head>
<h1 class="visually-hidden">Sverdle</h1>
<form
method="post"
action="?/enter"
use:enhance={() => {
// prevent default callback from resetting the form
return ({ result, update }) => {
shake = result.type === 'failure';
update({ reset: false });
};
}}
>
<a class="how-to-play" href={resolve('/(sverdle)/how-to-play')}>How to play</a>
<div class="grid" class:playing={!won} class:shake onanimationend={() => (shake = false)}>
{#each Array.from(Array(6).keys()) as row (row)}
{@const current = row === i}
<h2 class="visually-hidden">Row {row + 1}</h2>
<div class="row" class:current>
{#each Array.from(Array(5).keys()) as column (column)}
{@const guess = current ? currentGuess : data.guesses[row]}
{@const answer = data.answers[row]?.[column]}
{@const value = guess?.[column] ?? ''}
{@const selected = current && column === guess.length}
{@const exact = answer === 'x'}
{@const close = answer === 'c'}
{@const missing = answer === '_'}
<div class="letter" class:exact class:close class:missing class:selected>
{value}
<span class="visually-hidden">
{#if exact}
(correct)
{:else if close}
(present)
{:else if missing}
(absent)
{:else}
empty
{/if}
</span>
<input name="guess" disabled={!current} type="hidden" {value} />
</div>
{/each}
</div>
{/each}
</div>
<div class="controls">
{#if won || data.answers.length >= 6}
{#if !won && data.answer}
<p>the answer was "{data.answer}"</p>
{/if}
<button data-key="enter" class="restart selected" formaction="?/restart">
{won ? 'you won :)' : `game over :(`} play again?
</button>
{:else}
<div class="keyboard">
<button data-key="enter" class:selected={submittable} disabled={!submittable}>enter</button>
<button
onclick={update}
data-key="backspace"
formaction="?/update"
name="key"
value="backspace"
>
back
</button>
{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}
<div class="row">
{#each row as letter, index (index)}
<button
onclick={update}
data-key={letter}
class={classnames[letter]}
disabled={submittable}
formaction="?/update"
name="key"
value={letter}
aria-label="{letter} {description[letter] || ''}"
>
{letter}
</button>
{/each}
</div>
{/each}
</div>
{/if}
</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%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
flex: 1;
}
.how-to-play {
color: var(--color-text);
}
.how-to-play::before {
content: 'i';
display: inline-block;
font-size: 0.8em;
font-weight: 900;
width: 1em;
height: 1em;
padding: 0.2em;
line-height: 1;
border: 1.5px solid var(--color-text);
border-radius: 50%;
text-align: center;
margin: 0 0.5em 0 0;
position: relative;
top: -0.05em;
}
.grid {
--width: min(100vw, 40vh, 380px);
max-width: var(--width);
align-self: center;
justify-self: center;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.grid .row {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 0.2rem;
margin: 0 0 0.2rem 0;
}
@media (prefers-reduced-motion: no-preference) {
.grid.shake .row.current {
animation: wiggle 0.5s;
}
}
.grid.playing .row.current {
filter: drop-shadow(3px 3px 10px var(--color-bg-0));
}
.letter {
aspect-ratio: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
box-sizing: border-box;
text-transform: lowercase;
border: none;
font-size: calc(0.08 * var(--width));
border-radius: 2px;
background: white;
margin: 0;
color: rgba(0, 0, 0, 0.7);
}
.letter.missing {
background: rgba(255, 255, 255, 0.5);
color: rgba(0, 0, 0, 0.5);
}
.letter.exact {
background: var(--color-theme-2);
color: white;
}
.letter.close {
border: 2px solid var(--color-theme-2);
}
.selected {
outline: 2px solid var(--color-theme-1);
}
.controls {
text-align: center;
justify-content: center;
height: min(18vh, 10rem);
}
.keyboard {
--gap: 0.2rem;
position: relative;
display: flex;
flex-direction: column;
gap: var(--gap);
height: 100%;
}
.keyboard .row {
display: flex;
justify-content: center;
gap: 0.2rem;
flex: 1;
}
.keyboard button,
.keyboard button:disabled {
--size: min(8vw, 4vh, 40px);
background-color: white;
color: black;
width: var(--size);
border: none;
border-radius: 2px;
font-size: calc(var(--size) * 0.5);
margin: 0;
}
.keyboard button.exact {
background: var(--color-theme-2);
color: white;
}
.keyboard button.missing {
opacity: 0.5;
}
.keyboard button.close {
border: 2px solid var(--color-theme-2);
}
.keyboard button:focus {
background: var(--color-theme-1);
color: white;
outline: none;
}
.keyboard button[data-key='enter'],
.keyboard button[data-key='backspace'] {
position: absolute;
bottom: 0;
width: calc(1.5 * var(--size));
height: calc(1 / 3 * (100% - 2 * var(--gap)));
text-transform: uppercase;
font-size: calc(0.3 * var(--size));
padding-top: calc(0.15 * var(--size));
}
.keyboard button[data-key='enter'] {
right: calc(50% + 3.5 * var(--size) + 0.8rem);
}
.keyboard button[data-key='backspace'] {
left: calc(50% + 3.5 * var(--size) + 0.8rem);
}
.keyboard button[data-key='enter']:disabled {
opacity: 0.5;
}
.restart {
width: 100%;
padding: 1rem;
background: rgba(255, 255, 255, 0.5);
border-radius: 2px;
border: none;
}
.restart:focus,
.restart:hover {
background: var(--color-theme-1);
color: white;
outline: none;
}
@keyframes wiggle {
0% {
transform: translateX(0);
}
10% {
transform: translateX(-2px);
}
30% {
transform: translateX(4px);
}
50% {
transform: translateX(-6px);
}
70% {
transform: translateX(+4px);
}
90% {
transform: translateX(-2px);
}
100% {
transform: translateX(0);
}
}
</style>
@@ -0,0 +1,95 @@
<svelte:head>
<title>How to play Sverdle</title>
<meta name="description" content="How to play Sverdle" />
</svelte:head>
<div class="text-column">
<h1>How to play Sverdle</h1>
<p>
Sverdle is a clone of <a href="https://www.nytimes.com/games/wordle/index.html">Wordle</a>, the
word guessing game. To play, enter a five-letter English word. For example:
</p>
<div class="example">
<span class="close">r</span>
<span class="missing">i</span>
<span class="close">t</span>
<span class="missing">z</span>
<span class="exact">y</span>
</div>
<p>
The <span class="exact">y</span> is in the right place. <span class="close">r</span> and
<span class="close">t</span>
are the right letters, but in the wrong place. The other letters are wrong, and can be discarded.
Let's make another guess:
</p>
<div class="example">
<span class="exact">p</span>
<span class="exact">a</span>
<span class="exact">r</span>
<span class="exact">t</span>
<span class="exact">y</span>
</div>
<p>This time we guessed right! You have <strong>six</strong> guesses to get the word.</p>
<p>
Unlike the original Wordle, Sverdle runs on the server instead of in the browser, making it
impossible to cheat. It uses <code>&lt;form&gt;</code> and cookies to submit data, meaning you can
even play with JavaScript disabled!
</p>
</div>
<style>
span {
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 0.8em;
width: 2.4em;
height: 2.4em;
background-color: white;
box-sizing: border-box;
border-radius: 2px;
border-width: 2px;
color: rgba(0, 0, 0, 0.7);
}
.missing {
background: rgba(255, 255, 255, 0.5);
color: rgba(0, 0, 0, 0.5);
}
.close {
border-style: solid;
border-color: var(--color-theme-2);
}
.exact {
background: var(--color-theme-2);
color: white;
}
.example {
display: flex;
justify-content: flex-start;
margin: 1rem 0;
gap: 0.2rem;
}
.example span {
font-size: 1.4rem;
}
p span {
position: relative;
border-width: 1px;
border-radius: 1px;
font-size: 0.4em;
transform: scale(2) translate(0, -10%);
margin: 0 1em;
}
</style>
@@ -0,0 +1,8 @@
import { dev } from "$app/environment";
import { GAME_VERSION } from "$lib/game";
console.log(GAME_VERSION);
export const csr = dev;
export const prerender = true;
@@ -0,0 +1,10 @@
import { words } from "$lib/components/Words.svelte";
export const allowed = new Set([
...words,
"aahed",
"aalii",
"aargh",
"aarti",
"abaca",
]);
@@ -0,0 +1,56 @@
<script lang="ts">
import Header from '$lib/components/Header.svelte';
import './layout.css';
let { children } = $props();
</script>
<div class="app">
<Header />
<main>{@render children()}</main>
<footer>
<p>
visit
<a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a>
to learn about SvelteKit
</p>
</footer>
</div>
<style>
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
width: 100%;
max-width: 64rem;
margin: 0 auto;
box-sizing: border-box;
}
footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 12px;
}
footer a {
font-weight: bold;
}
@media (min-width: 480px) {
footer {
padding: 12px 0;
}
}
</style>
@@ -0,0 +1,104 @@
@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;
--font-mono: 'Fira Mono', monospace;
--color-bg-0: rgb(202, 216, 228);
--color-bg-1: hsl(209, 36%, 86%);
--color-bg-2: hsl(224, 44%, 95%);
--color-theme-1: #ff3e00;
--color-theme-2: #4075a6;
--color-text: rgba(0, 0, 0, 0.7);
--column-width: 42rem;
--column-margin-top: 4rem;
font-family: var(--font-body);
color: var(--color-text);
}
body {
min-height: 100vh;
margin: 0;
background-attachment: fixed;
background-color: var(--color-bg-1);
background-size: 100vw 100vh;
background-image: radial-gradient(50% 50% at 50% 50%, rgba(255, 255, 255, 0.75) 0%, rgba(255, 255, 255, 0) 100%),
linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%);
}
h1,
h2,
p {
font-weight: 400;
}
p {
line-height: 1.5;
}
a {
color: var(--color-theme-1);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1 {
font-size: 2rem;
text-align: center;
}
h2 {
font-size: 1rem;
}
pre {
font-size: 16px;
font-family: var(--font-mono);
background-color: rgba(255, 255, 255, 0.45);
border-radius: 3px;
box-shadow: 2px 2px 6px rgb(255 255 255 / 25%);
padding: 0.5em;
overflow-x: auto;
color: var(--color-text);
}
.text-column {
display: flex;
max-width: 48rem;
flex: 0.6;
flex-direction: column;
justify-content: center;
margin: 0 auto;
}
input,
button {
font-size: inherit;
font-family: inherit;
}
button:focus:not(:focus-visible) {
outline: none;
}
@media (min-width: 720px) {
h1 {
font-size: 2.4rem;
}
}
.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: auto;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}
@@ -0,0 +1,13 @@
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;
@@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}
@@ -0,0 +1,5 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({ plugins: [tailwindcss(), sveltekit()] });
+15
View File
@@ -0,0 +1,15 @@
"""Svelte language server tests."""
import shutil
def _test_npm_available() -> str:
"""Test if npm is available and return error reason if not."""
# Check if npm is installed
if not shutil.which("npm"):
return "npm is not installed or not in PATH"
return "" # No error, npm is available
NPM_UNAVAILABLE_REASON = _test_npm_available()
NPM_UNAVAILABLE = bool(NPM_UNAVAILABLE_REASON)
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
import logging
import os
import shutil
import subprocess
from pathlib import Path
import pytest
from filelock import FileLock
log = logging.getLogger(__name__)
repo_path = Path(__file__).resolve().parents[2] / "resources" / "repos" / "svelte" / "test_repo"
NODE_MODULES = repo_path / "node_modules"
SVELTE_MARKER = NODE_MODULES / "svelte" / "package.json"
SVELTE_KIT_ADAPTER_MARKER = NODE_MODULES / "@sveltejs" / "adapter-auto" / "package.json"
INSTALL_LOCK = repo_path / ".svelte-install.lock"
@pytest.fixture(scope="session", autouse=True)
def _install_svelte_test_repo_node_modules() -> None:
"""Populate the Svelte fixture's project dependencies via npm."""
if SVELTE_MARKER.exists() and SVELTE_KIT_ADAPTER_MARKER.exists():
log.info("Svelte test repo node_modules already populated; skipping npm install")
return
npm_executable = shutil.which("npm.cmd") or shutil.which("npm")
if npm_executable is None:
pytest.skip("npm is not available; cannot install Svelte test repo dependencies")
with FileLock(str(INSTALL_LOCK)):
if SVELTE_MARKER.exists() and SVELTE_KIT_ADAPTER_MARKER.exists():
log.info("Svelte test repo node_modules populated by another worker; skipping npm install")
return
log.warning("Installing npm dependencies into the Svelte test repo at %s.", repo_path)
proc = subprocess.run(
[npm_executable, "install"],
cwd=str(repo_path),
capture_output=True,
text=True,
check=False,
env=os.environ.copy(),
)
if proc.returncode != 0:
log.error("npm install failed (rc=%s).\nstdout:\n%s\nstderr:\n%s", proc.returncode, proc.stdout, proc.stderr)
pytest.skip(f"npm install failed in {repo_path} (rc={proc.returncode}); see logs for details")
if not SVELTE_MARKER.exists() or not SVELTE_KIT_ADAPTER_MARKER.exists():
pytest.skip("npm install completed but required Svelte fixture packages are missing")
log.info("Svelte test repo node_modules installed successfully")
+69
View File
@@ -0,0 +1,69 @@
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 Language
from solidlsp.ls_utils import SymbolUtils
from test.solidlsp.conftest import read_repo_file
from test.solidlsp.svelte import conftest as svelte_test_conftest
from test.solidlsp.util.diagnostics import assert_file_diagnostics
pytestmark = pytest.mark.svelte
class TestSvelteLanguageServer:
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.SVELTE], indirect=True)
def test_svelte_language_server_root_matches_repo_path(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
assert language_server.is_running()
assert repo_path.resolve() == svelte_test_conftest.repo_path.resolve()
assert Path(language_server.language_server.repo_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_svelte_and_typescript_files_in_symbol_tree(self, language_server: SolidLanguageServer) -> None:
symbols = language_server.request_full_symbol_tree()
assert SymbolUtils.symbol_tree_contains_name(symbols, "game"), "game variable not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "Game"), "Game class not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "words"), "words export not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "count"), "count export not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_document_symbols_inside_svelte_file(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "lib", "components", "Counter.svelte")
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
symbol_names = [symbol.get("name") for symbol in symbols[0]]
assert "offset" in symbol_names
assert "modulo" in symbol_names
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_definition_from_component_import_to_svelte_file(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "lib", "components", "Header.svelte")
coords = find_text_coordinates(read_repo_file(language_server, file_path), r"(count)")
definitions = language_server.request_definition(file_path, coords.line, coords.col)
assert len(definitions) == 1, definitions
assert definitions[0]["relativePath"].replace("\\", "/") == "src/lib/components/Counter.svelte"
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_diagnostics_in_typescript_file(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
os.path.join("src", "lib", "diagnostics_sample.ts"),
("missingGreeting", "missingConsumerValue"),
min_count=2,
)
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_diagnostics_in_svelte_file(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
os.path.join("src", "lib", "diagnostics_sample.svelte"),
("number", "string"),
min_count=1,
)
@@ -0,0 +1,26 @@
import os
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
pytestmark = pytest.mark.svelte
class TestSvelteReferences:
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_references_across_svelte_and_typescript(self, language_server: SolidLanguageServer) -> None:
refs = language_server.request_references(os.path.join("src", "lib", "components", "Words.svelte"), 1, 17)
ref_paths = {ref["relativePath"].replace("\\", "/") for ref in refs}
assert "src/routes/(sverdle)/words.server.ts" in ref_paths
assert "src/lib/game.ts" in ref_paths
assert "src/routes/(sverdle)/+page.svelte" in ref_paths
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_references_from_typescript_file(self, language_server: SolidLanguageServer) -> None:
refs = language_server.request_references(os.path.join("src", "lib", "game.ts"), 3, 13)
ref_paths = {ref["relativePath"].replace("\\", "/") for ref in refs}
assert "src/routes/(sverdle)/+page.server.ts" in ref_paths
+123
View File
@@ -0,0 +1,123 @@
import os
from collections.abc import Iterable
from urllib.parse import unquote
import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import TextEdit, WorkspaceEdit
from test.solidlsp.conftest import read_repo_file
pytestmark = pytest.mark.svelte
def _iter_workspace_edit_entries(workspace_edit: WorkspaceEdit) -> Iterable[tuple[str, TextEdit]]:
if workspace_edit.get("changes"):
for uri, edits in workspace_edit["changes"].items():
for edit in edits:
yield uri, edit
for change in workspace_edit.get("documentChanges") or []:
if "textDocument" not in change or "edits" not in change:
continue
uri = change["textDocument"]["uri"]
for edit in change["edits"]:
yield uri, edit
def _assert_rename_edit(
workspace_edit: WorkspaceEdit | None,
new_name: str,
expected_path_fragments: set[str],
) -> None:
assert workspace_edit is not None, "rename should return a WorkspaceEdit"
entries = list(_iter_workspace_edit_entries(workspace_edit))
assert entries, workspace_edit
edited_paths = {unquote(uri).replace("\\", "/") for uri, _edit in entries}
for expected_path in expected_path_fragments:
assert any(expected_path in edited_path for edited_path in edited_paths), (
f"Expected rename edit for {expected_path}, got {sorted(edited_paths)}"
)
for uri, edit in entries:
assert "range" in edit, f"TextEdit in {uri} should have a range"
assert "newText" in edit, f"TextEdit in {uri} should have newText"
assert new_name in edit["newText"], f"TextEdit in {uri} should include {new_name}, got {edit['newText']}"
assert edit["range"]["start"]["line"] >= 0
assert edit["range"]["start"]["character"] >= 0
class TestSvelteRename:
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_rename_svelte_export_updates_svelte_importers(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "lib", "components", "Counter.svelte")
coords = find_text_coordinates(read_repo_file(language_server, file_path), r"(count)")
workspace_edit = language_server.request_rename_symbol_edit(file_path, coords.line, coords.col, "score")
_assert_rename_edit(
workspace_edit,
"score",
{"src/lib/components/Counter.svelte", "src/lib/components/Header.svelte"},
)
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_rename_svelte_export_updates_ts_and_svelte_files(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "lib", "components", "Words.svelte")
coords = find_text_coordinates(read_repo_file(language_server, file_path), r"(words)")
workspace_edit = language_server.request_rename_symbol_edit(file_path, coords.line, coords.col, "vocabulary")
_assert_rename_edit(
workspace_edit,
"vocabulary",
{
"src/lib/components/Words.svelte",
"src/routes/(sverdle)/words.server.ts",
"src/lib/game.ts",
"src/routes/(sverdle)/+page.svelte",
},
)
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_rename_ts_export_declaration_site_workspace_edit(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "routes", "(sverdle)", "words.server.ts")
coords = find_text_coordinates(read_repo_file(language_server, file_path), r"(allowed)")
assert coords is not None
workspace_edit = language_server.request_rename_symbol_edit(file_path, coords.line, coords.col, "allowedWords")
_assert_rename_edit(
workspace_edit,
"allowedWords",
{"src/routes/(sverdle)/words.server.ts"},
)
@pytest.mark.parametrize("language_server", [Language.SVELTE], indirect=True)
def test_rename_ts_class_cross_file_workspace_edit_when_supported(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "lib", "game.ts")
coords = find_text_coordinates(read_repo_file(language_server, file_path), r"(Game)")
assert coords is not None
workspace_edit = language_server.request_rename_symbol_edit(file_path, coords.line, coords.col, "SverdleGame")
assert workspace_edit is not None, (
"SvelteLanguageServer.request_rename_symbol_edit returned None for a cross-file TS class rename; "
"companion SvelteTypeScriptServer (typescript-svelte-plugin) should provide this edit."
)
_assert_rename_edit(
workspace_edit,
"SverdleGame",
{
"src/lib/game.ts",
"src/routes/(sverdle)/+page.server.ts",
"src/lib/components/Counter.svelte",
},
)