code_moniker_workspace/source/
catalog.rs1use 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}