cairo-language-server 2.20.0

The Cairo Language Server
Documentation
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

use cairo_lang_filesystem::db::files_group_input;
use cairo_lang_filesystem::ids::CrateInput;
use salsa::Setter;

pub use self::configs_registry::{ConfigsRegistry, PackageConfig};
use crate::lang::db::AnalysisDatabase;
use crate::lang::proc_macros::controller::ProcMacroClientController;
use crate::project::Crate;
use crate::project::crate_data::CrateInfo;
use crate::state::{Owned, Snapshot};

mod configs_registry;

type WorkspaceRoot = PathBuf;

pub struct ProjectModel {
    // The two fields below keep exactly the same information;
    // therefore, their contents should be kept synchronised.
    // We keep both of them for efficiency and ease of use.
    /// Mapping from a workspace root to crates contained in the dependency graph of that workspace.
    loaded_workspaces: HashMap<WorkspaceRoot, HashMap<CrateInput, Crate>>,
    /// Mapping from a crate to roots of workspaces that contained this crate in their dependency graphs.
    loaded_crates: HashMap<CrateInput, HashSet<WorkspaceRoot>>,
    /// Used to determine when we can skip calling `scarb metadata` to update a project model.
    manifests_of_members_from_loaded_workspaces: Owned<HashSet<PathBuf>>,
    configs_registry: Owned<ConfigsRegistry>,
    /// Used to delay removing of crates from the db until the next workspace is loaded.
    /// It is done to ensure diagnostics are not randomly cleared after a project manifest change/
    /// db swap/reload workspace command.
    remove_crates_from_db_on_next_update: bool,
}

impl ProjectModel {
    pub fn new() -> Self {
        Self {
            loaded_workspaces: Default::default(),
            loaded_crates: Default::default(),
            manifests_of_members_from_loaded_workspaces: Default::default(),
            configs_registry: Default::default(),
            remove_crates_from_db_on_next_update: false,
        }
    }

    pub fn configs_registry(&self) -> Snapshot<ConfigsRegistry> {
        self.configs_registry.snapshot()
    }

    pub fn loaded_manifests(&self) -> Snapshot<HashSet<PathBuf>> {
        self.manifests_of_members_from_loaded_workspaces.snapshot()
    }

    pub fn clear_loaded_workspaces(&mut self) {
        self.loaded_workspaces.clear();
        self.loaded_crates.clear();
        self.manifests_of_members_from_loaded_workspaces.clear();
        self.configs_registry.clear();

        self.remove_crates_from_db_on_next_update = true;
    }

    pub fn load_workspace(
        &mut self,
        db: &mut AnalysisDatabase,
        workspace_crates: Vec<CrateInfo>,
        workspace_dir: PathBuf,
        proc_macro_controller: &ProcMacroClientController,
    ) {
        if self.remove_crates_from_db_on_next_update {
            self.remove_crates_from_db_on_next_update = false;

            files_group_input(db).set_crate_configs(db).to(Some(Default::default()));
        }

        let workspace_crates = workspace_crates
            .into_iter()
            .map(|cr_info| {
                if cr_info.is_member {
                    self.manifests_of_members_from_loaded_workspaces
                        .insert(cr_info.manifest_path.clone());
                }
                self.configs_registry.insert(cr_info.manifest_path, cr_info.package_config);

                (cr_info.cr.input(), cr_info.cr)
            })
            .collect();

        if let Some(old_crates) = self.loaded_workspaces.get(&workspace_dir) {
            if old_crates == &workspace_crates {
                return;
            }

            // Static because the borrow checker.
            ProjectModel::remove_crates(&mut self.loaded_crates, &workspace_dir, old_crates);
        };

        self.add_crates(workspace_crates, &workspace_dir);

        self.apply_changes_to_db(db, proc_macro_controller);
    }

    pub fn apply_changes_to_db(
        &self,
        db: &mut AnalysisDatabase,
        proc_macro_controller: &ProcMacroClientController,
    ) {
        for (cr, workspaces) in &self.loaded_crates {
            let same_crates: Vec<_> = workspaces
                .iter()
                .map(|ws| {
                    self.loaded_workspaces
                        .get(ws)
                        .expect("loaded_crates and loaded_workspaces are expected to be synchronised at this point")
                        .get(cr)
                        .expect("loaded_crates and loaded_workspaces are expected to be synchronised at this point")
                })
                .collect();

            let merged_builtin_plugins = same_crates
                .iter()
                .map(|cr| cr.builtin_plugins.clone())
                .reduce(|mut x, y| {
                    x.extend(y);
                    x
                })
                .expect("same_crates cannot be empty")
                .clone();
            let merged_settings = same_crates
                .iter()
                .map(|cr| cr.settings.clone())
                .reduce(|mut x, y| {
                    x.cfg_set =
                        x.cfg_set.map(|cfg_set| cfg_set.union(&y.cfg_set.unwrap_or_default()));
                    x.dependencies.extend(y.dependencies);
                    x
                })
                .expect("same_crates cannot be empty");
            let cr = Crate {
                settings: merged_settings,
                builtin_plugins: merged_builtin_plugins,
                ..same_crates.into_iter().next().expect("same_crates cannot be empty").clone()
            };

            let cr_input = cr.input();

            let proc_macro_plugin_suite =
                proc_macro_controller.proc_macro_plugin_suite_for_crate(&cr_input);

            cr.apply(db, proc_macro_plugin_suite.cloned());
        }
    }

    fn remove_crates(
        loaded_crates: &mut HashMap<CrateInput, HashSet<PathBuf>>,
        workspace_dir: &Path,
        old_crates: &HashMap<CrateInput, Crate>,
    ) {
        for old_cr in old_crates.keys() {
            loaded_crates.entry(old_cr.clone()).and_modify(|paths| {
                paths.remove(workspace_dir);
            });
        }

        loaded_crates.retain(|_, paths| !paths.is_empty());
    }

    fn add_crates(&mut self, workspace_crates: HashMap<CrateInput, Crate>, workspace_dir: &Path) {
        for cr in workspace_crates.keys() {
            self.loaded_crates.entry(cr.clone()).or_default().insert(workspace_dir.to_path_buf());
        }

        self.loaded_workspaces.insert(workspace_dir.to_path_buf(), workspace_crates);
    }
}