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