Skip to main content

code_moniker_workspace/source/
catalog.rs

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