1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
use std::sync::Arc;
use dashmap::DashMap;
use tower_lsp::lsp_types::{Diagnostic, Url};
use crate::ast::ParsedDoc;
use crate::config::DiagnosticsConfig;
use crate::document_store::DocumentStore;
use crate::semantic_diagnostics::issues_to_diagnostics;
/// Per-open-file state owned by `Backend` (Phase E4).
///
/// Previously this lived inside `DocumentStore`'s `map: DashMap<Url, Document>`,
/// but none of these fields are salsa-shaped: `text` is the live editor buffer,
/// `version` is an async-parse gate, and `parse_diagnostics` is a publish cache.
/// Keeping them on `Backend` leaves `DocumentStore` as a pure salsa-input wrapper.
#[derive(Default, Clone)]
pub(crate) struct OpenFile {
/// Live editor text.
pub(crate) text: String,
/// Monotonic counter bumped on every `set_open_text` / `close_open_file`;
/// used to discard stale async parse results.
pub(crate) version: u64,
/// Parse-level diagnostics most recently cached for publication.
pub(crate) parse_diagnostics: Vec<Diagnostic>,
}
/// Shared handle to open-file state. Cheaply cloneable — wraps an `Arc<DashMap>`
/// so it can be captured by async closures alongside `Arc<DocumentStore>`.
#[derive(Clone, Default)]
pub struct OpenFiles(Arc<DashMap<Url, OpenFile>>);
impl OpenFiles {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn set_open_text(&self, docs: &DocumentStore, uri: Url, text: String) -> u64 {
docs.mirror_text(&uri, &text);
let mut entry = self.0.entry(uri).or_default();
entry.version += 1;
entry.text = text;
entry.version
}
pub(crate) fn close(&self, docs: &DocumentStore, uri: &Url) {
self.0.remove(uri);
docs.evict_token_cache(uri);
}
pub(crate) fn current_version(&self, uri: &Url) -> Option<u64> {
self.0.get(uri).map(|e| e.version)
}
pub(crate) fn text(&self, uri: &Url) -> Option<String> {
self.0.get(uri).map(|e| e.text.clone())
}
pub(crate) fn set_parse_diagnostics(&self, uri: &Url, diagnostics: Vec<Diagnostic>) {
if let Some(mut entry) = self.0.get_mut(uri) {
entry.parse_diagnostics = diagnostics;
}
}
pub(crate) fn parse_diagnostics(&self, uri: &Url) -> Option<Vec<Diagnostic>> {
self.0.get(uri).map(|e| e.parse_diagnostics.clone())
}
pub(crate) fn all_with_diagnostics(&self) -> Vec<(Url, Vec<Diagnostic>, Option<i64>)> {
self.0
.iter()
.map(|e| {
(
e.key().clone(),
e.value().parse_diagnostics.clone(),
Some(e.value().version as i64),
)
})
.collect()
}
pub(crate) fn urls(&self) -> Vec<Url> {
self.0.iter().map(|e| e.key().clone()).collect()
}
pub(crate) fn contains(&self, uri: &Url) -> bool {
self.0.contains_key(uri)
}
/// Open-gated parsed doc: returns `Some` only when `uri` is currently open.
pub(crate) fn get_doc(&self, docs: &DocumentStore, uri: &Url) -> Option<Arc<ParsedDoc>> {
if !self.contains(uri) {
return None;
}
docs.get_doc_salsa(uri)
}
}
/// Build the full diagnostic bundle for an already-open file.
///
/// Reuses cached parse diagnostics from `OpenFiles` (set by the file's own
/// debounced parse) and recomputes the rest:
/// - `duplicate_declaration_diagnostics` is intra-file (AST walk over the
/// doc's own statements), so a dependency change does NOT change its
/// result — but it's cheap and keeps this helper a single source of
/// truth for "the diagnostic bundle for `uri`".
/// - `semantic_issues` is salsa-cached; for files unaffected by the
/// triggering change it's a cache hit.
///
/// Used both for the originating file (during `did_open`/`did_change`) and
/// when proactively republishing diagnostics to other open files after a
/// dependency edit. Salsa-blocking — call from a `spawn_blocking` if invoked
/// off the originating file's debounce path.
pub(crate) fn compute_open_file_diagnostics(
docs: &DocumentStore,
open_files: &OpenFiles,
uri: &Url,
diag_cfg: &DiagnosticsConfig,
) -> Vec<Diagnostic> {
use crate::semantic_diagnostics::duplicate_declaration_diagnostics;
let mut out = open_files.parse_diagnostics(uri).unwrap_or_default();
let source = open_files.text(uri).unwrap_or_default();
if let Some(d) = open_files.get_doc(docs, uri) {
out.extend(duplicate_declaration_diagnostics(&source, &d, diag_cfg));
}
if let Some(issues) = docs.get_semantic_issues_salsa(uri) {
out.extend(issues_to_diagnostics(&issues, uri, diag_cfg));
}
out
}