diff --git a/CHANGELOG.md b/CHANGELOG.md index eb813076..3a01bc6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/README.md b/README.md index ba885644..30d7c297 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 72cf829b..6f435c2f 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -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) diff --git a/pyproject.toml b/pyproject.toml index 85c56556..0115ca97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/solidlsp/language_servers/cue_language_server.py b/src/solidlsp/language_servers/cue_language_server.py new file mode 100644 index 00000000..b9a90a33 --- /dev/null +++ b/src/solidlsp/language_servers/cue_language_server.py @@ -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 --repo cue-lang/cue --json assets \ +# --jq '.assets[] | select(.name | test("(darwin|linux|windows)_(amd64|arm64)")) | {name, digest}' +# The `digest` field is `sha256:` — 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() diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 7efdf749..8f593833 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -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 diff --git a/test/conftest.py b/test/conftest.py index 70b3a8fe..de0ac507 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -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], diff --git a/test/resources/repos/cue/test_repo/.gitignore b/test/resources/repos/cue/test_repo/.gitignore new file mode 100644 index 00000000..a85a3ed6 --- /dev/null +++ b/test/resources/repos/cue/test_repo/.gitignore @@ -0,0 +1,2 @@ +cue.mod/gen/ +cue.mod/pkg/ diff --git a/test/resources/repos/cue/test_repo/cue.mod/module.cue b/test/resources/repos/cue/test_repo/cue.mod/module.cue new file mode 100644 index 00000000..6d828f8b --- /dev/null +++ b/test/resources/repos/cue/test_repo/cue.mod/module.cue @@ -0,0 +1,4 @@ +module: "example.com/testrepo" +language: { + version: "v0.16.0" +} diff --git a/test/resources/repos/cue/test_repo/lib.cue b/test/resources/repos/cue/test_repo/lib.cue new file mode 100644 index 00000000..a3ccc4c6 --- /dev/null +++ b/test/resources/repos/cue/test_repo/lib.cue @@ -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", +] diff --git a/test/resources/repos/cue/test_repo/main.cue b/test/resources/repos/cue/test_repo/main.cue new file mode 100644 index 00000000..79e47497 --- /dev/null +++ b/test/resources/repos/cue/test_repo/main.cue @@ -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 diff --git a/test/resources/repos/cue/test_repo/schema.cue b/test/resources/repos/cue/test_repo/schema.cue new file mode 100644 index 00000000..91c04131 --- /dev/null +++ b/test/resources/repos/cue/test_repo/schema.cue @@ -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" diff --git a/test/solidlsp/cue/__init__.py b/test/solidlsp/cue/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/solidlsp/cue/test_cue_basic.py b/test/solidlsp/cue/test_cue_basic.py new file mode 100644 index 00000000..f2a580b0 --- /dev/null +++ b/test/solidlsp/cue/test_cue_basic.py @@ -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, + )