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 let path_to_id = files.iter().map(|f| (f.path.clone(), f.id)).collect();
30 Self {
31 files,
32 path_to_id,
33 workspaces,
34 }
35 }
36
37 pub fn files(&self) -> &[DiscoveredFile] {
39 &self.files
40 }
41
42 pub fn workspaces(&self) -> &[WorkspaceInfo] {
44 &self.workspaces
45 }
46
47 pub fn file_by_id(&self, id: FileId) -> Option<&DiscoveredFile> {
49 self.files.get(id.0 as usize)
50 }
51
52 pub fn id_for_path(&self, path: &Path) -> Option<FileId> {
54 self.path_to_id.get(path).copied()
55 }
56
57 pub fn workspace_for_file(&self, id: FileId) -> Option<&WorkspaceInfo> {
59 let path = &self.files.get(id.0 as usize)?.path;
60 self.workspaces.iter().find(|ws| path.starts_with(&ws.root))
61 }
62
63 pub fn workspace_by_name(&self, name: &str) -> Option<&WorkspaceInfo> {
65 self.workspaces.iter().find(|ws| ws.name == name)
66 }
67
68 pub fn files_in_workspace(&self, ws: &WorkspaceInfo) -> Vec<FileId> {
70 self.files
71 .iter()
72 .filter(|f| f.path.starts_with(&ws.root))
73 .map(|f| f.id)
74 .collect()
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 fn make_file(id: u32, path: &str) -> DiscoveredFile {
83 DiscoveredFile {
84 id: FileId(id),
85 path: PathBuf::from(path),
86 size_bytes: 100,
87 }
88 }
89
90 fn make_workspace(name: &str, root: &str) -> WorkspaceInfo {
91 WorkspaceInfo {
92 root: PathBuf::from(root),
93 name: name.to_string(),
94 is_internal_dependency: false,
95 }
96 }
97
98 #[test]
99 fn id_for_path_lookup() {
100 let files = vec![
101 make_file(0, "/project/packages/a/src/index.ts"),
102 make_file(1, "/project/packages/b/src/index.ts"),
103 ];
104 let state = ProjectState::new(files, vec![]);
105 assert_eq!(
106 state.id_for_path(Path::new("/project/packages/a/src/index.ts")),
107 Some(FileId(0))
108 );
109 assert_eq!(
110 state.id_for_path(Path::new("/project/packages/b/src/index.ts")),
111 Some(FileId(1))
112 );
113 assert_eq!(state.id_for_path(Path::new("/project/missing.ts")), None);
114 }
115
116 #[test]
117 fn workspace_for_file_lookup() {
118 let files = vec![
119 make_file(0, "/project/packages/ui/src/button.ts"),
120 make_file(1, "/project/src/app.ts"),
121 ];
122 let workspaces = vec![make_workspace("ui", "/project/packages/ui")];
123 let state = ProjectState::new(files, workspaces);
124
125 assert_eq!(
126 state.workspace_for_file(FileId(0)).map(|ws| &ws.name),
127 Some(&"ui".to_string())
128 );
129 assert!(state.workspace_for_file(FileId(1)).is_none());
130 }
131
132 #[test]
133 fn workspace_by_name_lookup() {
134 let workspaces = vec![
135 make_workspace("ui", "/project/packages/ui"),
136 make_workspace("core", "/project/packages/core"),
137 ];
138 let state = ProjectState::new(vec![], workspaces);
139
140 assert!(state.workspace_by_name("ui").is_some());
141 assert!(state.workspace_by_name("core").is_some());
142 assert!(state.workspace_by_name("missing").is_none());
143 }
144
145 #[test]
146 fn files_in_workspace() {
147 let files = vec![
148 make_file(0, "/project/packages/ui/src/a.ts"),
149 make_file(1, "/project/packages/ui/src/b.ts"),
150 make_file(2, "/project/packages/core/src/c.ts"),
151 make_file(3, "/project/src/app.ts"),
152 ];
153 let workspaces = vec![
154 make_workspace("ui", "/project/packages/ui"),
155 make_workspace("core", "/project/packages/core"),
156 ];
157 let state = ProjectState::new(files, workspaces);
158
159 let ui_ws = state.workspace_by_name("ui").unwrap();
160 let ui_files = state.files_in_workspace(ui_ws);
161 assert_eq!(ui_files, vec![FileId(0), FileId(1)]);
162
163 let core_ws = state.workspace_by_name("core").unwrap();
164 let core_files = state.files_in_workspace(core_ws);
165 assert_eq!(core_files, vec![FileId(2)]);
166 }
167}