1use std::{
2 collections::HashMap,
3 fs,
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use bindings::Bindings;
9use config::Config;
10use exports::Exports;
11use file::{File, FileContents};
12use package::Package;
13use parking_lot::Mutex;
14use querry::Querry;
15use slab::Slab;
16use syntax::Syntax;
17use tracing::{error, info, trace};
18
19use crate::bsc::args::BscArgs;
20
21pub mod bindings;
22pub mod compdb;
23pub mod config;
24pub mod exports;
25pub mod file;
26pub mod package;
27pub mod querry;
28pub mod syntax;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31#[cfg_attr(test, derive(Default))]
32pub struct PkgHandle(u32);
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct FileHandle(u32);
36
37#[derive(Default)]
38struct ProjectFiles {
39 slab: Slab<Arc<File>>,
40 lookup: HashMap<PathBuf, FileHandle>,
41}
42
43#[derive(Default)]
45pub struct Project {
46 root: Option<PathBuf>,
47 config: Config,
48 compdb: Vec<Arc<BscArgs>>,
49
50 files: Mutex<ProjectFiles>,
52 packages: Slab<Package>,
53
54 pub file_contents: Querry<FileContents>,
55 content_overlay: HashMap<FileHandle, Arc<FileContents>>,
56
57 pub syntax: Querry<Syntax>,
58 pub exports: Querry<Exports>,
59 pub bindings: Querry<Bindings>,
60}
61
62impl Project {
63 pub fn new(root: Option<PathBuf>) -> Self {
64 Self {
65 root,
66 ..Default::default()
67 }
68 }
69
70 fn invalidate_all(&mut self) {
71 self.file_contents.clear(self);
73 self.syntax.clear(self);
74 self.exports.clear(self);
75 self.bindings.clear(self);
76 }
77
78 fn read_config(&mut self) {
79 let Some(root) = &self.root else {
80 return;
81 };
82
83 let toml_path = root.join("blues.toml");
84 let toml = match fs::read_to_string(&toml_path) {
85 Ok(toml) => toml,
86 Err(e) => {
87 error!(
88 "could not read project config at '{}': {e}",
89 toml_path.display()
90 );
91 return;
92 }
93 };
94
95 let config: Config = match toml::from_str(&toml) {
96 Ok(config) => config,
97 Err(e) => {
98 error!("failed to parse blues.toml: {e}");
99 return;
100 }
101 };
102
103 self.config = config;
104 }
105
106 fn read_compdb(&mut self) {
107 let Some(root) = &self.root else {
108 return;
109 };
110
111 let compdb_path = self
112 .config
113 .compdb_path
114 .clone()
115 .unwrap_or_else(|| "blues_compdb.json".into());
116 let compdb_path = root.join(compdb_path);
117
118 let compdb_str = match fs::read_to_string(&compdb_path) {
119 Ok(compdb_str) => compdb_str,
120 Err(e) => {
121 error!("could not read compdb at '{}': {e}", compdb_path.display());
122 return;
123 }
124 };
125
126 let compdb: Vec<compdb::Entry> = match serde_json::from_str(&compdb_str) {
127 Ok(compdb) => compdb,
128 Err(e) => {
129 error!("failed to parse compdb: {e}");
130 return;
131 }
132 };
133
134 let compdb = compdb
135 .into_iter()
136 .flat_map(|e| e.parse(root))
137 .map(Arc::new)
138 .collect::<Vec<_>>();
139
140 self.compdb = compdb;
141 }
142
143 pub fn initialize(&mut self) {
144 self.load_config();
145 self.scan_packages();
146 }
147
148 pub fn load_config(&mut self) {
150 self.read_config();
151 self.read_compdb();
152
153 self.invalidate_all();
154 }
155
156 pub fn lookup_file(&self, path: &Path) -> FileHandle {
157 let mut files = self.files.lock();
158
159 if let Some(h) = files.lookup.get(path) {
160 return *h;
161 }
162
163 let idx = files
164 .slab
165 .insert(Arc::new(File::new(Some(path.to_owned()))));
166 let handle = FileHandle(idx as u32);
167
168 files.lookup.insert(path.to_owned(), handle);
169 handle
170 }
171
172 pub fn make_dummy_file(&mut self) -> FileHandle {
173 let idx = self.files.lock().slab.insert(Arc::new(File::new(None)));
175 FileHandle(idx as u32)
176 }
177
178 pub fn get_file(&self, file: FileHandle) -> Arc<File> {
179 self.files.lock().slab.get(file.0 as usize).unwrap().clone()
180 }
181
182 fn pkg_for_bo(&self, bo: &Path) -> Option<PkgHandle> {
183 self.packages
184 .iter()
185 .find(|(_, pkg)| pkg.bo_path.as_deref() == Some(bo))
186 .map(|(idx, _)| PkgHandle(idx as u32))
187 }
188
189 pub fn get_pkg(&self, pkg: PkgHandle) -> &Package {
190 self.packages.get(pkg.0 as usize).unwrap()
191 }
192
193 pub fn try_pkg_for_file(&self, file: FileHandle) -> Option<PkgHandle> {
195 self.packages
196 .iter()
197 .find(|(_, pkg)| pkg.file == file)
198 .map(|(idx, _)| PkgHandle(idx as u32))
199 }
200
201 pub fn pkg_for_file(&mut self, file: FileHandle) -> PkgHandle {
204 if let Some(pkg) = self.try_pkg_for_file(file) {
205 return pkg;
206 }
207
208 info!("Creating dummy package");
209 let cli = self.compdb.first().cloned().unwrap_or_default();
214 let idx = self.packages.insert(Package::new(file, cli, None));
215 PkgHandle(idx as u32)
216 }
217
218 pub fn add_ovelay(&mut self, file: FileHandle, text: String) {
221 self.content_overlay
222 .insert(file, Arc::new(FileContents::new(text.into())));
223 self.file_contents.invalidate(self, &file);
224 }
225
226 pub fn remove_overlay(&mut self, file: FileHandle) {
229 if self.content_overlay.remove(&file).is_none() {
230 error!("remove_overlay called on non-overlaid file");
231 }
232 self.file_contents.invalidate(self, &file);
233 }
234
235 pub fn file_contents(&self, file: FileHandle, as_pkg: PkgHandle) -> Arc<FileContents> {
236 self.file_contents.get_as(self, file, as_pkg)
237 }
238
239 pub fn iter_packages(&self) -> impl Iterator<Item = PkgHandle> {
240 self.packages.iter().map(|(idx, _)| PkgHandle(idx as u32))
241 }
242
243 fn scan_packages(&mut self) {
244 fn insert_pkg(pkgs: &mut Slab<Package>, bo: PathBuf, file: FileHandle, cli: Arc<BscArgs>) {
245 if pkgs
246 .iter()
247 .any(|(_, pkg)| pkg.bo_path.as_deref() == Some(&bo))
248 {
249 return;
250 }
251 trace!("Found pacakge {}", bo.display());
252 pkgs.insert(Package::new(file, cli, Some(bo)));
253 }
254
255 for entry in &self.compdb {
256 if !entry.recursive {
257 let Some(file) = &entry.file else {
258 continue;
259 };
260 let Some(bo) = bo_for_source(file.clone(), entry.bdir.as_deref()) else {
261 error!("source path {} does not have a file name", file.display());
262 continue;
263 };
264 let file = self.lookup_file(file);
265 insert_pkg(&mut self.packages, bo, file, entry.clone())
266 } else {
267 for include_dir in &entry.include_dirs {
268 let files = match include_dir.read_dir() {
269 Ok(files) => files,
270 Err(e) => {
271 error!(
272 "could not open include directory '{}': {e}",
273 include_dir.display()
274 );
275 continue;
276 }
277 };
278 for file in files {
279 let file = match file {
280 Ok(file) => file,
281 Err(e) => {
282 error!(
283 "failed iterating directory '{}': {e}",
284 include_dir.display()
285 );
286 break;
287 }
288 };
289
290 let ty = match file.file_type() {
291 Ok(ty) => ty,
292 Err(e) => {
293 error!("could not get type of '{}': {e}", file.path().display());
294 continue;
295 }
296 };
297
298 if !ty.is_file() {
299 continue;
300 }
301
302 let path = file.path();
303 let Some(ext) = path.extension() else {
304 continue;
305 };
306
307 if ext == "bsv" || ext == "bs" {
308 let Some(bo) = bo_for_source(path.clone(), entry.bdir.as_deref())
309 else {
310 error!("source path {} does not have a file name", path.display());
311 continue;
312 };
313 let file = self.lookup_file(&path);
314 insert_pkg(&mut self.packages, bo, file, entry.clone())
315 } else if ext == "bo" {
316 }
319 }
320 }
321 }
322 }
323 }
324}
325
326pub fn bo_for_source(source: PathBuf, bdir: Option<&Path>) -> Option<PathBuf> {
327 match bdir {
328 Some(bdir) => source
329 .file_name()
330 .map(|name| bdir.join(name).with_extension("bo")),
331 None => Some(source.with_extension("bo")),
332 }
333}