From e5b0042a9029ab4f452e4d55c906f3283cc4905a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fr=C3=B3es=20Franco?= Date: Tue, 26 May 2026 22:47:46 -0300 Subject: [PATCH] fix(lsp): refresh diagnostics after initial workspace scan completes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `didOpen` for the wiki's index page fires almost immediately after `initialized`, but the background `index_workspace` task hasn't yet parsed the other pages. Link-health diagnostics computed at that point look up every cross-page link target in a near-empty index and flag them all as missing-page — and the diagnostics are never recomputed once the scan catches up, so the editor shows stale red squiggles on every link until the buffer is edited. After each wiki's initial scan finishes, walk every open document that lives in that wiki, recompute its diagnostics against the now- populated index, and re-publish. Snapshots both the URI list and each doc's `(ast, text, version)` so we don't hold DashMap iterators across awaits. Co-Authored-By: Claude Sonnet 4.6 --- crates/nuwiki-lsp/src/lib.rs | 83 +++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/crates/nuwiki-lsp/src/lib.rs b/crates/nuwiki-lsp/src/lib.rs index 0bf5ed6..d4b3aec 100644 --- a/crates/nuwiki-lsp/src/lib.rs +++ b/crates/nuwiki-lsp/src/lib.rs @@ -467,17 +467,35 @@ impl LanguageServer for Backend { .await; // Kick off the background workspace scan (P8) — one task per wiki. + // When each scan finishes, refresh diagnostics for any docs that were + // opened while the index was still empty (their link-health checks + // would have flagged every link as missing-page). let wikis = self.wikis.read().expect("wikis lock poisoned").clone(); for wiki in wikis { if wiki.config.root.as_os_str().is_empty() { continue; } - let client = self.client.clone(); + let scan_client = self.client.clone(); + let refresh_client = self.client.clone(); let registry = Arc::clone(&self.registry); let index = Arc::clone(&wiki.index); let root = wiki.config.root.clone(); + let documents = Arc::clone(&self.documents); + let wikis_arc = Arc::clone(&self.wikis); + let config = Arc::clone(&self.config); + let use_utf8 = Arc::clone(&self.use_utf8); + let wiki_id = wiki.id; tokio::spawn(async move { - index_workspace(root, index, client, registry).await; + index_workspace(root, index, scan_client, registry).await; + refresh_open_diagnostics_for_wiki( + wiki_id, + documents, + wikis_arc, + config, + refresh_client, + use_utf8, + ) + .await; }); } } @@ -1352,6 +1370,67 @@ async fn index_workspace( .await; } +/// After the initial workspace scan for `wiki_id` completes, recompute and +/// re-publish diagnostics for every open document that lives in that wiki. +/// Documents opened before the index was populated would otherwise show +/// stale link-health diagnostics flagging every cross-page link as +/// missing-page (the lookups happened against a near-empty index). +async fn refresh_open_diagnostics_for_wiki( + wiki_id: crate::wiki::WikiId, + documents: Arc>, + wikis: Arc>>, + config: Arc>, + client: Client, + use_utf8: Arc, +) { + // Snapshot the open URIs first so we don't hold a DashMap iterator + // across the await below. + let uris: Vec = documents.iter().map(|e| e.key().clone()).collect(); + + for uri in uris { + // Re-check wiki ownership inside the loop — a workspace/configuration + // change could re-shape the wikis list mid-refresh. + let Some(wiki) = ({ + let snapshot = wikis.read().ok(); + snapshot.and_then(|w| w.iter().find(|w| w.id == wiki_id).cloned()) + }) else { + return; + }; + if !wiki.contains(&uri) { + continue; + } + + // Take a snapshot of the live doc state. If it's gone (closed mid-scan) + // skip it. + let Some((ast, text, version)) = ({ + documents + .get(&uri) + .map(|d| (d.ast.clone(), d.text.clone(), d.version)) + }) else { + continue; + }; + + let utf8 = use_utf8.load(Ordering::Relaxed); + let diagnostics = { + let cfg = config.read().expect("config lock poisoned"); + let page_name = index::page_name_from_uri(&uri, Some(&wiki.config.root)); + let idx_guard = wiki.index.read().expect("index lock poisoned"); + collect_diagnostics( + &ast, + &text, + Some(&uri), + Some(&idx_guard), + Some(&page_name), + &cfg.diagnostic, + utf8, + ) + }; + client + .publish_diagnostics(uri, diagnostics, Some(version)) + .await; + } +} + fn span_to_lsp_range(span: &Span, text: &str, utf8: bool) -> Range { to_lsp_range(span, text, utf8) }