blues-lsp 0.1.3

LSP language server for the Bluespec SystemVerilog language
Documentation
use std::{
    collections::HashMap,
    fs,
    path::{Path, PathBuf},
    sync::Arc,
};

use bindings::Bindings;
use config::Config;
use exports::Exports;
// use defmap::VarMap;
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 defmap;
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>,
}

/// Global project data
#[derive(Default)]
pub struct Project {
    root: Option<PathBuf>,
    config: Config,
    compdb: Vec<Arc<BscArgs>>,

    // TODO: Move to seperate structs?
    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) {
        // invalidate all compiled packages
        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();
    }

    /// (Re)load project config files
    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 {
        // TODO: Don't leak, somehow?
        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()
    }

    /// Lookup *a* PkgHandle for a given source path.
    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))
    }

    /// Get *a* PkgHandle for a given source path, if one does not already exist,
    /// a dummy package will be created.
    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");
        // Guess cli arguments for this package.
        // For now, we just take the first thing in the list.
        // TODO: diagnostic to warn about innacurate cli args
        // TODO: don't leak?
        let cli = self.compdb.first().cloned().unwrap_or_default();
        let idx = self.packages.insert(Package::new(file, cli, None));
        PkgHandle(idx as u32)
    }

    /// Used to manually control contents of file at given path.
    /// Aka.: this is where textDocument/did{Open,Change} notifications go
    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);
    }

    /// Remove overlay status for a file.
    /// Aka.: textDocument/didClose
    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" {
                            // TODO: Make sure this doesn't erase file handles
                            // insert_pkg(&mut self.packages, path, None, entry.clone())
                        }
                    }
                }
            }
        }
    }
}

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")),
    }
}