use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
sync::Arc,
};
use bindings::Bindings;
use config::Config;
use exports::Exports;
use file::{File, FileContents};
use package::Package;
use parking_lot::Mutex;
use querry::Querry;
use slab::Slab;
use syntax::Syntax;
use tracing::{error, info, trace};
use crate::bsc::args::BscArgs;
pub mod compdb;
pub mod config;
pub mod bindings;
pub mod exports;
pub mod file;
pub mod package;
pub mod querry;
pub mod syntax;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(Default))]
pub struct PkgHandle(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileHandle(u32);
#[derive(Default)]
struct ProjectFiles {
slab: Slab<Arc<File>>,
lookup: HashMap<PathBuf, FileHandle>,
}
#[derive(Default)]
pub struct Project {
root: Option<PathBuf>,
config: Config,
compdb: Vec<Arc<BscArgs>>,
files: Mutex<ProjectFiles>,
packages: Slab<Package>,
pub file_contents: Querry<FileContents>,
content_overlay: HashMap<FileHandle, Arc<FileContents>>,
pub syntax: Querry<Syntax>,
pub exports: Querry<Exports>,
pub bindings: Querry<Bindings>,
}
impl Project {
pub fn new(root: Option<PathBuf>) -> Self {
Self {
root,
..Default::default()
}
}
fn invalidate_all(&mut self) {
self.file_contents.clear(self);
self.syntax.clear(self);
self.exports.clear(self);
self.bindings.clear(self);
}
fn read_config(&mut self) {
let Some(root) = &self.root else {
return;
};
let toml_path = root.join("blues.toml");
let toml = match fs::read_to_string(&toml_path) {
Ok(toml) => toml,
Err(e) => {
error!(
"could not read project config at '{}': {e}",
toml_path.display()
);
return;
}
};
let config: Config = match toml::from_str(&toml) {
Ok(config) => config,
Err(e) => {
error!("failed to parse blues.toml: {e}");
return;
}
};
self.config = config;
}
fn read_compdb(&mut self) {
let Some(root) = &self.root else {
return;
};
let compdb_path = self
.config
.compdb_path
.clone()
.unwrap_or_else(|| "blues_compdb.json".into());
let compdb_path = root.join(compdb_path);
let compdb_str = match fs::read_to_string(&compdb_path) {
Ok(compdb_str) => compdb_str,
Err(e) => {
error!("could not read compdb at '{}': {e}", compdb_path.display());
return;
}
};
let compdb: Vec<compdb::Entry> = match serde_json::from_str(&compdb_str) {
Ok(compdb) => compdb,
Err(e) => {
error!("failed to parse compdb: {e}");
return;
}
};
let compdb = compdb
.into_iter()
.flat_map(|e| e.parse(root))
.map(Arc::new)
.collect::<Vec<_>>();
self.compdb = compdb;
}
pub fn initialize(&mut self) {
self.load_config();
self.scan_packages();
}
pub fn load_config(&mut self) {
self.read_config();
self.read_compdb();
self.invalidate_all();
}
pub fn lookup_file(&self, path: &Path) -> FileHandle {
let mut files = self.files.lock();
if let Some(h) = files.lookup.get(path) {
return *h;
}
let idx = files
.slab
.insert(Arc::new(File::new(Some(path.to_owned()))));
let handle = FileHandle(idx as u32);
files.lookup.insert(path.to_owned(), handle);
handle
}
pub fn make_dummy_file(&mut self) -> FileHandle {
let idx = self.files.lock().slab.insert(Arc::new(File::new(None)));
FileHandle(idx as u32)
}
pub fn get_file(&self, file: FileHandle) -> Arc<File> {
self.files.lock().slab.get(file.0 as usize).unwrap().clone()
}
fn pkg_for_bo(&self, bo: &Path) -> Option<PkgHandle> {
self.packages
.iter()
.find(|(_, pkg)| pkg.bo_path.as_deref() == Some(bo))
.map(|(idx, _)| PkgHandle(idx as u32))
}
pub fn get_pkg(&self, pkg: PkgHandle) -> &Package {
self.packages.get(pkg.0 as usize).unwrap()
}
pub fn try_pkg_for_file(&self, file: FileHandle) -> Option<PkgHandle> {
self.packages
.iter()
.find(|(_, pkg)| pkg.file == file)
.map(|(idx, _)| PkgHandle(idx as u32))
}
pub fn pkg_for_file(&mut self, file: FileHandle) -> PkgHandle {
if let Some(pkg) = self.try_pkg_for_file(file) {
return pkg;
}
info!("Creating dummy package");
let cli = self.compdb.first().cloned().unwrap_or_default();
let idx = self.packages.insert(Package::new(file, cli, None));
PkgHandle(idx as u32)
}
pub fn add_ovelay(&mut self, file: FileHandle, text: String) {
self.content_overlay
.insert(file, Arc::new(FileContents::new(text.into())));
self.file_contents.invalidate(self, &file);
}
pub fn remove_overlay(&mut self, file: FileHandle) {
if self.content_overlay.remove(&file).is_none() {
error!("remove_overlay called on non-overlaid file");
}
self.file_contents.invalidate(self, &file);
}
pub fn file_contents(&self, file: FileHandle, as_pkg: PkgHandle) -> Arc<FileContents> {
self.file_contents.get_as(self, file, as_pkg)
}
pub fn iter_packages(&self) -> impl Iterator<Item = PkgHandle> {
self.packages.iter().map(|(idx, _)| PkgHandle(idx as u32))
}
fn scan_packages(&mut self) {
fn insert_pkg(pkgs: &mut Slab<Package>, bo: PathBuf, file: FileHandle, cli: Arc<BscArgs>) {
if pkgs
.iter()
.any(|(_, pkg)| pkg.bo_path.as_deref() == Some(&bo))
{
return;
}
trace!("Found pacakge {}", bo.display());
pkgs.insert(Package::new(file, cli, Some(bo)));
}
for entry in &self.compdb {
if !entry.recursive {
let Some(file) = &entry.file else {
continue;
};
let Some(bo) = bo_for_source(file.clone(), entry.bdir.as_deref()) else {
error!("source path {} does not have a file name", file.display());
continue;
};
let file = self.lookup_file(file);
insert_pkg(&mut self.packages, bo, file, entry.clone())
} else {
for include_dir in &entry.include_dirs {
let files = match include_dir.read_dir() {
Ok(files) => files,
Err(e) => {
error!(
"could not open include directory '{}': {e}",
include_dir.display()
);
continue;
}
};
for file in files {
let file = match file {
Ok(file) => file,
Err(e) => {
error!(
"failed iterating directory '{}': {e}",
include_dir.display()
);
break;
}
};
let ty = match file.file_type() {
Ok(ty) => ty,
Err(e) => {
error!("could not get type of '{}': {e}", file.path().display());
continue;
}
};
if !ty.is_file() {
continue;
}
let path = file.path();
let Some(ext) = path.extension() else {
continue;
};
if ext == "bsv" || ext == "bs" {
let Some(bo) = bo_for_source(path.clone(), entry.bdir.as_deref())
else {
error!("source path {} does not have a file name", path.display());
continue;
};
let file = self.lookup_file(&path);
insert_pkg(&mut self.packages, bo, file, entry.clone())
} else if ext == "bo" {
}
}
}
}
}
}
}
pub fn bo_for_source(source: PathBuf, bdir: Option<&Path>) -> Option<PathBuf> {
match bdir {
Some(bdir) => source
.file_name()
.map(|name| bdir.join(name).with_extension("bo")),
None => Some(source.with_extension("bo")),
}
}