diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9f577e..21156b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ Status of the `main` branch. Changes prior to the next official version change w thread (#2038) * Language Servers: + - Fix: Dart analysis server no longer receives rootUri/rootPath, which added the monorepo root as an extra analysis root and could pin a CPU core at idle (#2045) - Fix: The C# language server opened every `.csproj` found anywhere under the repository root, without consulting the project's ignore settings. On repositories that vendor third-party or sample C# projects, this loads projects the server cannot restore on every start, and their diff --git a/src/solidlsp/initialize_params.py b/src/solidlsp/initialize_params.py index f71f6e5d..87fd36d3 100644 --- a/src/solidlsp/initialize_params.py +++ b/src/solidlsp/initialize_params.py @@ -40,10 +40,11 @@ class InitializeParamsBuilder(ABC): class DefaultInitializeParamsBuilder(InitializeParamsBuilder): - def __init__(self, ls: "SolidLanguageServer", set_workspace_folders: bool = True): + def __init__(self, ls: "SolidLanguageServer", set_workspace_folders: bool = True, set_root_uri: bool = True): super().__init__() self._ls = ls self._set_workspace_folders = set_workspace_folders + self._set_root_uri = set_root_uri @staticmethod def _create_workspace_folder_entry(path: str) -> WorkspaceFolder: @@ -54,10 +55,22 @@ class DefaultInitializeParamsBuilder(InitializeParamsBuilder): root_abs_path = self._ls.repository_root_path self._set("processId", os.getpid()) - self._set("rootPath", root_abs_path) - self._set("rootUri", pathlib.Path(root_abs_path).as_uri()) self._set("clientInfo", {"name": "Serena"}) + # Some language servers treat rootUri as an additional analysis root on top of + # workspaceFolders, with no de-duplication, which can cause unbounded indexing. + # When set_root_uri is False, rootUri/rootPath are omitted so that workspaceFolders + # alone determine the analysis roots. + if self._set_root_uri: + self._set("rootPath", root_abs_path) + self._set("rootUri", pathlib.Path(root_abs_path).as_uri()) + else: + # Some servers reject initialize when the key is absent + # ("params.rootUri must not be undefined"). Send explicit null so the + # field is present but not used as an analysis root (#2045). + self._set("rootPath", None) + self._set("rootUri", None) + if self._set_workspace_folders: abs_workspace_paths = self._ls.config.get_absolute_workspace_folders(root_abs_path) log.info("Workspace folders: %s", abs_workspace_paths) diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py index 6bd890e9..c668d8ba 100644 --- a/src/solidlsp/language_servers/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -7,6 +7,7 @@ from collections.abc import Hashable from overrides import override +from solidlsp.initialize_params import DefaultInitializeParamsBuilder, InitializeParamsBuilder from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings @@ -73,6 +74,13 @@ class DartLanguageServer(SolidLanguageServer): # via either notification it sends for this (see _start_server). self.analysis_complete = threading.Event() + def _create_initialize_params_builder(self) -> InitializeParamsBuilder: + # The Dart analysis server treats rootUri as an additional analysis root on top of + # workspaceFolders, with no de-duplication, so on a monorepo root that is not a Dart + # package the whole tree is analysed and the server burns CPU at idle (oraios/serena#2045). + # Omit rootUri/rootPath and rely on workspaceFolders alone. + return DefaultInitializeParamsBuilder(self, set_root_uri=False) + @override def _document_symbols_cache_fingerprint(self) -> Hashable: normalize_symbol_name_version = 1 diff --git a/test/solidlsp/test_dart_root_uri.py b/test/solidlsp/test_dart_root_uri.py new file mode 100644 index 00000000..46d6f4d6 --- /dev/null +++ b/test/solidlsp/test_dart_root_uri.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: MIT + +import tempfile +from pathlib import Path + +from solidlsp.language_servers.dart_language_server import DartLanguageServer + + +def _make_dart_ls() -> DartLanguageServer: + ls = object.__new__(DartLanguageServer) + ls._custom_settings = {} + # Windows: as_uri() rejects drive-less paths like "/tmp/..." + project_dir = Path(tempfile.mkdtemp(prefix="fake-dart-project-")) / "project" + project_dir.mkdir(parents=True, exist_ok=True) + ls.repository_root_path = str(project_dir) + + class _Cfg: + @staticmethod + def get_absolute_workspace_folders(root): + return [root] + + @staticmethod + def get_absolute_additional_workspace_folders(root): + return [] + + ls.config = _Cfg() + # custom_settings property reads from _custom_settings on SolidLanguageServer + return ls + + +def test_dart_omits_root_uri(): + builder = _make_dart_ls()._create_initialize_params_builder() + params = builder.build() + # rootUri must be present (some servers reject an undefined key) but null, so that only + # workspaceFolders determine the analysis roots (oraios/serena#2045). + assert params["rootUri"] is None + assert params["rootPath"] is None + assert params["workspaceFolders"] diff --git a/test/solidlsp/test_initialize_params_root_uri.py b/test/solidlsp/test_initialize_params_root_uri.py new file mode 100644 index 00000000..7ab80c98 --- /dev/null +++ b/test/solidlsp/test_initialize_params_root_uri.py @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: MIT + +import tempfile +from pathlib import Path + +from solidlsp.initialize_params import DefaultInitializeParamsBuilder + + +class _FakeLS: + # Windows: as_uri() rejects drive-less paths like "/tmp/..." + repository_root_path = str(Path(tempfile.mkdtemp(prefix="fake-project-")) / "root") + + class config: + @staticmethod + def get_absolute_workspace_folders(root): + return [root] + + @staticmethod + def get_absolute_additional_workspace_folders(root): + return [] + + custom_settings: dict = {} + + +def test_default_builder_sets_root_uri(): + builder = DefaultInitializeParamsBuilder(_FakeLS()) + params = builder.build() + assert "rootUri" in params + assert "rootPath" in params + + +def test_builder_sends_null_root_uri_when_disabled(): + builder = DefaultInitializeParamsBuilder(_FakeLS(), set_root_uri=False) + params = builder.build() + # keys must be present (some servers reject an undefined rootUri); values are null + assert params["rootUri"] is None + assert params["rootPath"] is None + assert params["processId"] is not None + assert params["clientInfo"] == {"name": "Serena"}