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