feat(solidlsp): add Astro language server support (#2085)

* feat(solidlsp): add Astro language server support (T170)

* test(solidlsp): fix relative_paths type extraction in test_astro_basic

* refactor(solidlsp): replace asserts with explicit exceptions in Astro LSP

* docs(memories): document rule against assert in language server guide

* docs: add Astro language server documentation (README, docs, template, CHANGELOG)

* feat(solidlsp): implement dual-server architecture for Astro with companion TypeScript server (#2085)

- Implement companion AstroTypeScriptServer using @astrojs/ts-plugin for cross-file code intelligence between .ts and .astro components
- Route .ts/.js document symbols, definitions, references, and renames through companion TypeScript server
- Support two-way buffer edit forwarding between Astro LS and companion TS server
- Replace all assert statements with explicit SolidLSPException and FileNotFoundError
- Handle *.customData configuration requests for Volar HTML service
- Register .astro extension in src/serena/hooks.py
- Add test fixtures and tests for symbol retrieval and cross-file references from TypeScript to Astro components
- Update documentation and CHANGELOG

* fix(solidlsp): narrow astro_ls type to AstroLanguageServer in companion server test

* fix(solidlsp): prevent caching empty document symbols and poll initial compilation in Nextflow tests
This commit is contained in:
Aleksei Tcelishchev authored and GitHub committed 2026-09-23 15:38:19 +02:00
1 parent d764406ac6
commit 15502cee56
28 files changed
+1123 -23

No files matched your search

@@ -63,6 +63,7 @@ To implement a new language server using the DependencyProvider pattern:
**Implementation Pointers:**
- Override `create_launch_command_env` if the launch command needs environment variables to be set (defaults to `{}` in the base implementation)
- When calling subprocesses, e.g. to install dependencies, do not use `subprocess.run` directly; instead, use the `subprocess_run` helper function from `solidlsp.util.subprocess_util`
- **Do not use `assert` for runtime validations:** Python strips `assert` statements when run with `-O` (`python -O`). Environment checks (e.g. `node` or `npm` presence in `PATH`), directory/file verifications (e.g. SDK paths), and LSP capability handshake validations must use explicit `raise` statements (e.g., `SolidLSPException`, `FileNotFoundError`, or `RuntimeError`) rather than `assert` to avoid leaving the server half-initialized or failing silently.
You should look at at least one existing implementation of each base class to understand how they work.
+1
View File
@@ -107,6 +107,7 @@ Status of the `main` branch. Changes prior to the next official version change w
thread (#2038)
* Language Servers:
- Add Astro language server support via `@astrojs/language-server` with a companion TypeScript language server (`@astrojs/ts-plugin`) for cross-file code intelligence (#2085)
- Fix: Dart analysis server no longer receives rootUri/rootPath, which added the monorepo root as an extra analysis root and could pin a CPU core at idle (#2045)
- Fix: The C# language server opened every `.csproj` found anywhere under the repository root,
without consulting the project's ignore settings. On repositories that vendor third-party or
+1 -1
View File
@@ -110,7 +110,7 @@ Serena incorporates a powerful abstraction layer for the integration of language
The underlying language servers are typically open-source projects or at least freely available for use.
When using Serena's language server backend, we provide **support for over 40 programming languages**, including
Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Deno, Elixir, Elm, Erlang, Fortran, F#, GDScript, Gleam, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, LaTeX, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nextflow, Nix, OCaml, Pascal, Perl, PHP, PowerShell, Python, QML, R, Rego, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, SystemVerilog, Terraform, TOML, TypeScript, Vue, WGSL, Wolfram Language, YAML, and Zig.
Ada / SPARK, AL, Angular, Ansible, Astro, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Deno, Elixir, Elm, Erlang, Fortran, F#, GDScript, Gleam, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, LaTeX, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nextflow, Nix, OCaml, Pascal, Perl, PHP, PowerShell, Python, QML, R, Rego, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, SystemVerilog, Terraform, TOML, TypeScript, Vue, WGSL, Wolfram Language, YAML, and Zig.
### The Serena JetBrains Plugin
@@ -46,6 +46,8 @@ Some languages require additional installations or setup steps, as noted.
the upstream `@ansible/ansible-language-server@1.2.3` supports hover, completion, definition,
semantic tokens, and validation; document symbols, workspace symbols, references, and rename
are not supported by this version)
* **Astro**
(requires Node.js v18+ and npm; supports `.astro` files plus TypeScript/JavaScript files via `@astrojs/language-server`; a companion `typescript-language-server` + `@astrojs/ts-plugin` is spawned automatically for cross-file rename, go-to-definition, and references across `.ts`/`.js` and `.astro` files; use language `astro` for Astro projects instead of also enabling `typescript`)
* **Bash**
* **BSL** (1C:Enterprise / OneScript)
(requires Java 21+ on PATH; uses [bsl-language-server](https://github.com/1c-syntax/bsl-language-server) by 1c-syntax; the JAR is auto-downloaded and SHA-256-verified for the bundled default version; supports `.bsl` and `.os` files; configure optional `ls_path` or `bsl_ls_version` under `ls_specific_settings.bsl`)
+1
View File
@@ -386,6 +386,7 @@ markers = [
"qml: language server running for QML (uses Qt's qmlls)",
"gleam: language server running for Gleam (uses the Gleam compiler's bundled `gleam lsp`)",
"wolfram: language server running for Wolfram Language (requires Mathematica 13.0+ or Wolfram Engine 12.1+)",
"astro: language server running for Astro (uses @astrojs/language-server)",
]
[tool.codespell]
+1
View File
@@ -297,6 +297,7 @@ class PreToolUseRemindAboutSymbolicToolsHook(PreToolUseHook):
_CODE_FILE_EXTENSIONS: frozenset[str] = frozenset(
(
".al",
".astro",
".bash",
".c",
".clj",
+16 -15
View File
@@ -2,21 +2,21 @@
project_name: "project_name"
# list of language servers to start when using the LSP backend; choose from:
# ada al angular ansible bash
# bsl clojure cpp cpp_ccls crystal
# csharp csharp_omnisharp cue dart deno
# elixir elm erlang fortran fsharp
# gdscript gleam go groovy haskell
# haxe hlsl html java json
# julia julia_fatou kotlin latex lean4
# lua luau markdown matlab msl
# nextflow nix ocaml pascal perl
# php php_phpactor php_phpantom powershell python
# python_basedpyright python_jedi python_pyrefly python_ty qml
# r rego ruby ruby_solargraph rust
# scala scss solidity svelte swift
# systemverilog terraform toml typescript typescript_vts
# vue wolfram yaml zig
# ada al angular ansible astro
# bash bsl clojure cpp cpp_ccls
# crystal csharp csharp_omnisharp cue dart
# deno elixir elm erlang fortran
# fsharp gdscript gleam go groovy
# haskell haxe hlsl html java
# json julia julia_fatou kotlin latex
# lean4 lua luau markdown matlab
# msl nextflow nix ocaml pascal
# perl php php_phpactor php_phpantom powershell
# python python_basedpyright python_jedi python_pyrefly python_ty
# qml r rego ruby ruby_solargraph
# rust scala scss solidity svelte
# swift systemverilog terraform toml typescript
# typescript_vts vue wolfram yaml zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# For the current list, see values of the LanguageServerId enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
@@ -25,6 +25,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 Astro projects, use astro (subsumes typescript/javascript for .astro projects; requires npm)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For Deno projects, use deno (serves the same .ts/.js files as typescript; requires the deno CLI on PATH)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
@@ -0,0 +1,667 @@
"""
Provides Astro-specific instantiation of the LanguageServer class using
@astrojs/language-server with a companion TypeScript language server (@astrojs/ts-plugin).
Operates in a dual-server setup: the Astro LS handles .astro files (AST parsing,
document symbols), while a companion TypeScript LS (configured with @astrojs/ts-plugin)
handles definitions, references, document symbols, and rename for .ts/.js files and
cross-file resolution between .ts/.js and .astro files.
"""
from __future__ import annotations
import logging
import os
import pathlib
import shutil
from collections.abc import Callable
from typing import Any
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,
prefer_non_node_modules_definition,
)
from solidlsp.ls import (
DocumentSymbols,
LanguageServerDependencyProvider,
LanguageServerDependencyProviderSinglePath,
LSPFileBuffer,
SolidLanguageServer,
)
from solidlsp.ls_config import FilenameMatcher, LanguageServerConfig, LanguageServerId
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
ASTRO_EXT = frozenset({".astro"})
TS_EXT = frozenset({".ts", ".tsx", ".mts", ".cts"})
JS_EXT = frozenset({".js", ".jsx", ".mjs", ".cjs"})
_MAX_FAILED_FILES_IN_ERROR = 10
class AstroCompanionPreparationError(RuntimeError):
"""Raised when the companion TypeScript server cannot be prepared deterministically."""
def _is_ts_file(uri: str) -> bool:
return uri.lower().endswith(tuple(TS_EXT | JS_EXT))
def _is_astro_file(uri: str) -> bool:
return uri.lower().endswith(tuple(ASTRO_EXT))
class AstroTypeScriptServer(TypeScriptLanguageServer):
"""Companion TypeScript language server for Astro projects.
Loads ``@astrojs/ts-plugin`` so the TS graph becomes .astro-aware:
cross-file rename, find-references, and go-to-definition from .ts/.js files
into .astro consumers all work correctly through this companion.
Spawned and owned by :class:`AstroLanguageServer`; not instantiated directly.
"""
INDEXING_PROGRESS_TIMEOUT = 120.0
SERVER_READY_TIMEOUT = 30.0
class DependencyProvider(TypeScriptLanguageServer.DependencyProvider):
"""Returns the pre-installed typescript-language-server binary.
The binary is installed by ``AstroLanguageServer.DependencyProvider``;
this provider 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,
astro_plugin_path: str,
tsdk_path: str,
ts_ls_executable_path: str,
) -> None:
self._astro_plugin_path = astro_plugin_path
self._custom_tsdk_path = tsdk_path
self._explicit_ts_ls_executable = ts_ls_executable_path
super().__init__(config, repository_root_path, solidlsp_settings)
@override
def get_source_fn_matcher(self) -> FilenameMatcher:
# Include .astro so references returned by the plugin are not filtered out
return LanguageServerId.ASTRO.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:
""".astro files map to 'astro' to activate the plugin; TS/JS as normal."""
ext = os.path.splitext(relative_file_path)[1].lower()
if ext in ASTRO_EXT:
return "astro"
if ext == ".tsx":
return "typescriptreact"
if ext == ".jsx":
return "javascriptreact"
if ext in TS_EXT:
return "typescript"
if ext in JS_EXT:
return "javascript"
return "typescript"
@override
def _create_base_initialize_params(self) -> dict:
params = super()._create_base_initialize_params()
params["initializationOptions"] = {
"plugins": [
{
"name": "@astrojs/ts-plugin",
"location": self._astro_plugin_path,
"languages": ["astro"],
}
],
"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()
@override
def _handle_server_ready_timeout(self, timeout: float) -> None:
log.warning("Astro companion TypeScript server did not become ready within %.0fs; proceeding", timeout)
self.server_ready.set()
@override
def _handle_project_indexing_timeout(self, timeout: float) -> None:
log.warning(
"Astro companion TypeScript server project indexing did not complete within %.0fs (%s)",
timeout,
self.describe_indexing_state(),
)
class AstroLanguageServer(SolidLanguageServer):
"""
Astro language server using @astrojs/language-server with companion TypeScript LS.
"""
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:
if shutil.which("node") is None:
raise SolidLSPException("node is not installed or isn't in PATH. Please install NodeJS and try again.")
if shutil.which("npm") is None:
raise SolidLSPException("npm is not installed or isn't in PATH. Please install npm and try again.")
package_version = self._custom_settings.get("astro_language_server_version", "2.17.0")
typescript_version = self._custom_settings.get("typescript_version", self._ts_settings.get("typescript_version", "5.9.3"))
typescript_language_server_version = self._custom_settings.get(
"typescript_language_server_version",
self._ts_settings.get("typescript_language_server_version", "5.1.3"),
)
astro_ts_plugin_version = self._custom_settings.get("astro_ts_plugin_version", "1.10.10")
npm_registry = self._custom_settings.get("npm_registry", self._ts_settings.get("npm_registry"))
install_dir = os.path.join(self._ls_resources_dir, f"astro-lsp-{package_version}")
executable_path = os.path.join(install_dir, "node_modules", ".bin", "astro-ls")
ts_ls_executable = os.path.join(install_dir, "node_modules", ".bin", "typescript-language-server")
if os.name == "nt":
executable_path += ".cmd"
ts_ls_executable += ".cmd"
version_file = os.path.join(install_dir, ".installed_version")
expected_version = f"{package_version}_{typescript_version}_{typescript_language_server_version}_{astro_ts_plugin_version}"
needs_install = not os.path.exists(executable_path) or not os.path.exists(ts_ls_executable)
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:
needs_install = True
if needs_install:
log.info(
"Installing @astrojs/language-server@%s + @astrojs/ts-plugin@%s + typescript@%s + typescript-language-server@%s ...",
package_version,
astro_ts_plugin_version,
typescript_version,
typescript_language_server_version,
)
runtime_deps = [
RuntimeDependency(
id="@astrojs/language-server",
description="Astro language server",
command=build_npm_install_command("@astrojs/language-server", package_version, npm_registry),
platform_id="any",
),
RuntimeDependency(
id="@astrojs/ts-plugin",
description="Astro TypeScript plugin for cross-file code intelligence",
command=build_npm_install_command("@astrojs/ts-plugin", astro_ts_plugin_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",
),
]
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 @astrojs/language-server@{package_version} did not produce the expected binary."
)
return executable_path
def _create_launch_command(self, core_path: str) -> list[str]:
return [core_path, "--stdio"]
@override
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
ts_settings = self._solidlsp_settings.get_ls_specific_settings(LanguageServerId.TYPESCRIPT)
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir, ts_settings)
def __init__(self, config: LanguageServerConfig, repo_path: str, solidlsp_settings: SolidLSPSettings) -> None:
resolved_root = os.path.abspath(repo_path)
super().__init__(
config,
resolved_root,
None,
"astro",
solidlsp_settings,
)
self.repo_path: str = resolved_root
self.tsdk_path = self._get_tsdk_path()
self._ts_server: AstroTypeScriptServer | None = None
self._ts_server_started: bool = False
self._astro_files_indexed: bool = False
self._indexed_astro_file_uris: list[str] = []
def _get_install_dir(self) -> str:
version = self._custom_settings.get("astro_language_server_version", "2.17.0")
return os.path.join(self._ls_resources_dir, f"astro-lsp-{version}")
def _get_tsdk_path(self) -> str:
tsdk_candidate = os.path.join(self._get_install_dir(), "node_modules", "typescript", "lib")
if not os.path.isdir(tsdk_candidate):
raise FileNotFoundError(
f"TypeScript SDK not found at expected path: {tsdk_candidate}. Installation via DependencyProvider failed or version mismatch."
)
return tsdk_candidate
def _get_ts_ls_executable(self) -> str:
path = os.path.join(self._get_install_dir(), "node_modules", ".bin", "typescript-language-server")
if os.name == "nt":
path += ".cmd"
return path
def _get_astro_ts_plugin_path(self) -> str:
return os.path.join(self._get_install_dir(), "node_modules", "@astrojs", "ts-plugin")
def _find_all_astro_files(self) -> list[str]:
astro_files = []
repo = pathlib.Path(self.repo_path)
for astro_file in repo.rglob("*.astro"):
try:
relative = str(astro_file.relative_to(repo))
if "node_modules" not in relative and not relative.startswith("."):
astro_files.append(relative)
except Exception as exc:
log.debug("Error processing astro file %s: %s", astro_file, exc)
return astro_files
def _get_companion_indexing_timeout(self) -> float:
ts_settings = self._solidlsp_settings.get_ls_specific_settings(LanguageServerId.TYPESCRIPT)
timeout = self._custom_settings.get(
"indexing_timeout",
ts_settings.get("indexing_timeout", AstroTypeScriptServer.INDEXING_PROGRESS_TIMEOUT),
)
return float(timeout)
def _ensure_astro_files_indexed_on_ts_server(self) -> None:
if self._astro_files_indexed:
return
if self._ts_server is None:
return
log.info("Indexing .astro files on companion TypeScript server for cross-file awareness")
astro_files = self._find_all_astro_files()
log.debug("Found %d .astro files to index", len(astro_files))
self._ts_server.expect_indexing()
failed_astro_files = []
first_open_error: Exception | None = None
for astro_file in astro_files:
try:
with self._ts_server.open_file(astro_file) as file_buffer:
file_buffer.ref_count += 1
self._indexed_astro_file_uris.append(file_buffer.uri)
except Exception as exc:
log.debug("Failed to open %s on companion TS server: %s", astro_file, exc)
if first_open_error is None:
first_open_error = exc
failed_astro_files.append(astro_file)
if failed_astro_files:
shown_files = sorted(failed_astro_files)[:_MAX_FAILED_FILES_IN_ERROR]
remainder = len(failed_astro_files) - len(shown_files)
listing = ", ".join(shown_files) + (f" and {remainder} more" if remainder else "")
raise AstroCompanionPreparationError(
f"Failed to open {len(failed_astro_files)} Astro file(s) on companion TypeScript server: {listing}"
) from first_open_error
self._astro_files_indexed = True
log.info("Astro file indexing complete; waiting for companion TS server to finish processing")
timeout = self._get_companion_indexing_timeout()
if self._ts_server._wait_for_indexing_start_or_completion(timeout=timeout):
log.info("Companion TypeScript server finished indexing .astro files")
else:
log.warning(
"Companion TypeScript server did not finish indexing %d .astro files within %.0fs (%s); proceeding",
len(astro_files),
timeout,
self._ts_server.describe_indexing_state(),
)
def _cleanup_indexed_astro_files(self) -> None:
if not self._indexed_astro_file_uris or self._ts_server is None:
return
log.debug("Cleaning up %d indexed .astro files", len(self._indexed_astro_file_uris))
for uri in self._indexed_astro_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 astro file %s: %s", uri, exc)
self._indexed_astro_file_uris.clear()
def _start_typescript_server(self) -> None:
try:
ts_config = LanguageServerConfig(
ls_id=LanguageServerId.TYPESCRIPT,
trace_lsp_communication=False,
)
log.info("Creating companion AstroTypeScriptServer")
self._ts_server = AstroTypeScriptServer(
config=ts_config,
repository_root_path=self.repo_path,
solidlsp_settings=self._solidlsp_settings,
astro_plugin_path=self._get_astro_ts_plugin_path(),
tsdk_path=self.tsdk_path,
ts_ls_executable_path=self._get_ts_ls_executable(),
)
log.info("Starting companion AstroTypeScriptServer")
self._ts_server.start()
self._ts_server_started = True
log.info("Companion AstroTypeScriptServer ready")
self._ensure_astro_files_indexed_on_ts_server()
except (TimeoutError, AstroCompanionPreparationError):
log.exception("Failed to prepare companion AstroTypeScriptServer; aborting Astro server startup")
self._stop_typescript_server()
raise
except Exception:
log.exception("Error starting companion AstroTypeScriptServer; TS-side operations degrade to astro LS")
self._ts_server = None
self._ts_server_started = False
def _stop_typescript_server(self) -> None:
if self._ts_server is not None:
self._cleanup_indexed_astro_files()
try:
log.info("Stopping companion AstroTypeScriptServer")
self._ts_server.stop()
except Exception as exc:
log.warning("Error stopping companion AstroTypeScriptServer: %s", exc)
finally:
self._ts_server = None
self._ts_server_started = False
def _forward_edit_to_ts_server_if_needed(self, relative_file_path: str, edit_fn: Callable[[], object]) -> None:
if self._ts_server is None or not self._ts_server_started:
return
absolute_file_path = os.path.abspath(os.path.join(self.repo_path, relative_file_path))
uri = pathlib.Path(absolute_file_path).as_uri()
if uri in self._ts_server.open_file_buffers:
edit_fn()
@override
def insert_text_at_position(self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str) -> ls_types.Position:
result = super().insert_text_at_position(relative_file_path, line, column, text_to_be_inserted)
self._forward_edit_to_ts_server_if_needed(
relative_file_path,
lambda: self._ts_server.insert_text_at_position( # type: ignore[union-attr]
relative_file_path, line, column, text_to_be_inserted
),
)
return result
@override
def delete_text_between_positions(
self,
relative_file_path: str,
start: ls_types.Position,
end: ls_types.Position,
) -> str:
deleted_text = super().delete_text_between_positions(relative_file_path, start, end)
self._forward_edit_to_ts_server_if_needed(
relative_file_path,
lambda: self._ts_server.delete_text_between_positions( # type: ignore[union-attr]
relative_file_path, start, end
),
)
return deleted_text
def _create_base_initialize_params(self) -> dict:
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": {
"typescript": {
"tsdk": self.tsdk_path,
},
},
}
return 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:
if "registrations" not in params:
raise SolidLSPException(f"Expected registrations in client/registerCapability params: {params}")
def configuration_handler(params: dict) -> list:
items = params.get("items", [])
results = []
for item in items:
section = item.get("section", "") if isinstance(item, dict) else ""
if section.endswith(".customData"):
results.append([])
else:
results.append({})
return results
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/inlayHint/refresh", do_nothing)
self.server.on_request("workspace/semanticTokens/refresh", do_nothing)
self.server.start()
init_params = self._create_initialize_params()
init_response = self.server.send.initialize(init_params)
if "documentSymbolProvider" not in init_response.get("capabilities", {}):
raise SolidLSPException("Astro LSP did not advertise documentSymbolProvider")
if "definitionProvider" not in init_response.get("capabilities", {}):
raise SolidLSPException("Astro 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]:
symbol_refs = super().request_references(relative_file_path, line, column)
if _is_ts_file(relative_file_path):
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_astro_file(relative_file_path):
if self._ts_server is not None:
try:
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)
except Exception as exc:
log.debug("Companion TS references failed for %s: %s", relative_file_path, exc)
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:
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]:
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 request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
if _is_ts_file(relative_file_path) and self._ts_server is not None:
return self._ts_server.request_document_symbols(relative_file_path)
return super().request_document_symbols(relative_file_path, file_buffer)
@override
def request_text_document_diagnostics(
self,
relative_file_path: str,
start_line: int = 0,
end_line: int = -1,
min_severity: int = 4,
) -> list[ls_types.Diagnostic]:
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_text_document_diagnostics(relative_file_path, start_line, end_line, min_severity)
return super().request_text_document_diagnostics(relative_file_path, start_line, end_line, min_severity)
@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 ASTRO_EXT:
return "astro"
if ext == ".tsx":
return "typescriptreact"
if ext == ".jsx":
return "javascriptreact"
if ext in TS_EXT:
return "typescript"
if ext in JS_EXT:
return "javascript"
return self.language_id
@override
def _get_preferred_definition(self, definitions: list[ls_types.Location]) -> ls_types.Location:
return prefer_non_node_modules_definition(definitions)
@override
def is_ignored_dirname(self, dirname: str) -> bool:
return super().is_ignored_dirname(dirname) or dirname in [
"dist",
"build",
"coverage",
".astro",
]
+5 -4
View File
@@ -1974,11 +1974,12 @@ class SolidLanguageServer(ABC):
# no cached result: get the raw root symbols from the language server
document_symbols = self._build_document_symbols_from_raw_symbols(relative_file_path, file_buffer=file_data)
# update cache
# update cache (only cache non-empty results to avoid permanently caching unindexed responses)
content_hash = file_data.content_hash
log.debug("Updating cached document symbols for %s (hash=%s)", relative_file_path, content_hash)
self._document_symbols_cache[cache_key] = (content_hash, document_symbols)
self._document_symbols_cache_is_modified = True
if document_symbols.root_symbols:
log.debug("Updating cached document symbols for %s (hash=%s)", relative_file_path, content_hash)
self._document_symbols_cache[cache_key] = (content_hash, document_symbols)
self._document_symbols_cache_is_modified = True
return document_symbols
+16 -1
View File
@@ -174,6 +174,11 @@ class LanguageServerId(Enum):
Supports .svelte Single File Components plus TypeScript and JavaScript
files in Svelte projects. Requires Node.js v18+ and npm.
"""
ASTRO = "astro"
"""Astro language server using @astrojs/language-server.
Supports .astro Single File Components plus TypeScript and JavaScript
files in Astro projects. Requires Node.js v18+ and npm.
"""
POWERSHELL = "powershell"
PASCAL = "pascal"
"""Pascal Language Server (pasls) for Free Pascal and Lazarus projects.
@@ -404,7 +409,7 @@ class LanguageServerId(Enum):
# the "larger" language is only chosen when it matches more strongly
match self:
# languages that are supersets of others (Vue/Svelte are supersets of TypeScript/JavaScript)
case self.VUE | self.SVELTE:
case self.VUE | self.SVELTE | self.ASTRO:
return 1
# regular languages
case _:
@@ -591,6 +596,12 @@ class LanguageServerId(Enum):
for base_pattern in ["ts", "js"]:
path_patterns.append(f".{prefix}{base_pattern}")
return FilenameMatcher(*path_patterns)
case self.ASTRO:
path_patterns = [".astro"]
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:
@@ -726,6 +737,10 @@ class LanguageServerId(Enum):
from solidlsp.language_servers.svelte_language_server import SvelteLanguageServer
return SvelteLanguageServer
case self.ASTRO:
from solidlsp.language_servers.astro_language_server import AstroLanguageServer
return AstroLanguageServer
case self.GO:
from solidlsp.language_servers.gopls import Gopls
+1
View File
@@ -313,6 +313,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[LanguageServerId, list[MarkDecorator | Mark]] = {
LanguageServerId.ANGULAR: [pytest.mark.angular],
LanguageServerId.HTML: [pytest.mark.html],
LanguageServerId.SCSS: [pytest.mark.scss],
LanguageServerId.ASTRO: [pytest.mark.astro],
}
@@ -0,0 +1,5 @@
node_modules/
dist/
.astro/
package-lock.json
.astro-install.lock
@@ -0,0 +1,5 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
// Minimal config for testing
});
@@ -0,0 +1,8 @@
{
"name": "astro-test-repo",
"type": "module",
"version": "0.0.1",
"dependencies": {
"astro": "^5.0.0"
}
}
@@ -0,0 +1,24 @@
---
interface Props {
title: string;
body: string;
href: string;
}
const { href, title, body } = Astro.props;
function formatTitle(val: string): string {
return val.toUpperCase();
}
---
<li class="link-card">
<a href={href}>
<h2>
{formatTitle(title)}
<span>&rarr;</span>
</h2>
<p>
{body}
</p>
</a>
</li>
@@ -0,0 +1,7 @@
---
const year = new Date().getFullYear();
---
<footer>
<p>&copy; {year} Test Site</p>
</footer>
@@ -0,0 +1,13 @@
---
interface Props {
siteName: string;
}
const { siteName } = Astro.props;
---
<header>
<nav>
<a href="/">{siteName}</a>
</nav>
</header>
@@ -0,0 +1,19 @@
---
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
@@ -0,0 +1,18 @@
---
import Card from '../components/Card.astro';
import Layout from '../layouts/Layout.astro';
import Header from '../components/Header.astro';
import { createCounter } from '../stores/counter';
import { formatNumber } from '../utils/format';
const pageTitle = "Welcome to Astro";
const counter = createCounter();
---
<Layout title={pageTitle}>
<Header siteName="Test Site" />
<main>
<h1>{pageTitle}</h1>
<p>Count: {formatNumber(counter.count)}</p>
<Card href="https://astro.build" title="Docs" body="Documentation" />
</main>
</Layout>
@@ -0,0 +1,21 @@
export interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
}
export function createCounter(): CounterStore {
let count = 0;
return {
get count() {
return count;
},
increment() {
count++;
},
decrement() {
count--;
},
};
}
@@ -0,0 +1,13 @@
/**
* Format a number with thousands separators.
*/
export function formatNumber(value: number): string {
return value.toLocaleString();
}
/**
* Format a date to locale string.
*/
export function formatDate(date: Date): string {
return date.toLocaleDateString();
}
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"jsx": "preserve",
"strict": true
},
"include": ["**/*"],
"exclude": ["dist", "node_modules"]
}
+8
View File
@@ -523,6 +523,13 @@ FIND_SYMBOL_REFERENCES_CASES = [
FindSymbolCase(
ls_id=LanguageServerId.LATEX, id="latex_methods_section", symbol_name="Methods", expected_kind="Module", expected_file="main.tex"
).to_pytest_param(),
FindSymbolCase(
ls_id=LanguageServerId.ASTRO,
id="astro_props_interface",
symbol_name="Props",
expected_kind="Interface",
expected_file=os.path.join("src", "components", "Card.astro"),
).to_pytest_param(),
]
FIND_REFERENCE_CASES = [
@@ -793,6 +800,7 @@ def serena_config():
LanguageServerId.LEAN4,
LanguageServerId.MSL,
LanguageServerId.LATEX,
LanguageServerId.ASTRO,
]:
repo_path = get_repo_path(language)
if repo_path.exists():
+1
View File
@@ -0,0 +1 @@
"""Tests for Astro language server."""
+69
View File
@@ -0,0 +1,69 @@
"""
Pytest fixtures for Astro language server tests.
This conftest does NOT install the language server itself -- that is handled by
``RuntimeDependencyCollection`` inside ``AstroLanguageServer`` and lands in Serena's
managed ``ls_resources_dir`` like every other LS download.
What we install here is the test fixture project's npm dependencies (notably
``astro``) into the fixture's own ``node_modules``. The companion tsserver (via
``@astrojs/ts-plugin``) needs ``astro`` resolvable from the workspace to understand
``.astro`` modules; without it, cross-file references between ``.ts`` and ``.astro``
files return nothing and the tests would pass vacuously.
"""
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_ROOT = Path(__file__).resolve().parents[2] / "resources" / "repos" / "astro" / "test_repo"
NODE_MODULES = REPO_ROOT / "node_modules"
ASTRO_MARKER = NODE_MODULES / "astro" / "package.json"
INSTALL_LOCK = REPO_ROOT / ".astro-install.lock"
@pytest.fixture(scope="session", autouse=True)
def _install_astro_test_repo_node_modules() -> None:
"""Populate the Astro fixture's project dependencies via npm (cached across sessions)."""
if ASTRO_MARKER.exists():
log.info("Astro 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 Astro test repo dependencies")
with FileLock(str(INSTALL_LOCK)):
if ASTRO_MARKER.exists():
log.info("Astro test repo node_modules populated by another worker; skipping npm install")
return
log.warning(
"Installing npm dependencies into the Astro test repo at %s. This is a one-time cost per checkout.",
REPO_ROOT,
)
proc = subprocess.run(
[npm_executable, "install", "--no-audit", "--no-fund", "--loglevel=warn"],
cwd=str(REPO_ROOT),
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_ROOT} (rc={proc.returncode}); see logs for details")
if not ASTRO_MARKER.exists():
pytest.skip(f"npm install completed but {ASTRO_MARKER} is missing; cannot run Astro tests")
log.info("Astro test repo node_modules installed successfully")
+118
View File
@@ -0,0 +1,118 @@
"""
Basic integration and symbol retrieval tests for the Astro language server.
"""
import os
from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.conftest import document_symbol_names, request_all_symbols
pytestmark = pytest.mark.astro
class TestAstroLanguageServerBasics:
"""Smoke, symbol retrieval, and dual-server coordination tests for Astro."""
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ASTRO], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
def test_card_document_symbols(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "components", "Card.astro")
names = document_symbol_names(language_server, file_path)
# Frontmatter symbols
assert "Props" in names
assert "formatTitle" in names
# Template elements
assert any(n in names for n in ("li", "a", "h2", "p"))
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
def test_layout_document_symbols(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "layouts", "Layout.astro")
names = document_symbol_names(language_server, file_path)
assert "Props" in names
assert "title" in names
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
def test_index_document_symbols(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "pages", "index.astro")
names = document_symbol_names(language_server, file_path)
assert "pageTitle" in names
assert any(n in names for n in ("main", "h1", "Card", "Layout"))
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
def test_typescript_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""Verify TypeScript files in Astro project have document symbols routed to companion TS server."""
file_path = os.path.join("src", "stores", "counter.ts")
symbols = language_server.request_document_symbols(file_path)
assert symbols is not None, "Expected document symbols from companion TypeScript server"
all_symbols, _roots = symbols.get_all_symbols_and_roots()
symbol_names = [s["name"] for s in all_symbols]
assert "CounterStore" in symbol_names
assert "createCounter" in symbol_names
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
def test_format_utils_symbols(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "utils", "format.ts")
symbols = language_server.request_document_symbols(file_path)
assert symbols is not None
all_symbols, _roots = symbols.get_all_symbols_and_roots()
symbol_names = [s["name"] for s in all_symbols]
assert "formatNumber" in symbol_names
assert "formatDate" in symbol_names
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
def test_typescript_companion_server_starts(self, language_server: SolidLanguageServer) -> None:
"""Verify companion TypeScript server starts and attaches to AstroLanguageServer."""
from solidlsp.language_servers.astro_language_server import AstroLanguageServer
astro_ls = language_server.language_server
assert isinstance(astro_ls, AstroLanguageServer), "Expected AstroLanguageServer instance"
assert astro_ls._ts_server is not None, "Expected companion TypeScript server to be initialized"
assert astro_ls._ts_server_started, "Expected companion TypeScript server to be started"
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
def test_find_definition_within_typescript(self, language_server: SolidLanguageServer) -> None:
"""Verify go-to-definition within TypeScript files in Astro project."""
file_path = os.path.join("src", "stores", "counter.ts")
# Line 7 (0-indexed line 6), char 35: CounterStore return type annotation
definitions = language_server.request_definition(file_path, 6, 35)
assert definitions, "Expected at least one definition location"
def_loc = definitions[0]
assert def_loc["uri"].endswith("counter.ts")
assert def_loc["range"]["start"]["line"] == 0
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
def test_find_references_within_typescript(self, language_server: SolidLanguageServer) -> None:
"""Verify reference finding within TypeScript file."""
file_path = os.path.join("src", "stores", "counter.ts")
# CounterStore interface on line 0, char 20
references = language_server.request_references(file_path, 0, 20)
assert references, "Expected at least one reference location"
locations = {(ref["uri"].rsplit("/", 1)[-1], ref["range"]["start"]["line"]) for ref in references}
assert ("counter.ts", 0) in locations
assert ("counter.ts", 6) in locations
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None:
all_symbols = request_all_symbols(language_server)
relative_paths: set[str] = set()
for s in all_symbols:
loc = s.get("location")
if isinstance(loc, dict):
rel = loc.get("relativePath")
if isinstance(rel, str):
relative_paths.add(rel.replace("\\", "/"))
assert any("Card.astro" in p for p in relative_paths)
assert any("index.astro" in p for p in relative_paths)
@@ -0,0 +1,60 @@
"""
Cross-file symbol retrieval and reference tests for the Astro language server.
"""
import os
from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import LanguageServerId
pytestmark = pytest.mark.astro
class TestAstroSymbolRetrieval:
"""Symbol retrieval and cross-file code intelligence tests."""
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ASTRO], indirect=True)
def test_get_containing_symbol_in_typescript(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Verify symbols in .ts files are discoverable within an Astro project."""
counter_path = os.path.join("src", "stores", "counter.ts")
symbols = language_server.request_document_symbols(counter_path)
assert symbols is not None, "Expected document symbols but got None"
all_symbols, _roots = symbols.get_all_symbols_and_roots()
symbol_names = [s["name"] for s in all_symbols]
assert "CounterStore" in symbol_names
assert "createCounter" in symbol_names
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ASTRO], indirect=True)
def test_find_references_to_typescript_export(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Verify finding references to a TypeScript export from an .astro component.
createCounter is defined in counter.ts (line 6) and imported + called in
src/pages/index.astro (lines 4 and 7). This exercises the dual-server cross-file path:
the companion TypeScript server with @astrojs/ts-plugin must resolve the .astro usage.
"""
counter_path = os.path.join("src", "stores", "counter.ts")
references = language_server.request_references(counter_path, 6, 20)
assert references is not None, "Expected references but got None"
locations = {(ref["uri"].rsplit("/", 1)[-1], ref["range"]["start"]["line"]) for ref in references}
# Definition in counter.ts line 6
assert ("counter.ts", 6) in locations, f"Expected definition at counter.ts:6, got: {sorted(locations)}"
# Import in index.astro line 4
assert ("index.astro", 4) in locations, f"Expected import at index.astro:4, got: {sorted(locations)}"
# Invocation in index.astro line 7
assert ("index.astro", 7) in locations, f"Expected call at index.astro:7, got: {sorted(locations)}"
@pytest.mark.parametrize("language_server", [LanguageServerId.ASTRO], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ASTRO], indirect=True)
def test_go_to_definition_from_typescript(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Verify go-to-definition within TypeScript source in an Astro project."""
counter_path = os.path.join("src", "stores", "counter.ts")
definitions = language_server.request_definition(counter_path, 6, 35)
assert definitions, "Expected at least one definition location"
def_loc = definitions[0]
assert def_loc["uri"].endswith("counter.ts")
assert def_loc["range"]["start"]["line"] == 0
+11 -2
View File
@@ -24,8 +24,17 @@ class TestNextflowLanguageServer:
@pytest.mark.parametrize("language_server", [LanguageServerId.NEXTFLOW], indirect=True)
def test_document_symbols_workflows(self, language_server: SolidLanguageServer) -> None:
"""Workflows declared in a script are reported, with the ``workflow`` keyword stripped."""
all_symbols, _ = language_server.request_document_symbols("main.nf").get_all_symbols_and_roots()
names = [s["name"] for s in all_symbols]
import time
deadline = time.monotonic() + 10.0
names: list[str] = []
while time.monotonic() < deadline:
all_symbols, _ = language_server.request_document_symbols("main.nf").get_all_symbols_and_roots()
names = [s["name"] for s in all_symbols]
if "SAY_HELLO" in names:
break
time.sleep(0.5)
assert "SAY_HELLO" in names, f"SAY_HELLO not found in main.nf symbols. Found: {names}"
# the implicit entry workflow has no name of its own; the language server calls it "<entry>"
assert "<entry>" in names, f"entry workflow not found in main.nf symbols. Found: {names}"