|
| 1 | +""" |
| 2 | +Provides CUE-specific instantiation of the LanguageServer class, using the LSP mode of the |
| 3 | +``cue`` CLI (``cue lsp``) from the official CUE distribution. |
| 4 | +""" |
| 5 | + |
| 6 | +from __future__ import annotations |
| 7 | + |
| 8 | +import logging |
| 9 | +import os |
| 10 | +import pathlib |
| 11 | +import threading |
| 12 | +from typing import cast |
| 13 | + |
| 14 | +from overrides import override |
| 15 | + |
| 16 | +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer |
| 17 | +from solidlsp.ls_config import LanguageServerConfig |
| 18 | +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams |
| 19 | +from solidlsp.settings import SolidLSPSettings |
| 20 | + |
| 21 | +from .common import RuntimeDependency, RuntimeDependencyCollection |
| 22 | + |
| 23 | +log = logging.getLogger(__name__) |
| 24 | + |
| 25 | +# How to refresh the pinned SHA256s when bumping DEFAULT_CUE_VERSION: |
| 26 | +# gh release view <tag> --repo cue-lang/cue --json assets \ |
| 27 | +# --jq '.assets[] | select(.name | test("(darwin|linux|windows)_(amd64|arm64)")) | {name, digest}' |
| 28 | +# The `digest` field is `sha256:<hex>` — copy the hex portion into DEFAULT_CUE_SHA256_BY_PLATFORM |
| 29 | +# keyed by the Serena PlatformId (osx-arm64, osx-x64, linux-arm64, linux-x64, win-x64, win-arm64). |
| 30 | +DEFAULT_CUE_VERSION = "v0.16.1" |
| 31 | +DEFAULT_CUE_SHA256_BY_PLATFORM = { |
| 32 | + "osx-arm64": "a72b0cddb377c52d1b003bed9a335d893b70cd75a182cd5e3fee8bae30ddb6d6", |
| 33 | + "osx-x64": "97b0d78e4c5ee49ff72145fd6ef4f4bab0bb332d55f29660de3fec2af5ec96a9", |
| 34 | + "linux-arm64": "3cc715a9e969f87b93c4fa34cfaef5388b93e96efa20b248e8ad6826abd25a83", |
| 35 | + "linux-x64": "5d644c1305a2b86504c8dcd2ec829cf5b4999efc2cf51ee375624e0455f774ae", |
| 36 | + "win-x64": "2f24123f458229fcf283db534bd86692ad1074da806defee0f0cc62976c0397c", |
| 37 | + "win-arm64": "e0c15ce53f73e8609b0e8ce6507298f3474b334ac5eb0c826c9497a811fd0cce", |
| 38 | +} |
| 39 | + |
| 40 | + |
| 41 | +def _cue_sha(version: str, platform_key: str) -> str | None: |
| 42 | + if version == DEFAULT_CUE_VERSION: |
| 43 | + return DEFAULT_CUE_SHA256_BY_PLATFORM.get(platform_key) |
| 44 | + return None |
| 45 | + |
| 46 | + |
| 47 | +CUE_ALLOWED_HOSTS = ( |
| 48 | + "github.com", |
| 49 | + "release-assets.githubusercontent.com", |
| 50 | + "objects.githubusercontent.com", |
| 51 | +) |
| 52 | + |
| 53 | + |
| 54 | +class CueLanguageServer(SolidLanguageServer): |
| 55 | + """ |
| 56 | + Provides a CUE-specific instantiation of the language server, driven by ``cue lsp`` from the |
| 57 | + official CUE CLI distribution. |
| 58 | +
|
| 59 | + Recognised entries in ``ls_specific_settings["cue"]``: |
| 60 | + - ``ls_path``: Absolute path to a pre-installed ``cue`` binary. Bypasses Serena's |
| 61 | + auto-download mechanism; useful when the user already has ``cue`` on ``$PATH`` |
| 62 | + (e.g. via ``brew install cue`` or ``go install``). |
| 63 | + - ``cue_version``: Override the pinned cue version downloaded by Serena |
| 64 | + (default: the bundled Serena version). Setting this to a version other than |
| 65 | + ``DEFAULT_CUE_VERSION`` skips SHA256 verification (checksums for arbitrary |
| 66 | + versions are unknown), so pair it with ``ls_path`` if integrity matters. |
| 67 | + """ |
| 68 | + |
| 69 | + CUE_ALLOWED_HOSTS = CUE_ALLOWED_HOSTS |
| 70 | + |
| 71 | + # Directories worth pruning for CUE projects. cue.mod/gen/ contains generated Go->CUE |
| 72 | + # bindings that shouldn't be traversed for symbolic operations; cue.mod/pkg/ holds |
| 73 | + # fetched module dependencies (analogous to node_modules / vendor). |
| 74 | + _IGNORED_DIRS = frozenset({"cue.mod/gen", "cue.mod/pkg"}) |
| 75 | + |
| 76 | + @override |
| 77 | + def is_ignored_dirname(self, dirname: str) -> bool: |
| 78 | + return super().is_ignored_dirname(dirname) or dirname in self._IGNORED_DIRS |
| 79 | + |
| 80 | + @classmethod |
| 81 | + def _runtime_dependencies(cls, version: str) -> RuntimeDependencyCollection: |
| 82 | + """Builds the platform-specific runtime dependency set for the given cue release. |
| 83 | +
|
| 84 | + :param version: the cue release tag (e.g. ``v0.16.1``); the leading ``v`` is stripped when |
| 85 | + constructing the archive filename, since cue releases embed the bare version there. |
| 86 | + """ |
| 87 | + version_no_v = version.lstrip("v") |
| 88 | + cue_releases = f"https://github.com/cue-lang/cue/releases/download/{version}" |
| 89 | + return RuntimeDependencyCollection( |
| 90 | + [ |
| 91 | + RuntimeDependency( |
| 92 | + id="cue", |
| 93 | + url=f"{cue_releases}/cue_v{version_no_v}_darwin_arm64.tar.gz", |
| 94 | + platform_id="osx-arm64", |
| 95 | + archive_type="gztar", |
| 96 | + binary_name="cue", |
| 97 | + sha256=_cue_sha(version, "osx-arm64"), |
| 98 | + allowed_hosts=CUE_ALLOWED_HOSTS, |
| 99 | + ), |
| 100 | + RuntimeDependency( |
| 101 | + id="cue", |
| 102 | + url=f"{cue_releases}/cue_v{version_no_v}_darwin_amd64.tar.gz", |
| 103 | + platform_id="osx-x64", |
| 104 | + archive_type="gztar", |
| 105 | + binary_name="cue", |
| 106 | + sha256=_cue_sha(version, "osx-x64"), |
| 107 | + allowed_hosts=CUE_ALLOWED_HOSTS, |
| 108 | + ), |
| 109 | + RuntimeDependency( |
| 110 | + id="cue", |
| 111 | + url=f"{cue_releases}/cue_v{version_no_v}_linux_arm64.tar.gz", |
| 112 | + platform_id="linux-arm64", |
| 113 | + archive_type="gztar", |
| 114 | + binary_name="cue", |
| 115 | + sha256=_cue_sha(version, "linux-arm64"), |
| 116 | + allowed_hosts=CUE_ALLOWED_HOSTS, |
| 117 | + ), |
| 118 | + RuntimeDependency( |
| 119 | + id="cue", |
| 120 | + url=f"{cue_releases}/cue_v{version_no_v}_linux_amd64.tar.gz", |
| 121 | + platform_id="linux-x64", |
| 122 | + archive_type="gztar", |
| 123 | + binary_name="cue", |
| 124 | + sha256=_cue_sha(version, "linux-x64"), |
| 125 | + allowed_hosts=CUE_ALLOWED_HOSTS, |
| 126 | + ), |
| 127 | + RuntimeDependency( |
| 128 | + id="cue", |
| 129 | + url=f"{cue_releases}/cue_v{version_no_v}_windows_amd64.zip", |
| 130 | + platform_id="win-x64", |
| 131 | + archive_type="zip", |
| 132 | + binary_name="cue.exe", |
| 133 | + sha256=_cue_sha(version, "win-x64"), |
| 134 | + allowed_hosts=CUE_ALLOWED_HOSTS, |
| 135 | + ), |
| 136 | + RuntimeDependency( |
| 137 | + id="cue", |
| 138 | + url=f"{cue_releases}/cue_v{version_no_v}_windows_arm64.zip", |
| 139 | + platform_id="win-arm64", |
| 140 | + archive_type="zip", |
| 141 | + binary_name="cue.exe", |
| 142 | + sha256=_cue_sha(version, "win-arm64"), |
| 143 | + allowed_hosts=CUE_ALLOWED_HOSTS, |
| 144 | + ), |
| 145 | + ] |
| 146 | + ) |
| 147 | + |
| 148 | + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): |
| 149 | + """Creates a CueLanguageServer instance. |
| 150 | +
|
| 151 | + Not meant to be instantiated directly — use :meth:`SolidLanguageServer.create` instead. |
| 152 | + """ |
| 153 | + super().__init__( |
| 154 | + config, |
| 155 | + repository_root_path, |
| 156 | + None, |
| 157 | + "cue", |
| 158 | + solidlsp_settings, |
| 159 | + ) |
| 160 | + self.server_ready = threading.Event() |
| 161 | + |
| 162 | + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: |
| 163 | + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) |
| 164 | + |
| 165 | + class DependencyProvider(LanguageServerDependencyProviderSinglePath): |
| 166 | + """Resolves a ``cue`` executable, downloading the pinned release if it isn't cached yet.""" |
| 167 | + |
| 168 | + def _get_or_install_core_dependency(self) -> str: |
| 169 | + cue_version = self._custom_settings.get("cue_version", DEFAULT_CUE_VERSION) |
| 170 | + deps = CueLanguageServer._runtime_dependencies(cue_version) |
| 171 | + dependency = deps.get_single_dep_for_current_platform() |
| 172 | + |
| 173 | + install_dir = os.path.join(self._ls_resources_dir, f"cue-{cue_version}") |
| 174 | + cue_executable_path = deps.binary_path(install_dir) |
| 175 | + if not os.path.exists(cue_executable_path): |
| 176 | + log.info(f"Downloading and extracting cue from {dependency.url} to {install_dir}") |
| 177 | + deps.install(install_dir) |
| 178 | + if not os.path.exists(cue_executable_path): |
| 179 | + raise FileNotFoundError(f"Download failed? Could not find cue executable at {cue_executable_path}") |
| 180 | + os.chmod(cue_executable_path, 0o755) |
| 181 | + return cue_executable_path |
| 182 | + |
| 183 | + def _create_launch_command(self, core_path: str) -> list[str]: |
| 184 | + # cue's LSP mode is activated via the `lsp` subcommand; it speaks LSP over stdio. |
| 185 | + return [core_path, "lsp"] |
| 186 | + |
| 187 | + def _get_initialize_params(self) -> InitializeParams: |
| 188 | + """Returns the init params for ``cue lsp``.""" |
| 189 | + repository_absolute_path = self.repository_root_path |
| 190 | + root_uri = pathlib.Path(repository_absolute_path).as_uri() |
| 191 | + |
| 192 | + result = { |
| 193 | + "processId": os.getpid(), |
| 194 | + "rootPath": repository_absolute_path, |
| 195 | + "rootUri": root_uri, |
| 196 | + "capabilities": { |
| 197 | + "workspace": { |
| 198 | + "applyEdit": True, |
| 199 | + "workspaceEdit": {"documentChanges": True}, |
| 200 | + "symbol": {"symbolKind": {"valueSet": list(range(1, 27))}}, |
| 201 | + "workspaceFolders": True, |
| 202 | + "configuration": True, |
| 203 | + }, |
| 204 | + "textDocument": { |
| 205 | + "synchronization": {"didSave": True, "dynamicRegistration": True}, |
| 206 | + "publishDiagnostics": {"relatedInformation": True, "tagSupport": {"valueSet": [1, 2]}}, |
| 207 | + "definition": {"linkSupport": True, "dynamicRegistration": True}, |
| 208 | + "references": {"dynamicRegistration": True}, |
| 209 | + "hover": {"contentFormat": ["markdown", "plaintext"], "dynamicRegistration": True}, |
| 210 | + "documentSymbol": { |
| 211 | + "dynamicRegistration": True, |
| 212 | + "hierarchicalDocumentSymbolSupport": True, |
| 213 | + "symbolKind": {"valueSet": list(range(1, 27))}, |
| 214 | + }, |
| 215 | + "completion": { |
| 216 | + "dynamicRegistration": True, |
| 217 | + "completionItem": {"snippetSupport": False, "documentationFormat": ["markdown", "plaintext"]}, |
| 218 | + }, |
| 219 | + }, |
| 220 | + "general": {"positionEncodings": ["utf-16"]}, |
| 221 | + }, |
| 222 | + "initializationOptions": {}, |
| 223 | + "trace": "off", |
| 224 | + "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}], |
| 225 | + } |
| 226 | + return cast(InitializeParams, result) |
| 227 | + |
| 228 | + def _start_server(self) -> None: |
| 229 | + def register_capability_handler(params: dict) -> None: |
| 230 | + return |
| 231 | + |
| 232 | + def workspace_configuration_handler(params: dict) -> list[dict]: |
| 233 | + # cue lsp asks the client for its configuration shortly after initialization |
| 234 | + # and will not start servicing requests until it gets a reply. We return an |
| 235 | + # empty object per requested item — cue's defaults are fine for our use. |
| 236 | + items = params.get("items", []) if isinstance(params, dict) else [] |
| 237 | + return [{} for _ in items] |
| 238 | + |
| 239 | + def window_log_message(msg: dict) -> None: |
| 240 | + log.info(f"LSP: window/logMessage: {msg}") |
| 241 | + |
| 242 | + def do_nothing(params: dict) -> None: |
| 243 | + return |
| 244 | + |
| 245 | + # wire up notification/request handlers before starting the process |
| 246 | + self.server.on_request("client/registerCapability", register_capability_handler) |
| 247 | + self.server.on_request("workspace/configuration", workspace_configuration_handler) |
| 248 | + self.server.on_notification("window/logMessage", window_log_message) |
| 249 | + self.server.on_notification("$/progress", do_nothing) |
| 250 | + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) |
| 251 | + |
| 252 | + log.info("Starting cue lsp server process") |
| 253 | + self.server.start() |
| 254 | + |
| 255 | + initialize_params = self._get_initialize_params() |
| 256 | + |
| 257 | + log.info("Sending initialize request from LSP client to LSP server and awaiting response") |
| 258 | + init_response = self.server.send.initialize(initialize_params) |
| 259 | + |
| 260 | + # sanity-check the server advertises the core capabilities we rely on |
| 261 | + capabilities = init_response["capabilities"] |
| 262 | + assert "textDocumentSync" in capabilities |
| 263 | + assert "definitionProvider" in capabilities |
| 264 | + assert "documentSymbolProvider" in capabilities |
| 265 | + assert "referencesProvider" in capabilities |
| 266 | + |
| 267 | + self.server.notify.initialized({}) |
| 268 | + # cue lsp is ready to serve immediately after the initialized notification |
| 269 | + self.server_ready.set() |
0 commit comments