diff --git a/.gitignore b/.gitignore
index 78b8833a..ec2ca442 100644
--- a/.gitignore
+++ b/.gitignore
@@ -251,4 +251,7 @@ test/resources/repos/scala/.bloop/
stack.yaml.lock
dist-newstyle/
cabal.project.local*
-.ghc.environment.*
\ No newline at end of file
+.ghc.environment.*
+
+zz-misc/
+vue-implementation/
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index edf91cd1..7e1f629f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -9,11 +9,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `uv run poe type-check` - Run mypy type checking - ONLY allowed type checking command
- `uv run poe test` - Run tests with default markers (excludes java/rust by default)
- `uv run poe test -m "python or go"` - Run specific language tests
+- `uv run poe test -m vue` - Run Vue tests
- `uv run poe lint` - Check code style without fixing
**Test Markers:**
Available pytest markers for selective testing:
-- `python`, `go`, `java`, `rust`, `typescript`, `php`, `perl`, `csharp`, `elixir`, `terraform`, `clojure`, `swift`, `bash`, `ruby`, `ruby_solargraph`
+- `python`, `go`, `java`, `rust`, `typescript`, `vue`, `php`, `perl`, `csharp`, `elixir`, `terraform`, `clojure`, `swift`, `bash`, `ruby`, `ruby_solargraph`
- `snapshot` - for symbolic editing operation tests
**Project Management:**
@@ -100,7 +101,7 @@ Configuration is loaded from (in order of precedence):
- **Symbol-based editing** - Uses LSP for precise code manipulation
- **Caching strategy** - Reduces language server overhead
- **Error recovery** - Automatic language server restart on crashes
-- **Multi-language support** - 16+ languages with LSP integration
+- **Multi-language support** - 19 languages with LSP integration (including Vue)
- **MCP protocol** - Exposes tools to AI agents via Model Context Protocol
- **Async operation** - Non-blocking language server interactions
diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md
index 230d64e8..692f3b2a 100644
--- a/docs/01-about/020_programming-languages.md
+++ b/docs/01-about/020_programming-languages.md
@@ -58,6 +58,8 @@ With Serena, we provide direct, out-of-the-box support for:
(requires some [manual setup](../03-special-guides/scala_setup_guide_for_serena); uses Metals LSP)
* **Swift**
* **TypeScript**
+* **Vue**
+ (3.x with TypeScript; requires Node.js v18+ and npm; supports .vue Single File Components with monorepo detection)
* **YAML**
* **Zig**
(requires installation of ZLS - Zig Language Server)
diff --git a/pyproject.toml b/pyproject.toml
index 196f99ca..9fce285f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -275,6 +275,7 @@ markers = [
"kotlin: language server running for kotlin",
"rust: language server running for Rust",
"typescript: language server running for TypeScript",
+ "vue: language server running for Vue (uses TypeScript LSP)",
"php: language server running for PHP",
"perl: language server running for Perl",
"csharp: language server running for C#",
diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py
index c3fb2a4a..a60c9f3a 100644
--- a/src/serena/config/serena_config.py
+++ b/src/serena/config/serena_config.py
@@ -174,15 +174,18 @@ class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
f"Read the documentation for more information."
)
# sort languages by number of files found
- languages_and_percentages = sorted(language_composition.items(), key=lambda item: item[1], reverse=True)
+ languages_and_percentages = sorted(
+ language_composition.items(), key=lambda item: (item[1], item[0].get_priority()), reverse=True
+ )
# find the language with the highest percentage and enable it
top_language_pair = languages_and_percentages[0]
other_language_pairs = languages_and_percentages[1:]
- languages_to_use = [top_language_pair[0]]
+ languages_to_use: list[str] = [top_language_pair[0].value]
# if in interactive mode, ask the user which other languages to enable
if len(other_language_pairs) > 0 and interactive:
print(
- "Detected and enabled main language '%s' (%.2f%% of source files)." % (top_language_pair[0], top_language_pair[1])
+ "Detected and enabled main language '%s' (%.2f%% of source files)."
+ % (top_language_pair[0].value, top_language_pair[1])
)
print(f"Additionally detected {len(other_language_pairs)} other language(s).\n")
print("Note: Enable only languages you need symbolic retrieval/editing capabilities for.")
@@ -190,9 +193,9 @@ class ProjectConfig(ToolInclusionDefinition, ToStringMixin):
print(" system-level installations/configuration (see Serena documentation).")
print("\nWhich additional languages do you want to enable?")
for lang, perc in other_language_pairs:
- enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang, perc), default=False)
+ enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang.value, perc), default=False)
if enable:
- languages_to_use.append(lang)
+ languages_to_use.append(lang.value)
print()
else:
languages_to_use = [lang.value for lang in languages]
diff --git a/src/serena/util/inspection.py b/src/serena/util/inspection.py
index 668cf545..c67cc1f3 100644
--- a/src/serena/util/inspection.py
+++ b/src/serena/util/inspection.py
@@ -19,13 +19,13 @@ def iter_subclasses(cls: type[T], recursive: bool = True) -> Generator[type[T],
yield from iter_subclasses(subclass, recursive)
-def determine_programming_language_composition(repo_path: str) -> dict[str, float]:
+def determine_programming_language_composition(repo_path: str) -> dict[Language, float]:
"""
Determine the programming language composition of a repository.
:param repo_path: Path to the repository to analyze
- :return: Dictionary mapping language names to percentages of files matching each language
+ :return: Dictionary mapping languages to percentages of files matching each language
"""
all_files = find_all_non_ignored_files(repo_path)
@@ -33,7 +33,7 @@ def determine_programming_language_composition(repo_path: str) -> dict[str, floa
return {}
# Count files for each language
- language_counts: dict[str, int] = {}
+ language_counts: dict[Language, int] = {}
total_files = len(all_files)
for language in Language.iter_all(include_experimental=False):
@@ -47,12 +47,12 @@ def determine_programming_language_composition(repo_path: str) -> dict[str, floa
count += 1
if count > 0:
- language_counts[str(language)] = count
+ language_counts[language] = count
# Convert counts to percentages
- language_percentages: dict[str, float] = {}
- for language_name, count in language_counts.items():
+ language_percentages: dict[Language, float] = {}
+ for language, count in language_counts.items():
percentage = (count / total_files) * 100
- language_percentages[language_name] = round(percentage, 2)
+ language_percentages[language] = round(percentage, 2)
return language_percentages
diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py
index 10c5cc69..4734da45 100644
--- a/src/solidlsp/language_servers/typescript_language_server.py
+++ b/src/solidlsp/language_servers/typescript_language_server.py
@@ -12,6 +12,7 @@ from typing import Any, cast
from overrides import override
from sensai.util.logging import LogTime
+from solidlsp import ls_types
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
@@ -39,9 +40,32 @@ if not PlatformUtils.get_platform_id().value.startswith("win"):
pass
+def prefer_non_node_modules_definition(definitions: list[ls_types.Location]) -> ls_types.Location:
+ """
+ Select the preferred definition, preferring source files over type definitions.
+
+ TypeScript language servers often return both type definitions (.d.ts files
+ in node_modules) and source definitions. This function prefers:
+ 1. Files not in node_modules
+ 2. Falls back to first definition if all are in node_modules
+
+ :param definitions: A non-empty list of definition locations.
+ :return: The preferred definition location.
+ """
+ for d in definitions:
+ rel_path = d.get("relativePath", "")
+ if rel_path and "node_modules" not in rel_path:
+ return d
+ return definitions[0]
+
+
class TypeScriptLanguageServer(SolidLanguageServer):
"""
Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript.
+
+ You can pass the following entries in ls_specific_settings["typescript"]:
+ - typescript_version: Version of TypeScript to install (default: "5.9.3")
+ - typescript_language_server_version: Version of typescript-language-server to install (default: "5.1.3")
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
@@ -91,18 +115,23 @@ class TypeScriptLanguageServer(SolidLanguageServer):
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment"
+ # Get version settings from ls_specific_settings or use defaults
+ language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance())
+ typescript_version = language_specific_config.get("typescript_version", "5.9.3")
+ typescript_language_server_version = language_specific_config.get("typescript_language_server_version", "5.1.3")
+
deps = RuntimeDependencyCollection(
[
RuntimeDependency(
id="typescript",
description="typescript package",
- command=["npm", "install", "--prefix", "./", "typescript@5.5.4"],
+ command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"],
platform_id="any",
),
RuntimeDependency(
id="typescript-language-server",
description="typescript-language-server package",
- command=["npm", "install", "--prefix", "./", "typescript-language-server@4.3.3"],
+ command=["npm", "install", "--prefix", "./", f"typescript-language-server@{typescript_language_server_version}"],
platform_id="any",
),
]
@@ -114,19 +143,39 @@ class TypeScriptLanguageServer(SolidLanguageServer):
is_npm_installed = shutil.which("npm") is not None
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
- # Verify both node and npm are installed
- is_node_installed = shutil.which("node") is not None
- assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
- is_npm_installed = shutil.which("npm") is not None
- assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
-
- # Install typescript and typescript-language-server if not already installed
+ # Install typescript and typescript-language-server if not already installed or version mismatch
tsserver_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "ts-lsp")
tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server")
+
+ # Check if installation is needed based on executable AND version
+ version_file = os.path.join(tsserver_ls_dir, ".installed_version")
+ expected_version = f"{typescript_version}_{typescript_language_server_version}"
+
+ needs_install = False
if not os.path.exists(tsserver_executable_path):
- log.info(f"Typescript Language Server executable not found at {tsserver_executable_path}. Installing...")
+ log.info(f"Typescript Language Server executable not found at {tsserver_executable_path}.")
+ needs_install = True
+ elif os.path.exists(version_file):
+ with open(version_file) as f:
+ installed_version = f.read().strip()
+ if installed_version != expected_version:
+ log.info(
+ f"TypeScript Language Server version mismatch: installed={installed_version}, expected={expected_version}. Reinstalling..."
+ )
+ needs_install = True
+ else:
+ # No version file exists, assume old installation needs refresh
+ log.info("TypeScript Language Server version file not found. Reinstalling to ensure correct version...")
+ needs_install = True
+
+ if needs_install:
+ log.info("Installing TypeScript Language Server dependencies...")
with LogTime("Installation of TypeScript language server dependencies", logger=log):
deps.install(tsserver_ls_dir)
+ # Write version marker file
+ with open(version_file, "w") as f:
+ f.write(expected_version)
+ log.info("TypeScript language server dependencies installed successfully")
if not os.path.exists(tsserver_executable_path):
raise FileNotFoundError(
@@ -134,8 +183,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
)
return [tsserver_executable_path, "--stdio"]
- @staticmethod
- def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
+ def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the TypeScript Language Server.
"""
@@ -156,6 +204,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"signatureHelp": {"dynamicRegistration": True},
"codeAction": {"dynamicRegistration": True},
+ "rename": {"dynamicRegistration": True, "prepareSupport": True},
},
"workspace": {
"workspaceFolders": True,
@@ -252,3 +301,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
@override
def _get_wait_time_for_cross_file_referencing(self) -> float:
return 1
+
+ @override
+ def _get_preferred_definition(self, definitions: list[ls_types.Location]) -> ls_types.Location:
+ return prefer_non_node_modules_definition(definitions)
diff --git a/src/solidlsp/language_servers/vue_language_server.py b/src/solidlsp/language_servers/vue_language_server.py
new file mode 100644
index 00000000..ee419345
--- /dev/null
+++ b/src/solidlsp/language_servers/vue_language_server.py
@@ -0,0 +1,809 @@
+"""
+Vue Language Server implementation using @vue/language-server (Volar) with companion TypeScript LS.
+Operates in hybrid mode: Vue LS handles .vue files, TypeScript LS handles .ts/.js files.
+"""
+
+import logging
+import os
+import pathlib
+import shutil
+import threading
+from pathlib import Path
+from time import sleep
+from typing import Any
+
+from overrides import override
+
+from solidlsp import ls_types
+from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
+from solidlsp.language_servers.typescript_language_server import (
+ TypeScriptLanguageServer,
+ prefer_non_node_modules_definition,
+)
+from solidlsp.ls import LSPFileBuffer, SolidLanguageServer
+from solidlsp.ls_config import Language, LanguageServerConfig
+from solidlsp.ls_exceptions import SolidLSPException
+from solidlsp.ls_types import Location
+from solidlsp.ls_utils import PathUtils
+from solidlsp.lsp_protocol_handler import lsp_types
+from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, ExecuteCommandParams, InitializeParams, SymbolInformation
+from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
+from solidlsp.settings import SolidLSPSettings
+
+log = logging.getLogger(__name__)
+
+
+class VueTypeScriptServer(TypeScriptLanguageServer):
+ """TypeScript LS configured with @vue/typescript-plugin for Vue file support."""
+
+ _pending_ts_ls_executable: list[str] | None = None
+
+ @classmethod
+ @override
+ def get_language_enum_instance(cls) -> Language:
+ """Return TYPESCRIPT since this is a TypeScript language server variant.
+
+ Note: VueTypeScriptServer is a companion server that uses TypeScript's language server
+ with the Vue TypeScript plugin. It reports as TYPESCRIPT to maintain compatibility
+ with the TypeScript language server infrastructure.
+ """
+ return Language.TYPESCRIPT
+
+ @classmethod
+ @override
+ def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> list[str]:
+ if cls._pending_ts_ls_executable is not None:
+ return cls._pending_ts_ls_executable
+ return ["typescript-language-server", "--stdio"]
+
+ @override
+ def _get_language_id_for_file(self, relative_file_path: str) -> str:
+ """Return the correct language ID for files.
+
+ Vue files must be opened with language ID "vue" for the @vue/typescript-plugin
+ to process them correctly. The plugin is configured with "languages": ["vue"]
+ in the initialization options.
+ """
+ ext = os.path.splitext(relative_file_path)[1].lower()
+ if ext == ".vue":
+ return "vue"
+ elif ext in (".ts", ".tsx", ".mts", ".cts"):
+ return "typescript"
+ elif ext in (".js", ".jsx", ".mjs", ".cjs"):
+ return "javascript"
+ else:
+ return "typescript"
+
+ def __init__(
+ self,
+ config: LanguageServerConfig,
+ repository_root_path: str,
+ solidlsp_settings: SolidLSPSettings,
+ vue_plugin_path: str,
+ tsdk_path: str,
+ ts_ls_executable_path: list[str],
+ ):
+ self._vue_plugin_path = vue_plugin_path
+ self._custom_tsdk_path = tsdk_path
+ VueTypeScriptServer._pending_ts_ls_executable = ts_ls_executable_path
+ super().__init__(config, repository_root_path, solidlsp_settings)
+ VueTypeScriptServer._pending_ts_ls_executable = None
+
+ @override
+ def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
+ params = super()._get_initialize_params(repository_absolute_path)
+
+ params["initializationOptions"] = {
+ "plugins": [
+ {
+ "name": "@vue/typescript-plugin",
+ "location": self._vue_plugin_path,
+ "languages": ["vue"],
+ }
+ ],
+ "tsserver": {
+ "path": self._custom_tsdk_path,
+ },
+ }
+
+ if "workspace" in params["capabilities"]:
+ params["capabilities"]["workspace"]["executeCommand"] = {"dynamicRegistration": True}
+
+ 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 VueLanguageServer(SolidLanguageServer):
+ """
+ Language server for Vue Single File Components using @vue/language-server (Volar) with companion TypeScript LS.
+
+ You can pass the following entries in ls_specific_settings["vue"]:
+ - vue_language_server_version: Version of @vue/language-server to install (default: "3.1.5")
+
+ Note: TypeScript versions are configured via ls_specific_settings["typescript"]:
+ - typescript_version: Version of TypeScript to install (default: "5.9.3")
+ - typescript_language_server_version: Version of typescript-language-server to install (default: "5.1.3")
+ """
+
+ TS_SERVER_READY_TIMEOUT = 5.0
+ VUE_SERVER_READY_TIMEOUT = 3.0
+ # Windows requires more time due to slower I/O and process operations.
+ VUE_INDEXING_WAIT_TIME = 4.0 if os.name == "nt" else 2.0
+
+ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
+ vue_lsp_executable_path, self.tsdk_path, self._ts_ls_cmd = self._setup_runtime_dependencies(config, solidlsp_settings)
+ self._vue_ls_dir = os.path.join(self.ls_resources_dir(solidlsp_settings), "vue-lsp")
+ super().__init__(
+ config,
+ repository_root_path,
+ ProcessLaunchInfo(cmd=vue_lsp_executable_path, cwd=repository_root_path),
+ "vue",
+ solidlsp_settings,
+ )
+ self.server_ready = threading.Event()
+ self.initialize_searcher_command_available = threading.Event()
+ self._ts_server: VueTypeScriptServer | None = None
+ self._ts_server_started = False
+ self._vue_files_indexed = False
+ self._indexed_vue_file_uris: list[str] = []
+
+ @override
+ def is_ignored_dirname(self, dirname: str) -> bool:
+ return super().is_ignored_dirname(dirname) or dirname in [
+ "node_modules",
+ "dist",
+ "build",
+ "coverage",
+ ".nuxt",
+ ".output",
+ ]
+
+ @override
+ def _get_language_id_for_file(self, relative_file_path: str) -> str:
+ ext = os.path.splitext(relative_file_path)[1].lower()
+ if ext == ".vue":
+ return "vue"
+ elif ext in (".ts", ".tsx", ".mts", ".cts"):
+ return "typescript"
+ elif ext in (".js", ".jsx", ".mjs", ".cjs"):
+ return "javascript"
+ else:
+ return "vue"
+
+ def _is_typescript_file(self, file_path: str) -> bool:
+ ext = os.path.splitext(file_path)[1].lower()
+ return ext in (".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs")
+
+ def _find_all_vue_files(self) -> list[str]:
+ vue_files = []
+ repo_path = Path(self.repository_root_path)
+
+ for vue_file in repo_path.rglob("*.vue"):
+ try:
+ relative_path = str(vue_file.relative_to(repo_path))
+ if "node_modules" not in relative_path and not relative_path.startswith("."):
+ vue_files.append(relative_path)
+ except Exception as e:
+ log.debug(f"Error processing Vue file {vue_file}: {e}")
+
+ return vue_files
+
+ def _ensure_vue_files_indexed_on_ts_server(self) -> None:
+ if self._vue_files_indexed:
+ return
+
+ assert self._ts_server is not None
+ log.info("Indexing .vue files on TypeScript server for cross-file references")
+ vue_files = self._find_all_vue_files()
+ log.debug(f"Found {len(vue_files)} .vue files to index")
+
+ for vue_file in vue_files:
+ try:
+ with self._ts_server.open_file(vue_file) as file_buffer:
+ file_buffer.ref_count += 1
+ self._indexed_vue_file_uris.append(file_buffer.uri)
+ except Exception as e:
+ log.debug(f"Failed to open {vue_file} on TS server: {e}")
+
+ self._vue_files_indexed = True
+ log.info("Vue file indexing on TypeScript server complete")
+
+ sleep(self._get_vue_indexing_wait_time())
+ log.debug("Wait period after Vue file indexing complete")
+
+ def _get_vue_indexing_wait_time(self) -> float:
+ return self.VUE_INDEXING_WAIT_TIME
+
+ def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
+ uri = PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))
+ request_params = {
+ "textDocument": {"uri": uri},
+ "position": {"line": line, "character": column},
+ "context": {"includeDeclaration": False},
+ }
+
+ return self.server.send.references(request_params) # type: ignore[arg-type]
+
+ def _send_ts_references_request(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
+ assert self._ts_server is not None
+ uri = PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))
+ request_params = {
+ "textDocument": {"uri": uri},
+ "position": {"line": line, "character": column},
+ "context": {"includeDeclaration": True},
+ }
+
+ with self._ts_server.open_file(relative_file_path):
+ response = self._ts_server.handler.send.references(request_params) # type: ignore[arg-type]
+
+ result: list[ls_types.Location] = []
+ if response is not None:
+ for item in response:
+ abs_path = PathUtils.uri_to_path(item["uri"])
+ if not Path(abs_path).is_relative_to(self.repository_root_path):
+ log.debug(f"Found reference outside repository: {abs_path}, skipping")
+ continue
+
+ rel_path = Path(abs_path).relative_to(self.repository_root_path)
+ if self.is_ignored_path(str(rel_path)):
+ log.debug(f"Ignoring reference in {rel_path}")
+ continue
+
+ new_item: dict = {}
+ new_item.update(item) # type: ignore[arg-type]
+ new_item["absolutePath"] = str(abs_path)
+ new_item["relativePath"] = str(rel_path)
+ result.append(ls_types.Location(**new_item)) # type: ignore
+
+ return result
+
+ def request_file_references(self, relative_file_path: str) -> list:
+ if not self.server_started:
+ log.error("request_file_references called before Language Server started")
+ raise SolidLSPException("Language Server not started")
+
+ absolute_file_path = os.path.join(self.repository_root_path, relative_file_path)
+ uri = PathUtils.path_to_uri(absolute_file_path)
+
+ request_params = {"textDocument": {"uri": uri}}
+
+ log.info(f"Sending volar/client/findFileReference request for {relative_file_path}")
+ log.info(f"Request URI: {uri}")
+ log.info(f"Request params: {request_params}")
+
+ try:
+ with self.open_file(relative_file_path):
+ log.debug(f"Sending volar/client/findFileReference for {relative_file_path}")
+ log.debug(f"Request params: {request_params}")
+
+ response = self.server.send_request("volar/client/findFileReference", request_params)
+
+ log.debug(f"Received response type: {type(response)}")
+
+ log.info(f"Received file references response: {response}")
+ log.info(f"Response type: {type(response)}")
+
+ if response is None:
+ log.debug(f"No file references found for {relative_file_path}")
+ return []
+
+ # Response should be an array of Location objects
+ if not isinstance(response, list):
+ log.warning(f"Unexpected response format from volar/client/findFileReference: {type(response)}")
+ return []
+
+ ret: list[Location] = []
+ for item in response:
+ if not isinstance(item, dict) or "uri" not in item:
+ log.debug(f"Skipping invalid location item: {item}")
+ continue
+
+ abs_path = PathUtils.uri_to_path(item["uri"]) # type: ignore[arg-type]
+ if not Path(abs_path).is_relative_to(self.repository_root_path):
+ log.warning(f"Found file reference outside repository: {abs_path}, skipping")
+ continue
+
+ rel_path = Path(abs_path).relative_to(self.repository_root_path)
+ if self.is_ignored_path(str(rel_path)):
+ log.debug(f"Ignoring file reference in {rel_path}")
+ continue
+
+ new_item: dict = {}
+ new_item.update(item) # type: ignore[arg-type]
+ new_item["absolutePath"] = str(abs_path)
+ new_item["relativePath"] = str(rel_path)
+ ret.append(Location(**new_item)) # type: ignore
+
+ log.debug(f"Found {len(ret)} file references for {relative_file_path}")
+ return ret
+
+ except Exception as e:
+ log.warning(f"Error requesting file references for {relative_file_path}: {e}")
+ return []
+
+ @override
+ def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
+ if not self.server_started:
+ log.error("request_references called before Language Server started")
+ raise SolidLSPException("Language Server not started")
+
+ if not self._has_waited_for_cross_file_references:
+ sleep(self._get_wait_time_for_cross_file_referencing())
+ self._has_waited_for_cross_file_references = True
+
+ self._ensure_vue_files_indexed_on_ts_server()
+ symbol_refs = self._send_ts_references_request(relative_file_path, line=line, column=column)
+
+ if relative_file_path.endswith(".vue"):
+ log.info(f"Attempting to find file-level references for Vue component {relative_file_path}")
+ file_refs = self.request_file_references(relative_file_path)
+ log.info(f"file_refs result: {len(file_refs)} references found")
+
+ seen = set()
+ for ref in symbol_refs:
+ key = (ref["uri"], ref["range"]["start"]["line"], ref["range"]["start"]["character"])
+ seen.add(key)
+
+ for file_ref in file_refs:
+ key = (file_ref["uri"], file_ref["range"]["start"]["line"], file_ref["range"]["start"]["character"])
+ if key not in seen:
+ symbol_refs.append(file_ref)
+ seen.add(key)
+
+ log.info(f"Total references for {relative_file_path}: {len(symbol_refs)} (symbol refs + file refs, deduplicated)")
+
+ return symbol_refs
+
+ @override
+ def request_definition(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
+ if not self.server_started:
+ log.error("request_definition called before Language Server started")
+ raise SolidLSPException("Language Server not started")
+
+ assert 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)
+
+ @override
+ def request_rename_symbol_edit(self, relative_file_path: str, line: int, column: int, new_name: str) -> ls_types.WorkspaceEdit | None:
+ if not self.server_started:
+ log.error("request_rename_symbol_edit called before Language Server started")
+ raise SolidLSPException("Language Server not started")
+
+ assert 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)
+
+ @classmethod
+ def _setup_runtime_dependencies(
+ cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
+ ) -> tuple[list[str], str, list[str]]:
+ is_node_installed = shutil.which("node") is not None
+ assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
+ is_npm_installed = shutil.which("npm") is not None
+ assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
+
+ # Get TypeScript version settings from TypeScript language server settings
+ typescript_config = solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT)
+ typescript_version = typescript_config.get("typescript_version", "5.9.3")
+ typescript_language_server_version = typescript_config.get("typescript_language_server_version", "5.1.3")
+ vue_config = solidlsp_settings.get_ls_specific_settings(Language.VUE)
+ vue_language_server_version = vue_config.get("vue_language_server_version", "3.1.5")
+
+ deps = RuntimeDependencyCollection(
+ [
+ RuntimeDependency(
+ id="vue-language-server",
+ description="Vue language server package (Volar)",
+ command=["npm", "install", "--prefix", "./", f"@vue/language-server@{vue_language_server_version}"],
+ platform_id="any",
+ ),
+ RuntimeDependency(
+ id="typescript",
+ description="TypeScript (required for tsdk)",
+ command=["npm", "install", "--prefix", "./", f"typescript@{typescript_version}"],
+ platform_id="any",
+ ),
+ RuntimeDependency(
+ id="typescript-language-server",
+ description="TypeScript language server (for Vue LS 3.x tsserver forwarding)",
+ command=[
+ "npm",
+ "install",
+ "--prefix",
+ "./",
+ f"typescript-language-server@{typescript_language_server_version}",
+ ],
+ platform_id="any",
+ ),
+ ]
+ )
+
+ vue_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "vue-lsp")
+ vue_executable_path = os.path.join(vue_ls_dir, "node_modules", ".bin", "vue-language-server")
+ ts_ls_executable_path = os.path.join(vue_ls_dir, "node_modules", ".bin", "typescript-language-server")
+
+ if os.name == "nt":
+ vue_executable_path += ".cmd"
+ ts_ls_executable_path += ".cmd"
+
+ tsdk_path = os.path.join(vue_ls_dir, "node_modules", "typescript", "lib")
+
+ # Check if installation is needed based on executables AND version
+ version_file = os.path.join(vue_ls_dir, ".installed_version")
+ expected_version = f"{vue_language_server_version}_{typescript_version}_{typescript_language_server_version}"
+
+ needs_install = False
+ if not os.path.exists(vue_executable_path) or not os.path.exists(ts_ls_executable_path):
+ log.info("Vue/TypeScript Language Server executables not found.")
+ needs_install = True
+ elif os.path.exists(version_file):
+ with open(version_file) as f:
+ installed_version = f.read().strip()
+ if installed_version != expected_version:
+ log.info(
+ f"Vue Language Server version mismatch: installed={installed_version}, expected={expected_version}. Reinstalling..."
+ )
+ needs_install = True
+ else:
+ # No version file exists, assume old installation needs refresh
+ log.info("Vue Language Server version file not found. Reinstalling to ensure correct version...")
+ needs_install = True
+
+ if needs_install:
+ log.info("Installing Vue/TypeScript Language Server dependencies...")
+ deps.install(vue_ls_dir)
+ # Write version marker file
+ with open(version_file, "w") as f:
+ f.write(expected_version)
+ log.info("Vue language server dependencies installed successfully")
+
+ if not os.path.exists(vue_executable_path):
+ raise FileNotFoundError(
+ f"vue-language-server executable not found at {vue_executable_path}, something went wrong with the installation."
+ )
+
+ if not os.path.exists(ts_ls_executable_path):
+ raise FileNotFoundError(
+ f"typescript-language-server executable not found at {ts_ls_executable_path}, something went wrong with the installation."
+ )
+
+ return [vue_executable_path, "--stdio"], tsdk_path, [ts_ls_executable_path, "--stdio"]
+
+ def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
+ root_uri = pathlib.Path(repository_absolute_path).as_uri()
+ initialize_params = {
+ "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},
+ },
+ "workspace": {
+ "workspaceFolders": True,
+ "didChangeConfiguration": {"dynamicRegistration": True},
+ "symbol": {"dynamicRegistration": True},
+ },
+ },
+ "processId": os.getpid(),
+ "rootPath": repository_absolute_path,
+ "rootUri": root_uri,
+ "workspaceFolders": [
+ {
+ "uri": root_uri,
+ "name": os.path.basename(repository_absolute_path),
+ }
+ ],
+ "initializationOptions": {
+ "vue": {
+ "hybridMode": True,
+ },
+ "typescript": {
+ "tsdk": self.tsdk_path,
+ },
+ },
+ }
+ return initialize_params # type: ignore
+
+ def _start_typescript_server(self) -> None:
+ try:
+ vue_ts_plugin_path = os.path.join(self._vue_ls_dir, "node_modules", "@vue", "typescript-plugin")
+
+ ts_config = LanguageServerConfig(
+ code_language=Language.TYPESCRIPT,
+ trace_lsp_communication=False,
+ )
+
+ log.info("Creating companion VueTypeScriptServer")
+ self._ts_server = VueTypeScriptServer(
+ config=ts_config,
+ repository_root_path=self.repository_root_path,
+ solidlsp_settings=self._solidlsp_settings,
+ vue_plugin_path=vue_ts_plugin_path,
+ tsdk_path=self.tsdk_path,
+ ts_ls_executable_path=self._ts_ls_cmd,
+ )
+
+ log.info("Starting companion TypeScript server")
+ self._ts_server.start()
+
+ log.info("Waiting for companion TypeScript server to be ready...")
+ if not self._ts_server.server_ready.wait(timeout=self.TS_SERVER_READY_TIMEOUT):
+ log.warning(
+ f"Timeout waiting for companion TypeScript server to be ready after {self.TS_SERVER_READY_TIMEOUT} seconds, proceeding anyway"
+ )
+ self._ts_server.server_ready.set()
+
+ self._ts_server_started = True
+ log.info("Companion TypeScript server ready")
+ except Exception as e:
+ log.error(f"Error starting TypeScript server: {e}")
+ self._ts_server = None
+ self._ts_server_started = False
+ raise
+
+ def _forward_tsserver_request(self, method: str, params: dict) -> Any:
+ if self._ts_server is None:
+ log.error("Cannot forward tsserver request - TypeScript server not started")
+ return None
+
+ try:
+ execute_params: ExecuteCommandParams = {
+ "command": "typescript.tsserverRequest",
+ "arguments": [method, params, {"isAsync": True, "lowPriority": True}],
+ }
+ result = self._ts_server.handler.send.execute_command(execute_params)
+ log.debug(f"TypeScript server raw response for {method}: {result}")
+
+ if isinstance(result, dict) and "body" in result:
+ return result["body"]
+ return result
+ except Exception as e:
+ log.error(f"Error forwarding tsserver request {method}: {e}")
+ return None
+
+ def _cleanup_indexed_vue_files(self) -> None:
+ if not self._indexed_vue_file_uris or self._ts_server is None:
+ return
+
+ log.debug(f"Cleaning up {len(self._indexed_vue_file_uris)} indexed Vue files")
+ for uri in self._indexed_vue_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]
+ log.debug(f"Closed indexed Vue file: {uri}")
+ except Exception as e:
+ log.debug(f"Error closing indexed Vue file {uri}: {e}")
+
+ self._indexed_vue_file_uris.clear()
+
+ def _stop_typescript_server(self) -> None:
+ if self._ts_server is not None:
+ try:
+ log.info("Stopping companion TypeScript server")
+ self._ts_server.stop()
+ except Exception as e:
+ log.warning(f"Error stopping TypeScript server: {e}")
+ finally:
+ self._ts_server = None
+ self._ts_server_started = False
+
+ @override
+ def _start_server(self) -> None:
+ self._start_typescript_server()
+
+ def register_capability_handler(params: dict) -> None:
+ assert "registrations" in params
+ for registration in params["registrations"]:
+ if registration["method"] == "workspace/executeCommand":
+ self.initialize_searcher_command_available.set()
+ return
+
+ def configuration_handler(params: dict) -> list:
+ items = params.get("items", [])
+ return [{} for _ in items]
+
+ def do_nothing(params: dict) -> None:
+ return
+
+ def window_log_message(msg: dict) -> None:
+ log.info(f"LSP: window/logMessage: {msg}")
+ message_text = msg.get("message", "")
+ if "initialized" in message_text.lower() or "ready" in message_text.lower():
+ log.info("Vue language server ready signal detected")
+ self.server_ready.set()
+ self.completions_available.set()
+
+ def tsserver_request_notification_handler(params: list) -> None:
+ try:
+ if params and len(params) > 0 and len(params[0]) >= 2:
+ request_id = params[0][0]
+ method = params[0][1]
+ method_params = params[0][2] if len(params[0]) > 2 else {}
+ log.debug(f"Received tsserver/request: id={request_id}, method={method}")
+
+ if method == "_vue:projectInfo":
+ file_path = method_params.get("file", "")
+ tsconfig_path = self._find_tsconfig_for_file(file_path)
+ result = {"configFileName": tsconfig_path} if tsconfig_path else None
+ response = [[request_id, result]]
+ self.server.notify.send_notification("tsserver/response", response)
+ log.debug(f"Sent tsserver/response for projectInfo: {tsconfig_path}")
+ else:
+ result = self._forward_tsserver_request(method, method_params)
+ response = [[request_id, result]]
+ self.server.notify.send_notification("tsserver/response", response)
+ log.debug(f"Forwarded tsserver/response for {method}: {result}")
+ else:
+ log.warning(f"Unexpected tsserver/request params format: {params}")
+ except Exception as e:
+ log.error(f"Error handling tsserver/request: {e}")
+
+ self.server.on_request("client/registerCapability", register_capability_handler)
+ self.server.on_request("workspace/configuration", configuration_handler)
+ self.server.on_notification("tsserver/request", tsserver_request_notification_handler)
+ self.server.on_notification("window/logMessage", window_log_message)
+ self.server.on_notification("$/progress", do_nothing)
+ self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
+
+ log.info("Starting Vue server process")
+ self.server.start()
+ initialize_params = self._get_initialize_params(self.repository_root_path)
+
+ log.info("Sending initialize request from LSP client to LSP server and awaiting response")
+ init_response = self.server.send.initialize(initialize_params)
+ log.debug(f"Received initialize response from Vue server: {init_response}")
+
+ assert init_response["capabilities"]["textDocumentSync"] in [1, 2]
+
+ self.server.notify.initialized({})
+
+ log.info("Waiting for Vue language server to be ready...")
+ if not self.server_ready.wait(timeout=self.VUE_SERVER_READY_TIMEOUT):
+ log.info("Timeout waiting for Vue server ready signal, proceeding anyway")
+ self.server_ready.set()
+ self.completions_available.set()
+ else:
+ log.info("Vue server initialization complete")
+
+ def _find_tsconfig_for_file(self, file_path: str) -> str | None:
+ if not file_path:
+ tsconfig_path = os.path.join(self.repository_root_path, "tsconfig.json")
+ return tsconfig_path if os.path.exists(tsconfig_path) else None
+
+ current_dir = os.path.dirname(file_path)
+ repo_root = os.path.abspath(self.repository_root_path)
+
+ while current_dir and current_dir.startswith(repo_root):
+ tsconfig_path = os.path.join(current_dir, "tsconfig.json")
+ if os.path.exists(tsconfig_path):
+ return tsconfig_path
+ parent = os.path.dirname(current_dir)
+ if parent == current_dir:
+ break
+ current_dir = parent
+
+ tsconfig_path = os.path.join(repo_root, "tsconfig.json")
+ return tsconfig_path if os.path.exists(tsconfig_path) else None
+
+ @override
+ def _get_wait_time_for_cross_file_referencing(self) -> float:
+ return 5.0
+
+ @override
+ def stop(self, shutdown_timeout: float = 5.0) -> None:
+ self._cleanup_indexed_vue_files()
+ self._stop_typescript_server()
+ super().stop(shutdown_timeout)
+
+ @override
+ def _get_preferred_definition(self, definitions: list[ls_types.Location]) -> ls_types.Location:
+ return prefer_non_node_modules_definition(definitions)
+
+ @override
+ def _request_document_symbols(
+ self, relative_file_path: str, file_data: LSPFileBuffer | None
+ ) -> list[SymbolInformation] | list[DocumentSymbol] | None:
+ """
+ Override to filter out shorthand property references in Vue files.
+
+ In Vue, when using shorthand syntax in defineExpose like `defineExpose({ pressCount })`,
+ the Vue LSP returns both:
+ - The Variable definition (e.g., `const pressCount = ref(0)`)
+ - A Property symbol for the shorthand reference (e.g., `pressCount` in defineExpose)
+
+ This causes duplicate symbols with the same name, which breaks symbol lookup.
+ We filter out Property symbols that have a matching Variable with the same name
+ at a different location (the definition), keeping only the definition.
+ """
+ symbols = super()._request_document_symbols(relative_file_path, file_data)
+
+ if symbols is None or len(symbols) == 0:
+ return symbols
+
+ # Only process DocumentSymbol format (hierarchical symbols with children)
+ # SymbolInformation format doesn't have the same issue
+ if not isinstance(symbols[0], dict) or "range" not in symbols[0]:
+ return symbols
+
+ return self._filter_shorthand_property_duplicates(symbols)
+
+ def _filter_shorthand_property_duplicates(
+ self, symbols: list[DocumentSymbol] | list[SymbolInformation]
+ ) -> list[DocumentSymbol] | list[SymbolInformation]:
+ """
+ Filter out Property symbols that have a matching Variable symbol with the same name.
+
+ This handles Vue's shorthand property syntax in defineExpose, where the same
+ identifier appears as both a Variable definition and a Property reference.
+ """
+ VARIABLE_KIND = 13 # SymbolKind.Variable
+ PROPERTY_KIND = 7 # SymbolKind.Property
+
+ def filter_symbols(syms: list[dict]) -> list[dict]:
+ # Collect all Variable symbol names with their line numbers
+ variable_names: dict[str, set[int]] = {}
+ for sym in syms:
+ if sym.get("kind") == VARIABLE_KIND:
+ name = sym.get("name", "")
+ line = sym.get("range", {}).get("start", {}).get("line", -1)
+ if name not in variable_names:
+ variable_names[name] = set()
+ variable_names[name].add(line)
+
+ # Filter: keep symbols that are either:
+ # 1. Not a Property, or
+ # 2. A Property without a matching Variable name at a different location
+ filtered = []
+ for sym in syms:
+ name = sym.get("name", "")
+ kind = sym.get("kind")
+ line = sym.get("range", {}).get("start", {}).get("line", -1)
+
+ # If it's a Property with a matching Variable name at a DIFFERENT line, skip it
+ if kind == PROPERTY_KIND and name in variable_names:
+ # Check if there's a Variable definition at a different line
+ var_lines = variable_names[name]
+ if any(var_line != line for var_line in var_lines):
+ # This is a shorthand reference, skip it
+ log.debug(
+ f"Filtering shorthand property reference '{name}' at line {line} "
+ f"(Variable definition exists at line(s) {var_lines})"
+ )
+ continue
+
+ # Recursively filter children
+ children = sym.get("children", [])
+ if children:
+ sym = dict(sym) # Create a copy to avoid mutating the original
+ sym["children"] = filter_symbols(children)
+
+ filtered.append(sym)
+
+ return filtered
+
+ return filter_symbols(list(symbols)) # type: ignore
diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py
index 30e3949c..f9ce89b8 100644
--- a/src/solidlsp/ls.py
+++ b/src/solidlsp/ls.py
@@ -463,6 +463,14 @@ class SolidLanguageServer(ABC):
def _start_server(self) -> None:
pass
+ def _get_language_id_for_file(self, relative_file_path: str) -> str:
+ """Return the language ID for a file.
+
+ Override in subclasses to return file-specific language IDs.
+ Default implementation returns self.language_id.
+ """
+ return self.language_id
+
@contextmanager
def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]:
"""
@@ -488,13 +496,14 @@ class SolidLanguageServer(ABC):
contents = FileUtils.read_file(absolute_file_path, self._encoding)
version = 0
- self.open_file_buffers[uri] = LSPFileBuffer(uri, contents, version, self.language_id, 1)
+ language_id = self._get_language_id_for_file(relative_file_path)
+ self.open_file_buffers[uri] = LSPFileBuffer(uri, contents, version, language_id, 1)
self.server.notify.did_open_text_document(
{
LSPConstants.TEXT_DOCUMENT: { # type: ignore
LSPConstants.URI: uri,
- LSPConstants.LANGUAGE_ID: self.language_id,
+ LSPConstants.LANGUAGE_ID: language_id,
LSPConstants.VERSION: 0,
LSPConstants.TEXT: contents,
}
@@ -1630,6 +1639,23 @@ class SolidLanguageServer(ABC):
include_body=include_body,
)
+ def _get_preferred_definition(self, definitions: list[ls_types.Location]) -> ls_types.Location:
+ """
+ Select the preferred definition from a list of definitions.
+
+ When multiple definitions are returned (e.g., both source and type definitions),
+ this method determines which one to use. The base implementation simply returns
+ the first definition.
+
+ Subclasses can override this method to implement language-specific preferences.
+ For example, TypeScript/Vue servers may prefer source files over .d.ts type
+ definition files.
+
+ :param definitions: A non-empty list of definition locations.
+ :return: The preferred definition location.
+ """
+ return definitions[0]
+
def request_defining_symbol(
self,
relative_file_path: str,
@@ -1658,8 +1684,8 @@ class SolidLanguageServer(ABC):
if not definitions:
return None
- # Use the first definition location
- definition = definitions[0]
+ # Select the preferred definition (subclasses can override _get_preferred_definition)
+ definition = self._get_preferred_definition(definitions)
def_path = definition["relativePath"]
assert def_path is not None
def_line = definition["range"]["start"]["line"]
@@ -1874,5 +1900,14 @@ class SolidLanguageServer(ABC):
def language_server(self) -> Self:
return self
+ @property
+ def handler(self) -> SolidLanguageServerHandler:
+ """Access the underlying language server handler.
+
+ Useful for advanced operations like sending custom commands
+ or registering notification handlers.
+ """
+ return self.server
+
def is_running(self) -> bool:
return self.server.is_running()
diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py
index 79d1e739..faf00055 100644
--- a/src/solidlsp/ls_config.py
+++ b/src/solidlsp/ls_config.py
@@ -61,6 +61,7 @@ class Language(str, Enum):
JULIA = "julia"
FORTRAN = "fortran"
HASKELL = "haskell"
+ VUE = "vue"
# Experimental or deprecated Language Servers
TYPESCRIPT_VTS = "typescript_vts"
"""Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls"""
@@ -99,6 +100,23 @@ class Language(str, Enum):
def __str__(self) -> str:
return self.value
+ def get_priority(self) -> int:
+ """
+ :return: priority of the language for breaking ties between languages; higher is more important.
+ """
+ # experimental languages have the lowest priority
+ if self.is_experimental():
+ return 0
+ # 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:
+ return 1
+ # regular languages
+ case _:
+ return 2
+
def get_source_fn_matcher(self) -> FilenameMatcher:
match self:
case self.PYTHON | self.PYTHON_JEDI:
@@ -175,6 +193,13 @@ class Language(str, Enum):
)
case self.HASKELL:
return FilenameMatcher("*.hs", "*.lhs")
+ case self.VUE:
+ path_patterns = ["*.vue"]
+ for prefix in ["c", "m", ""]:
+ for postfix in ["x", ""]:
+ for base_pattern in ["ts", "js"]:
+ path_patterns.append(f"*.{prefix}{base_pattern}{postfix}")
+ return FilenameMatcher(*path_patterns)
case _:
raise ValueError(f"Unhandled language: {self}")
@@ -216,6 +241,10 @@ class Language(str, Enum):
from solidlsp.language_servers.vts_language_server import VtsLanguageServer
return VtsLanguageServer
+ case self.VUE:
+ from solidlsp.language_servers.vue_language_server import VueLanguageServer
+
+ return VueLanguageServer
case self.GO:
from solidlsp.language_servers.gopls import Gopls
diff --git a/test/resources/repos/vue/test_repo/.gitignore b/test/resources/repos/vue/test_repo/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/test/resources/repos/vue/test_repo/index.html b/test/resources/repos/vue/test_repo/index.html
new file mode 100644
index 00000000..75e52237
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Vue Calculator
+
+
+
+
+
+
diff --git a/test/resources/repos/vue/test_repo/package-lock.json b/test/resources/repos/vue/test_repo/package-lock.json
new file mode 100644
index 00000000..7a73f505
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/package-lock.json
@@ -0,0 +1,1486 @@
+{
+ "name": "vue-calculator-test-fixture",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "vue-calculator-test-fixture",
+ "version": "1.0.0",
+ "dependencies": {
+ "pinia": "^2.1.0",
+ "vue": "^3.4.0"
+ },
+ "devDependencies": {
+ "@vue/language-server": "^2.0.0",
+ "typescript": "~5.5.4"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
+ "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.15"
+ }
+ },
+ "node_modules/@volar/language-core/node_modules/@volar/source-map": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz",
+ "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/language-server": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.4.15.tgz",
+ "integrity": "sha512-aSzvL3lgQ+RPU3uWA9wW85sfZ0tb+oKplfnOwG/c1iRMuVEJRofmcnjyN0JEOKbBR7GuPSbeUdLAI0AIL+TFew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@volar/language-service": "2.4.15",
+ "@volar/typescript": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "request-light": "^0.7.0",
+ "vscode-languageserver": "^9.0.1",
+ "vscode-languageserver-protocol": "^3.17.5",
+ "vscode-languageserver-textdocument": "^1.0.11",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@volar/language-server/node_modules/@volar/language-service": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.15.tgz",
+ "integrity": "sha512-o7ctGyQNQAZqT15xHamE0fTzPZHeDnHWz0m/KJekSPc2W4AHiEbJ2RNDLzEK4e0EjrpdeEe3FB9KQvOvjq+I6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "vscode-languageserver-protocol": "^3.17.5",
+ "vscode-languageserver-textdocument": "^1.0.11",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@volar/language-server/node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@volar/language-server/node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/language-server/node_modules/request-light": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz",
+ "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/language-server/node_modules/vscode-languageserver": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
+ "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vscode-languageserver-protocol": "3.17.5"
+ },
+ "bin": {
+ "installServerIntoExtension": "bin/installServerIntoExtension"
+ }
+ },
+ "node_modules/@volar/language-server/node_modules/vscode-languageserver-textdocument": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/test-utils": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/test-utils/-/test-utils-2.4.15.tgz",
+ "integrity": "sha512-TgFzv+0HWKS9ZjA6p4kuXgnzGJR2wt5f+0L97o7LYOdBSaAIn+EgWPqTAYWZC4Z2PYEKS0jkt5waOyak18k6/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@volar/language-server": "2.4.15",
+ "vscode-languageserver-textdocument": "^1.0.11",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@volar/test-utils/node_modules/vscode-languageserver-textdocument": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz",
+ "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.25",
+ "@vue/shared": "3.5.25"
+ }
+ },
+ "node_modules/@vue/compiler-dom/node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@vue/compiler-dom/node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@vue/compiler-dom/node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@vue/compiler-dom/node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@vue/compiler-dom/node_modules/@vue/compiler-core": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
+ "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/shared": "3.5.25",
+ "entities": "^4.5.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/@vue/compiler-dom/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-dom/node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz",
+ "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/compiler-core": "3.5.25",
+ "@vue/compiler-dom": "3.5.25",
+ "@vue/compiler-ssr": "3.5.25",
+ "@vue/shared": "3.5.25",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.6",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/@vue/compiler-core": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
+ "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/shared": "3.5.25",
+ "entities": "^4.5.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/@vue/compiler-ssr": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz",
+ "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.25",
+ "@vue/shared": "3.5.25"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz",
+ "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/compiler-vue2": "^2.7.16",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^1.0.3",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-core/node_modules/@vue/compiler-vue2": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+ "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/language-core/node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@vue/language-core/node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core/node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/@vue/language-core/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@vue/language-core/node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core/node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-server": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/language-server/-/language-server-2.2.12.tgz",
+ "integrity": "sha512-sh64ihjDn1nHmLez5m/UuXiTmiTU10Jt/QvhjMPllPeUwCvck89ndXp1nXkvEDoRGm7xdV9zopZM2IiEZFQTPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@volar/language-server": "2.4.15",
+ "@volar/test-utils": "2.4.15",
+ "@vue/language-core": "2.2.12",
+ "@vue/language-service": "2.2.12",
+ "@vue/typescript-plugin": "2.2.12",
+ "vscode-languageserver-protocol": "^3.17.5",
+ "vscode-uri": "^3.0.8"
+ },
+ "bin": {
+ "vue-language-server": "bin/vue-language-server.js"
+ }
+ },
+ "node_modules/@vue/language-service": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/language-service/-/language-service-2.2.12.tgz",
+ "integrity": "sha512-+YBLP82vyuXzO5Wkw23vMl+EPCZicv1EQtEjp/n/Wt4PjbPBSgR5KCuc4edcIA1rL70fsEPlSna3K+ibyZ2QIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "@volar/language-service": "2.4.15",
+ "@volar/typescript": "2.4.15",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/language-core": "2.2.12",
+ "@vue/shared": "^3.5.0",
+ "@vue/typescript-plugin": "2.2.12",
+ "alien-signals": "^1.0.3",
+ "path-browserify": "^1.0.1",
+ "volar-service-css": "0.0.64",
+ "volar-service-emmet": "0.0.64",
+ "volar-service-html": "0.0.64",
+ "volar-service-json": "0.0.64",
+ "volar-service-pug": "0.0.64",
+ "volar-service-pug-beautify": "0.0.64",
+ "volar-service-typescript": "0.0.64",
+ "volar-service-typescript-twoslash-queries": "0.0.64",
+ "vscode-css-languageservice": "^6.3.1",
+ "vscode-html-languageservice": "^5.2.0",
+ "vscode-languageserver-textdocument": "^1.0.11",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/@emmetio/abbreviation": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz",
+ "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@emmetio/scanner": "^1.0.4"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/@emmetio/css-abbreviation": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz",
+ "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@emmetio/scanner": "^1.0.4"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/@emmetio/css-parser": {
+ "version": "0.4.0",
+ "resolved": "git+ssh://git@github.com/ramya-rao-a/css-parser.git#370c480ac103bd17c7bcfb34bf5d577dc40d3660",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@emmetio/stream-reader": "^2.2.0",
+ "@emmetio/stream-reader-utils": "^0.1.0"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/@emmetio/html-matcher": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@emmetio/html-matcher/-/html-matcher-1.3.0.tgz",
+ "integrity": "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@emmetio/scanner": "^1.0.0"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/@emmetio/scanner": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz",
+ "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/@emmetio/stream-reader": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz",
+ "integrity": "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/@emmetio/stream-reader-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz",
+ "integrity": "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/@johnsoncodehk/pug-beautify": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@johnsoncodehk/pug-beautify/-/pug-beautify-0.2.2.tgz",
+ "integrity": "sha512-qqNS/YD0Nck5wtQLCPHAfGVgWbbGafxSPjNh0ekYPFSNNqnDH2kamnduzYly8IiADmeVx/MfAE1njMEjVeHTMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/@volar/language-service": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.15.tgz",
+ "integrity": "sha512-o7ctGyQNQAZqT15xHamE0fTzPZHeDnHWz0m/KJekSPc2W4AHiEbJ2RNDLzEK4e0EjrpdeEe3FB9KQvOvjq+I6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "vscode-languageserver-protocol": "^3.17.5",
+ "vscode-languageserver-textdocument": "^1.0.11",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/@vscode/emmet-helper": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.11.0.tgz",
+ "integrity": "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emmet": "^2.4.3",
+ "jsonc-parser": "^2.3.0",
+ "vscode-languageserver-textdocument": "^1.0.1",
+ "vscode-languageserver-types": "^3.15.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/@vscode/l10n": {
+ "version": "0.0.18",
+ "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz",
+ "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/alien-signals": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
+ "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/character-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
+ "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-regex": "^1.0.3"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/emmet": {
+ "version": "2.4.11",
+ "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz",
+ "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "./packages/scanner",
+ "./packages/abbreviation",
+ "./packages/css-abbreviation",
+ "./"
+ ],
+ "dependencies": {
+ "@emmetio/abbreviation": "^2.3.3",
+ "@emmetio/css-abbreviation": "^2.1.8"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/is-expression": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz",
+ "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^7.1.1",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/jsonc-parser": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz",
+ "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/pug-error": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz",
+ "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/pug-lexer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
+ "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "character-parser": "^2.2.0",
+ "is-expression": "^4.0.0",
+ "pug-error": "^2.0.0"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/pug-parser": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz",
+ "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pug-error": "^2.0.0",
+ "token-stream": "1.0.0"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/token-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
+ "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/typescript-auto-import-cache": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz",
+ "integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.8"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/volar-service-css": {
+ "version": "0.0.64",
+ "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.64.tgz",
+ "integrity": "sha512-BtLq85adIft+Q0PZqllKXMVH7HHk6UmM7Opuj43IhR4AOVkqbNIcsiXhuh12ITVJGTkCGa9xXAPzClorPya6EQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vscode-css-languageservice": "^6.3.0",
+ "vscode-languageserver-textdocument": "^1.0.11",
+ "vscode-uri": "^3.0.8"
+ },
+ "peerDependencies": {
+ "@volar/language-service": "~2.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@volar/language-service": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/volar-service-emmet": {
+ "version": "0.0.64",
+ "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.64.tgz",
+ "integrity": "sha512-cIGKpyOXKB9Dsqz3MJD1DF1GYEPfLE4VBoEtqewfx88qpf0dr/WbDnRhuljZ+VFlavkre4MvELzkCt8m9F6GqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@emmetio/css-parser": "ramya-rao-a/css-parser#vscode",
+ "@emmetio/html-matcher": "^1.3.0",
+ "@vscode/emmet-helper": "^2.9.3",
+ "vscode-uri": "^3.0.8"
+ },
+ "peerDependencies": {
+ "@volar/language-service": "~2.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@volar/language-service": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/volar-service-html": {
+ "version": "0.0.64",
+ "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.64.tgz",
+ "integrity": "sha512-5xknMYKmZBFzp2399RlsnGce25PfNu9ViXa1s63Q8NP6xeXcF3lInFsV+1o2DWBoXZdnXcuRvWOA+K+JIZLEcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vscode-html-languageservice": "^5.3.0",
+ "vscode-languageserver-textdocument": "^1.0.11",
+ "vscode-uri": "^3.0.8"
+ },
+ "peerDependencies": {
+ "@volar/language-service": "~2.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@volar/language-service": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/volar-service-json": {
+ "version": "0.0.64",
+ "resolved": "https://registry.npmjs.org/volar-service-json/-/volar-service-json-0.0.64.tgz",
+ "integrity": "sha512-kZsqmeDR0w/iVkkoxfp4/DcblYmyFXBImVLzgIgyd685XZrb21wVmKcRsD/V+6l0N7/pGwN6m2scxt/mJmFkGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vscode-json-languageservice": "^5.4.0",
+ "vscode-uri": "^3.0.8"
+ },
+ "peerDependencies": {
+ "@volar/language-service": "~2.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@volar/language-service": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/volar-service-pug": {
+ "version": "0.0.64",
+ "resolved": "https://registry.npmjs.org/volar-service-pug/-/volar-service-pug-0.0.64.tgz",
+ "integrity": "sha512-rVJ2ySENJFPzzEr4fVlC81ANR4dTh3Axr6Az56KyZ5GV0C9yRk5QqCj5+eOYb2GSoVtnEbzOyYpDTRSuH++lfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-service": "~2.4.0",
+ "muggle-string": "^0.4.1",
+ "pug-lexer": "^5.0.1",
+ "pug-parser": "^6.0.0",
+ "volar-service-html": "0.0.64",
+ "vscode-html-languageservice": "^5.3.0",
+ "vscode-languageserver-textdocument": "^1.0.11"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/volar-service-pug-beautify": {
+ "version": "0.0.64",
+ "resolved": "https://registry.npmjs.org/volar-service-pug-beautify/-/volar-service-pug-beautify-0.0.64.tgz",
+ "integrity": "sha512-HNVTYGjGKaUkvryCQvhN/kYZ6aP6I1ySECMA7I8SpMqraYN/BDKI185tsRsm5tGmPrEyCgRDKn/GGIAsSj7I7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@johnsoncodehk/pug-beautify": "^0.2.2"
+ },
+ "peerDependencies": {
+ "@volar/language-service": "~2.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@volar/language-service": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/volar-service-typescript": {
+ "version": "0.0.64",
+ "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.64.tgz",
+ "integrity": "sha512-FN2H97iqjR1id8AM4fH7lTXuTx2on9zD6QlUFllaiHKqgNrEITlQwm/9Ujrd9ST7MUzhgIKyUsa2WlanX9kkMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-browserify": "^1.0.1",
+ "semver": "^7.6.2",
+ "typescript-auto-import-cache": "^0.3.5",
+ "vscode-languageserver-textdocument": "^1.0.11",
+ "vscode-nls": "^5.2.0",
+ "vscode-uri": "^3.0.8"
+ },
+ "peerDependencies": {
+ "@volar/language-service": "~2.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@volar/language-service": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/volar-service-typescript-twoslash-queries": {
+ "version": "0.0.64",
+ "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.64.tgz",
+ "integrity": "sha512-DQlArCVUwbH3Ym0Uc/qSrgus/ngQa4LbTNbLsIkWMovxwziPA1c2yCFFY7a6s4Qs8pPXO757ryu99IeX0UK+4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vscode-uri": "^3.0.8"
+ },
+ "peerDependencies": {
+ "@volar/language-service": "~2.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@volar/language-service": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/vscode-css-languageservice": {
+ "version": "6.3.8",
+ "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.8.tgz",
+ "integrity": "sha512-dBk/9ullEjIMbfSYAohGpDOisOVU1x2MQHOeU12ohGJQI7+r0PCimBwaa/pWpxl/vH4f7ibrBfxIZY3anGmHKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vscode/l10n": "^0.0.18",
+ "vscode-languageserver-textdocument": "^1.0.12",
+ "vscode-languageserver-types": "3.17.5",
+ "vscode-uri": "^3.1.0"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/vscode-html-languageservice": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.0.tgz",
+ "integrity": "sha512-FIVz83oGw2tBkOr8gQPeiREInnineCKGCz3ZD1Pi6opOuX3nSRkc4y4zLLWsuop+6ttYX//XZCI6SLzGhRzLmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vscode/l10n": "^0.0.18",
+ "vscode-languageserver-textdocument": "^1.0.12",
+ "vscode-languageserver-types": "^3.17.5",
+ "vscode-uri": "^3.1.0"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/vscode-json-languageservice": {
+ "version": "5.6.3",
+ "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.6.3.tgz",
+ "integrity": "sha512-UDF7sJF5t7mzUzXL6dsClkvnHS4xnDL/gOMKGQiizRHmswlk/xSPGZxEvAtszWQF0ImNcJ0j9l+rHuefGzit1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vscode/l10n": "^0.0.18",
+ "jsonc-parser": "^3.3.1",
+ "vscode-languageserver-textdocument": "^1.0.12",
+ "vscode-languageserver-types": "^3.17.5",
+ "vscode-uri": "^3.1.0"
+ }
+ },
+ "node_modules/@vue/language-service/node_modules/vscode-json-languageservice/node_modules/jsonc-parser": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
+ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/vscode-languageserver-textdocument": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/vscode-languageserver-types": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-service/node_modules/vscode-nls": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz",
+ "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz",
+ "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.25",
+ "@vue/runtime-core": "3.5.25",
+ "@vue/shared": "3.5.25",
+ "csstype": "^3.1.3"
+ }
+ },
+ "node_modules/@vue/runtime-dom/node_modules/@vue/reactivity": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz",
+ "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.25"
+ }
+ },
+ "node_modules/@vue/runtime-dom/node_modules/@vue/runtime-core": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz",
+ "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.25",
+ "@vue/shared": "3.5.25"
+ }
+ },
+ "node_modules/@vue/runtime-dom/node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz",
+ "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.25",
+ "@vue/shared": "3.5.25"
+ },
+ "peerDependencies": {
+ "vue": "3.5.25"
+ }
+ },
+ "node_modules/@vue/server-renderer/node_modules/@vue/compiler-ssr": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz",
+ "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.25",
+ "@vue/shared": "3.5.25"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz",
+ "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/typescript-plugin": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/@vue/typescript-plugin/-/typescript-plugin-2.2.12.tgz",
+ "integrity": "sha512-HFByqrJ7+kWDpAoGiTcWe0Gxy9R0MkEtE+QTlLnEgDS7+l7uSaPnK4ZfzpX7rMfNwOZK0fUD58mMvmCWUnBCSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.15",
+ "@vue/language-core": "2.2.12",
+ "@vue/shared": "^3.5.0"
+ }
+ },
+ "node_modules/@vue/typescript-plugin/node_modules/@volar/typescript": {
+ "version": "2.4.15",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz",
+ "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.15",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/typescript-plugin/node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pinia": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
+ "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.3",
+ "vue-demi": "^0.14.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.4.4",
+ "vue": "^2.7.0 || ^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.5.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
+ "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vscode-jsonrpc": "8.2.0",
+ "vscode-languageserver-types": "3.17.5"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": {
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.25",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
+ "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.25",
+ "@vue/compiler-sfc": "3.5.25",
+ "@vue/runtime-dom": "3.5.25",
+ "@vue/server-renderer": "3.5.25",
+ "@vue/shared": "3.5.25"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/test/resources/repos/vue/test_repo/package.json b/test/resources/repos/vue/test_repo/package.json
new file mode 100644
index 00000000..846a5b1b
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "vue-calculator-test-fixture",
+ "version": "1.0.0",
+ "type": "module",
+ "description": "Vue 3 + Pinia + TypeScript test fixtures for Serena LSP testing",
+ "dependencies": {
+ "vue": "^3.4.0",
+ "pinia": "^2.1.0"
+ },
+ "devDependencies": {
+ "@vue/language-server": "^2.0.0",
+ "typescript": "~5.5.4"
+ }
+}
diff --git a/test/resources/repos/vue/test_repo/src/App.vue b/test/resources/repos/vue/test_repo/src/App.vue
new file mode 100644
index 00000000..a5ece8a0
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/src/App.vue
@@ -0,0 +1,230 @@
+
+
+
+
+
+
+
diff --git a/test/resources/repos/vue/test_repo/src/components/CalculatorDisplay.vue b/test/resources/repos/vue/test_repo/src/components/CalculatorDisplay.vue
new file mode 100644
index 00000000..622709b0
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/src/components/CalculatorDisplay.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
Current Calculation
+
+ {{ currentCalculation }}
+
+
+
+
+
+
+
+ No calculations yet
+
+
+
+
+ {{ entry.expression }}
+ = {{ entry.result }}
+ {{ formatDate(entry.timestamp) }}
+
+
+
+
+ Showing {{ maxHistoryItems }} of {{ recentHistory.length }} entries
+
+
+
+
+
+
diff --git a/test/resources/repos/vue/test_repo/src/components/CalculatorInput.vue b/test/resources/repos/vue/test_repo/src/components/CalculatorInput.vue
new file mode 100644
index 00000000..2d7bc86d
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/src/components/CalculatorInput.vue
@@ -0,0 +1,300 @@
+
+
+
+
+
+
+
diff --git a/test/resources/repos/vue/test_repo/src/composables/useFormatter.ts b/test/resources/repos/vue/test_repo/src/composables/useFormatter.ts
new file mode 100644
index 00000000..9c64106f
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/src/composables/useFormatter.ts
@@ -0,0 +1,126 @@
+import { ref, computed } from 'vue'
+import type { Ref, ComputedRef } from 'vue'
+import type { FormatOptions } from '@/types'
+
+/**
+ * Composable for formatting numbers with various options.
+ * Demonstrates: composable pattern, refs, computed, type imports
+ */
+export function useFormatter(initialPrecision: number = 2) {
+ // State
+ const precision = ref(initialPrecision)
+ const useGrouping = ref(true)
+ const locale = ref('en-US')
+
+ // Computed properties
+ const formatOptions = computed((): FormatOptions => ({
+ maxDecimals: precision.value,
+ useGrouping: useGrouping.value
+ }))
+
+ // Methods
+ const formatNumber = (value: number): string => {
+ return value.toLocaleString(locale.value, {
+ minimumFractionDigits: precision.value,
+ maximumFractionDigits: precision.value,
+ useGrouping: useGrouping.value
+ })
+ }
+
+ const formatCurrency = (value: number, currency: string = 'USD'): string => {
+ return value.toLocaleString(locale.value, {
+ style: 'currency',
+ currency,
+ minimumFractionDigits: precision.value,
+ maximumFractionDigits: precision.value
+ })
+ }
+
+ const formatPercentage = (value: number): string => {
+ return `${(value * 100).toFixed(precision.value)}%`
+ }
+
+ const setPrecision = (newPrecision: number): void => {
+ if (newPrecision >= 0 && newPrecision <= 10) {
+ precision.value = newPrecision
+ }
+ }
+
+ const toggleGrouping = (): void => {
+ useGrouping.value = !useGrouping.value
+ }
+
+ const setLocale = (newLocale: string): void => {
+ locale.value = newLocale
+ }
+
+ // Return composable API
+ return {
+ // State (readonly)
+ precision: computed(() => precision.value),
+ useGrouping: computed(() => useGrouping.value),
+ locale: computed(() => locale.value),
+ formatOptions,
+
+ // Methods
+ formatNumber,
+ formatCurrency,
+ formatPercentage,
+ setPrecision,
+ toggleGrouping,
+ setLocale
+ }
+}
+
+/**
+ * Composable for time formatting.
+ * Demonstrates: simpler composable, pure functions
+ */
+export function useTimeFormatter() {
+ const formatTime = (date: Date): string => {
+ return date.toLocaleTimeString('en-US', {
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ })
+ }
+
+ const formatDate = (date: Date): string => {
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ })
+ }
+
+ const formatDateTime = (date: Date): string => {
+ return `${formatDate(date)} ${formatTime(date)}`
+ }
+
+ const getRelativeTime = (date: Date): string => {
+ const now = new Date()
+ const diffMs = now.getTime() - date.getTime()
+ const diffSecs = Math.floor(diffMs / 1000)
+ const diffMins = Math.floor(diffSecs / 60)
+ const diffHours = Math.floor(diffMins / 60)
+ const diffDays = Math.floor(diffHours / 24)
+
+ if (diffSecs < 60) return 'just now'
+ if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`
+ if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`
+ return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`
+ }
+
+ return {
+ formatTime,
+ formatDate,
+ formatDateTime,
+ getRelativeTime
+ }
+}
+
+/**
+ * Type definitions for return types
+ */
+export type UseFormatterReturn = ReturnType
+export type UseTimeFormatterReturn = ReturnType
diff --git a/test/resources/repos/vue/test_repo/src/composables/useTheme.ts b/test/resources/repos/vue/test_repo/src/composables/useTheme.ts
new file mode 100644
index 00000000..706f1fe7
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/src/composables/useTheme.ts
@@ -0,0 +1,113 @@
+import { ref, computed, watch, inject, provide, type InjectionKey, type Ref } from 'vue'
+
+/**
+ * Theme configuration type
+ */
+export interface ThemeConfig {
+ isDark: boolean
+ primaryColor: string
+ fontSize: number
+}
+
+/**
+ * Injection key for theme - demonstrates provide/inject pattern
+ */
+export const ThemeKey: InjectionKey[> = Symbol('theme')
+
+/**
+ * Composable for theme management with watchers.
+ * Demonstrates: watch, provide/inject, localStorage interaction
+ */
+export function useThemeProvider() {
+ // Initialize theme from localStorage or defaults
+ const loadThemeFromStorage = (): ThemeConfig => {
+ const stored = localStorage.getItem('app-theme')
+ if (stored) {
+ try {
+ return JSON.parse(stored)
+ } catch {
+ // Fall through to defaults
+ }
+ }
+ return {
+ isDark: false,
+ primaryColor: '#667eea',
+ fontSize: 16
+ }
+ }
+
+ const theme = ref(loadThemeFromStorage())
+
+ // Computed properties
+ const isDarkMode = computed(() => theme.value.isDark)
+ const themeClass = computed(() => theme.value.isDark ? 'dark-theme' : 'light-theme')
+
+ // Watch for theme changes and persist to localStorage
+ watch(
+ theme,
+ (newTheme) => {
+ localStorage.setItem('app-theme', JSON.stringify(newTheme))
+ document.documentElement.className = newTheme.isDark ? 'dark' : 'light'
+ },
+ { deep: true }
+ )
+
+ // Methods
+ const toggleDarkMode = (): void => {
+ theme.value.isDark = !theme.value.isDark
+ }
+
+ const setPrimaryColor = (color: string): void => {
+ theme.value.primaryColor = color
+ }
+
+ const setFontSize = (size: number): void => {
+ if (size >= 12 && size <= 24) {
+ theme.value.fontSize = size
+ }
+ }
+
+ const resetTheme = (): void => {
+ theme.value = {
+ isDark: false,
+ primaryColor: '#667eea',
+ fontSize: 16
+ }
+ }
+
+ // Provide theme to child components
+ provide(ThemeKey, theme)
+
+ return {
+ theme,
+ isDarkMode,
+ themeClass,
+ toggleDarkMode,
+ setPrimaryColor,
+ setFontSize,
+ resetTheme
+ }
+}
+
+/**
+ * Composable for consuming theme in child components.
+ * Demonstrates: inject pattern
+ */
+export function useTheme() {
+ const theme = inject(ThemeKey)
+
+ if (!theme) {
+ throw new Error('useTheme must be used within a component that provides ThemeKey')
+ }
+
+ const isDark = computed(() => theme.value.isDark)
+ const primaryColor = computed(() => theme.value.primaryColor)
+ const fontSize = computed(() => theme.value.fontSize)
+
+ return {
+ theme,
+ isDark,
+ primaryColor,
+ fontSize
+ }
+}
diff --git a/test/resources/repos/vue/test_repo/src/main.ts b/test/resources/repos/vue/test_repo/src/main.ts
new file mode 100644
index 00000000..764d9587
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/src/main.ts
@@ -0,0 +1,9 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import App from './App.vue'
+
+const app = createApp(App)
+const pinia = createPinia()
+
+app.use(pinia)
+app.mount('#app')
diff --git a/test/resources/repos/vue/test_repo/src/stores/calculator.ts b/test/resources/repos/vue/test_repo/src/stores/calculator.ts
new file mode 100644
index 00000000..4c8a4415
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/src/stores/calculator.ts
@@ -0,0 +1,179 @@
+import { defineStore } from 'pinia'
+import type { HistoryEntry, Operation, CalculatorState } from '@/types'
+
+export const useCalculatorStore = defineStore('calculator', {
+ state: (): CalculatorState => ({
+ currentValue: 0,
+ previousValue: null,
+ operation: null,
+ history: [],
+ displayValue: '0'
+ }),
+
+ getters: {
+ /**
+ * Get the most recent history entries (last 10)
+ */
+ recentHistory: (state): HistoryEntry[] => {
+ return state.history.slice(-10).reverse()
+ },
+
+ /**
+ * Check if calculator has any history
+ */
+ hasHistory: (state): boolean => {
+ return state.history.length > 0
+ },
+
+ /**
+ * Get the current display text
+ */
+ display: (state): string => {
+ return state.displayValue
+ }
+ },
+
+ actions: {
+ /**
+ * Set a number value
+ */
+ setNumber(value: number) {
+ this.currentValue = value
+ this.displayValue = value.toString()
+ },
+
+ /**
+ * Append a digit to the current value
+ */
+ appendDigit(digit: number) {
+ if (this.displayValue === '0') {
+ this.displayValue = digit.toString()
+ } else {
+ this.displayValue += digit.toString()
+ }
+ this.currentValue = parseFloat(this.displayValue)
+ },
+
+ /**
+ * Add two numbers
+ */
+ add() {
+ if (this.previousValue !== null && this.operation) {
+ this.executeOperation()
+ }
+ this.previousValue = this.currentValue
+ this.operation = 'add'
+ this.displayValue = '0'
+ },
+
+ /**
+ * Subtract two numbers
+ */
+ subtract() {
+ if (this.previousValue !== null && this.operation) {
+ this.executeOperation()
+ }
+ this.previousValue = this.currentValue
+ this.operation = 'subtract'
+ this.displayValue = '0'
+ },
+
+ /**
+ * Multiply two numbers
+ */
+ multiply() {
+ if (this.previousValue !== null && this.operation) {
+ this.executeOperation()
+ }
+ this.previousValue = this.currentValue
+ this.operation = 'multiply'
+ this.displayValue = '0'
+ },
+
+ /**
+ * Divide two numbers
+ */
+ divide() {
+ if (this.previousValue !== null && this.operation) {
+ this.executeOperation()
+ }
+ this.previousValue = this.currentValue
+ this.operation = 'divide'
+ this.displayValue = '0'
+ },
+
+ /**
+ * Execute the pending operation
+ */
+ executeOperation() {
+ if (this.previousValue === null || this.operation === null) {
+ return
+ }
+
+ let result = 0
+ const prev = this.previousValue
+ const current = this.currentValue
+ let expression = ''
+
+ switch (this.operation) {
+ case 'add':
+ result = prev + current
+ expression = `${prev} + ${current}`
+ break
+ case 'subtract':
+ result = prev - current
+ expression = `${prev} - ${current}`
+ break
+ case 'multiply':
+ result = prev * current
+ expression = `${prev} × ${current}`
+ break
+ case 'divide':
+ if (current === 0) {
+ this.displayValue = 'Error'
+ this.clear()
+ return
+ }
+ result = prev / current
+ expression = `${prev} ÷ ${current}`
+ break
+ }
+
+ // Add to history
+ this.history.push({
+ expression,
+ result,
+ timestamp: new Date()
+ })
+
+ this.currentValue = result
+ this.displayValue = result.toString()
+ this.previousValue = null
+ this.operation = null
+ },
+
+ /**
+ * Calculate the equals operation
+ */
+ equals() {
+ this.executeOperation()
+ },
+
+ /**
+ * Clear the calculator state
+ */
+ clear() {
+ this.currentValue = 0
+ this.previousValue = null
+ this.operation = null
+ this.displayValue = '0'
+ },
+
+ /**
+ * Clear all history
+ */
+ clearHistory() {
+ this.history = []
+ }
+ }
+})
diff --git a/test/resources/repos/vue/test_repo/src/types/index.ts b/test/resources/repos/vue/test_repo/src/types/index.ts
new file mode 100644
index 00000000..2766ee2a
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/src/types/index.ts
@@ -0,0 +1,32 @@
+/**
+ * Represents a single calculation in the history
+ */
+export interface HistoryEntry {
+ expression: string
+ result: number
+ timestamp: Date
+}
+
+/**
+ * Valid calculator operations
+ */
+export type Operation = 'add' | 'subtract' | 'multiply' | 'divide' | null
+
+/**
+ * The complete state of the calculator
+ */
+export interface CalculatorState {
+ currentValue: number
+ previousValue: number | null
+ operation: Operation
+ history: HistoryEntry[]
+ displayValue: string
+}
+
+/**
+ * Format options for displaying numbers
+ */
+export interface FormatOptions {
+ maxDecimals?: number
+ useGrouping?: boolean
+}
diff --git a/test/resources/repos/vue/test_repo/tsconfig.json b/test/resources/repos/vue/test_repo/tsconfig.json
new file mode 100644
index 00000000..8c9fa9fc
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "preserve",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "plugins": [
+ {
+ "name": "@vue/typescript-plugin"
+ }
+ ],
+ "include": ["src/**/*.ts", "src/**/*.vue"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/test/resources/repos/vue/test_repo/tsconfig.node.json b/test/resources/repos/vue/test_repo/tsconfig.node.json
new file mode 100644
index 00000000..97ede7ee
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/test/resources/repos/vue/test_repo/vite.config.ts b/test/resources/repos/vue/test_repo/vite.config.ts
new file mode 100644
index 00000000..4343664e
--- /dev/null
+++ b/test/resources/repos/vue/test_repo/vite.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { fileURLToPath, URL } from 'node:url'
+
+export default defineConfig({
+ plugins: [vue()],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url))
+ }
+ }
+})
diff --git a/test/serena/__snapshots__/test_symbol_editing.ambr b/test/serena/__snapshots__/test_symbol_editing.ambr
index 92603c2d..347862ca 100644
--- a/test/serena/__snapshots__/test_symbol_editing.ambr
+++ b/test/serena/__snapshots__/test_symbol_editing.ambr
@@ -78,6 +78,229 @@
'''
# ---
+# name: test_delete_symbol_vue[test_case0]
+ '''
+
+
+
+
+
+
+
+
+ '''
+# ---
# name: test_insert_in_rel_to_symbol[test_case0-after]
'''
"""
@@ -590,6 +813,466 @@
'''
# ---
+# name: test_insert_in_rel_to_symbol_vue[test_case0-after]
+ '''
+
+
+
+
+
+
+
+
+ '''
+# ---
+# name: test_insert_in_rel_to_symbol_vue[test_case0-before]
+ '''
+
+
+
+
+
+
+
+
+ '''
+# ---
# name: test_insert_python_class_after
'''
"""
@@ -1207,3 +1890,641 @@
'''
# ---
+# name: test_replace_body_vue[test_case0]
+ '''
+
+
+
+
+
+
+
+
+ '''
+# ---
+# name: test_replace_body_vue_ts_file[test_case0]
+ '''
+ import { defineStore } from 'pinia'
+ import type { HistoryEntry, Operation, CalculatorState } from '@/types'
+
+ export const useCalculatorStore = defineStore('calculator', {
+ state: (): CalculatorState => ({
+ currentValue: 0,
+ previousValue: null,
+ operation: null,
+ history: [],
+ displayValue: '0'
+ }),
+
+ getters: {
+ /**
+ * Get the most recent history entries (last 10)
+ */
+ recentHistory: (state): HistoryEntry[] => {
+ return state.history.slice(-10).reverse()
+ },
+
+ /**
+ * Check if calculator has any history
+ */
+ hasHistory: (state): boolean => {
+ return state.history.length > 0
+ },
+
+ /**
+ * Get the current display text
+ */
+ display: (state): string => {
+ return state.displayValue
+ }
+ },
+
+ actions: {
+ /**
+ * Set a number value
+ */
+ setNumber(value: number) {
+ this.currentValue = value
+ this.displayValue = value.toString()
+ },
+
+ /**
+ * Append a digit to the current value
+ */
+ appendDigit(digit: number) {
+ if (this.displayValue === '0') {
+ this.displayValue = digit.toString()
+ } else {
+ this.displayValue += digit.toString()
+ }
+ this.currentValue = parseFloat(this.displayValue)
+ },
+
+ /**
+ * Add two numbers
+ */
+ add() {
+ if (this.previousValue !== null && this.operation) {
+ this.executeOperation()
+ }
+ this.previousValue = this.currentValue
+ this.operation = 'add'
+ this.displayValue = '0'
+ },
+
+ /**
+ * Subtract two numbers
+ */
+ subtract() {
+ if (this.previousValue !== null && this.operation) {
+ this.executeOperation()
+ }
+ this.previousValue = this.currentValue
+ this.operation = 'subtract'
+ this.displayValue = '0'
+ },
+
+ /**
+ * Multiply two numbers
+ */
+ multiply() {
+ if (this.previousValue !== null && this.operation) {
+ this.executeOperation()
+ }
+ this.previousValue = this.currentValue
+ this.operation = 'multiply'
+ this.displayValue = '0'
+ },
+
+ /**
+ * Divide two numbers
+ */
+ divide() {
+ if (this.previousValue !== null && this.operation) {
+ this.executeOperation()
+ }
+ this.previousValue = this.currentValue
+ this.operation = 'divide'
+ this.displayValue = '0'
+ },
+
+ /**
+ * Execute the pending operation
+ */
+ executeOperation() {
+ if (this.previousValue === null || this.operation === null) {
+ return
+ }
+
+ let result = 0
+ const prev = this.previousValue
+ const current = this.currentValue
+ let expression = ''
+
+ switch (this.operation) {
+ case 'add':
+ result = prev + current
+ expression = `${prev} + ${current}`
+ break
+ case 'subtract':
+ result = prev - current
+ expression = `${prev} - ${current}`
+ break
+ case 'multiply':
+ result = prev * current
+ expression = `${prev} × ${current}`
+ break
+ case 'divide':
+ if (current === 0) {
+ this.displayValue = 'Error'
+ this.clear()
+ return
+ }
+ result = prev / current
+ expression = `${prev} ÷ ${current}`
+ break
+ }
+
+ // Add to history
+ this.history.push({
+ expression,
+ result,
+ timestamp: new Date()
+ })
+
+ this.currentValue = result
+ this.displayValue = result.toString()
+ this.previousValue = null
+ this.operation = null
+ },
+
+ /**
+ * Calculate the equals operation
+ */
+ equals() {
+ this.executeOperation()
+ },
+
+ /**
+ * Clear the calculator state
+ */
+ function clear() {
+ // Modified: Reset to initial state with a log
+ console.log('Clearing calculator state');
+ displayValue.value = '0';
+ expression.value = '';
+ operationHistory.value = [];
+ lastResult.value = undefined;
+ },
+
+ /**
+ * Clear all history
+ */
+ clearHistory() {
+ this.history = []
+ }
+ }
+ })
+
+ '''
+# ---
+# name: test_replace_body_vue_with_disambiguation[test_case0]
+ '''
+
+
+
+
+
+
+
+
+ '''
+# ---
diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py
index f22973ee..048b3dbc 100644
--- a/test/serena/config/test_serena_config.py
+++ b/test/serena/config/test_serena_config.py
@@ -43,6 +43,16 @@ class TestProjectConfigAutogenerate:
assert config.project_name == self.project_path.name
assert config.languages == [Language.PYTHON]
+ def test_autogenerate_with_js_files(self):
+ """Test successful autogeneration with JavaScript source files."""
+ # Create files for multiple languages
+ (self.project_path / "small.js").write_text("console.log('JS');")
+
+ # Run autogenerate - should pick Python as dominant
+ config = ProjectConfig.autogenerate(self.project_path, save_to_disk=False)
+
+ assert config.languages == [Language.TYPESCRIPT]
+
def test_autogenerate_with_multiple_languages(self):
"""Test autogeneration picks dominant language when multiple are present."""
# Create files for multiple languages
diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py
index a0b577e7..b885c558 100644
--- a/test/serena/test_symbol_editing.py
+++ b/test/serena/test_symbol_editing.py
@@ -509,3 +509,142 @@ def test_rename_symbol(snapshot: SnapshotAssertion):
"renamed_typed_module_var",
)
test_case.run_test(content_after_ground_truth=snapshot)
+
+
+# ===== VUE WRITE OPERATIONS TESTS =====
+
+VUE_TEST_FILE = os.path.join("src", "components", "CalculatorButton.vue")
+VUE_STORE_FILE = os.path.join("src", "stores", "calculator.ts")
+
+NEW_VUE_HANDLER = """const handleDoubleClick = () => {
+ pressCount.value++;
+ emit('click', props.label);
+}"""
+
+
+@pytest.mark.parametrize(
+ "test_case",
+ [
+ pytest.param(
+ DeleteSymbolTest(
+ Language.VUE,
+ VUE_TEST_FILE,
+ "handleMouseEnter",
+ ),
+ marks=pytest.mark.vue,
+ ),
+ ],
+)
+def test_delete_symbol_vue(test_case: DeleteSymbolTest, snapshot: SnapshotAssertion) -> None:
+ test_case.run_test(content_after_ground_truth=snapshot)
+
+
+@pytest.mark.parametrize("mode", ["before", "after"])
+@pytest.mark.parametrize(
+ "test_case",
+ [
+ pytest.param(
+ InsertInRelToSymbolTest(
+ Language.VUE,
+ VUE_TEST_FILE,
+ "handleClick",
+ NEW_VUE_HANDLER,
+ ),
+ marks=pytest.mark.vue,
+ ),
+ ],
+)
+def test_insert_in_rel_to_symbol_vue(
+ test_case: InsertInRelToSymbolTest,
+ mode: Literal["before", "after"],
+ snapshot: SnapshotAssertion,
+) -> None:
+ test_case.set_mode(mode)
+ test_case.run_test(content_after_ground_truth=snapshot)
+
+
+VUE_REPLACED_HANDLECLICK_BODY = """const handleClick = () => {
+ if (!props.disabled) {
+ pressCount.value = 0; // Reset instead of incrementing
+ emit('click', props.label);
+ }
+}"""
+
+
+@pytest.mark.parametrize(
+ "test_case",
+ [
+ pytest.param(
+ ReplaceBodyTest(
+ Language.VUE,
+ VUE_TEST_FILE,
+ "handleClick",
+ VUE_REPLACED_HANDLECLICK_BODY,
+ ),
+ marks=pytest.mark.vue,
+ ),
+ ],
+)
+def test_replace_body_vue(test_case: ReplaceBodyTest, snapshot: SnapshotAssertion) -> None:
+ test_case.run_test(content_after_ground_truth=snapshot)
+
+
+VUE_REPLACED_PRESSCOUNT_BODY = """const pressCount = ref(100)"""
+
+
+@pytest.mark.parametrize(
+ "test_case",
+ [
+ pytest.param(
+ ReplaceBodyTest(
+ Language.VUE,
+ VUE_TEST_FILE,
+ "pressCount",
+ VUE_REPLACED_PRESSCOUNT_BODY,
+ ),
+ marks=pytest.mark.vue,
+ ),
+ ],
+)
+def test_replace_body_vue_with_disambiguation(test_case: ReplaceBodyTest, snapshot: SnapshotAssertion) -> None:
+ """Test symbol disambiguation when replacing body in Vue files.
+
+ This test verifies the fix for the Vue LSP symbol duplication issue.
+ When the LSP returns two symbols with the same name (e.g., pressCount appears both as
+ a definition `const pressCount = ref(0)` and as a shorthand property in `defineExpose({ pressCount })`),
+ the _find_unique_symbol method should prefer the symbol with the larger range (the definition).
+
+ The test exercises this by calling replace_body on 'pressCount', which internally calls
+ _find_unique_symbol and should correctly select the definition (line 40, 19 chars) over
+ the reference (line 97, 10 chars).
+ """
+ test_case.run_test(content_after_ground_truth=snapshot)
+
+
+VUE_STORE_REPLACED_CLEAR_BODY = """function clear() {
+ // Modified: Reset to initial state with a log
+ console.log('Clearing calculator state');
+ displayValue.value = '0';
+ expression.value = '';
+ operationHistory.value = [];
+ lastResult.value = undefined;
+}"""
+
+
+@pytest.mark.parametrize(
+ "test_case",
+ [
+ pytest.param(
+ ReplaceBodyTest(
+ Language.VUE,
+ VUE_STORE_FILE,
+ "clear",
+ VUE_STORE_REPLACED_CLEAR_BODY,
+ ),
+ marks=pytest.mark.vue,
+ ),
+ ],
+)
+def test_replace_body_vue_ts_file(test_case: ReplaceBodyTest, snapshot: SnapshotAssertion) -> None:
+ """Test that TypeScript files within Vue projects can be edited."""
+ test_case.run_test(content_after_ground_truth=snapshot)
diff --git a/test/solidlsp/vue/__init__.py b/test/solidlsp/vue/__init__.py
new file mode 100644
index 00000000..a587a13a
--- /dev/null
+++ b/test/solidlsp/vue/__init__.py
@@ -0,0 +1,15 @@
+"""Vue 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)
diff --git a/test/solidlsp/vue/test_vue_basic.py b/test/solidlsp/vue/test_vue_basic.py
new file mode 100644
index 00000000..c46f35aa
--- /dev/null
+++ b/test/solidlsp/vue/test_vue_basic.py
@@ -0,0 +1,362 @@
+import os
+
+import pytest
+
+from solidlsp import SolidLanguageServer
+from solidlsp.ls_config import Language
+from solidlsp.ls_utils import SymbolUtils
+
+
+@pytest.mark.vue
+class TestVueLanguageServer:
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_vue_files_in_symbol_tree(self, language_server: SolidLanguageServer) -> None:
+ symbols = language_server.request_full_symbol_tree()
+ assert SymbolUtils.symbol_tree_contains_name(symbols, "App"), "App not found in symbol tree"
+ assert SymbolUtils.symbol_tree_contains_name(symbols, "CalculatorButton"), "CalculatorButton not found in symbol tree"
+ assert SymbolUtils.symbol_tree_contains_name(symbols, "CalculatorInput"), "CalculatorInput not found in symbol tree"
+ assert SymbolUtils.symbol_tree_contains_name(symbols, "CalculatorDisplay"), "CalculatorDisplay not found in symbol tree"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
+ store_file = os.path.join("src", "stores", "calculator.ts")
+ symbols = language_server.request_document_symbols(store_file).get_all_symbols_and_roots()
+
+ # Find useCalculatorStore function
+ store_symbol = None
+ for sym in symbols[0]:
+ if sym.get("name") == "useCalculatorStore":
+ store_symbol = sym
+ break
+
+ assert store_symbol is not None, "useCalculatorStore function not found"
+
+ # Get references
+ sel_start = store_symbol["selectionRange"]["start"]
+ refs = language_server.request_references(store_file, sel_start["line"], sel_start["character"])
+
+ # Should have multiple references: definition + usage in App.vue, CalculatorInput.vue, CalculatorDisplay.vue
+ assert len(refs) >= 4, f"useCalculatorStore should have at least 4 references (definition + 3 usages), got {len(refs)}"
+
+ # Verify we have references from .vue files
+ vue_refs = [ref for ref in refs if ".vue" in ref.get("relativePath", "")]
+ assert len(vue_refs) >= 3, f"Should have at least 3 Vue component references, got {len(vue_refs)}"
+
+
+@pytest.mark.vue
+class TestVueDualLspArchitecture:
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_typescript_server_coordination(self, language_server: SolidLanguageServer) -> None:
+ ts_file = os.path.join("src", "stores", "calculator.ts")
+ ts_symbols = language_server.request_document_symbols(ts_file).get_all_symbols_and_roots()
+ ts_symbol_names = [s.get("name") for s in ts_symbols[0]]
+
+ assert len(ts_symbols[0]) >= 5, f"TypeScript server should return multiple symbols for calculator.ts, got {len(ts_symbols[0])}"
+ assert "useCalculatorStore" in ts_symbol_names, "TypeScript server should extract store function"
+
+ # Verify Vue server can parse .vue files
+ vue_file = os.path.join("src", "App.vue")
+ vue_symbols = language_server.request_document_symbols(vue_file).get_all_symbols_and_roots()
+ vue_symbol_names = [s.get("name") for s in vue_symbols[0]]
+
+ assert len(vue_symbols[0]) >= 15, f"Vue server should return at least 15 symbols for App.vue, got {len(vue_symbols[0])}"
+ assert "appTitle" in vue_symbol_names, "Vue server should extract ref declarations from script setup"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_cross_file_references_vue_to_typescript(self, language_server: SolidLanguageServer) -> None:
+ store_file = os.path.join("src", "stores", "calculator.ts")
+ store_symbols = language_server.request_document_symbols(store_file).get_all_symbols_and_roots()
+
+ store_symbol = None
+ for sym in store_symbols[0]:
+ if sym.get("name") == "useCalculatorStore":
+ store_symbol = sym
+ break
+
+ if not store_symbol or "selectionRange" not in store_symbol:
+ pytest.skip("useCalculatorStore symbol not found - test fixture may need updating")
+
+ # Request references for this symbol
+ sel_start = store_symbol["selectionRange"]["start"]
+ refs = language_server.request_references(store_file, sel_start["line"], sel_start["character"])
+
+ # Verify we found references: definition + usage in App.vue, CalculatorInput.vue, CalculatorDisplay.vue
+ assert len(refs) >= 4, f"useCalculatorStore should have at least 4 references (definition + 3 usages), found {len(refs)} references"
+
+ # Verify references include .vue files (components that import the store)
+ vue_refs = [ref for ref in refs if ".vue" in ref.get("uri", "")]
+ assert (
+ len(vue_refs) >= 3
+ ), f"Should find at least 3 references in Vue components, found {len(vue_refs)}: {[ref.get('uri', '') for ref in vue_refs]}"
+
+ # Verify specific components that use the store
+ expected_vue_files = ["App.vue", "CalculatorInput.vue", "CalculatorDisplay.vue"]
+ found_components = []
+ for expected_file in expected_vue_files:
+ matching_refs = [ref for ref in vue_refs if expected_file in ref.get("uri", "")]
+ if matching_refs:
+ found_components.append(expected_file)
+
+ assert len(found_components) > 0, (
+ f"Should find references in at least one component that uses the store. "
+ f"Expected any of {expected_vue_files}, found references in: {[ref.get('uri', '') for ref in vue_refs]}"
+ )
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_cross_file_references_typescript_to_vue(self, language_server: SolidLanguageServer) -> None:
+ types_file = os.path.join("src", "types", "index.ts")
+ types_symbols = language_server.request_document_symbols(types_file).get_all_symbols_and_roots()
+ types_symbol_names = [s.get("name") for s in types_symbols[0]]
+
+ # Operation type is used in calculator.ts and CalculatorInput.vue
+ assert "Operation" in types_symbol_names, "Operation type should exist in types file"
+
+ operation_symbol = None
+ for sym in types_symbols[0]:
+ if sym.get("name") == "Operation":
+ operation_symbol = sym
+ break
+
+ if not operation_symbol or "selectionRange" not in operation_symbol:
+ pytest.skip("Operation type symbol not found - test fixture may need updating")
+
+ # Request references for the Operation type
+ sel_start = operation_symbol["selectionRange"]["start"]
+ refs = language_server.request_references(types_file, sel_start["line"], sel_start["character"])
+
+ # Verify we found references: definition + usage in calculator.ts and Vue files
+ assert len(refs) >= 2, f"Operation type should have at least 2 references (definition + usages), found {len(refs)} references"
+
+ # The Operation type should be referenced in both .ts files (calculator.ts) and potentially .vue files
+ all_ref_uris = [ref.get("uri", "") for ref in refs]
+ has_ts_refs = any(".ts" in uri and "types" not in uri for uri in all_ref_uris)
+
+ assert (
+ has_ts_refs
+ ), f"Operation type should be referenced in TypeScript files like calculator.ts. Found references in: {all_ref_uris}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_reference_deduplication(self, language_server: SolidLanguageServer) -> None:
+ store_file = os.path.join("src", "stores", "calculator.ts")
+ store_symbols = language_server.request_document_symbols(store_file).get_all_symbols_and_roots()
+
+ # Find a commonly-used symbol (useCalculatorStore)
+ store_symbol = None
+ for sym in store_symbols[0]:
+ if sym.get("name") == "useCalculatorStore":
+ store_symbol = sym
+ break
+
+ if not store_symbol or "selectionRange" not in store_symbol:
+ pytest.skip("useCalculatorStore symbol not found - test fixture may need updating")
+
+ # Request references
+ sel_start = store_symbol["selectionRange"]["start"]
+ refs = language_server.request_references(store_file, sel_start["line"], sel_start["character"])
+
+ # Check for duplicate references (same file, line, and character)
+ seen_locations = set()
+ duplicates = []
+
+ for ref in refs:
+ # Create a unique key for this reference location
+ uri = ref.get("uri", "")
+ if "range" in ref:
+ line = ref["range"]["start"]["line"]
+ character = ref["range"]["start"]["character"]
+ location_key = (uri, line, character)
+
+ if location_key in seen_locations:
+ duplicates.append(location_key)
+ else:
+ seen_locations.add(location_key)
+
+ assert len(duplicates) == 0, (
+ f"Found {len(duplicates)} duplicate reference locations. "
+ f"The dual-LSP architecture should deduplicate references from both servers. "
+ f"Duplicates: {duplicates}"
+ )
+
+
+@pytest.mark.vue
+class TestVueEdgeCases:
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None:
+ full_tree = language_server.request_full_symbol_tree()
+
+ # Helper to extract all file paths from symbol tree
+ def extract_paths_from_tree(symbols, paths=None):
+ """Recursively extract file paths from symbol tree."""
+ if paths is None:
+ paths = []
+
+ if isinstance(symbols, list):
+ for symbol in symbols:
+ extract_paths_from_tree(symbol, paths)
+ elif isinstance(symbols, dict):
+ # Check if this symbol has a location
+ if "location" in symbols and "uri" in symbols["location"]:
+ uri = symbols["location"]["uri"]
+ # Extract the path after file://
+ if uri.startswith("file://"):
+ file_path = uri[7:] # Remove "file://"
+ paths.append(file_path)
+
+ # Recurse into children
+ if "children" in symbols:
+ extract_paths_from_tree(symbols["children"], paths)
+
+ return paths
+
+ all_paths = extract_paths_from_tree(full_tree)
+
+ # Verify we have files from expected directories
+ # Note: Symbol tree may include duplicate paths (one per symbol in file)
+ components_files = list({p for p in all_paths if "components" in p and ".vue" in p})
+ stores_files = list({p for p in all_paths if "stores" in p and ".ts" in p})
+ composables_files = list({p for p in all_paths if "composables" in p and ".ts" in p})
+
+ assert len(components_files) == 3, (
+ f"Symbol tree should include exactly 3 unique Vue components (CalculatorButton, CalculatorInput, CalculatorDisplay). "
+ f"Found {len(components_files)} unique component files: {[p.split('/')[-1] for p in sorted(components_files)]}"
+ )
+
+ assert len(stores_files) == 1, (
+ f"Symbol tree should include exactly 1 unique store file (calculator.ts). "
+ f"Found {len(stores_files)} unique store files: {[p.split('/')[-1] for p in sorted(stores_files)]}"
+ )
+
+ assert len(composables_files) == 2, (
+ f"Symbol tree should include exactly 2 unique composable files (useFormatter.ts, useTheme.ts). "
+ f"Found {len(composables_files)} unique composable files: {[p.split('/')[-1] for p in sorted(composables_files)]}"
+ )
+
+ # Verify specific expected files exist in the tree
+ expected_files = [
+ "CalculatorButton.vue",
+ "CalculatorInput.vue",
+ "CalculatorDisplay.vue",
+ "App.vue",
+ "calculator.ts",
+ "useFormatter.ts",
+ "useTheme.ts",
+ ]
+
+ for expected_file in expected_files:
+ matching_files = [p for p in all_paths if expected_file in p]
+ assert len(matching_files) > 0, f"Expected file '{expected_file}' should be in symbol tree. All paths: {all_paths}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_document_overview(self, language_server: SolidLanguageServer) -> None:
+ app_file = os.path.join("src", "App.vue")
+ overview = language_server.request_document_overview(app_file)
+
+ # Overview should return a list of top-level symbols
+ assert isinstance(overview, list), f"Overview should be a list, got: {type(overview)}"
+ assert len(overview) >= 1, f"App.vue should have at least 1 top-level symbol in overview, got {len(overview)}"
+
+ # Extract symbol names from overview
+ symbol_names = [s.get("name") for s in overview if isinstance(s, dict)]
+
+ # Vue LSP returns SFC structure (template/script/style sections) for .vue files
+ # This is expected behavior - overview shows the file's high-level structure
+ assert (
+ len(symbol_names) >= 1
+ ), f"Should have at least 1 symbol name in overview (e.g., 'App' or SFC section), got {len(symbol_names)}: {symbol_names}"
+
+ # Test overview for a TypeScript file
+ store_file = os.path.join("src", "stores", "calculator.ts")
+ store_overview = language_server.request_document_overview(store_file)
+
+ assert isinstance(store_overview, list), f"Store overview should be a list, got: {type(store_overview)}"
+ assert len(store_overview) >= 1, f"calculator.ts should have at least 1 top-level symbol in overview, got {len(store_overview)}"
+
+ store_symbol_names = [s.get("name") for s in store_overview if isinstance(s, dict)]
+ assert (
+ "useCalculatorStore" in store_symbol_names
+ ), f"useCalculatorStore should be in store file overview. Found {len(store_symbol_names)} symbols: {store_symbol_names}"
+
+ # Test overview for another Vue component
+ button_file = os.path.join("src", "components", "CalculatorButton.vue")
+ button_overview = language_server.request_document_overview(button_file)
+
+ assert isinstance(button_overview, list), f"Button overview should be a list, got: {type(button_overview)}"
+ assert (
+ len(button_overview) >= 1
+ ), f"CalculatorButton.vue should have at least 1 top-level symbol in overview, got {len(button_overview)}"
+
+ # For Vue files, overview provides SFC structure which is useful for navigation
+ # The detailed symbols are available via request_document_symbols
+ button_symbol_names = [s.get("name") for s in button_overview if isinstance(s, dict)]
+ assert len(button_symbol_names) >= 1, (
+ f"CalculatorButton.vue should have at least 1 symbol in overview (e.g., 'CalculatorButton' or SFC section). "
+ f"Found {len(button_symbol_names)} symbols: {button_symbol_names}"
+ )
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_directory_overview(self, language_server: SolidLanguageServer) -> None:
+ components_dir = os.path.join("src", "components")
+ dir_overview = language_server.request_dir_overview(components_dir)
+
+ # Directory overview should be a dict mapping file paths to symbol lists
+ assert isinstance(dir_overview, dict), f"Directory overview should be a dict, got: {type(dir_overview)}"
+ assert len(dir_overview) == 3, f"src/components directory should have exactly 3 files in overview, got {len(dir_overview)}"
+
+ # Verify all component files are included
+ expected_components = ["CalculatorButton.vue", "CalculatorInput.vue", "CalculatorDisplay.vue"]
+
+ for expected_component in expected_components:
+ # Find files that match this component name
+ matching_files = [path for path in dir_overview.keys() if expected_component in path]
+ assert len(matching_files) == 1, (
+ f"Component '{expected_component}' should appear exactly once in directory overview. "
+ f"Found {len(matching_files)} matches. All files: {list(dir_overview.keys())}"
+ )
+
+ # Verify the matched file has symbols
+ file_path = matching_files[0]
+ symbols = dir_overview[file_path]
+ assert isinstance(symbols, list), f"Symbols for {file_path} should be a list, got {type(symbols)}"
+ assert len(symbols) >= 1, f"Component {expected_component} should have at least 1 symbol in overview, got {len(symbols)}"
+
+ # Test overview for stores directory
+ stores_dir = os.path.join("src", "stores")
+ stores_overview = language_server.request_dir_overview(stores_dir)
+
+ assert isinstance(stores_overview, dict), f"Stores overview should be a dict, got: {type(stores_overview)}"
+ assert (
+ len(stores_overview) == 1
+ ), f"src/stores directory should have exactly 1 file (calculator.ts) in overview, got {len(stores_overview)}"
+
+ # Verify calculator.ts is included
+ calculator_files = [path for path in stores_overview.keys() if "calculator.ts" in path]
+ assert len(calculator_files) == 1, (
+ f"calculator.ts should appear exactly once in stores directory overview. "
+ f"Found {len(calculator_files)} matches. All files: {list(stores_overview.keys())}"
+ )
+
+ # Verify the store file has symbols
+ store_path = calculator_files[0]
+ store_symbols = stores_overview[store_path]
+ store_symbol_names = [s.get("name") for s in store_symbols if isinstance(s, dict)]
+ assert (
+ "useCalculatorStore" in store_symbol_names
+ ), f"calculator.ts should have useCalculatorStore in overview. Found {len(store_symbol_names)} symbols: {store_symbol_names}"
+
+ # Test overview for composables directory
+ composables_dir = os.path.join("src", "composables")
+ composables_overview = language_server.request_dir_overview(composables_dir)
+
+ assert isinstance(composables_overview, dict), f"Composables overview should be a dict, got: {type(composables_overview)}"
+ assert (
+ len(composables_overview) == 2
+ ), f"src/composables directory should have exactly 2 files in overview, got {len(composables_overview)}"
+
+ # Verify composable files are included
+ expected_composables = ["useFormatter.ts", "useTheme.ts"]
+ for expected_composable in expected_composables:
+ matching_files = [path for path in composables_overview.keys() if expected_composable in path]
+ assert len(matching_files) == 1, (
+ f"Composable '{expected_composable}' should appear exactly once in directory overview. "
+ f"Found {len(matching_files)} matches. All files: {list(composables_overview.keys())}"
+ )
diff --git a/test/solidlsp/vue/test_vue_error_cases.py b/test/solidlsp/vue/test_vue_error_cases.py
new file mode 100644
index 00000000..5396666f
--- /dev/null
+++ b/test/solidlsp/vue/test_vue_error_cases.py
@@ -0,0 +1,422 @@
+import os
+import sys
+
+import pytest
+
+from solidlsp import SolidLanguageServer
+from solidlsp.ls_config import Language
+
+pytestmark = pytest.mark.vue
+
+IS_WINDOWS = sys.platform == "win32"
+
+
+class TypeScriptServerBehavior:
+ """Platform-specific TypeScript language server behavior for invalid positions.
+
+ On Windows: TS server returns empty results for invalid positions
+ On macOS/Linux: TS server raises exceptions with "Bad line number" or "Debug Failure"
+ """
+
+ @staticmethod
+ def raises_on_invalid_position() -> bool:
+ return not IS_WINDOWS
+
+ @staticmethod
+ def returns_empty_on_invalid_position() -> bool:
+ return IS_WINDOWS
+
+
+class TestVueInvalidPositions:
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_negative_line_number(self, language_server: SolidLanguageServer) -> None:
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ result = language_server.request_containing_symbol(file_path, -1, 0)
+
+ assert result is None or result == {}, f"Negative line number should return None or empty dict, got: {result}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_negative_character_number(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting containing symbol with negative character number.
+
+ Expected behavior: Should return None or empty dict, not crash.
+ """
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ # Request containing symbol at invalid negative character
+ result = language_server.request_containing_symbol(file_path, 10, -1)
+
+ # Should handle gracefully - return None or empty dict
+ assert result is None or result == {}, f"Negative character number should return None or empty dict, got: {result}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_line_number_beyond_file_length(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting containing symbol beyond file length.
+
+ Expected behavior: Raises IndexError when trying to access line beyond file bounds.
+ This happens in the wrapper code before even reaching the language server.
+ """
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ # Request containing symbol at line 99999 (way beyond file length)
+ # The wrapper code will raise an IndexError when checking if the line is empty
+ with pytest.raises(IndexError) as exc_info:
+ language_server.request_containing_symbol(file_path, 99999, 0)
+
+ # Verify it's an index error for list access
+ assert "list index out of range" in str(exc_info.value), f"Expected 'list index out of range' error, got: {exc_info.value}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_character_number_beyond_line_length(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting containing symbol beyond line length.
+
+ Expected behavior: Should return None or empty dict, not crash.
+ """
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ # Request containing symbol at character 99999 (way beyond line length)
+ result = language_server.request_containing_symbol(file_path, 10, 99999)
+
+ # Should handle gracefully - return None or empty dict
+ assert result is None or result == {}, f"Character beyond line length should return None or empty dict, got: {result}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_references_at_negative_line(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting references with negative line number."""
+ from solidlsp.ls_exceptions import SolidLSPException
+
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ if TypeScriptServerBehavior.returns_empty_on_invalid_position():
+ result = language_server.request_references(file_path, -1, 0)
+ assert result == [], f"Expected empty list on Windows, got: {result}"
+ else:
+ with pytest.raises(SolidLSPException) as exc_info:
+ language_server.request_references(file_path, -1, 0)
+ assert "Bad line number" in str(exc_info.value) or "Debug Failure" in str(exc_info.value)
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_definition_at_invalid_position(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting definition at invalid position."""
+ from solidlsp.ls_exceptions import SolidLSPException
+
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ if TypeScriptServerBehavior.returns_empty_on_invalid_position():
+ result = language_server.request_definition(file_path, -1, 0)
+ assert result == [], f"Expected empty list on Windows, got: {result}"
+ else:
+ with pytest.raises(SolidLSPException) as exc_info:
+ language_server.request_definition(file_path, -1, 0)
+ assert "Bad line number" in str(exc_info.value) or "Debug Failure" in str(exc_info.value)
+
+
+class TestVueNonExistentFiles:
+ """Tests for handling non-existent files."""
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_document_symbols_nonexistent_file(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting document symbols from non-existent file.
+
+ Expected behavior: Should raise FileNotFoundError or return empty result.
+ """
+ nonexistent_file = os.path.join("src", "components", "NonExistent.vue")
+
+ # Should raise an appropriate exception or return empty result
+ try:
+ result = language_server.request_document_symbols(nonexistent_file)
+ # If no exception, verify result is empty or indicates file not found
+ symbols = result.get_all_symbols_and_roots()
+ assert len(symbols[0]) == 0, f"Non-existent file should return empty symbols, got {len(symbols[0])} symbols"
+ except (FileNotFoundError, Exception) as e:
+ # Expected - file doesn't exist
+ assert True, f"Appropriately raised exception for non-existent file: {e}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_containing_symbol_nonexistent_file(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting containing symbol from non-existent file.
+
+ Expected behavior: Should raise FileNotFoundError or return None.
+ """
+ nonexistent_file = os.path.join("src", "components", "NonExistent.vue")
+
+ # Should raise an appropriate exception or return None
+ try:
+ result = language_server.request_containing_symbol(nonexistent_file, 10, 10)
+ # If no exception, verify result indicates file not found
+ assert result is None or result == {}, f"Non-existent file should return None or empty dict, got: {result}"
+ except (FileNotFoundError, Exception) as e:
+ # Expected - file doesn't exist
+ assert True, f"Appropriately raised exception for non-existent file: {e}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_references_nonexistent_file(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting references from non-existent file.
+
+ Expected behavior: Should raise FileNotFoundError or return empty list.
+ """
+ nonexistent_file = os.path.join("src", "components", "NonExistent.vue")
+
+ # Should raise an appropriate exception or return empty list
+ try:
+ result = language_server.request_references(nonexistent_file, 10, 10)
+ # If no exception, verify result is empty
+ assert result is None or isinstance(result, list), f"Non-existent file should return None or list, got: {result}"
+ if isinstance(result, list):
+ assert len(result) == 0, f"Non-existent file should return empty list, got {len(result)} references"
+ except (FileNotFoundError, Exception) as e:
+ # Expected - file doesn't exist
+ assert True, f"Appropriately raised exception for non-existent file: {e}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_definition_nonexistent_file(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting definition from non-existent file.
+
+ Expected behavior: Should raise FileNotFoundError or return empty list.
+ """
+ nonexistent_file = os.path.join("src", "components", "NonExistent.vue")
+
+ # Should raise an appropriate exception or return empty list
+ try:
+ result = language_server.request_definition(nonexistent_file, 10, 10)
+ # If no exception, verify result is empty
+ assert isinstance(result, list), f"request_definition should return a list, got: {type(result)}"
+ assert len(result) == 0, f"Non-existent file should return empty list, got {len(result)} definitions"
+ except (FileNotFoundError, Exception) as e:
+ # Expected - file doesn't exist
+ assert True, f"Appropriately raised exception for non-existent file: {e}"
+
+
+class TestVueUndefinedSymbols:
+ """Tests for handling undefined or unreferenced symbols."""
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_references_for_unreferenced_symbol(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting references for a symbol that has no references.
+
+ Expected behavior: Should return empty list (only the definition itself if include_self=True).
+ """
+ # Find a symbol that likely has no external references
+ file_path = os.path.join("src", "components", "CalculatorButton.vue")
+
+ # Get document symbols
+ symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
+
+ # Find pressCount - this is exposed but may not be referenced elsewhere
+ press_count_symbol = next((s for s in symbols[0] if s.get("name") == "pressCount"), None)
+
+ if not press_count_symbol or "selectionRange" not in press_count_symbol:
+ pytest.skip("pressCount symbol not found - test fixture may need updating")
+
+ # Request references without include_self
+ sel_start = press_count_symbol["selectionRange"]["start"]
+ refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
+
+ # Should return a list (may be empty or contain only definition)
+ assert isinstance(refs, list), f"request_references should return a list, got {type(refs)}"
+
+ # For an unreferenced symbol, should have 0-1 references (0 without include_self, 1 with)
+ # The exact count depends on the language server implementation
+ assert len(refs) <= 5, (
+ f"pressCount should have few or no external references. "
+ f"Got {len(refs)} references. This is not necessarily an error, just documenting behavior."
+ )
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_containing_symbol_at_whitespace_only_line(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting containing symbol at a whitespace-only line.
+
+ Expected behavior: Should return None, empty dict, or the parent symbol.
+ """
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ # Try position at line 1 (typically a blank line or template start in Vue SFCs)
+ result = language_server.request_containing_symbol(file_path, 1, 0)
+
+ # Should handle gracefully - return None, empty dict, or a valid parent symbol
+ assert (
+ result is None or result == {} or isinstance(result, dict)
+ ), f"Whitespace line should return None, empty dict, or valid symbol. Got: {result}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_definition_at_keyword_position(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting definition at language keyword position.
+
+ Expected behavior: Should return empty list or handle gracefully.
+ """
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ # Try to get definition at a keyword like "const", "import", etc.
+ # Line 2 typically has "import" statement - try position on "import" keyword
+ result = language_server.request_definition(file_path, 2, 0)
+
+ # Should handle gracefully - return empty list or valid definitions
+ assert isinstance(result, list), f"request_definition should return a list, got {type(result)}"
+
+
+class TestVueEdgeCasePositions:
+ """Tests for edge case positions (0,0 and file boundaries)."""
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_containing_symbol_at_file_start(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting containing symbol at position (0,0).
+
+ Expected behavior: Should return None, empty dict, or a valid symbol.
+ This position typically corresponds to the start of the file (e.g., tag).
+ """
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ # Request containing symbol at position 0,0 (file start)
+ result = language_server.request_containing_symbol(file_path, 0, 0)
+
+ # Should handle gracefully
+ assert (
+ result is None or result == {} or isinstance(result, dict)
+ ), f"Position 0,0 should return None, empty dict, or valid symbol. Got: {result}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_references_at_file_start(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting references at position (0,0).
+
+ Expected behavior: Should return None or empty list.
+ """
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ # Request references at position 0,0 (file start)
+ result = language_server.request_references(file_path, 0, 0)
+
+ # Should handle gracefully
+ assert result is None or isinstance(result, list), f"Position 0,0 should return None or list. Got: {type(result)}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_definition_at_file_start(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting definition at position (0,0).
+
+ Expected behavior: Should return empty list.
+ """
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ # Request definition at position 0,0 (file start)
+ result = language_server.request_definition(file_path, 0, 0)
+
+ # Should handle gracefully
+ assert isinstance(result, list), f"request_definition should return a list. Got: {type(result)}"
+
+ @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True)
+ def test_containing_symbol_in_template_section(self, language_server: SolidLanguageServer) -> None:
+ """Test requesting containing symbol in the template section.
+
+ Expected behavior: Template positions typically have no containing symbol (return None or empty).
+ The Vue language server may not track template symbols the same way as script symbols.
+ """
+ file_path = os.path.join("src", "components", "CalculatorInput.vue")
+
+ # Position likely in template section (early in file, before ]