diff --git a/CHANGELOG.md b/CHANGELOG.md index f4b43d42..cdd4735e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ Status of the `main` branch. Changes prior to the next official version change w Serena's own tools to close the gap (#1852) * Language Servers: + - Fix: TypeScript and VTS now disable automatic type acquisition as intended, while VTS + preserves explicit user settings across initialization and configuration requests (#1989) + VTS initialization options now override defaults per top-level key rather than replacing the + entire configuration; a user-provided `typescript` block replaces the ATA default too. + `initializationOptions` takes precedence over the legacy `initialization_options` alias. - Add FreeBSD mapping to platform detection - Remove unnecessary platform checks from the following language servers, expanding the set of supported platforms accordingly: Elixir Tools, Intelephense, Perl, TypeScript, VTS diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index 551bc450..258cd65f 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -339,9 +339,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): # slow, network-dependent, and nondeterministic (and can hang on offline/locked-down # machines). Serena relies on the types already installed in the project instead. "initializationOptions": { - "preferences": { - "disableAutomaticTypingAcquisition": True, - }, + "disableAutomaticTypingAcquisition": True, }, "capabilities": { "textDocument": { diff --git a/src/solidlsp/language_servers/vts_language_server.py b/src/solidlsp/language_servers/vts_language_server.py index cd2a2c40..7dd3dfde 100644 --- a/src/solidlsp/language_servers/vts_language_server.py +++ b/src/solidlsp/language_servers/vts_language_server.py @@ -8,6 +8,7 @@ import logging import os import shutil import threading +from typing import cast from overrides import override @@ -34,16 +35,18 @@ class VtsLanguageServer(SolidLanguageServer): Supported entries in ``ls_specific_settings["typescript_vts"]``: - ``vtsls_version``: version of ``@vtsls/language-server`` to install (default: ``"0.2.9"``). - ``npm_registry``: custom npm registry for the managed install. - - ``initialization_options``: optional dict forwarded verbatim as LSP - ``initializationOptions``. Useful for Yarn PnP projects, e.g.:: + - ``initializationOptions``: optional LSP configuration, overriding defaults per top-level key. + Automatic type acquisition is disabled by default, but a user-provided ``typescript`` block + replaces that entire default block. Useful for Yarn PnP projects, e.g.:: - initialization_options: + initializationOptions: typescript: tsdk: "project/.yarn/sdks/typescript/lib" vtsls: autoUseWorkspaceTsdk: true See https://github.com/yioneko/vtsls/issues/169 for the PnP recipe. + - ``initialization_options``: legacy alias, used only when ``initializationOptions`` is absent. """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): @@ -109,35 +112,18 @@ class VtsLanguageServer(SolidLanguageServer): assert os.path.exists(vts_executable_path), "vtsls executable not found. Please install @vtsls/language-server and try again." return f"{vts_executable_path} --stdio" - @property - def _initialization_options(self) -> dict: - """ - Validated user-provided ``initializationOptions``. - - :raises ValueError: if ``ls_specific_settings.typescript_vts.initialization_options`` - is set to a value that is not a dict. - """ - opts = self._custom_settings.get("initialization_options") - if opts is None: - return {} - if not isinstance(opts, dict): - raise ValueError(f"ls_specific_settings.typescript_vts.initialization_options must be a dict, got {type(opts).__name__}") - return opts - def _create_base_initialize_params(self) -> dict: """ Returns the initialize params for the VTS Language Server. - - If ``initialization_options`` is set in ``ls_specific_settings["typescript_vts"]``, - it is forwarded verbatim as LSP ``initializationOptions``. """ + # normalize the legacy key for the shared builder without changing caller-owned settings + settings = self._custom_settings.settings + if "initialization_options" in settings and "initializationOptions" not in settings: + self._custom_settings.settings = {**settings, "initializationOptions": settings["initialization_options"]} + initialize_params: dict = { "locale": "en", - "initializationOptions": { - "preferences": { - "disableAutomaticTypingAcquisition": True, - }, - }, + "initializationOptions": {"typescript": {"disableAutomaticTypeAcquisition": True}}, "capabilities": { "textDocument": { "synchronization": {"didSave": True, "dynamicRegistration": True}, @@ -161,10 +147,6 @@ class VtsLanguageServer(SolidLanguageServer): }, } - if self._initialization_options: - log.info("Forwarding user-provided initializationOptions to vtsls: %s", self._initialization_options) - initialize_params["initializationOptions"] = self._initialization_options - return initialize_params def _start_server(self) -> None: @@ -192,12 +174,13 @@ class VtsLanguageServer(SolidLanguageServer): def execute_client_command_handler(params: dict) -> list: return [] - init_options = self._initialization_options + # share the final configuration across initialization, requests and notifications + initialize_params = self._create_initialize_params() + init_options = cast(dict, initialize_params["initializationOptions"]) def workspace_configuration_handler(params: dict) -> list[object]: - # vtsls pulls settings for sections like "typescript", "vtsls", "javascript". - # Return the matching sub-dicts from the user-provided initialization_options. - return [init_options.get(item.get("section", ""), {}) for item in params["items"]] + # vtsls requests the whole configuration (empty section) during startup + return [init_options.get(item["section"], {}) if item.get("section") else init_options for item in params["items"]] def do_nothing(params: dict) -> None: return @@ -222,7 +205,6 @@ class VtsLanguageServer(SolidLanguageServer): log.info("Starting VTS 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) @@ -243,7 +225,7 @@ class VtsLanguageServer(SolidLanguageServer): # vtsls also reads settings via workspace/didChangeConfiguration (in addition # to initializationOptions and workspace/configuration pulls). Push the same - # user-provided settings on all three channels for maximum compatibility, + # effective settings on all three channels for maximum compatibility, # e.g. so that `typescript.tsdk` is honoured for Yarn PnP projects. if init_options: self.server.notify.workspace_did_change_configuration({"settings": init_options}) diff --git a/test/solidlsp/typescript/test_typescript_automatic_type_acquisition.py b/test/solidlsp/typescript/test_typescript_automatic_type_acquisition.py new file mode 100644 index 00000000..b5b4fd08 --- /dev/null +++ b/test/solidlsp/typescript/test_typescript_automatic_type_acquisition.py @@ -0,0 +1,113 @@ +"""Automatic type acquisition must be disabled on the semantic TypeScript worker.""" + +import copy +import os +import threading +from pathlib import Path +from unittest.mock import Mock + +import psutil +import pytest + +from solidlsp.language_servers.vts_language_server import VtsLanguageServer +from solidlsp.ls_config import LanguageServerConfig, LanguageServerId +from solidlsp.settings import SolidLSPSettings +from test.conftest import get_repo_path, start_ls_context + + +@pytest.mark.typescript +@pytest.mark.parametrize("backend", [LanguageServerId.TYPESCRIPT, LanguageServerId.TYPESCRIPT_VTS]) +def test_default_disables_type_acquisition_in_running_server(backend: LanguageServerId) -> None: + parent = psutil.Process(os.getpid()) + existing_pids = {child.pid for child in parent.children(recursive=True)} + + # VTS uses the same fixture project, but has no default repository alias. + with start_ls_context(backend, repo_path=str(get_repo_path(LanguageServerId.TYPESCRIPT))) as ls: + symbols = ls.request_document_symbols("index.ts").get_all_symbols_and_roots()[0] + assert any(symbol["name"] == "DemoClass" for symbol in symbols) + + semantic_workers = [] + typings_installers = [] + for child in parent.children(recursive=True): + if child.pid in existing_pids: + continue + try: + command = child.cmdline() + except psutil.NoSuchProcess: + continue + filenames = {Path(arg).name for arg in command} + if "typingsInstaller.js" in filenames: + typings_installers.append(command) + if "tsserver.js" in filenames and "partialSemantic" not in command and "--syntaxOnly" not in command: + semantic_workers.append(command) + + # The syntax worker disables ATA independently, even on the broken baseline. + assert semantic_workers, "No semantic tsserver was observed after a successful symbol request" + assert all("--disableAutomaticTypingAcquisition" in command for command in semantic_workers), semantic_workers + assert not typings_installers, typings_installers + + +@pytest.mark.parametrize( + ("custom_settings", "expected"), + [ + ({}, {"typescript": {"disableAutomaticTypeAcquisition": True}}), + ( + {"initialization_options": {"typescript": {"tsdk": "workspace/typescript/lib"}, "vtsls": {"autoUseWorkspaceTsdk": True}}}, + { + "typescript": {"tsdk": "workspace/typescript/lib"}, + "vtsls": {"autoUseWorkspaceTsdk": True}, + }, + ), + ( + {"initialization_options": {"typescript": {"disableAutomaticTypeAcquisition": False}}}, + {"typescript": {"disableAutomaticTypeAcquisition": False}}, + ), + ( + {"initializationOptions": {"typescript": {"tsdk": "workspace/typescript/lib"}}}, + {"typescript": {"tsdk": "workspace/typescript/lib"}}, + ), + ( + {"initialization_options": {"vtsls": {"autoUseWorkspaceTsdk": True}}}, + {"typescript": {"disableAutomaticTypeAcquisition": True}, "vtsls": {"autoUseWorkspaceTsdk": True}}, + ), + ( + { + "initialization_options": { + "typescript": {"disableAutomaticTypeAcquisition": False}, + "vtsls": {"autoUseWorkspaceTsdk": True}, + }, + "initializationOptions": {"typescript": {"tsdk": "workspace/typescript/lib"}}, + }, + {"typescript": {"tsdk": "workspace/typescript/lib"}}, + ), + ], +) +def test_vts_sends_consistent_effective_configuration(tmp_path: Path, custom_settings: dict, expected: dict) -> None: + original_settings = copy.deepcopy(custom_settings) + + # Use the real startup/configuration path with only the remote LSP transport replaced. + server = object.__new__(VtsLanguageServer) + server._custom_settings = SolidLSPSettings.CustomLSSettings(custom_settings) + server.repository_root_path = str(tmp_path) + server.config = LanguageServerConfig(ls_id=LanguageServerId.TYPESCRIPT_VTS) + server.server_ready = threading.Event() + server.server_ready.set() + server.initialize_searcher_command_available = threading.Event() + transport = Mock() + transport.send.initialize.return_value = {"capabilities": {"textDocumentSync": 1, "completionProvider": {}}} + handlers = {} + transport.on_request.side_effect = handlers.__setitem__ + server.server = transport + + server._start_server() + + assert transport.send.initialize.call_args.args[0]["initializationOptions"] == expected + configuration = handlers["workspace/configuration"] + assert configuration({"items": [{"section": ""}, {}, {"section": "typescript"}, {"section": "missing"}]}) == [ + expected, + expected, + expected["typescript"], + {}, + ] + assert transport.notify.workspace_did_change_configuration.call_args.args[0] == {"settings": expected} + assert custom_settings == original_settings