Skip to main content

code_moniker_workspace/source/
catalog.rs

1use std::path::{Path, PathBuf};
2
3use crate::environment;
4use crate::snapshot::{
5	SourceCatalog, SourceUnit, WorkspaceCancellation, WorkspaceFailure, WorkspaceRequest,
6	WorkspaceResource, WorkspaceResult,
7};
8
9use super::content::{LocalResourceCache, SourceCatalogMaterial};
10use super::identity::LocalIdentityResolver;
11
12pub trait SourceCatalogPort {
13	fn load_catalog(&mut self, request: &WorkspaceRequest) -> WorkspaceResult<SourceCatalog>;
14	fn load_catalog_cancellable(
15		&mut self,
16		request: &WorkspaceRequest,
17		cancellation: &WorkspaceCancellation,
18	) -> WorkspaceResult<SourceCatalog> {
19		cancellation.check(WorkspaceResource::SourceCatalog)?;
20		let catalog = self.load_catalog(request)?;
21		cancellation.check(WorkspaceResource::SourceCatalog)?;
22		Ok(catalog)
23	}
24
25	fn extend_catalog(
26		&mut self,
27		current: &SourceCatalog,
28		paths: &[PathBuf],
29	) -> WorkspaceResult<Option<SourceCatalog>>;
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct LocalSourceCatalogOptions {
34	pub paths: Vec<PathBuf>,
35	pub files: Option<Vec<PathBuf>>,
36	pub project: Option<String>,
37	pub identity: LocalIdentityResolver,
38}
39
40impl LocalSourceCatalogOptions {
41	pub fn new(paths: Vec<PathBuf>, project: Option<String>) -> Self {
42		Self {
43			paths,
44			files: None,
45			project,
46			identity: LocalIdentityResolver::default(),
47		}
48	}
49
50	pub fn with_files(mut self, files: Vec<PathBuf>) -> Self {
51		self.files = Some(files);
52		self
53	}
54
55	pub fn with_identity(mut self, identity: LocalIdentityResolver) -> Self {
56		self.identity = identity;
57		self
58	}
59}
60
61pub struct LocalSourceCatalog {
62	options: LocalSourceCatalogOptions,
63	cache: LocalResourceCache,
64}
65
66impl LocalSourceCatalog {
67	pub fn new(options: LocalSourceCatalogOptions, cache: LocalResourceCache) -> Self {
68		Self { options, cache }
69	}
70}
71
72impl SourceCatalogPort for LocalSourceCatalog {
73	fn load_catalog(&mut self, _request: &WorkspaceRequest) -> WorkspaceResult<SourceCatalog> {
74		load_local_catalog(self, &WorkspaceCancellation::default())
75	}
76
77	fn load_catalog_cancellable(
78		&mut self,
79		_request: &WorkspaceRequest,
80		cancellation: &WorkspaceCancellation,
81	) -> WorkspaceResult<SourceCatalog> {
82		load_local_catalog(self, cancellation)
83	}
84
85	fn extend_catalog(
86		&mut self,
87		current: &SourceCatalog,
88		paths: &[PathBuf],
89	) -> WorkspaceResult<Option<SourceCatalog>> {
90		extend_local_catalog(&self.cache, current, paths)
91	}
92}
93
94fn load_local_catalog(
95	catalog: &mut LocalSourceCatalog,
96	cancellation: &WorkspaceCancellation,
97) -> WorkspaceResult<SourceCatalog> {
98	let sources = if let Some(files) = &catalog.options.files {
99		let [root] = catalog.options.paths.as_slice() else {
100			return Err(WorkspaceFailure::new(
101				WorkspaceResource::SourceCatalog,
102				"explicit source files require exactly one source root",
103			));
104		};
105		environment::discover_source_files(root, files, catalog.options.project.clone())
106	} else {
107		crate::sources::discover_cancellable(
108			&catalog.options.paths,
109			catalog.options.project.clone(),
110			cancellation,
111		)
112	}
113	.map_err(|err| WorkspaceFailure::new(WorkspaceResource::SourceCatalog, err.to_string()))?;
114	cancellation.check(WorkspaceResource::SourceCatalog)?;
115	let generation = catalog.cache.next_generation();
116	let units = sources
117		.files
118		.iter()
119		.enumerate()
120		.map(|(file_idx, file)| {
121			SourceUnit::with_language(
122				catalog.options.identity.source_id(file_idx, &file.rel_path),
123				file.rel_path.display().to_string(),
124				file.lang.tag(),
125			)
126		})
127		.collect::<Vec<_>>();
128	catalog.cache.insert_sources(
129		generation,
130		SourceCatalogMaterial {
131			sources,
132			identity: catalog.options.identity.clone(),
133		},
134	);
135	Ok(SourceCatalog::new(generation, units))
136}
137
138fn extend_local_catalog(
139	cache: &LocalResourceCache,
140	current: &SourceCatalog,
141	paths: &[PathBuf],
142) -> WorkspaceResult<Option<SourceCatalog>> {
143	let Some(mut material) = cache.source_material(current.generation) else {
144		return Ok(None);
145	};
146	let added = new_source_files(&material, paths);
147	let flipped = flip_retired_slots(&mut material, paths);
148	if added.is_empty() && !flipped {
149		return Ok(None);
150	}
151	material.sources.files.extend(added);
152	let generation = cache.next_generation();
153	let units = catalog_units(&material);
154	cache.insert_sources(generation, material);
155	Ok(Some(SourceCatalog::new(generation, units)))
156}
157
158fn flip_retired_slots(material: &mut SourceCatalogMaterial, paths: &[PathBuf]) -> bool {
159	let mut flipped = false;
160	for path in paths {
161		let file_idx = material
162			.normalized_file_index(path)
163			.or_else(|| material.normalized_file_index(&canonical_lookup_path(path)));
164		let Some(file_idx) = file_idx else {
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 canonical_lookup_path(path: &Path) -> PathBuf {
179	if let Ok(canonical) = path.canonicalize() {
180		return canonical;
181	}
182	if let (Some(parent), Some(name)) = (path.parent(), path.file_name())
183		&& let Ok(parent) = parent.canonicalize()
184	{
185		return parent.join(name);
186	}
187	path.to_path_buf()
188}
189
190fn new_source_files(
191	material: &SourceCatalogMaterial,
192	paths: &[PathBuf],
193) -> Vec<crate::sources::SourceFile> {
194	let mut added: Vec<crate::sources::SourceFile> = Vec::new();
195	for path in paths {
196		if !path.is_file() || material.normalized_file_index(path).is_some() {
197			continue;
198		}
199		let Some(file) = crate::sources::source_file_for_new_path(&material.sources, path) else {
200			continue;
201		};
202		let duplicate = material.normalized_file_index(&file.path).is_some()
203			|| added.iter().any(|existing| existing.path == file.path);
204		if !duplicate {
205			added.push(file);
206		}
207	}
208	added
209}
210
211fn catalog_units(material: &SourceCatalogMaterial) -> Vec<SourceUnit> {
212	material
213		.sources
214		.files
215		.iter()
216		.enumerate()
217		.filter(|(_, file)| !file.retired)
218		.map(|(file_idx, file)| {
219			SourceUnit::with_language(
220				material.identity.source_id(file_idx, &file.rel_path),
221				file.rel_path.display().to_string(),
222				file.lang.tag(),
223			)
224		})
225		.collect()
226}