fix(lsp): refresh diagnostics after initial workspace scan completes

`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 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 22:47:46 -03:00
parent 769580af75
commit e5b0042a90
+81 -2
View File
@@ -467,17 +467,35 @@ impl LanguageServer for Backend {
.await; .await;
// Kick off the background workspace scan (P8) — one task per wiki. // 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(); let wikis = self.wikis.read().expect("wikis lock poisoned").clone();
for wiki in wikis { for wiki in wikis {
if wiki.config.root.as_os_str().is_empty() { if wiki.config.root.as_os_str().is_empty() {
continue; 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 registry = Arc::clone(&self.registry);
let index = Arc::clone(&wiki.index); let index = Arc::clone(&wiki.index);
let root = wiki.config.root.clone(); 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 { 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; .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<DashMap<Url, DocumentState>>,
wikis: Arc<RwLock<Vec<Wiki>>>,
config: Arc<RwLock<Config>>,
client: Client,
use_utf8: Arc<AtomicBool>,
) {
// Snapshot the open URIs first so we don't hold a DashMap iterator
// across the await below.
let uris: Vec<Url> = 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 { fn span_to_lsp_range(span: &Span, text: &str, utf8: bool) -> Range {
to_lsp_range(span, text, utf8) to_lsp_range(span, text, utf8)
} }