mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 22:14:54 +00:00
Support nixd ls_path and config_path
This commit is contained in:
1 parent
61ca9ba5de
commit
d86fd6c0c8
4 files changed
+460
-146
No files matched your search
@@ -12,6 +12,7 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Allow language server priorities to be configured in `serena_config.yml` (for auto-detection during
|
||||
project creation)
|
||||
- Add `python_basedpyright` as an alternative Python language server
|
||||
- Nix/nixd: support custom `ls_path` launchers and external JSON settings through `config_path` #1737
|
||||
|
||||
* JetBrains:
|
||||
- `jet_brains_find_symbol`: Disallow wildcard-only search, delegating to overview tool if request is for file
|
||||
|
||||
@@ -247,7 +247,7 @@ ls_specific_settings:
|
||||
These settings are supported by all language servers whose dependency provider derives from
|
||||
`LanguageServerDependencyProviderBaseCommand`, and `ls_path` is additionally exposed by some implementations explicitly.
|
||||
Common examples include: `ansible`, `bash`, `bsl`, `clojure`, `cpp`, `cpp_ccls`, `hlsl`, `html`, `kotlin`, `lean4`, `luau`, `markdown`, `php`,
|
||||
`php_phpactor`, `python`, `rust`, `scss`, `solidity`, `systemverilog`, `toml`, `typescript`, and `yaml`.
|
||||
`nix`, `php_phpactor`, `python`, `rust`, `scss`, `solidity`, `systemverilog`, `toml`, `typescript`, and `yaml`.
|
||||
|
||||
If `ls_path` is set, Serena's managed download or install is bypassed for that language server.
|
||||
In that case, any server-specific version or registry settings do not apply.
|
||||
@@ -844,6 +844,44 @@ Supported settings:
|
||||
| `matlab_path` | auto-detected | Path to the MATLAB installation. This overrides `MATLAB_PATH` and auto-detection, but not Serena's managed extension download. |
|
||||
| `matlab_extension_version` | `1.3.9` | Override the MathWorks VS Code extension version Serena downloads. |
|
||||
|
||||
#### Nix
|
||||
|
||||
Serena uses [nixd](https://github.com/nix-community/nixd) for Nix support.
|
||||
|
||||
Supported settings:
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `ls_path` | PATH/common-path discovery followed by managed installation | Absolute path to a nixd executable or launcher. When set, Serena bypasses its nixd discovery, installation, and version check. |
|
||||
| `config_path` | `null` | Absolute path to a UTF-8 JSON file containing the value of the `nixd` settings section. A leading `~` is expanded. |
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
nix:
|
||||
ls_path: /absolute/path/to/nixd-project
|
||||
config_path: /absolute/path/to/nixd-settings.json
|
||||
```
|
||||
|
||||
The JSON document contains the settings object directly, without an outer `nixd` key:
|
||||
|
||||
```json
|
||||
{
|
||||
"formatting": {
|
||||
"command": ["alejandra"]
|
||||
},
|
||||
"nixpkgs": {
|
||||
"expr": "import <nixpkgs> { }"
|
||||
},
|
||||
"options": {}
|
||||
}
|
||||
```
|
||||
|
||||
Serena loads this file once when creating the language server, uses it as nixd's `initializationOptions`, and serves the same effective
|
||||
settings through LSP `workspace/configuration` requests. Existing `initializationOptions` configured under `ls_specific_settings.nix`
|
||||
remain top-level overrides and are reflected in both paths. Restart Serena after changing the JSON file.
|
||||
|
||||
|
||||
#### Pascal (`pasls`)
|
||||
|
||||
|
||||
@@ -5,19 +5,23 @@ Provides Nix specific instantiation of the LanguageServer class using nixd (Nix
|
||||
Note: Windows is not supported as Nix itself doesn't support Windows natively.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.dependency_provider import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath
|
||||
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
from solidlsp.util.subprocess_util import subprocess_run
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,6 +31,118 @@ class NixLanguageServer(SolidLanguageServer):
|
||||
Provides Nix specific instantiation of the LanguageServer class using nixd.
|
||||
"""
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
"""Provides the nixd launch command and managed dependency fallback."""
|
||||
|
||||
@staticmethod
|
||||
def _get_nixd_path() -> str | None:
|
||||
"""Return an existing nixd executable path, if one can be found."""
|
||||
nixd_path = shutil.which("nixd")
|
||||
if nixd_path:
|
||||
return nixd_path
|
||||
|
||||
home = Path.home()
|
||||
possible_paths = [
|
||||
home / ".local" / "bin" / "nixd",
|
||||
home / ".serena" / "language_servers" / "nixd" / "nixd",
|
||||
home / ".nix-profile" / "bin" / "nixd",
|
||||
Path("/usr/local/bin/nixd"),
|
||||
Path("/run/current-system/sw/bin/nixd"),
|
||||
Path("/opt/homebrew/bin/nixd"),
|
||||
Path("/usr/local/opt/nixd/bin/nixd"),
|
||||
]
|
||||
|
||||
if platform.system() == "Windows":
|
||||
possible_paths.extend(
|
||||
[
|
||||
home / "AppData" / "Local" / "nixd" / "nixd.exe",
|
||||
home / ".serena" / "language_servers" / "nixd" / "nixd.exe",
|
||||
]
|
||||
)
|
||||
|
||||
for path in possible_paths:
|
||||
if path.exists():
|
||||
return str(path)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _install_nixd_with_nix() -> str | None:
|
||||
"""Install nixd through Nix and return the resulting executable path."""
|
||||
if not shutil.which("nix"):
|
||||
return None
|
||||
|
||||
log.info("Installing nixd using nix... This may take a few minutes.")
|
||||
try:
|
||||
result = subprocess_run(
|
||||
["nix", "profile", "install", "github:nix-community/nixd"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=600,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
nixd_path = shutil.which("nixd")
|
||||
if nixd_path:
|
||||
log.info("Successfully installed nixd at: %s", nixd_path)
|
||||
return nixd_path
|
||||
else:
|
||||
result = subprocess_run(
|
||||
["nix-env", "-iA", "nixpkgs.nixd"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=600,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
nixd_path = shutil.which("nixd")
|
||||
if nixd_path:
|
||||
log.info("Successfully installed nixd at: %s", nixd_path)
|
||||
return nixd_path
|
||||
log.error("Failed to install nixd: %s", result.stderr)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
log.error("Nix install timed out after 10 minutes")
|
||||
except Exception:
|
||||
log.exception("Error installing nixd with nix")
|
||||
|
||||
return None
|
||||
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
"""Return a working nixd path, installing nixd when necessary."""
|
||||
if not shutil.which("nix"):
|
||||
log.error("Nix is not installed. nixd requires Nix to function properly.")
|
||||
raise RuntimeError("Nix is required for nixd. Please install Nix from https://nixos.org/download.html")
|
||||
|
||||
nixd_path = self._get_nixd_path()
|
||||
if not nixd_path:
|
||||
log.info("nixd not found. Attempting to install...")
|
||||
nixd_path = self._install_nixd_with_nix()
|
||||
|
||||
if not nixd_path:
|
||||
raise RuntimeError(
|
||||
"nixd (Nix Language Server) is not installed.\n"
|
||||
"Please install nixd using one of the following methods:\n"
|
||||
" - Using Nix flakes: nix profile install github:nix-community/nixd\n"
|
||||
" - From nixpkgs: nix-env -iA nixpkgs.nixd\n"
|
||||
" - On macOS with Homebrew: brew install nixd\n\n"
|
||||
"After installation, make sure 'nixd' is in your PATH."
|
||||
)
|
||||
|
||||
try:
|
||||
result = subprocess_run([nixd_path, "--version"], capture_output=True, text=True, check=False, timeout=5)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"nixd failed to run: {result.stderr}")
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"Failed to verify nixd installation: {exc}") from exc
|
||||
|
||||
return nixd_path
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
"""Return the nixd stdio launch command."""
|
||||
return [core_path]
|
||||
|
||||
def _extend_nix_symbol_range_to_include_semicolon(
|
||||
self, symbol: ls_types.UnifiedSymbolInformation, file_content: str
|
||||
) -> ls_types.UnifiedSymbolInformation:
|
||||
@@ -104,149 +220,94 @@ class NixLanguageServer(SolidLanguageServer):
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["result", ".direnv"] or dirname.startswith("result-")
|
||||
|
||||
@staticmethod
|
||||
def _get_nixd_version():
|
||||
"""Get the installed nixd version or None if not found."""
|
||||
def _create_default_nixd_settings() -> dict[str, Any]:
|
||||
"""Return the default settings sent to nixd."""
|
||||
return {
|
||||
"nixpkgs": {"expr": "import <nixpkgs> { }"},
|
||||
"formatting": {"command": ["nixpkgs-fmt"]},
|
||||
"options": {
|
||||
"enable": True,
|
||||
"target": {
|
||||
"installable": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _load_nixd_settings(cls, custom_settings: SolidLSPSettings.CustomLSSettings) -> dict[str, Any]:
|
||||
"""Load nixd settings from ``config_path`` or return the built-in defaults.
|
||||
|
||||
:param custom_settings: Nix-specific language-server settings.
|
||||
:return: The value of the nixd configuration section.
|
||||
:raises ValueError: If ``config_path`` or its JSON document has an invalid shape.
|
||||
:raises RuntimeError: If the configuration file cannot be read.
|
||||
"""
|
||||
config_path_value = custom_settings.get("config_path")
|
||||
if config_path_value is None:
|
||||
return cls._create_default_nixd_settings()
|
||||
if not isinstance(config_path_value, str) or not config_path_value.strip():
|
||||
raise ValueError("ls_specific_settings.nix.config_path must be a non-empty absolute path")
|
||||
|
||||
config_path = Path(config_path_value).expanduser()
|
||||
if not config_path.is_absolute():
|
||||
raise ValueError(f"ls_specific_settings.nix.config_path must be absolute: {config_path_value!r}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(["nixd", "--version"], capture_output=True, text=True, check=False)
|
||||
if result.returncode == 0:
|
||||
# nixd outputs version like: nixd 2.0.0
|
||||
return result.stdout.strip()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
return None
|
||||
with config_path.open(encoding="utf-8") as config_file:
|
||||
settings = json.load(config_file)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(
|
||||
f"Invalid JSON in nixd configuration file '{config_path}': {exc.msg} (line {exc.lineno}, column {exc.colno})"
|
||||
) from exc
|
||||
except OSError as exc:
|
||||
raise RuntimeError(f"Failed to read nixd configuration file '{config_path}': {exc}") from exc
|
||||
|
||||
if not isinstance(settings, dict):
|
||||
raise ValueError(
|
||||
f"Invalid nixd configuration file '{config_path}': expected a JSON object containing the value of the 'nixd' section"
|
||||
)
|
||||
return settings
|
||||
|
||||
@staticmethod
|
||||
def _check_nixd_installed():
|
||||
"""Check if nixd is installed in the system."""
|
||||
return shutil.which("nixd") is not None
|
||||
def _resolve_nixd_configuration_section(settings: dict[str, Any], section: object) -> Any:
|
||||
"""Resolve a ``nixd`` configuration section from the effective settings."""
|
||||
if section == "nixd":
|
||||
return deepcopy(settings)
|
||||
if not isinstance(section, str) or not section.startswith("nixd."):
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def _get_nixd_path():
|
||||
"""Get the path to nixd executable."""
|
||||
# First check if it's in PATH
|
||||
nixd_path = shutil.which("nixd")
|
||||
if nixd_path:
|
||||
return nixd_path
|
||||
value: Any = settings
|
||||
for key in section.removeprefix("nixd.").split("."):
|
||||
if not key or not isinstance(value, dict) or key not in value:
|
||||
return {}
|
||||
value = value[key]
|
||||
return deepcopy(value)
|
||||
|
||||
# Check common installation locations
|
||||
home = Path.home()
|
||||
possible_paths = [
|
||||
home / ".local" / "bin" / "nixd",
|
||||
home / ".serena" / "language_servers" / "nixd" / "nixd",
|
||||
home / ".nix-profile" / "bin" / "nixd",
|
||||
Path("/usr/local/bin/nixd"),
|
||||
Path("/run/current-system/sw/bin/nixd"), # NixOS system profile
|
||||
Path("/opt/homebrew/bin/nixd"), # Homebrew on Apple Silicon
|
||||
Path("/usr/local/opt/nixd/bin/nixd"), # Homebrew on Intel Mac
|
||||
@classmethod
|
||||
def _get_workspace_configuration(cls, params: object, settings: dict[str, Any]) -> list[Any]:
|
||||
"""Return configuration values matching each requested item in order."""
|
||||
if not isinstance(params, dict):
|
||||
return []
|
||||
|
||||
items = params.get("items", [])
|
||||
if not isinstance(items, list):
|
||||
return []
|
||||
|
||||
return [
|
||||
cls._resolve_nixd_configuration_section(settings, item.get("section") if isinstance(item, dict) else None) for item in items
|
||||
]
|
||||
|
||||
# Add Windows-specific paths
|
||||
if platform.system() == "Windows":
|
||||
possible_paths.extend(
|
||||
[
|
||||
home / "AppData" / "Local" / "nixd" / "nixd.exe",
|
||||
home / ".serena" / "language_servers" / "nixd" / "nixd.exe",
|
||||
]
|
||||
)
|
||||
|
||||
for path in possible_paths:
|
||||
if path.exists():
|
||||
return str(path)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _install_nixd_with_nix():
|
||||
"""Install nixd using nix if available."""
|
||||
# Check if nix is available
|
||||
if not shutil.which("nix"):
|
||||
return None
|
||||
|
||||
print("Installing nixd using nix... This may take a few minutes.")
|
||||
try:
|
||||
# Try to install nixd using nix profile
|
||||
result = subprocess.run(
|
||||
["nix", "profile", "install", "github:nix-community/nixd"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=600, # 10 minute timeout for building
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Check if nixd is now in PATH
|
||||
nixd_path = shutil.which("nixd")
|
||||
if nixd_path:
|
||||
print(f"Successfully installed nixd at: {nixd_path}")
|
||||
return nixd_path
|
||||
else:
|
||||
# Try nix-env as fallback
|
||||
result = subprocess.run(
|
||||
["nix-env", "-iA", "nixpkgs.nixd"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=600,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
nixd_path = shutil.which("nixd")
|
||||
if nixd_path:
|
||||
print(f"Successfully installed nixd at: {nixd_path}")
|
||||
return nixd_path
|
||||
print(f"Failed to install nixd: {result.stderr}")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print("Nix install timed out after 10 minutes")
|
||||
except Exception as e:
|
||||
print(f"Error installing nixd with nix: {e}")
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _setup_runtime_dependency():
|
||||
"""
|
||||
Check if required Nix runtime dependencies are available.
|
||||
Attempts to install nixd if not present.
|
||||
"""
|
||||
# First check if Nix is available (nixd needs it at runtime)
|
||||
if not shutil.which("nix"):
|
||||
print("WARNING: Nix is not installed. nixd requires Nix to function properly.")
|
||||
raise RuntimeError("Nix is required for nixd. Please install Nix from https://nixos.org/download.html")
|
||||
|
||||
nixd_path = NixLanguageServer._get_nixd_path()
|
||||
|
||||
if not nixd_path:
|
||||
print("nixd not found. Attempting to install...")
|
||||
|
||||
# Try to install with nix if available
|
||||
nixd_path = NixLanguageServer._install_nixd_with_nix()
|
||||
|
||||
if not nixd_path:
|
||||
raise RuntimeError(
|
||||
"nixd (Nix Language Server) is not installed.\n"
|
||||
"Please install nixd using one of the following methods:\n"
|
||||
" - Using Nix flakes: nix profile install github:nix-community/nixd\n"
|
||||
" - From nixpkgs: nix-env -iA nixpkgs.nixd\n"
|
||||
" - On macOS with Homebrew: brew install nixd\n\n"
|
||||
"After installation, make sure 'nixd' is in your PATH."
|
||||
)
|
||||
|
||||
# Verify nixd works
|
||||
try:
|
||||
result = subprocess.run([nixd_path, "--version"], capture_output=True, text=True, check=False, timeout=5)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"nixd failed to run: {result.stderr}")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to verify nixd installation: {e}")
|
||||
|
||||
return nixd_path
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
nixd_path = self._setup_runtime_dependency()
|
||||
custom_settings = solidlsp_settings.get_ls_specific_settings(config.ls_id)
|
||||
self._nixd_settings = self._load_nixd_settings(custom_settings)
|
||||
|
||||
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=nixd_path, cwd=repository_root_path), "nix", solidlsp_settings)
|
||||
super().__init__(config, repository_root_path, None, "nix", solidlsp_settings)
|
||||
self.request_id = 0
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
"""Create the provider that resolves the nixd launch command."""
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
|
||||
def _create_base_initialize_params(self) -> dict:
|
||||
"""
|
||||
Returns the initialize params for nixd.
|
||||
@@ -313,26 +374,23 @@ class NixLanguageServer(SolidLanguageServer):
|
||||
},
|
||||
},
|
||||
},
|
||||
"initializationOptions": {
|
||||
# nixd specific options
|
||||
"nixpkgs": {"expr": "import <nixpkgs> { }"},
|
||||
"formatting": {"command": ["nixpkgs-fmt"]}, # or ["alejandra"] or ["nixfmt"]
|
||||
"options": {
|
||||
"enable": True,
|
||||
"target": {
|
||||
"installable": "", # Will be auto-detected from flake.nix if present
|
||||
},
|
||||
},
|
||||
},
|
||||
"initializationOptions": deepcopy(self._nixd_settings),
|
||||
}
|
||||
return initialize_params
|
||||
|
||||
def _start_server(self):
|
||||
"""Start nixd server process"""
|
||||
initialize_params = self._create_initialize_params()
|
||||
nixd_settings = initialize_params.get("initializationOptions", {})
|
||||
if not isinstance(nixd_settings, dict):
|
||||
nixd_settings = {}
|
||||
|
||||
def register_capability_handler(params):
|
||||
return
|
||||
|
||||
def workspace_configuration_handler(params):
|
||||
return self._get_workspace_configuration(params, nixd_settings)
|
||||
|
||||
def window_log_message(msg):
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
|
||||
@@ -340,13 +398,13 @@ class NixLanguageServer(SolidLanguageServer):
|
||||
return
|
||||
|
||||
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 nixd server process")
|
||||
self.server.start()
|
||||
initialize_params = self._create_initialize_params()
|
||||
|
||||
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
"""Unit tests for nixd launch and configuration handling."""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.nixd_ls import NixLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
def _make_provider(
|
||||
tmp_path: Path,
|
||||
custom_settings: dict[str, Any] | None = None,
|
||||
) -> NixLanguageServer.DependencyProvider:
|
||||
return NixLanguageServer.DependencyProvider(
|
||||
SolidLSPSettings.CustomLSSettings(custom_settings or {}),
|
||||
str(tmp_path),
|
||||
)
|
||||
|
||||
|
||||
def _make_server(
|
||||
tmp_path: Path,
|
||||
custom_settings: dict[str, Any] | None = None,
|
||||
) -> NixLanguageServer:
|
||||
settings = SolidLSPSettings(
|
||||
solidlsp_dir=str(tmp_path / "global"),
|
||||
project_data_path=str(tmp_path / "project"),
|
||||
ls_specific_settings={LanguageServerId.NIX: custom_settings or {}},
|
||||
)
|
||||
server_interface = Mock()
|
||||
with patch.object(NixLanguageServer, "_create_language_server_interface", return_value=server_interface):
|
||||
return NixLanguageServer(
|
||||
LanguageServerConfig(ls_id=LanguageServerId.NIX),
|
||||
str(tmp_path),
|
||||
settings,
|
||||
)
|
||||
|
||||
|
||||
def test_ls_path_bypasses_managed_dependency_resolution(tmp_path: Path) -> None:
|
||||
provider = _make_provider(tmp_path, {"ls_path": "/custom/nixd-project"})
|
||||
|
||||
with patch.object(
|
||||
provider,
|
||||
"_get_or_install_core_dependency",
|
||||
side_effect=AssertionError("managed dependency resolution must not run when ls_path is configured"),
|
||||
) as managed_resolution:
|
||||
assert provider.create_launch_command() == ["/custom/nixd-project"]
|
||||
|
||||
managed_resolution.assert_not_called()
|
||||
|
||||
|
||||
def test_default_launch_command_uses_managed_dependency_resolution(tmp_path: Path) -> None:
|
||||
provider = _make_provider(tmp_path)
|
||||
|
||||
with patch.object(provider, "_get_or_install_core_dependency", return_value="/usr/bin/nixd") as managed_resolution:
|
||||
assert provider.create_launch_command() == ["/usr/bin/nixd"]
|
||||
|
||||
managed_resolution.assert_called_once_with()
|
||||
|
||||
|
||||
def test_default_nixd_settings_preserve_existing_behavior() -> None:
|
||||
assert NixLanguageServer._load_nixd_settings(SolidLSPSettings.CustomLSSettings({})) == {
|
||||
"nixpkgs": {"expr": "import <nixpkgs> { }"},
|
||||
"formatting": {"command": ["nixpkgs-fmt"]},
|
||||
"options": {
|
||||
"enable": True,
|
||||
"target": {"installable": ""},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_config_path_loads_bare_nixd_settings_object(tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "nixd-settings.json"
|
||||
expected = {
|
||||
"formatting": {"command": ["alejandra"]},
|
||||
"options": {"nixos": {"expr": "flake.nixosConfigurations.host.options"}},
|
||||
}
|
||||
config_path.write_text(json.dumps(expected), encoding="utf-8")
|
||||
|
||||
settings = SolidLSPSettings.CustomLSSettings({"config_path": str(config_path)})
|
||||
|
||||
assert NixLanguageServer._load_nixd_settings(settings) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config_path", ["", "relative/nixd-settings.json"])
|
||||
def test_config_path_must_be_non_empty_and_absolute(config_path: str) -> None:
|
||||
settings = SolidLSPSettings.CustomLSSettings({"config_path": config_path})
|
||||
|
||||
with pytest.raises(ValueError, match="config_path must be"):
|
||||
NixLanguageServer._load_nixd_settings(settings)
|
||||
|
||||
|
||||
def test_missing_config_path_reports_file(tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "missing.json"
|
||||
settings = SolidLSPSettings.CustomLSSettings({"config_path": str(config_path)})
|
||||
|
||||
with pytest.raises(RuntimeError, match=re.escape(f"Failed to read nixd configuration file '{config_path}'")):
|
||||
NixLanguageServer._load_nixd_settings(settings)
|
||||
|
||||
|
||||
def test_malformed_config_reports_json_location(tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "malformed.json"
|
||||
config_path.write_text('{"formatting":', encoding="utf-8")
|
||||
settings = SolidLSPSettings.CustomLSSettings({"config_path": str(config_path)})
|
||||
|
||||
with pytest.raises(ValueError, match=r"Invalid JSON.*line 1, column 15"):
|
||||
NixLanguageServer._load_nixd_settings(settings)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("document", [[], None, "nixd"])
|
||||
def test_config_document_must_be_an_object(tmp_path: Path, document: object) -> None:
|
||||
config_path = tmp_path / "nixd-settings.json"
|
||||
config_path.write_text(json.dumps(document), encoding="utf-8")
|
||||
settings = SolidLSPSettings.CustomLSSettings({"config_path": str(config_path)})
|
||||
|
||||
with pytest.raises(ValueError, match="expected a JSON object"):
|
||||
NixLanguageServer._load_nixd_settings(settings)
|
||||
|
||||
|
||||
def test_workspace_configuration_preserves_order_and_resolves_nested_sections() -> None:
|
||||
settings = {
|
||||
"formatting": {"command": ["alejandra"]},
|
||||
"options": {"nixos": {"expr": "flake.nixosConfigurations.host.options"}},
|
||||
}
|
||||
params = {
|
||||
"items": [
|
||||
{"section": "nixd.options.nixos"},
|
||||
{"section": "editor"},
|
||||
{"section": "nixd.formatting"},
|
||||
{"section": "nixd"},
|
||||
{"scopeUri": "file:///workspace"},
|
||||
"invalid-item",
|
||||
]
|
||||
}
|
||||
|
||||
assert NixLanguageServer._get_workspace_configuration(params, settings) == [
|
||||
{"expr": "flake.nixosConfigurations.host.options"},
|
||||
{},
|
||||
{"command": ["alejandra"]},
|
||||
settings,
|
||||
{},
|
||||
{},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("params", [None, [], {"items": "invalid"}])
|
||||
def test_workspace_configuration_rejects_malformed_params(params: object) -> None:
|
||||
assert NixLanguageServer._get_workspace_configuration(params, {}) == []
|
||||
|
||||
|
||||
def test_initialization_and_workspace_configuration_share_effective_settings(tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "nixd-settings.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"formatting": {"command": ["nixpkgs-fmt"]},
|
||||
"options": {"nixos": {"expr": "flake.nixosConfigurations.host.options"}},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
server = _make_server(
|
||||
tmp_path,
|
||||
{
|
||||
"config_path": str(config_path),
|
||||
"initializationOptions": {
|
||||
"formatting": {"command": ["alejandra"]},
|
||||
"diagnostic": {"suppress": ["sema-extra-with"]},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
initialize_params = server._create_initialize_params()
|
||||
effective_settings = cast(dict[str, Any], initialize_params.get("initializationOptions"))
|
||||
|
||||
assert effective_settings == {
|
||||
"formatting": {"command": ["alejandra"]},
|
||||
"options": {"nixos": {"expr": "flake.nixosConfigurations.host.options"}},
|
||||
"diagnostic": {"suppress": ["sema-extra-with"]},
|
||||
}
|
||||
assert server._get_workspace_configuration({"items": [{"section": "nixd"}]}, effective_settings) == [effective_settings]
|
||||
|
||||
|
||||
def test_start_registers_configuration_handler_before_start_and_initialize(tmp_path: Path) -> None:
|
||||
server = _make_server(tmp_path)
|
||||
events: list[str] = []
|
||||
request_handlers: dict[str, Any] = {}
|
||||
|
||||
def on_request(method: str, handler: Any) -> None:
|
||||
events.append(f"request:{method}")
|
||||
request_handlers[method] = handler
|
||||
|
||||
def initialize(_params: object) -> dict[str, Any]:
|
||||
events.append("initialize")
|
||||
return {
|
||||
"capabilities": {
|
||||
"textDocumentSync": 1,
|
||||
"definitionProvider": True,
|
||||
"documentSymbolProvider": True,
|
||||
"referencesProvider": True,
|
||||
}
|
||||
}
|
||||
|
||||
language_server_interface = cast(Any, server.server)
|
||||
language_server_interface.on_request.side_effect = on_request
|
||||
language_server_interface.start.side_effect = lambda: events.append("start")
|
||||
language_server_interface.send.initialize.side_effect = initialize
|
||||
|
||||
server._start_server()
|
||||
|
||||
assert events.index("request:workspace/configuration") < events.index("start") < events.index("initialize")
|
||||
assert request_handlers["workspace/configuration"]({"items": [{"section": "nixd.formatting"}]}) == [{"command": ["nixpkgs-fmt"]}]
|
||||
Reference in new issue
Block a user