diff --git a/CHANGELOG.md b/CHANGELOG.md index ff6a60af..fea2756c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -141,6 +141,10 @@ Status of the `main` branch. Changes prior to the next official version change w `(int X, string Y)`, had their name corrupted to include a trailing `:` because the parenthesis in the type was mistaken for a method's parameter list; `find_symbol` on the real name then returned nothing + - Fix: TypeScript's `_has_waited_for_cross_file_references` latch was set after the first + cross-file query and never reset, so a later query that opened a file from a project tsserver + had not loaded yet (e.g. a monorepo package) skipped the indexing wait even while that + project's own `$/progress` indexing was still in flight (#1937) - Fix: Nextflow's `_flush_deferred_workspace_scan` marked the workspace scan flushed even when both of its `completion` probes failed, permanently skipping the flush (and silencing retries) for the rest of the session (#1871) diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index ca293961..e3ae0868 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -575,18 +575,29 @@ class TypeScriptLanguageServer(SolidLanguageServer): @override def _wait_for_cross_file_references_if_needed(self) -> None: - if self._has_waited_for_cross_file_references: + timeout = self._get_indexing_timeout() + if not self._has_waited_for_cross_file_references: + start_grace = self._get_indexing_start_grace() + self._log_cross_file_indexing_wait_outcome( + self._wait_for_indexing_start_or_completion(timeout=timeout, start_grace=start_grace), timeout + ) + self._has_waited_for_cross_file_references = True return - timeout = self._get_indexing_timeout() - start_grace = self._get_indexing_start_grace() - if self._wait_for_indexing_start_or_completion(timeout=timeout, start_grace=start_grace): + # The latch above only covers the first query; a later one can still open a file from a + # project tsserver has not loaded before, starting a fresh $/progress cycle to drain. + with self._progress_lock: + indexing_in_progress = bool(self._active_progress_tokens) + if indexing_in_progress: + self._log_cross_file_indexing_wait_outcome(self.wait_for_indexing(timeout=timeout), timeout) + + def _log_cross_file_indexing_wait_outcome(self, completed: bool, timeout: float) -> None: + if completed: log.info("TypeScript cross-file indexing complete") else: log.warning( "TypeScript cross-file indexing did not complete within %.0fs; proceeding (%s)", timeout, self.describe_indexing_state() ) - self._has_waited_for_cross_file_references = True @override def _get_preferred_definition(self, definitions: list[ls_types.Location]) -> ls_types.Location: diff --git a/test/solidlsp/test_typescript_timeout_policy.py b/test/solidlsp/test_typescript_timeout_policy.py index f71867ca..ac6b7128 100644 --- a/test/solidlsp/test_typescript_timeout_policy.py +++ b/test/solidlsp/test_typescript_timeout_policy.py @@ -284,6 +284,42 @@ class TestWaitForCrossFileReferencesUsesConfiguredGrace: server._wait_for_cross_file_references_if_needed() assert time.monotonic() - start < 0.1 + def test_second_call_drains_a_progress_token_already_in_flight(self) -> None: + """oraios/serena#1937: a later cross-file query can open a file from a project tsserver + has not loaded yet (e.g. a monorepo package), starting a fresh $/progress cycle. The + once-only latch must not let that query return while the fresh token is still active. + """ + server = _bare_ts_server(TypeScriptLanguageServer, {"indexing_start_grace": 0.05}) + server._has_waited_for_cross_file_references = True + server._active_progress_tokens.add("tsserver/project-b-load") + server._indexing_complete.clear() + + def _drain_shortly() -> None: + time.sleep(0.2) + with server._progress_lock: + server._active_progress_tokens.discard("tsserver/project-b-load") + server._indexing_complete.set() + + threading.Thread(target=_drain_shortly, daemon=True).start() + + start = time.monotonic() + server._wait_for_cross_file_references_if_needed() + elapsed = time.monotonic() - start + + # proves the wait actually blocked for the still-active token, not a fast no-op return + assert elapsed >= 0.15 + assert server._active_progress_tokens == set() + + def test_second_call_proceeds_on_timeout_while_a_token_never_drains(self) -> None: + server = _bare_ts_server(TypeScriptLanguageServer, {"indexing_timeout": 0.05}) + server._has_waited_for_cross_file_references = True + server._active_progress_tokens.add("tsserver/project-b-load") + server._indexing_complete.clear() + + server._wait_for_cross_file_references_if_needed() # must log and return, not raise or hang + + assert server._active_progress_tokens == {"tsserver/project-b-load"} + class _FakeSolidLSPSettings: """Minimal settings stand-in: returns a fixed TypeScript indexing_timeout so the guard tests can