code_moniker_workspace/source/
catalog.rs1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
3
4use crate::environment;
5use crate::snapshot::{
6 SourceCatalog, SourceUnit, WorkspaceCancellation, WorkspaceFailure, WorkspaceRequest,
7 WorkspaceResource, WorkspaceResult,
8};
9use crate::sources::{SourceFile, SourceRoot};
10
11use super::content::{
12 LocalResourceCache, MEMORY_SOURCE_ROOT, MEMORY_SOURCE_ROOT_LABEL, MemorySourceSet,
13 SourceCatalogMaterial, memory_source_path,
14};
15use super::identity::LocalIdentityResolver;
16
17pub trait SourceCatalogPort {
18 fn load_catalog(&mut self, request: &WorkspaceRequest) -> WorkspaceResult<SourceCatalog>;
19 fn load_catalog_cancellable(
20 &mut self,
21 request: &WorkspaceRequest,
22 cancellation: &WorkspaceCancellation,
23 ) -> WorkspaceResult<SourceCatalog> {
24 cancellation.check(WorkspaceResource::SourceCatalog)?;
25 let catalog = self.load_catalog(request)?;
26 cancellation.check(WorkspaceResource::SourceCatalog)?;
27 Ok(catalog)
28 }
29
30 fn extend_catalog(
31 &mut self,
32 current: &SourceCatalog,
33 paths: &[PathBuf],
34 ) -> WorkspaceResult<Option<SourceCatalog>>;
35}
36
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct LocalSourceCatalogOptions {
39 pub paths: Vec<PathBuf>,
40 pub files: Option<Vec<PathBuf>>,
41 pub project: Option<String>,
42 pub identity: LocalIdentityResolver,
43}
44
45impl LocalSourceCatalogOptions {
46 pub fn new(paths: Vec<PathBuf>, project: Option<String>) -> Self {
47 Self {
48 paths,
49 files: None,
50 project,
51 identity: LocalIdentityResolver::default(),
52 }
53 }
54
55 pub fn with_files(mut self, files: Vec<PathBuf>) -> Self {
56 self.files = Some(files);
57 self
58 }
59
60 pub fn with_identity(mut self, identity: LocalIdentityResolver) -> Self {
61 self.identity = identity;
62 self
63 }
64}
65
66pub struct LocalSourceCatalog {
67 options: LocalSourceCatalogOptions,
68 cache: LocalResourceCache,
69}
70
71impl LocalSourceCatalog {
72 pub fn new(options: LocalSourceCatalogOptions, cache: LocalResourceCache) -> Self {
73 Self { options, cache }
74 }
75}
76
77impl SourceCatalogPort for LocalSourceCatalog {
78 fn load_catalog(&mut self, _request: &WorkspaceRequest) -> WorkspaceResult<SourceCatalog> {
79 load_local_catalog(self, &WorkspaceCancellation::default())
80 }
81
82 fn load_catalog_cancellable(
83 &mut self,
84 _request: &WorkspaceRequest,
85 cancellation: &WorkspaceCancellation,
86 ) -> WorkspaceResult<SourceCatalog> {
87 load_local_catalog(self, cancellation)
88 }
89
90 fn extend_catalog(
91 &mut self,
92 current: &SourceCatalog,
93 paths: &[PathBuf],
94 ) -> WorkspaceResult<Option<SourceCatalog>> {
95 extend_local_catalog(&self.cache, current, paths)
96 }
97}
98
99fn load_local_catalog(
100 catalog: &mut LocalSourceCatalog,
101 cancellation: &WorkspaceCancellation,
102) -> WorkspaceResult<SourceCatalog> {
103 let sources = if let Some(files) = &catalog.options.files {
104 let [root] = catalog.options.paths.as_slice() else {
105 return Err(WorkspaceFailure::new(
106 WorkspaceResource::SourceCatalog,
107 "explicit source files require exactly one source root",
108 ));
109 };
110 environment::discover_source_files(root, files, catalog.options.project.clone())
111 } else {
112 crate::sources::discover_cancellable(
113 &catalog.options.paths,
114 catalog.options.project.clone(),
115 cancellation,
116 )
117 }
118 .map_err(|err| WorkspaceFailure::new(WorkspaceResource::SourceCatalog, format!("{err:#}")))?;
119 cancellation.check(WorkspaceResource::SourceCatalog)?;
120 let mut material = SourceCatalogMaterial {
121 sources,
122 identity: catalog.options.identity.clone(),
123 memory_sources: BTreeMap::new(),
124 memory_slots: BTreeSet::new(),
125 memory_revisions: BTreeMap::new(),
126 };
127 sync_memory_source_sets(&mut material, &catalog.cache.memory_source_sets());
128 let generation = catalog.cache.next_generation();
129 let units = catalog_units(&material);
130 catalog.cache.insert_sources(generation, material);
131 Ok(SourceCatalog::new(generation, units))
132}
133
134fn extend_local_catalog(
135 cache: &LocalResourceCache,
136 current: &SourceCatalog,
137 paths: &[PathBuf],
138) -> WorkspaceResult<Option<SourceCatalog>> {
139 let Some(mut material) = cache.source_material(current.generation) else {
140 return Ok(None);
141 };
142 let added = new_source_files(&material, paths);
143 let flipped = flip_retired_slots(&mut material, paths);
144 let memory_changed = sync_memory_source_sets(&mut material, &cache.memory_source_sets());
145 if added.is_empty() && !flipped && !memory_changed {
146 return Ok(None);
147 }
148 material.sources.files.extend(added);
149 let generation = cache.next_generation();
150 let units = catalog_units(&material);
151 cache.insert_sources(generation, material);
152 Ok(Some(SourceCatalog::new(generation, units)))
153}
154
155fn flip_retired_slots(material: &mut SourceCatalogMaterial, paths: &[PathBuf]) -> bool {
156 let mut flipped = false;
157 for path in paths {
158 let file_idx = material
159 .normalized_file_index(path)
160 .or_else(|| material.normalized_file_index(&canonical_lookup_path(path)));
161 let Some(file_idx) = file_idx else {
162 continue;
163 };
164 if material.is_memory_slot(&material.sources.files[file_idx].path) {
165 continue;
166 }
167 let exists = material.sources.files[file_idx].path.is_file();
168 let file = &mut material.sources.files[file_idx];
169 if file.retired != exists {
170 continue;
171 }
172 file.retired = !exists;
173 flipped = true;
174 }
175 flipped
176}
177
178fn sync_memory_source_sets(
179 material: &mut SourceCatalogMaterial,
180 source_sets: &BTreeMap<String, MemorySourceSet>,
181) -> bool {
182 let previous_sources = std::mem::take(&mut material.memory_sources);
183 let previous_revisions = std::mem::take(&mut material.memory_revisions);
184 let mut desired = desired_memory_sources(material, source_sets);
185 let mut changed = false;
186 for file in &mut material.sources.files {
187 if !material.memory_slots.contains(&file.path) {
188 continue;
189 }
190 match desired.remove(&file.path) {
191 Some((next, content)) => {
192 changed |= !same_source_file(file, &next);
193 *file = next;
194 material
195 .memory_sources
196 .insert(file.path.to_path_buf(), content);
197 }
198 None => {
199 if !file.retired {
200 file.retired = true;
201 changed = true;
202 }
203 material.memory_sources.remove(&file.path);
204 }
205 }
206 }
207 for (path, (file, content)) in desired {
208 material.memory_slots.insert(path.to_path_buf());
209 material.memory_sources.insert(path, content);
210 material.sources.files.push(file);
211 changed = true;
212 }
213
214 material.memory_revisions = source_sets
215 .iter()
216 .map(|(srcset, source_set)| (srcset.to_owned(), source_set.revision.to_owned()))
217 .collect();
218 changed
219 || material.memory_sources != previous_sources
220 || material.memory_revisions != previous_revisions
221}
222
223fn desired_memory_sources(
224 material: &mut SourceCatalogMaterial,
225 source_sets: &BTreeMap<String, MemorySourceSet>,
226) -> BTreeMap<PathBuf, (SourceFile, String)> {
227 let mut desired = BTreeMap::new();
228 if source_sets.is_empty() {
229 return desired;
230 }
231 let root_idx = memory_source_root_index(material);
232 for (srcset, source_set) in source_sets {
233 for document in &source_set.documents {
234 let path = memory_source_path(srcset, &document.uri);
235 let uri = PathBuf::from(&document.uri);
236 let mut ctx = material.sources.roots[root_idx].ctx.clone();
237 ctx.srcset = Some(srcset.to_string());
238 let file = SourceFile {
239 source: root_idx,
240 path: path.to_path_buf(),
241 rel_path: uri.to_path_buf(),
242 anchor: uri.to_path_buf(),
243 lang: document.lang,
244 root_moniker: environment::source_root_moniker(document.lang, &uri, &ctx),
245 source_group: None,
246 srcset: Some(srcset.to_string()),
247 retired: false,
248 };
249 desired.insert(path, (file, document.content.to_owned()));
250 }
251 }
252 desired
253}
254
255fn memory_source_root_index(material: &mut SourceCatalogMaterial) -> usize {
256 if let Some(index) = material
257 .sources
258 .roots
259 .iter()
260 .position(|root| root.input == Path::new(MEMORY_SOURCE_ROOT))
261 {
262 return index;
263 }
264 let project = material
265 .sources
266 .roots
267 .iter()
268 .find_map(|root| root.ctx.project.clone());
269 let index = material.sources.roots.len();
270 let path = PathBuf::from(MEMORY_SOURCE_ROOT);
271 material.sources.roots.push(SourceRoot {
272 input: path.clone(),
273 path,
274 label: MEMORY_SOURCE_ROOT_LABEL.to_string(),
275 ctx: crate::extract::Context {
276 project,
277 ..Default::default()
278 },
279 source_groups: Default::default(),
280 });
281 index
282}
283
284fn same_source_file(current: &SourceFile, next: &SourceFile) -> bool {
285 current.source == next.source
286 && current.path == next.path
287 && current.rel_path == next.rel_path
288 && current.anchor == next.anchor
289 && current.lang == next.lang
290 && current.root_moniker == next.root_moniker
291 && current.source_group == next.source_group
292 && current.srcset == next.srcset
293 && current.retired == next.retired
294}
295
296fn canonical_lookup_path(path: &Path) -> PathBuf {
297 if let Ok(canonical) = path.canonicalize() {
298 return canonical;
299 }
300 if let (Some(parent), Some(name)) = (path.parent(), path.file_name())
301 && let Ok(parent) = parent.canonicalize()
302 {
303 return parent.join(name);
304 }
305 path.to_path_buf()
306}
307
308fn new_source_files(
309 material: &SourceCatalogMaterial,
310 paths: &[PathBuf],
311) -> Vec<crate::sources::SourceFile> {
312 let mut added: Vec<crate::sources::SourceFile> = Vec::new();
313 for path in paths {
314 if !path.is_file() || material.normalized_file_index(path).is_some() {
315 continue;
316 }
317 let Some(file) = crate::sources::source_file_for_new_path(&material.sources, path) else {
318 continue;
319 };
320 let duplicate = material.normalized_file_index(&file.path).is_some()
321 || added.iter().any(|existing| existing.path == file.path);
322 if !duplicate {
323 added.push(file);
324 }
325 }
326 added
327}
328
329fn catalog_units(material: &SourceCatalogMaterial) -> Vec<SourceUnit> {
330 material
331 .sources
332 .files
333 .iter()
334 .enumerate()
335 .filter(|(_, file)| !file.retired)
336 .map(|(file_idx, file)| {
337 SourceUnit::with_language(
338 material.identity.source_id(file_idx, &file.rel_path),
339 crate::path_util::portable_path(&file.rel_path),
340 file.lang.tag(),
341 )
342 })
343 .collect()
344}