From c72c07ae295fac568b68e3f86e5a79dfda5d9fb3 Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Sat, 15 Aug 2026 19:05:13 +0000 Subject: [PATCH] fix(nextflow): don't mark workspace scan flushed when both probes fail _flush_deferred_workspace_scan sets _workspace_scan_flushed unconditionally after its two completion() flush attempts, even when both raised. A later find_referencing_symbols call then silently skips the flush and answers from a possibly-incomplete workspace AST cache for the rest of the session, with no exception and no retry. Fixes #1871 --- CHANGELOG.md | 3 + .../nextflow_language_server.py | 4 +- .../test_nextflow_workspace_scan_flush.py | 65 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 test/solidlsp/test_nextflow_workspace_scan_flush.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 161709e7..f4ad4d6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Status of the `main` branch. Changes prior to the next official version change w project list in `serena_config.yml` * Language Servers: + - 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) - Fix: Dart's `$/analyzerStatus` notifications were logged as unhandled-method warnings during analysis (#1855) - Fix: Scala cross-file queries waited a fixed 5s after the first file was opened, which on a cold Metals is long before its build import, indexing and compilation have finished; the first diff --git a/src/solidlsp/language_servers/nextflow_language_server.py b/src/solidlsp/language_servers/nextflow_language_server.py index 0b3fcf17..a2215549 100644 --- a/src/solidlsp/language_servers/nextflow_language_server.py +++ b/src/solidlsp/language_servers/nextflow_language_server.py @@ -320,12 +320,14 @@ class NextflowLanguageServer(SolidLanguageServer): "textDocument": {"uri": self._resolve_file_uri(relative_file_path)}, "position": {"line": 0, "character": 0}, } + flushed = False for _ in range(2): try: self.server.send.completion(params) + flushed = True except Exception as e: log.debug("Completion request used to flush the Nextflow workspace scan failed: %s", e) - self._workspace_scan_flushed = True + self._workspace_scan_flushed = flushed @override def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None: diff --git a/test/solidlsp/test_nextflow_workspace_scan_flush.py b/test/solidlsp/test_nextflow_workspace_scan_flush.py new file mode 100644 index 00000000..e4be852e --- /dev/null +++ b/test/solidlsp/test_nextflow_workspace_scan_flush.py @@ -0,0 +1,65 @@ +"""Regression test for the Nextflow deferred-workspace-scan flush flag. + +``_flush_deferred_workspace_scan`` sends two ``completion`` requests purely to force the +server's debounced workspace scan, discarding their results; ``_workspace_scan_flushed`` +exists only to skip that work once it is known to have happened. If both requests raise, +nothing was actually flushed and the flag must stay clear so the next call retries, +without spawning a real language server process. +""" + +from solidlsp.language_servers.nextflow_language_server import NextflowLanguageServer + + +class _FakeCompletionSender: + def __init__(self, *, always_raise: bool = False) -> None: + self.calls = 0 + self._always_raise = always_raise + + def completion(self, params: dict) -> None: + self.calls += 1 + if self._always_raise: + raise TimeoutError("no response from the Nextflow language server") + + +class _FakeServer: + def __init__(self, sender: _FakeCompletionSender) -> None: + self.send = sender + + +def _bare_nextflow_ls(sender: _FakeCompletionSender, repo_path: str) -> NextflowLanguageServer: + """Create an instance without running __init__ (no JVM, no process), setting only the + state the flush machinery touches; same technique as test_typescript_timeout_policy.py. + """ + ls = object.__new__(NextflowLanguageServer) + ls.repository_root_path = repo_path + ls._workspace_scan_flushed = False + ls.server = _FakeServer(sender) + return ls + + +def test_flag_stays_clear_when_both_completion_calls_fail(tmp_path) -> None: + sender = _FakeCompletionSender(always_raise=True) + ls = _bare_nextflow_ls(sender, str(tmp_path)) + + ls._flush_deferred_workspace_scan("main.nf") + + assert sender.calls == 2 + assert ls._workspace_scan_flushed is False + + # a later call must retry, not silently skip forever + ls._flush_deferred_workspace_scan("main.nf") + assert sender.calls == 4 + + +def test_flag_is_set_when_a_completion_call_succeeds(tmp_path) -> None: + sender = _FakeCompletionSender(always_raise=False) + ls = _bare_nextflow_ls(sender, str(tmp_path)) + + ls._flush_deferred_workspace_scan("main.nf") + + assert sender.calls == 2 + assert ls._workspace_scan_flushed is True + + # once flushed, a later call is a no-op + ls._flush_deferred_workspace_scan("main.nf") + assert sender.calls == 2