Skip to main content

blues_lsp/project/
mod.rs

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/// Global project data
44#[derive(Default)]
45pub struct Project {
46    root: Option<PathBuf>,
47    config: Config,
48    compdb: Vec<Arc<BscArgs>>,
49
50    // TODO: Move to seperate structs?
51    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        // invalidate all compiled packages
72        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    /// (Re)load project config files
149    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        // TODO: Don't leak, somehow?
174        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    /// Lookup *a* PkgHandle for a given source path.
194    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    /// Get *a* PkgHandle for a given source path, if one does not already exist,
202    /// a dummy package will be created.
203    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        // Guess cli arguments for this package.
210        // For now, we just take the first thing in the list.
211        // TODO: diagnostic to warn about innacurate cli args
212        // TODO: don't leak?
213        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    /// Used to manually control contents of file at given path.
219    /// Aka.: this is where textDocument/did{Open,Change} notifications go
220    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    /// Remove overlay status for a file.
227    /// Aka.: textDocument/didClose
228    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                            // TODO: Make sure this doesn't erase file handles
317                            // insert_pkg(&mut self.packages, path, None, entry.clone())
318                        }
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}