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