diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba8a4e0..4ee9de6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Status of the `main` branch. Changes prior to the next official version change w - `jet_brains_find_symbol`: Disallow wildcard-only search, delegating to overview tool if request is for file * Language Servers: + - Rust: reduce rust-analyzer memory usage and reload churn by disabling cache priming and Cargo autoreload while preserving diagnostics. - `typescript`: Fix: on large projects, the first `find_referencing_symbols`/`request_references` call could silently race tsserver's project load and return incomplete results, because the fixed 2s grace for tsserver to *start* reporting `$/progress` (distinct from the separate, already diff --git a/src/solidlsp/language_servers/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer.py index 1ee08896..5b53c17e 100644 --- a/src/solidlsp/language_servers/rust_analyzer.py +++ b/src/solidlsp/language_servers/rust_analyzer.py @@ -12,7 +12,13 @@ import threading from overrides import override -from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer +from solidlsp import ls_types +from solidlsp.ls import ( + LanguageServerDependencyProvider, + LanguageServerDependencyProviderSinglePath, + LSPConstants, + SolidLanguageServer, +) from solidlsp.ls_config import LanguageServerConfig from solidlsp.settings import SolidLSPSettings from solidlsp.util.subprocess_util import subprocess_run @@ -505,7 +511,10 @@ class RustAnalyzer(SolidLanguageServer): "showUnlinkedFileNotification": True, "showDependenciesExplorer": True, "assist": {"emitMustUse": False, "expressionFillDefault": "todo"}, - "cachePriming": {"enable": True, "numThreads": 0}, + # Eager cache priming and automatic Cargo reloads can repeatedly + # index large, actively-built workspaces. Keep checkOnSave enabled + # because Rust diagnostics rely on flycheck. + "cachePriming": {"enable": False, "numThreads": 0}, "cargo": { "autoreload": True, "buildScripts": { @@ -675,6 +684,36 @@ class RustAnalyzer(SolidLanguageServer): } return initialize_params + @override + def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float: + timeout = super()._get_published_diagnostics_wait_timeout(pull_diagnostics_failed) + # Rust diagnostics are often published asynchronously after the pull-diagnostics request, + # so keep a wider fallback wait window across all platforms. + return max(timeout, 8.0) + + @override + def request_text_document_diagnostics( + self, + relative_file_path: str, + start_line: int = 0, + end_line: int = -1, + min_severity: int = 4, + ) -> list[ls_types.Diagnostic]: + uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity) + + # With cache priming disabled, startup can finish before rust-analyzer + # schedules its initial flycheck. Trigger the retained checkOnSave path + # explicitly whenever diagnostics are requested. + with self.open_file(relative_file_path): + self.server.notify.did_save_text_document( + { # ty: ignore[invalid-argument-type] # dict built from LSPConstants keys; shape matches the TypedDict + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.URI: uri, + } + } + ) + return super().request_text_document_diagnostics(relative_file_path, start_line, end_line, min_severity) + def _start_server(self) -> None: """ Starts the Rust Analyzer Language Server diff --git a/test/solidlsp/rust/test_rust_analyzer_settings.py b/test/solidlsp/rust/test_rust_analyzer_settings.py new file mode 100644 index 00000000..3d9b46e9 --- /dev/null +++ b/test/solidlsp/rust/test_rust_analyzer_settings.py @@ -0,0 +1,49 @@ +from contextlib import nullcontext +from typing import Any, cast +from unittest.mock import MagicMock + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.language_servers.rust_analyzer import RustAnalyzer + +pytestmark = pytest.mark.rust + + +def _make_server() -> RustAnalyzer: + return object.__new__(RustAnalyzer) + + +def test_initialization_uses_lightweight_indexing_settings() -> None: + initialization_options = _make_server()._create_base_initialize_params()["initializationOptions"] + + assert initialization_options["cachePriming"]["enable"] is False + assert initialization_options["checkOnSave"] is True + + +@pytest.mark.parametrize("pull_diagnostics_failed", [False, True]) +def test_published_diagnostics_wait_has_eight_second_minimum(pull_diagnostics_failed: bool) -> None: + assert _make_server()._get_published_diagnostics_wait_timeout(pull_diagnostics_failed) == 8.0 + + +def test_published_diagnostics_wait_preserves_larger_base_timeout(monkeypatch: pytest.MonkeyPatch) -> None: + def wider_base_timeout(_server: SolidLanguageServer, _pull_diagnostics_failed: bool) -> float: + return 12.0 + + monkeypatch.setattr(SolidLanguageServer, "_get_published_diagnostics_wait_timeout", wider_base_timeout) + + assert _make_server()._get_published_diagnostics_wait_timeout(False) == 12.0 + + +def test_diagnostics_request_triggers_check_on_save(monkeypatch: pytest.MonkeyPatch) -> None: + server = _make_server() + server_interface = MagicMock() + server.server = cast(Any, server_interface) + uri = "file:///workspace/src/lib.rs" + + monkeypatch.setattr(server, "_validate_text_document_diagnostics_request", lambda *_args: uri) + monkeypatch.setattr(server, "open_file", lambda _relative_file_path: nullcontext()) + monkeypatch.setattr(SolidLanguageServer, "request_text_document_diagnostics", lambda *_args, **_kwargs: []) + + assert server.request_text_document_diagnostics("src/lib.rs") == [] + server_interface.notify.did_save_text_document.assert_called_once_with({"textDocument": {"uri": uri}})