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) }