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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
//! File watcher handler for bibliography files.
use std::path::PathBuf;
use salsa::Durability;
use lsp_types::{DidChangeWatchedFilesParams, MessageType, Uri};
use super::super::helpers;
use crate::lsp::DocumentState;
use crate::lsp::global_state::GlobalState;
use crate::lsp::uri_ext::UriExt;
pub(crate) fn did_change_watched_files(gs: &mut GlobalState, params: DidChangeWatchedFilesParams) {
// A watcher event means the filesystem changed in a way salsa cannot see
// through its inputs: `collect_includes` / `find_project_documents` probe the
// filesystem directly (residual G3 reads), so a newly-created include is
// invisible to a memoized `project_graph`. Interning each changed path adds
// any new file to the `FileSet`, which re-runs `project_graph` (the only
// reader of the set) so those probes are re-evaluated --- a targeted, in-graph
// replacement for the former global `CacheGeneration` bump, which also
// invalidated every document's `metadata` memo (audit §3.3 / G3).
let changed_paths: Vec<PathBuf> = params
.changes
.iter()
.filter_map(|change| change.uri.to_file_path().map(|p| p.into_owned()))
.collect();
for path in &changed_paths {
gs.salsa.intern_file(Some(path.clone()));
}
// Reloading the open documents' referenced files on the writer then loads any
// newly-created file (flipping its `None`->`Some` text input) before the
// cached-text sync and re-lint below, so both observe fresh content.
let open_docs: Vec<(crate::salsa::FileText, crate::salsa::FileConfig, PathBuf)> = gs
.document_map
.values()
.filter_map(|state| Some((state.salsa_file, state.salsa_config, state.path.clone()?)))
.collect();
for (salsa_file, salsa_config, path) in open_docs {
crate::lsp::documents::load_project_files(gs, salsa_file, salsa_config, path);
}
for change in params.changes {
let Some(path) = change.uri.to_file_path().map(|p| p.into_owned()) else {
continue;
};
let extension = path.extension().and_then(|e| e.to_str());
let is_bibliography = matches!(
extension,
Some("bib") | Some("json") | Some("yaml") | Some("yml") | Some("ris")
);
// Always keep salsa's cached file text in sync when possible.
if let Ok(contents) = std::fs::read_to_string(&path)
&& gs.salsa.update_file_text_if_cached_with_durability(
&path,
contents,
Durability::MEDIUM,
)
{
gs.sender.log_message(
MessageType::INFO,
format!("Updated cached file: {}", path.display()),
);
}
// `.yml`/`.yaml` can be a project manifest (`_quarto.yml`/`_metadata.yml`/
// `_bookdown.yml`/`_output.yml` or a `metadata-files:` include) as well as
// a bibliography. A manifest change won't match any document's
// bibliography paths, so it needs its own reference check.
let is_manifest = matches!(extension, Some("yaml") | Some("yml"));
if !is_bibliography && !is_manifest {
continue;
}
gs.sender.log_message(
MessageType::INFO,
format!("Referenced file changed: {}", path.display()),
);
// Find all open documents that reference the changed file — as a
// bibliography or as a project manifest — and re-lint them so the change
// takes effect immediately (bib indices refresh; manifest parse errors
// re-publish on, or clear from, the manifest's own URI). Consult salsa so
// the reads observe the freshly-synced content above.
let states: Vec<(String, DocumentState)> = gs
.document_map
.iter()
.map(|(uri_str, state)| (uri_str.clone(), state.clone()))
.collect();
let mut affected_documents: Vec<Uri> = Vec::new();
for (uri_str, state) in states {
// Only saved documents reference files on disk.
let Some(doc_path) = state.path.clone() else {
continue;
};
let Ok(uri) = uri_str.parse::<Uri>() else {
continue;
};
let mut relint = false;
if is_bibliography {
let parsed_yaml_regions = crate::salsa::parsed_yaml_regions_for_file(
&gs.salsa,
state.salsa_file,
state.salsa_config,
);
if helpers::is_yaml_frontmatter_valid(parsed_yaml_regions) {
let metadata =
crate::salsa::metadata(&gs.salsa, state.salsa_file, state.salsa_config);
if let Some(bib_info) = metadata.bibliography.as_ref()
&& bib_info.paths.iter().any(|p| p == &path)
{
relint = true;
}
}
}
if !relint && is_manifest {
let graph = crate::salsa::project_structure(
&gs.salsa,
state.salsa_file,
state.salsa_config,
);
relint = graph
.dependencies(&doc_path, Some(crate::salsa::EdgeKind::ProjectConfig))
.into_iter()
.chain(
graph.dependencies(&doc_path, Some(crate::salsa::EdgeKind::MetadataFile)),
)
.any(|p| p == path);
}
if relint {
affected_documents.push(uri);
}
}
// A referenced-file change is infrequent, so run the full pass (external
// linters included) for each affected document.
for uri in affected_documents {
gs.spawn_lint(uri, false, true);
}
}
}