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