Skip to main content

code_moniker_workspace/source/
catalog.rs

1use 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}