1use std::path::{Path, PathBuf};
4
5use rustc_hash::FxHashMap;
6
7use fallow_config::WorkspaceInfo;
8
9use fallow_types::discover::{DiscoveredFile, FileId, StableFileKey};
10
11pub struct ProjectState {
21 files: Vec<DiscoveredFile>,
22 path_to_id: FxHashMap<PathBuf, FileId>,
23 workspaces: Vec<WorkspaceInfo>,
24}
25
26impl ProjectState {
27 #[must_use]
29 pub fn new(files: Vec<DiscoveredFile>, workspaces: Vec<WorkspaceInfo>) -> Self {
30 debug_assert!(
31 files.iter().enumerate().all(|(i, f)| f.id.0 as usize == i),
32 "FileIds must be densely packed starting at 0"
33 );
34 let path_to_id = files.iter().map(|f| (f.path.clone(), f.id)).collect();
35 Self {
36 files,
37 path_to_id,
38 workspaces,
39 }
40 }
41
42 #[must_use]
44 pub fn files(&self) -> &[DiscoveredFile] {
45 &self.files
46 }
47
48 #[must_use]
50 pub fn workspaces(&self) -> &[WorkspaceInfo] {
51 &self.workspaces
52 }
53
54 #[must_use]
56 pub fn file_by_id(&self, id: FileId) -> Option<&DiscoveredFile> {
57 self.files.get(id.0 as usize)
58 }
59
60 #[must_use]
62 pub fn id_for_path(&self, path: &Path) -> Option<FileId> {
63 self.path_to_id.get(path).copied()
64 }
65
66 #[must_use]
68 pub fn stable_key_for_file(&self, root: &Path, id: FileId) -> Option<StableFileKey> {
69 let file = self.file_by_id(id)?;
70 Some(StableFileKey::from_root_relative(root, &file.path))
71 }
72
73 #[must_use]
75 pub fn id_for_stable_key(&self, root: &Path, key: &StableFileKey) -> Option<FileId> {
76 self.files
77 .iter()
78 .find(|file| StableFileKey::from_root_relative(root, &file.path) == *key)
79 .map(|file| file.id)
80 }
81
82 #[must_use]
84 pub fn workspace_for_file(&self, id: FileId) -> Option<&WorkspaceInfo> {
85 let path = &self.files.get(id.0 as usize)?.path;
86 self.workspaces.iter().find(|ws| path.starts_with(&ws.root))
87 }
88
89 #[must_use]
91 pub fn workspace_by_name(&self, name: &str) -> Option<&WorkspaceInfo> {
92 self.workspaces.iter().find(|ws| ws.name == name)
93 }
94
95 #[must_use]
97 pub fn files_in_workspace(&self, ws: &WorkspaceInfo) -> Vec<FileId> {
98 self.files
99 .iter()
100 .filter(|f| f.path.starts_with(&ws.root))
101 .map(|f| f.id)
102 .collect()
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn make_file(id: u32, path: &str) -> DiscoveredFile {
111 DiscoveredFile {
112 id: FileId(id),
113 path: PathBuf::from(path),
114 size_bytes: 100,
115 }
116 }
117
118 fn make_workspace(name: &str, root: &str) -> WorkspaceInfo {
119 WorkspaceInfo {
120 root: PathBuf::from(root),
121 name: name.to_string(),
122 is_internal_dependency: false,
123 }
124 }
125
126 #[test]
127 fn id_for_path_lookup() {
128 let files = vec![
129 make_file(0, "/project/packages/a/src/index.ts"),
130 make_file(1, "/project/packages/b/src/index.ts"),
131 ];
132 let state = ProjectState::new(files, vec![]);
133 assert_eq!(
134 state.id_for_path(Path::new("/project/packages/a/src/index.ts")),
135 Some(FileId(0))
136 );
137 assert_eq!(
138 state.id_for_path(Path::new("/project/packages/b/src/index.ts")),
139 Some(FileId(1))
140 );
141 assert_eq!(state.id_for_path(Path::new("/project/missing.ts")), None);
142 }
143
144 #[test]
145 fn stable_key_for_file_is_root_relative() {
146 let files = vec![make_file(0, "/project/src/a.ts")];
147 let state = ProjectState::new(files, vec![]);
148
149 let key = state
150 .stable_key_for_file(Path::new("/project"), FileId(0))
151 .unwrap();
152
153 assert_eq!(key.as_str(), "src/a.ts");
154 assert_eq!(
155 state.id_for_stable_key(Path::new("/project"), &key),
156 Some(FileId(0))
157 );
158 }
159
160 #[test]
161 fn stable_key_survives_file_id_shift() {
162 let before = ProjectState::new(
163 vec![
164 make_file(0, "/project/src/a.ts"),
165 make_file(1, "/project/src/c.ts"),
166 ],
167 vec![],
168 );
169 let after = ProjectState::new(
170 vec![
171 make_file(0, "/project/src/a.ts"),
172 make_file(1, "/project/src/b.ts"),
173 make_file(2, "/project/src/c.ts"),
174 ],
175 vec![],
176 );
177 let root = Path::new("/project");
178 let key = before.stable_key_for_file(root, FileId(1)).unwrap();
179
180 assert_eq!(key.as_str(), "src/c.ts");
181 assert_eq!(before.id_for_stable_key(root, &key), Some(FileId(1)));
182 assert_eq!(after.id_for_stable_key(root, &key), Some(FileId(2)));
183 }
184
185 #[test]
186 fn workspace_for_file_lookup() {
187 let files = vec![
188 make_file(0, "/project/packages/ui/src/button.ts"),
189 make_file(1, "/project/src/app.ts"),
190 ];
191 let workspaces = vec![make_workspace("ui", "/project/packages/ui")];
192 let state = ProjectState::new(files, workspaces);
193
194 assert_eq!(
195 state.workspace_for_file(FileId(0)).map(|ws| &ws.name),
196 Some(&"ui".to_string())
197 );
198 assert!(state.workspace_for_file(FileId(1)).is_none());
199 }
200
201 #[test]
202 fn workspace_by_name_lookup() {
203 let workspaces = vec![
204 make_workspace("ui", "/project/packages/ui"),
205 make_workspace("core", "/project/packages/core"),
206 ];
207 let state = ProjectState::new(vec![], workspaces);
208
209 assert!(state.workspace_by_name("ui").is_some());
210 assert!(state.workspace_by_name("core").is_some());
211 assert!(state.workspace_by_name("missing").is_none());
212 }
213
214 #[test]
215 fn files_in_workspace() {
216 let files = vec![
217 make_file(0, "/project/packages/ui/src/a.ts"),
218 make_file(1, "/project/packages/ui/src/b.ts"),
219 make_file(2, "/project/packages/core/src/c.ts"),
220 make_file(3, "/project/src/app.ts"),
221 ];
222 let workspaces = vec![
223 make_workspace("ui", "/project/packages/ui"),
224 make_workspace("core", "/project/packages/core"),
225 ];
226 let state = ProjectState::new(files, workspaces);
227
228 let ui_ws = state.workspace_by_name("ui").unwrap();
229 let ui_files = state.files_in_workspace(ui_ws);
230 assert_eq!(ui_files, vec![FileId(0), FileId(1)]);
231
232 let core_ws = state.workspace_by_name("core").unwrap();
233 let core_files = state.files_in_workspace(core_ws);
234 assert_eq!(core_files, vec![FileId(2)]);
235 }
236
237 #[test]
238 fn file_by_id_valid() {
239 let files = vec![
240 make_file(0, "/project/src/a.ts"),
241 make_file(1, "/project/src/b.ts"),
242 ];
243 let state = ProjectState::new(files, vec![]);
244 let file = state.file_by_id(FileId(0)).unwrap();
245 assert_eq!(file.path, PathBuf::from("/project/src/a.ts"));
246 assert_eq!(file.id, FileId(0));
247 }
248
249 #[test]
250 fn file_by_id_out_of_bounds() {
251 let files = vec![make_file(0, "/project/src/a.ts")];
252 let state = ProjectState::new(files, vec![]);
253 assert!(state.file_by_id(FileId(999)).is_none());
254 }
255
256 #[test]
257 fn workspace_for_file_out_of_bounds() {
258 let files = vec![make_file(0, "/project/src/a.ts")];
259 let workspaces = vec![make_workspace("app", "/project")];
260 let state = ProjectState::new(files, workspaces);
261 assert!(state.workspace_for_file(FileId(999)).is_none());
262 }
263
264 #[test]
265 fn empty_state() {
266 let state = ProjectState::new(vec![], vec![]);
267 assert!(state.files().is_empty());
268 assert!(state.workspaces().is_empty());
269 assert!(state.file_by_id(FileId(0)).is_none());
270 assert!(state.id_for_path(Path::new("/any")).is_none());
271 assert!(state.workspace_by_name("any").is_none());
272 }
273
274 #[test]
275 fn files_returns_all_files() {
276 let files = vec![
277 make_file(0, "/project/src/a.ts"),
278 make_file(1, "/project/src/b.ts"),
279 ];
280 let state = ProjectState::new(files, vec![]);
281 assert_eq!(state.files().len(), 2);
282 assert_eq!(state.files()[0].id, FileId(0));
283 assert_eq!(state.files()[1].id, FileId(1));
284 }
285
286 #[test]
287 fn workspaces_returns_all_workspaces() {
288 let workspaces = vec![
289 make_workspace("a", "/project/packages/a"),
290 make_workspace("b", "/project/packages/b"),
291 ];
292 let state = ProjectState::new(vec![], workspaces);
293 assert_eq!(state.workspaces().len(), 2);
294 }
295
296 #[test]
297 fn files_in_workspace_empty_when_no_match() {
298 let files = vec![make_file(0, "/other/path/file.ts")];
299 let workspaces = vec![make_workspace("ui", "/project/packages/ui")];
300 let state = ProjectState::new(files, workspaces);
301 let ws = state.workspace_by_name("ui").unwrap();
302 assert!(state.files_in_workspace(ws).is_empty());
303 }
304
305 #[test]
306 fn workspace_for_file_nested_workspaces() {
307 let files = vec![make_file(0, "/project/packages/ui/components/Button.ts")];
308 let workspaces = vec![
309 make_workspace("root", "/project"),
310 make_workspace("ui", "/project/packages/ui"),
311 ];
312 let state = ProjectState::new(files, workspaces);
313 let ws = state.workspace_for_file(FileId(0)).unwrap();
314 assert_eq!(ws.name, "root");
315 }
316}