diff --git a/CHANGELOG.md b/CHANGELOG.md index ed376740..10688d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index adee35eb..65b5e6cc 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -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 { }" + }, + "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`) diff --git a/src/solidlsp/language_servers/nixd_ls.py b/src/solidlsp/language_servers/nixd_ls.py index eacd6364..430cf46a 100644 --- a/src/solidlsp/language_servers/nixd_ls.py +++ b/src/solidlsp/language_servers/nixd_ls.py @@ -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 { }"}, + "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 { }"}, - "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) diff --git a/test/solidlsp/nix/test_nix_server.py b/test/solidlsp/nix/test_nix_server.py new file mode 100644 index 00000000..06a216e4 --- /dev/null +++ b/test/solidlsp/nix/test_nix_server.py @@ -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 { }"}, + "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"]}]