mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-03 09:13:49 +00:00
Add Svelte support (#1457)
Uses svelte LS and typescript LS as companion server
This commit is contained in:
1 parent
c3d9aeafae
commit
04630c1abb
35 files changed
+2148
-5
No files matched your search
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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><form></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()] });
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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",
|
||||
},
|
||||
)
|
||||
Reference in new issue
Block a user