fix(typescript): drain in-flight indexing on later cross-file queries (#1978)

_wait_for_cross_file_references_if_needed latched after the first cross-file
query and never waited again, even when a later query opened a file from a
project tsserver had not loaded yet and started a fresh $/progress cycle.
find_referencing_symbols and find_references would then return whatever
tsserver had indexed so far, silently partial, with nothing distinguishing it
from a complete answer.

The latch still covers the first query's start-grace wait unchanged. A later
query now also checks whether a progress token is currently active and, if
so, waits for it to drain before returning.

Fixes #1937
This commit is contained in:
Amir Fathi authored and GitHub committed 2026-09-23 15:09:02 +02:00
1 parent f581cf642d
commit cf54869af8
3 files changed
+56 -5

No files matched your search

+4
View File
@@ -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)
@@ -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:
@@ -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