diff --git a/CHANGELOG.md b/CHANGELOG.md index e214da84..ed376740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ Status of the `main` branch. Changes prior to the next official version change w * JetBrains: - `jet_brains_find_symbol`: Disallow wildcard-only search, delegating to overview tool if request is for file +* Language Servers: + - `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 + configurable `indexing_timeout` used to wait for it to *drain*) was hardcoded and not large enough + for projects where the initial project-graph resolution itself takes longer than that. The grace + is now `indexing_start_grace` (default 5.0s), configurable the same way as `indexing_timeout` and + `server_ready_timeout` #1586 + * Hooks: - Add `serena-hooks --client=grok`, including Grok-native PreToolUse allow/deny output. - PreToolUse remind hook: coerce non-string shell command values instead of failing, and recognize diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index fe0a1a21..adee35eb 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -1118,8 +1118,9 @@ Supported settings: | `typescript_version` | `5.9.3` | Override the bundled `typescript` npm package version Serena installs when `ls_path` is not set. | | `typescript_language_server_version` | `5.1.3` | Override the bundled `typescript-language-server` npm package version Serena installs when `ls_path` is not set. | | `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | -| `indexing_timeout` | `30.0` | Timeout in seconds for waiting on tsserver's `$/progress` project-indexing signal (both at startup and before the first cross-file reference query). If indexing does not complete within this window, Serena logs a warning and proceeds anyway. Increase it for very large projects. | +| `indexing_timeout` | `30.0` | Timeout in seconds for waiting on tsserver's `$/progress` project-indexing signal to *drain* once it has started (both at startup and before the first cross-file reference query). If indexing does not complete within this window, Serena logs a warning and proceeds anyway. Increase it for very large projects. | | `server_ready_timeout` | `10.0` | Timeout in seconds for waiting on the server-ready signal after initialization. If the signal does not arrive within this window, Serena logs a message and proceeds anyway. | +| `indexing_start_grace` | `5.0` | Timeout in seconds to wait for tsserver to *start* reporting `$/progress` before the first cross-file reference query. tsserver must resolve the project graph before it can emit the first progress token, and that can take longer than the default on a very large project; if it takes longer than this window, Serena assumes no indexing was needed and may return incomplete cross-file references. Raising `indexing_timeout` alone does not help here, since this grace elapses first. Increase this for very large projects if `find_referencing_symbols`/`request_references` returns incomplete results shortly after project load. | #### Svelte diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index 1485c425..063b2524 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -74,6 +74,8 @@ class TypeScriptLanguageServer(SolidLanguageServer): - typescript_language_server_version: Version of typescript-language-server to install (default: "5.1.3") - indexing_timeout: float, timeout in seconds for project indexing (default: 30.0) - server_ready_timeout: float, timeout in seconds for the server-ready signal (default: 10.0) + - indexing_start_grace: float, timeout in seconds to wait for tsserver to *start* + reporting indexing progress before the first cross-file reference query (default: 5.0) """ @classmethod @@ -84,6 +86,11 @@ class TypeScriptLanguageServer(SolidLanguageServer): # well within this window; the timeout is only hit if the server never sends progress. INDEXING_PROGRESS_TIMEOUT = 30.0 SERVER_READY_TIMEOUT = 10.0 + # How long to wait for tsserver to *start* emitting $/progress after opening files, before + # assuming no indexing is needed. tsserver has to resolve the project graph before it can even + # create the first progress token, and that resolution is proportional to project size, so a + # large project can genuinely take longer than a small one just to begin reporting. + INDEXING_START_GRACE = 5.0 def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): """ @@ -123,7 +130,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): :param start_grace: Maximum seconds to wait for progress to begin after opening files. :return: True if indexing completed or no progress began within the grace period, False on timeout. """ - grace = self._INDEXING_START_GRACE_S if start_grace is None else start_grace + grace = self.INDEXING_START_GRACE if start_grace is None else start_grace # wait for progress to begin progress_deadline = time.monotonic() + grace @@ -171,6 +178,10 @@ class TypeScriptLanguageServer(SolidLanguageServer): """:return: maximum seconds to wait for TypeScript project indexing.""" return float(self._custom_settings.get("indexing_timeout", self.INDEXING_PROGRESS_TIMEOUT)) + def _get_indexing_start_grace(self) -> float: + """:return: maximum seconds to wait for tsserver to start reporting indexing progress.""" + return float(self._custom_settings.get("indexing_start_grace", self.INDEXING_START_GRACE)) + def _handle_server_ready_timeout(self, timeout: float) -> None: """Handle a TypeScript server-ready timeout. @@ -531,17 +542,14 @@ class TypeScriptLanguageServer(SolidLanguageServer): if not self._has_waited_for_cross_file_references: self.expect_indexing() - #: how long to wait for tsserver to *start* reporting indexing progress after didOpen; - #: matches the fixed wait the base implementation previously used - _INDEXING_START_GRACE_S = 2.0 - @override def _wait_for_cross_file_references_if_needed(self) -> None: if self._has_waited_for_cross_file_references: return timeout = self._get_indexing_timeout() - if self._wait_for_indexing_start_or_completion(timeout=timeout): + start_grace = self._get_indexing_start_grace() + if self._wait_for_indexing_start_or_completion(timeout=timeout, start_grace=start_grace): log.info("TypeScript cross-file indexing complete") else: log.warning( diff --git a/test/solidlsp/test_typescript_timeout_policy.py b/test/solidlsp/test_typescript_timeout_policy.py index f88ae17d..ad2274b0 100644 --- a/test/solidlsp/test_typescript_timeout_policy.py +++ b/test/solidlsp/test_typescript_timeout_policy.py @@ -63,6 +63,14 @@ class TestBaseTypeScriptTimeoutPolicy: assert server._get_server_ready_timeout() == 1.5 assert server._get_indexing_timeout() == 2.5 + def test_indexing_start_grace_default(self) -> None: + server = _bare_ts_server(TypeScriptLanguageServer) + assert server._get_indexing_start_grace() == 5.0 + + def test_indexing_start_grace_configurable_via_ls_specific_settings(self) -> None: + server = _bare_ts_server(TypeScriptLanguageServer, {"indexing_start_grace": 12.0}) + assert server._get_indexing_start_grace() == 12.0 + class TestSvelteCompanionTimeoutPolicy: """The Svelte companion must be strict: raise instead of serving from a cold/partial program.""" @@ -141,6 +149,36 @@ class TestWaitForIndexingStartOrCompletion: timer.cancel() +class TestWaitForCrossFileReferencesUsesConfiguredGrace: + """The actual find-references call path (not just the helper in isolation) must honor + indexing_start_grace: this is the mechanism behind oraios/serena#1586, where a large + project's tsserver takes longer than the (previously hardcoded, unconfigurable) grace to + even start reporting progress, so the first cross-file reference query silently races a + still-loading project and returns incomplete results. + """ + + def test_configured_grace_bounds_the_real_call_path(self) -> None: + server = _bare_ts_server(TypeScriptLanguageServer, {"indexing_start_grace": 0.05}) + server._has_waited_for_cross_file_references = False + server.expect_indexing() # mirrors _pre_open_for_cross_file_references having already run + + start = time.monotonic() + server._wait_for_cross_file_references_if_needed() + elapsed = time.monotonic() - start + + assert server._has_waited_for_cross_file_references is True + # bounded by the configured 0.05s grace, not the 5.0s default (let alone the old 2.0s one) + assert elapsed < 1.0 + + def test_second_call_is_a_noop_regardless_of_grace(self) -> None: + server = _bare_ts_server(TypeScriptLanguageServer, {"indexing_start_grace": 0.05}) + server._has_waited_for_cross_file_references = True + + start = time.monotonic() + server._wait_for_cross_file_references_if_needed() + assert time.monotonic() - start < 0.1 + + class _FakeSolidLSPSettings: """Minimal settings stand-in: returns a fixed TypeScript indexing_timeout so the guard tests can prove the svelte-key value takes precedence over the typescript-key value.