Add Svelte support (#1457)

Uses svelte LS and typescript LS as companion server
This commit is contained in:
Eduardo Magalhães authored and GitHub committed 2026-05-14 12:52:34 +02:00
1 parent c3d9aeafae
commit 04630c1abb
35 files changed
+2148 -5

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/
+1
View File
@@ -35,6 +35,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#, 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, 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
@@ -132,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**
+20
View File
@@ -992,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`
+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:
@@ -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"]
+17 -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.
@@ -272,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 _:
@@ -440,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:
@@ -545,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
+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",
},
)