mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-03 09:13:49 +00:00
Add CUE language support via cue lsp (#1474)
This commit is contained in:
1 parent
cbfe68a4cd
commit
ffd5dfd3e7
14 files changed
+483
-1
No files matched your search
@@ -2,6 +2,9 @@
|
||||
|
||||
Status of the `main` branch. Changes prior to the next official version change will appear here.
|
||||
|
||||
* Language Servers:
|
||||
- Add **CUE** support via the LSP mode of the official [`cue` CLI](https://github.com/cue-lang/cue) (`cue lsp`).
|
||||
|
||||
# v1.5.0 (2026-05-18)
|
||||
|
||||
* General:
|
||||
|
||||
@@ -110,7 +110,7 @@ Serena incorporates a powerful abstraction layer for the integration of language
|
||||
The underlying language servers are typically open-source projects or at least freely available for use.
|
||||
|
||||
When using Serena's language server backend, we provide **support for over 40 programming languages**, including
|
||||
Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GDScript, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
|
||||
Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, CUE, Dart, Elixir, Elm, Erlang, Fortran, F#, GDScript, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Svelte, Swift, TOML, TypeScript, WGSL, YAML, and Zig.
|
||||
|
||||
### The Serena JetBrains Plugin
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ Some languages require additional installations or setup steps, as noted.
|
||||
* **Crystal**
|
||||
(requires [Crystalline](https://github.com/elbywan/crystalline) language server to be installed and available on PATH;
|
||||
note: Crystalline has limited go-to-definition support and does not support find-references)
|
||||
* **CUE**
|
||||
* **Dart**
|
||||
* **Elixir**
|
||||
(requires Elixir installation; Expert language server is downloaded automatically)
|
||||
|
||||
@@ -303,6 +303,7 @@ addopts = "--snapshot-patch-pycharm-diff"
|
||||
markers = [
|
||||
"clojure: language server running for Clojure",
|
||||
"crystal: language server running for Crystal",
|
||||
"cue: language server running for CUE",
|
||||
"python: language server running for Python",
|
||||
"go: language server running for Go",
|
||||
"java: language server running for Java",
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
Provides CUE-specific instantiation of the LanguageServer class, using the LSP mode of the
|
||||
``cue`` CLI (``cue lsp``) from the official CUE distribution.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import threading
|
||||
from typing import cast
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
from .common import RuntimeDependency, RuntimeDependencyCollection
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# How to refresh the pinned SHA256s when bumping DEFAULT_CUE_VERSION:
|
||||
# gh release view <tag> --repo cue-lang/cue --json assets \
|
||||
# --jq '.assets[] | select(.name | test("(darwin|linux|windows)_(amd64|arm64)")) | {name, digest}'
|
||||
# The `digest` field is `sha256:<hex>` — copy the hex portion into DEFAULT_CUE_SHA256_BY_PLATFORM
|
||||
# keyed by the Serena PlatformId (osx-arm64, osx-x64, linux-arm64, linux-x64, win-x64, win-arm64).
|
||||
DEFAULT_CUE_VERSION = "v0.16.1"
|
||||
DEFAULT_CUE_SHA256_BY_PLATFORM = {
|
||||
"osx-arm64": "a72b0cddb377c52d1b003bed9a335d893b70cd75a182cd5e3fee8bae30ddb6d6",
|
||||
"osx-x64": "97b0d78e4c5ee49ff72145fd6ef4f4bab0bb332d55f29660de3fec2af5ec96a9",
|
||||
"linux-arm64": "3cc715a9e969f87b93c4fa34cfaef5388b93e96efa20b248e8ad6826abd25a83",
|
||||
"linux-x64": "5d644c1305a2b86504c8dcd2ec829cf5b4999efc2cf51ee375624e0455f774ae",
|
||||
"win-x64": "2f24123f458229fcf283db534bd86692ad1074da806defee0f0cc62976c0397c",
|
||||
"win-arm64": "e0c15ce53f73e8609b0e8ce6507298f3474b334ac5eb0c826c9497a811fd0cce",
|
||||
}
|
||||
|
||||
|
||||
def _cue_sha(version: str, platform_key: str) -> str | None:
|
||||
if version == DEFAULT_CUE_VERSION:
|
||||
return DEFAULT_CUE_SHA256_BY_PLATFORM.get(platform_key)
|
||||
return None
|
||||
|
||||
|
||||
CUE_ALLOWED_HOSTS = (
|
||||
"github.com",
|
||||
"release-assets.githubusercontent.com",
|
||||
"objects.githubusercontent.com",
|
||||
)
|
||||
|
||||
|
||||
class CueLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides a CUE-specific instantiation of the language server, driven by ``cue lsp`` from the
|
||||
official CUE CLI distribution.
|
||||
|
||||
Recognised entries in ``ls_specific_settings["cue"]``:
|
||||
- ``ls_path``: Absolute path to a pre-installed ``cue`` binary. Bypasses Serena's
|
||||
auto-download mechanism; useful when the user already has ``cue`` on ``$PATH``
|
||||
(e.g. via ``brew install cue`` or ``go install``).
|
||||
- ``cue_version``: Override the pinned cue version downloaded by Serena
|
||||
(default: the bundled Serena version). Setting this to a version other than
|
||||
``DEFAULT_CUE_VERSION`` skips SHA256 verification (checksums for arbitrary
|
||||
versions are unknown), so pair it with ``ls_path`` if integrity matters.
|
||||
"""
|
||||
|
||||
CUE_ALLOWED_HOSTS = CUE_ALLOWED_HOSTS
|
||||
|
||||
# Directories worth pruning for CUE projects. cue.mod/gen/ contains generated Go->CUE
|
||||
# bindings that shouldn't be traversed for symbolic operations; cue.mod/pkg/ holds
|
||||
# fetched module dependencies (analogous to node_modules / vendor).
|
||||
_IGNORED_DIRS = frozenset({"cue.mod/gen", "cue.mod/pkg"})
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in self._IGNORED_DIRS
|
||||
|
||||
@classmethod
|
||||
def _runtime_dependencies(cls, version: str) -> RuntimeDependencyCollection:
|
||||
"""Builds the platform-specific runtime dependency set for the given cue release.
|
||||
|
||||
:param version: the cue release tag (e.g. ``v0.16.1``); the leading ``v`` is stripped when
|
||||
constructing the archive filename, since cue releases embed the bare version there.
|
||||
"""
|
||||
version_no_v = version.lstrip("v")
|
||||
cue_releases = f"https://github.com/cue-lang/cue/releases/download/{version}"
|
||||
return RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="cue",
|
||||
url=f"{cue_releases}/cue_v{version_no_v}_darwin_arm64.tar.gz",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="gztar",
|
||||
binary_name="cue",
|
||||
sha256=_cue_sha(version, "osx-arm64"),
|
||||
allowed_hosts=CUE_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="cue",
|
||||
url=f"{cue_releases}/cue_v{version_no_v}_darwin_amd64.tar.gz",
|
||||
platform_id="osx-x64",
|
||||
archive_type="gztar",
|
||||
binary_name="cue",
|
||||
sha256=_cue_sha(version, "osx-x64"),
|
||||
allowed_hosts=CUE_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="cue",
|
||||
url=f"{cue_releases}/cue_v{version_no_v}_linux_arm64.tar.gz",
|
||||
platform_id="linux-arm64",
|
||||
archive_type="gztar",
|
||||
binary_name="cue",
|
||||
sha256=_cue_sha(version, "linux-arm64"),
|
||||
allowed_hosts=CUE_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="cue",
|
||||
url=f"{cue_releases}/cue_v{version_no_v}_linux_amd64.tar.gz",
|
||||
platform_id="linux-x64",
|
||||
archive_type="gztar",
|
||||
binary_name="cue",
|
||||
sha256=_cue_sha(version, "linux-x64"),
|
||||
allowed_hosts=CUE_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="cue",
|
||||
url=f"{cue_releases}/cue_v{version_no_v}_windows_amd64.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="cue.exe",
|
||||
sha256=_cue_sha(version, "win-x64"),
|
||||
allowed_hosts=CUE_ALLOWED_HOSTS,
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="cue",
|
||||
url=f"{cue_releases}/cue_v{version_no_v}_windows_arm64.zip",
|
||||
platform_id="win-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="cue.exe",
|
||||
sha256=_cue_sha(version, "win-arm64"),
|
||||
allowed_hosts=CUE_ALLOWED_HOSTS,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""Creates a CueLanguageServer instance.
|
||||
|
||||
Not meant to be instantiated directly — use :meth:`SolidLanguageServer.create` instead.
|
||||
"""
|
||||
super().__init__(
|
||||
config,
|
||||
repository_root_path,
|
||||
None,
|
||||
"cue",
|
||||
solidlsp_settings,
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
"""Resolves a ``cue`` executable, downloading the pinned release if it isn't cached yet."""
|
||||
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
cue_version = self._custom_settings.get("cue_version", DEFAULT_CUE_VERSION)
|
||||
deps = CueLanguageServer._runtime_dependencies(cue_version)
|
||||
dependency = deps.get_single_dep_for_current_platform()
|
||||
|
||||
install_dir = os.path.join(self._ls_resources_dir, f"cue-{cue_version}")
|
||||
cue_executable_path = deps.binary_path(install_dir)
|
||||
if not os.path.exists(cue_executable_path):
|
||||
log.info(f"Downloading and extracting cue from {dependency.url} to {install_dir}")
|
||||
deps.install(install_dir)
|
||||
if not os.path.exists(cue_executable_path):
|
||||
raise FileNotFoundError(f"Download failed? Could not find cue executable at {cue_executable_path}")
|
||||
os.chmod(cue_executable_path, 0o755)
|
||||
return cue_executable_path
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
# cue's LSP mode is activated via the `lsp` subcommand; it speaks LSP over stdio.
|
||||
return [core_path, "lsp"]
|
||||
|
||||
def _get_initialize_params(self) -> InitializeParams:
|
||||
"""Returns the init params for ``cue lsp``."""
|
||||
repository_absolute_path = self.repository_root_path
|
||||
root_uri = pathlib.Path(repository_absolute_path).as_uri()
|
||||
|
||||
result = {
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"capabilities": {
|
||||
"workspace": {
|
||||
"applyEdit": True,
|
||||
"workspaceEdit": {"documentChanges": True},
|
||||
"symbol": {"symbolKind": {"valueSet": list(range(1, 27))}},
|
||||
"workspaceFolders": True,
|
||||
"configuration": True,
|
||||
},
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True, "dynamicRegistration": True},
|
||||
"publishDiagnostics": {"relatedInformation": True, "tagSupport": {"valueSet": [1, 2]}},
|
||||
"definition": {"linkSupport": True, "dynamicRegistration": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"hover": {"contentFormat": ["markdown", "plaintext"], "dynamicRegistration": True},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
"symbolKind": {"valueSet": list(range(1, 27))},
|
||||
},
|
||||
"completion": {
|
||||
"dynamicRegistration": True,
|
||||
"completionItem": {"snippetSupport": False, "documentationFormat": ["markdown", "plaintext"]},
|
||||
},
|
||||
},
|
||||
"general": {"positionEncodings": ["utf-16"]},
|
||||
},
|
||||
"initializationOptions": {},
|
||||
"trace": "off",
|
||||
"workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
|
||||
}
|
||||
return cast(InitializeParams, result)
|
||||
|
||||
def _start_server(self) -> None:
|
||||
def register_capability_handler(params: dict) -> None:
|
||||
return
|
||||
|
||||
def workspace_configuration_handler(params: dict) -> list[dict]:
|
||||
# cue lsp asks the client for its configuration shortly after initialization
|
||||
# and will not start servicing requests until it gets a reply. We return an
|
||||
# empty object per requested item — cue's defaults are fine for our use.
|
||||
items = params.get("items", []) if isinstance(params, dict) else []
|
||||
return [{} for _ in items]
|
||||
|
||||
def window_log_message(msg: dict) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
|
||||
def do_nothing(params: dict) -> None:
|
||||
return
|
||||
|
||||
# wire up notification/request handlers before starting the process
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_request("workspace/configuration", workspace_configuration_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 cue lsp server process")
|
||||
self.server.start()
|
||||
|
||||
initialize_params = self._get_initialize_params()
|
||||
|
||||
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
|
||||
# sanity-check the server advertises the core capabilities we rely on
|
||||
capabilities = init_response["capabilities"]
|
||||
assert "textDocumentSync" in capabilities
|
||||
assert "definitionProvider" in capabilities
|
||||
assert "documentSymbolProvider" in capabilities
|
||||
assert "referencesProvider" in capabilities
|
||||
|
||||
self.server.notify.initialized({})
|
||||
# cue lsp is ready to serve immediately after the initialized notification
|
||||
self.server_ready.set()
|
||||
@@ -68,6 +68,7 @@ class Language(str, Enum):
|
||||
SWIFT = "swift"
|
||||
BASH = "bash"
|
||||
CRYSTAL = "crystal"
|
||||
CUE = "cue"
|
||||
ZIG = "zig"
|
||||
LUA = "lua"
|
||||
LUAU = "luau"
|
||||
@@ -400,6 +401,8 @@ class Language(str, Enum):
|
||||
return FilenameMatcher(".sh", ".bash")
|
||||
case self.CRYSTAL:
|
||||
return FilenameMatcher(".cr")
|
||||
case self.CUE:
|
||||
return FilenameMatcher(".cue")
|
||||
case self.YAML:
|
||||
return FilenameMatcher(".yaml", ".yml")
|
||||
case self.JSON:
|
||||
@@ -624,6 +627,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.crystal_language_server import CrystalLanguageServer
|
||||
|
||||
return CrystalLanguageServer
|
||||
case self.CUE:
|
||||
from solidlsp.language_servers.cue_language_server import CueLanguageServer
|
||||
|
||||
return CueLanguageServer
|
||||
case self.YAML:
|
||||
from solidlsp.language_servers.yaml_language_server import YamlLanguageServer
|
||||
|
||||
|
||||
@@ -248,6 +248,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = {
|
||||
],
|
||||
Language.CPP: [pytest.mark.cpp],
|
||||
Language.CPP_CCLS: [pytest.mark.cpp],
|
||||
Language.CUE: [pytest.mark.cue],
|
||||
Language.CSHARP: [pytest.mark.csharp],
|
||||
Language.FSHARP: [pytest.mark.fsharp],
|
||||
Language.GO: [pytest.mark.go],
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
cue.mod/gen/
|
||||
cue.mod/pkg/
|
||||
@@ -0,0 +1,4 @@
|
||||
module: "example.com/testrepo"
|
||||
language: {
|
||||
version: "v0.16.0"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package testrepo
|
||||
|
||||
// #BuildGreeting is a template/closure: given a #Person under `for_`, it produces a #Greeting
|
||||
// under `result`. Used by main.cue to build the canonical greeting.
|
||||
#BuildGreeting: {
|
||||
for_: #Person
|
||||
result: #Greeting & {
|
||||
recipient: for_
|
||||
message: "Hello, \(for_.name)!"
|
||||
}
|
||||
}
|
||||
|
||||
// locales enumerates the locales the greeter supports; defaultLocale (schema.cue) must be one of them.
|
||||
locales: [...string] & [
|
||||
"en-US",
|
||||
"en-GB",
|
||||
"de-DE",
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
package testrepo
|
||||
|
||||
// alice is a concrete #Person value; used to build the canonical greeting below.
|
||||
alice: #Person & {
|
||||
name: "Alice"
|
||||
age: 30
|
||||
email: "alice@example.com"
|
||||
}
|
||||
|
||||
// greetingForAlice references both #Greeting (from schema.cue) and #BuildGreeting (from lib.cue).
|
||||
greetingForAlice: (#BuildGreeting & {for_: alice}).result
|
||||
|
||||
// locale pins the output locale; validated against locales defined in lib.cue.
|
||||
locale: defaultLocale
|
||||
@@ -0,0 +1,17 @@
|
||||
package testrepo
|
||||
|
||||
// #Person is the schema for a person record used by main.cue and lib.cue.
|
||||
#Person: {
|
||||
name!: string
|
||||
age?: int & >=0
|
||||
email: string
|
||||
}
|
||||
|
||||
// #Greeting describes a greeting message addressed to a #Person.
|
||||
#Greeting: {
|
||||
recipient: #Person
|
||||
message: string
|
||||
}
|
||||
|
||||
// defaultLocale is the fallback locale used when no locale is set on a greeting.
|
||||
defaultLocale: "en-US"
|
||||
Whitespace-only changes.
@@ -0,0 +1,145 @@
|
||||
"""Basic integration tests for the CUE language server (``cue lsp``)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
|
||||
|
||||
|
||||
@pytest.mark.cue
|
||||
class TestCueLanguageServer:
|
||||
"""Verifies that ``cue lsp`` drives the symbol and reference APIs Serena depends on.
|
||||
|
||||
The test repo (``test/resources/repos/cue/test_repo``) defines three CUE files that
|
||||
share package ``testrepo`` and cross-reference each other's definitions:
|
||||
|
||||
- ``schema.cue`` defines ``#Person``, ``#Greeting``, ``defaultLocale``.
|
||||
- ``lib.cue`` defines ``#BuildGreeting`` (which uses ``#Person`` and ``#Greeting``) and
|
||||
``locales``.
|
||||
- ``main.cue`` defines ``alice: #Person & {...}``, ``greetingForAlice: (#BuildGreeting & {...})``,
|
||||
and ``locale: defaultLocale``.
|
||||
|
||||
Line/character positions below are 0-indexed (LSP convention) and were captured from
|
||||
``cue lsp v0.16.1`` responses against this repository.
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.CUE], indirect=True)
|
||||
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""The server starts and reports the expected repository root."""
|
||||
assert language_server.is_running()
|
||||
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
|
||||
def test_document_symbols_schema(self, language_server: SolidLanguageServer) -> None:
|
||||
"""``schema.cue`` exposes its top-level definitions with the expected hierarchy."""
|
||||
# request hierarchical document symbols for schema.cue
|
||||
all_symbols, root_symbols = language_server.request_document_symbols("schema.cue").get_all_symbols_and_roots()
|
||||
|
||||
# schema.cue's three top-level definitions must appear as roots
|
||||
root_names = [s.get("name") for s in root_symbols]
|
||||
for expected in ("#Person", "#Greeting", "defaultLocale"):
|
||||
assert expected in root_names, f"{expected} missing from schema.cue roots: {root_names}"
|
||||
|
||||
# #Person's field children must be nested under it (hierarchical response, not flat)
|
||||
person = next(s for s in root_symbols if s.get("name") == "#Person")
|
||||
person_children = [c.get("name") for c in person.get("children", [])]
|
||||
for expected_field in ("name", "age", "email"):
|
||||
assert expected_field in person_children, f"{expected_field} missing under #Person: {person_children}"
|
||||
|
||||
# fields must not also appear at root level (that would be the flat fallback)
|
||||
assert "name" not in root_names, f"name should be a child of #Person, not a root. Roots: {root_names}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
|
||||
def test_full_symbol_tree_contains_cross_file_names(self, language_server: SolidLanguageServer) -> None:
|
||||
"""The repository-wide symbol tree contains definitions from all three CUE files."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
|
||||
for expected in (
|
||||
"#Person", # schema.cue
|
||||
"#Greeting", # schema.cue
|
||||
"defaultLocale", # schema.cue
|
||||
"#BuildGreeting", # lib.cue
|
||||
"locales", # lib.cue
|
||||
"alice", # main.cue
|
||||
"greetingForAlice", # main.cue
|
||||
"locale", # main.cue
|
||||
):
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, expected), f"{expected} missing from full symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.CUE], indirect=True)
|
||||
def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""``#Person`` used in ``main.cue`` resolves to its definition in ``schema.cue``."""
|
||||
# main.cue line 3 (0-indexed): "alice: #Person & {"
|
||||
# ^ char 7 — cursor on '#Person'
|
||||
definitions = language_server.request_definition(str(repo_path / "main.cue"), 3, 8)
|
||||
|
||||
assert definitions, f"Expected a definition for #Person, got {definitions=}"
|
||||
# definition must live in schema.cue at line 3 (0-indexed), char 0
|
||||
target = definitions[0]
|
||||
assert target["uri"].endswith("schema.cue")
|
||||
assert target["range"]["start"] == {"line": 3, "character": 0}
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.CUE], indirect=True)
|
||||
def test_find_references_across_files_person(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""``#Person`` has references in ``lib.cue`` and ``main.cue`` beyond its declaration."""
|
||||
schema_path = str(repo_path / "schema.cue")
|
||||
# schema.cue line 3 (0-indexed): "#Person: {" — cursor on the identifier
|
||||
references = language_server.request_references(schema_path, 3, 1)
|
||||
|
||||
assert references, f"Expected references for #Person, got {references=}"
|
||||
|
||||
# collapse to (filename, start_line) tuples for set-based assertions
|
||||
ref_pairs = {(ref["uri"].split("/")[-1], ref["range"]["start"]["line"]) for ref in references}
|
||||
|
||||
# must include use sites in lib.cue (line 5, in `for_: #Person`) and
|
||||
# main.cue (line 3, in `alice: #Person & {`)
|
||||
assert ("lib.cue", 5) in ref_pairs, f"Expected lib.cue:5 reference, got {sorted(ref_pairs)}"
|
||||
assert ("main.cue", 3) in ref_pairs, f"Expected main.cue:3 reference, got {sorted(ref_pairs)}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.CUE], indirect=True)
|
||||
def test_find_references_within_file_build_greeting(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""``#BuildGreeting`` (defined in lib.cue) is referenced from main.cue."""
|
||||
lib_path = str(repo_path / "lib.cue")
|
||||
# lib.cue line 4 (0-indexed): "#BuildGreeting: {"
|
||||
references = language_server.request_references(lib_path, 4, 1)
|
||||
|
||||
assert references, f"Expected references for #BuildGreeting, got {references=}"
|
||||
|
||||
ref_pairs = {(ref["uri"].split("/")[-1], ref["range"]["start"]["line"]) for ref in references}
|
||||
# use site in main.cue is line 10: "greetingForAlice: (#BuildGreeting & {for_: alice}).result"
|
||||
assert ("main.cue", 10) in ref_pairs, f"Expected main.cue:10 reference, got {sorted(ref_pairs)}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
|
||||
@pytest.mark.parametrize("repo_path", [Language.CUE], indirect=True)
|
||||
def test_find_references_default_locale(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
||||
"""``defaultLocale`` (schema.cue) is used in main.cue."""
|
||||
schema_path = str(repo_path / "schema.cue")
|
||||
# schema.cue line 16 (0-indexed): 'defaultLocale: "en-US"'
|
||||
references = language_server.request_references(schema_path, 16, 1)
|
||||
|
||||
assert references, f"Expected references for defaultLocale, got {references=}"
|
||||
ref_pairs = {(ref["uri"].split("/")[-1], ref["range"]["start"]["line"]) for ref in references}
|
||||
# use site in main.cue is line 13: "locale: defaultLocale"
|
||||
assert ("main.cue", 13) in ref_pairs, f"Expected main.cue:13 reference, got {sorted(ref_pairs)}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
|
||||
def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None:
|
||||
"""CUE symbols must have bare names (no whitespace/bracket/paren/comma/colon pollution)."""
|
||||
# `.` is allowed because the synthetic directory symbol for `cue.mod/` contains a literal
|
||||
# period — CUE modules are always anchored under a directory named `cue.mod`.
|
||||
# `#` in names like `#Person` is part of the identifier in CUE's grammar and is not
|
||||
# in the forbidden-char set.
|
||||
malformed_symbols = [s for s in request_all_symbols(language_server) if has_malformed_name(s, period_allowed=True)]
|
||||
if malformed_symbols:
|
||||
pytest.fail(
|
||||
f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}",
|
||||
pytrace=False,
|
||||
)
|
||||
Reference in new issue
Block a user