1use std::path::{Path, PathBuf};
4
5use rustc_hash::FxHashMap;
6
7use fallow_config::WorkspaceInfo;
8
9use fallow_types::discover::{DiscoveredFile, FileId};
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 workspace_for_file(&self, id: FileId) -> Option<&WorkspaceInfo> {
69 let path = &self.files.get(id.0 as usize)?.path;
70 self.workspaces.iter().find(|ws| path.starts_with(&ws.root))
71 }
72
73 #[must_use]
75 pub fn workspace_by_name(&self, name: &str) -> Option<&WorkspaceInfo> {
76 self.workspaces.iter().find(|ws| ws.name == name)
77 }
78
79 #[must_use]
81 pub fn files_in_workspace(&self, ws: &WorkspaceInfo) -> Vec<FileId> {
82 self.files
83 .iter()
84 .filter(|f| f.path.starts_with(&ws.root))
85 .map(|f| f.id)
86 .collect()
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 fn make_file(id: u32, path: &str) -> DiscoveredFile {
95 DiscoveredFile {
96 id: FileId(id),
97 path: PathBuf::from(path),
98 size_bytes: 100,
99 }
100 }
101
102 fn make_workspace(name: &str, root: &str) -> WorkspaceInfo {
103 WorkspaceInfo {
104 root: PathBuf::from(root),
105 name: name.to_string(),
106 is_internal_dependency: false,
107 }
108 }
109
110 #[test]
111 fn id_for_path_lookup() {
112 let files = vec![
113 make_file(0, "/project/packages/a/src/index.ts"),
114 make_file(1, "/project/packages/b/src/index.ts"),
115 ];
116 let state = ProjectState::new(files, vec![]);
117 assert_eq!(
118 state.id_for_path(Path::new("/project/packages/a/src/index.ts")),
119 Some(FileId(0))
120 );
121 assert_eq!(
122 state.id_for_path(Path::new("/project/packages/b/src/index.ts")),
123 Some(FileId(1))
124 );
125 assert_eq!(state.id_for_path(Path::new("/project/missing.ts")), None);
126 }
127
128 #[test]
129 fn workspace_for_file_lookup() {
130 let files = vec![
131 make_file(0, "/project/packages/ui/src/button.ts"),
132 make_file(1, "/project/src/app.ts"),
133 ];
134 let workspaces = vec![make_workspace("ui", "/project/packages/ui")];
135 let state = ProjectState::new(files, workspaces);
136
137 assert_eq!(
138 state.workspace_for_file(FileId(0)).map(|ws| &ws.name),
139 Some(&"ui".to_string())
140 );
141 assert!(state.workspace_for_file(FileId(1)).is_none());
142 }
143
144 #[test]
145 fn workspace_by_name_lookup() {
146 let workspaces = vec![
147 make_workspace("ui", "/project/packages/ui"),
148 make_workspace("core", "/project/packages/core"),
149 ];
150 let state = ProjectState::new(vec![], workspaces);
151
152 assert!(state.workspace_by_name("ui").is_some());
153 assert!(state.workspace_by_name("core").is_some());
154 assert!(state.workspace_by_name("missing").is_none());
155 }
156
157 #[test]
158 fn files_in_workspace() {
159 let files = vec![
160 make_file(0, "/project/packages/ui/src/a.ts"),
161 make_file(1, "/project/packages/ui/src/b.ts"),
162 make_file(2, "/project/packages/core/src/c.ts"),
163 make_file(3, "/project/src/app.ts"),
164 ];
165 let workspaces = vec![
166 make_workspace("ui", "/project/packages/ui"),
167 make_workspace("core", "/project/packages/core"),
168 ];
169 let state = ProjectState::new(files, workspaces);
170
171 let ui_ws = state.workspace_by_name("ui").unwrap();
172 let ui_files = state.files_in_workspace(ui_ws);
173 assert_eq!(ui_files, vec![FileId(0), FileId(1)]);
174
175 let core_ws = state.workspace_by_name("core").unwrap();
176 let core_files = state.files_in_workspace(core_ws);
177 assert_eq!(core_files, vec![FileId(2)]);
178 }
179
180 #[test]
181 fn file_by_id_valid() {
182 let files = vec![
183 make_file(0, "/project/src/a.ts"),
184 make_file(1, "/project/src/b.ts"),
185 ];
186 let state = ProjectState::new(files, vec![]);
187 let file = state.file_by_id(FileId(0)).unwrap();
188 assert_eq!(file.path, PathBuf::from("/project/src/a.ts"));
189 assert_eq!(file.id, FileId(0));
190 }
191
192 #[test]
193 fn file_by_id_out_of_bounds() {
194 let files = vec![make_file(0, "/project/src/a.ts")];
195 let state = ProjectState::new(files, vec![]);
196 assert!(state.file_by_id(FileId(999)).is_none());
197 }
198
199 #[test]
200 fn workspace_for_file_out_of_bounds() {
201 let files = vec![make_file(0, "/project/src/a.ts")];
202 let workspaces = vec![make_workspace("app", "/project")];
203 let state = ProjectState::new(files, workspaces);
204 assert!(state.workspace_for_file(FileId(999)).is_none());
205 }
206
207 #[test]
208 fn empty_state() {
209 let state = ProjectState::new(vec![], vec![]);
210 assert!(state.files().is_empty());
211 assert!(state.workspaces().is_empty());
212 assert!(state.file_by_id(FileId(0)).is_none());
213 assert!(state.id_for_path(Path::new("/any")).is_none());
214 assert!(state.workspace_by_name("any").is_none());
215 }
216
217 #[test]
218 fn files_returns_all_files() {
219 let files = vec![
220 make_file(0, "/project/src/a.ts"),
221 make_file(1, "/project/src/b.ts"),
222 ];
223 let state = ProjectState::new(files, vec![]);
224 assert_eq!(state.files().len(), 2);
225 assert_eq!(state.files()[0].id, FileId(0));
226 assert_eq!(state.files()[1].id, FileId(1));
227 }
228
229 #[test]
230 fn workspaces_returns_all_workspaces() {
231 let workspaces = vec![
232 make_workspace("a", "/project/packages/a"),
233 make_workspace("b", "/project/packages/b"),
234 ];
235 let state = ProjectState::new(vec![], workspaces);
236 assert_eq!(state.workspaces().len(), 2);
237 }
238
239 #[test]
240 fn files_in_workspace_empty_when_no_match() {
241 let files = vec![make_file(0, "/other/path/file.ts")];
242 let workspaces = vec![make_workspace("ui", "/project/packages/ui")];
243 let state = ProjectState::new(files, workspaces);
244 let ws = state.workspace_by_name("ui").unwrap();
245 assert!(state.files_in_workspace(ws).is_empty());
246 }
247
248 #[test]
249 fn workspace_for_file_nested_workspaces() {
250 let files = vec![make_file(0, "/project/packages/ui/components/Button.ts")];
253 let workspaces = vec![
254 make_workspace("root", "/project"),
255 make_workspace("ui", "/project/packages/ui"),
256 ];
257 let state = ProjectState::new(files, workspaces);
258 let ws = state.workspace_for_file(FileId(0)).unwrap();
260 assert_eq!(ws.name, "root");
261 }
262}