use std::collections::HashSet;
use std::path::PathBuf;
use cairo_lang_compiler::db::validate_corelib;
use cairo_lang_compiler::project::{setup_project, update_crate_roots_from_project_config};
use cairo_lang_filesystem::db::{CrateIdentifier, FilesGroup};
use cairo_lang_filesystem::ids::CrateId;
use cairo_lang_filesystem::set_crate_config;
use cairo_lang_project::ProjectConfig;
use crossbeam::channel::{Receiver, Sender};
use lsp_types::DiagnosticSeverity;
use lsp_types::notification::ShowMessage;
use lsp_types::{MessageType, ShowMessageParams};
use tracing::{debug, error, warn};
pub use self::crate_data::{Crate, CrateInfo, extract_custom_file_stems};
pub use self::model::ConfigsRegistry;
pub use self::project_manifest_path::*;
pub use self::scarb_manifest_diagnostics::{
ScarbMetadataMessage, scarb_metadata_messages_to_diagnostics,
};
use self::scarb_manifest_diagnostics::{
collect_scarb_manifest_diagnostics, manifest_diagnostics_from_ndjson,
scarb_metadata_messages_contain_only_errors,
};
use crate::ide::code_lens::FileChange;
use crate::lang::db::AnalysisDatabase;
use crate::lsp::ext::CorelibVersionMismatch;
use crate::project::model::ProjectModel;
use crate::project::scarb::extract_crates;
use crate::project::unmanaged_core_crate::try_to_init_unmanaged_core_if_not_present;
use crate::server::client::Notifier;
use crate::server::is_cairo_file_path;
use crate::server::schedule::thread;
use crate::server::schedule::thread::{JoinHandle, ThreadPriority};
use crate::state::{Snapshot, State};
use crate::toolchain::scarb::ScarbToolchain;
pub mod builtin_plugins;
mod crate_data;
mod model;
mod project_manifest_path;
mod scarb;
mod scarb_manifest_diagnostics;
mod unmanaged_core_crate;
pub struct ProjectController {
model: ProjectModel,
scarb_toolchain: ScarbToolchain,
requests_sender: Sender<ProjectUpdateRequest>,
response_receiver: Receiver<ProjectUpdate>,
_thread: JoinHandle,
}
impl ProjectController {
pub fn initialize(scarb_toolchain: ScarbToolchain, notifier: Notifier) -> Self {
let (requests_sender, requests_receiver) = crossbeam::channel::unbounded();
let (response_sender, response_receiver) = crossbeam::channel::unbounded();
let thread = ProjectControllerThread::spawn(
requests_receiver,
response_sender,
scarb_toolchain.clone(),
notifier,
);
ProjectController {
model: ProjectModel::new(),
scarb_toolchain,
requests_sender,
response_receiver,
_thread: thread,
}
}
pub fn configs_registry(&self) -> Snapshot<ConfigsRegistry> {
self.model.configs_registry()
}
pub fn response_receiver(&self) -> Receiver<ProjectUpdate> {
self.response_receiver.clone()
}
pub fn request_updating_project_for_file(&self, file_path: PathBuf) {
if self.scarb_toolchain.is_from_scarb_cache(&file_path) {
return;
}
self.send_request(ProjectUpdateRequest {
file_path,
loaded_manifests: self.model.loaded_manifests(),
})
}
pub fn clear_loaded_workspaces(&mut self) {
self.model.clear_loaded_workspaces();
}
#[tracing::instrument(skip_all, fields(project_update))]
pub fn handle_update(state: &mut State, notifier: Notifier, project_update: ProjectUpdate) {
let db = &mut state.db;
match project_update {
ProjectUpdate::Scarb {
crates,
workspace_dir,
workspace_manifest_path,
diagnostics,
} => {
state.diagnostics_controller.clear_scarb_manifest_diagnostics(&crates, ¬ifier);
debug!("updating crate roots from scarb metadata: {crates:#?}");
state
.proc_macro_controller
.request_defined_macros(db, workspace_manifest_path.clone());
state.project_controller.model.load_workspace(
db,
crates,
workspace_dir,
&state.proc_macro_controller,
);
state.diagnostics_controller.publish_scarb_manifest_diagnostics(
&workspace_manifest_path,
diagnostics,
DiagnosticSeverity::WARNING,
db,
¬ifier,
);
state.analysis_progress_controller.project_model_loaded();
}
ProjectUpdate::ScarbMetadataFailed { manifest_path, diagnostics } => {
state.diagnostics_controller.publish_scarb_manifest_diagnostics(
&manifest_path,
diagnostics,
DiagnosticSeverity::ERROR,
db,
¬ifier,
);
try_to_init_unmanaged_core_if_not_present(
db,
&state.config,
&state.scarb_toolchain,
);
}
ProjectUpdate::CairoProjectToml(maybe_project_config) => {
if let Some(project_config) = *maybe_project_config {
update_crate_roots_from_project_config(db, &project_config);
if contains_core_from_scarb_cache(&project_config, &state.scarb_toolchain) {
let core_id = CrateId::core(db);
let mut core_settings = db.crate_config(core_id).unwrap().clone();
core_settings.settings.cfg_set = Some(
core_settings
.settings
.cfg_set
.unwrap_or_default()
.union(&AnalysisDatabase::initial_cfg_set_for_deps()),
);
set_crate_config!(db, core_id, Some(core_settings));
}
state.analysis_progress_controller.project_model_loaded();
}
try_to_init_unmanaged_core_if_not_present(
db,
&state.config,
&state.scarb_toolchain,
);
}
ProjectUpdate::NoConfig(file_path) => {
try_to_init_unmanaged_core_if_not_present(
db,
&state.config,
&state.scarb_toolchain,
);
if let Err(err) = setup_project(&mut *db, &file_path) {
error!(
"error loading file {} as a single crate: {err}",
file_path.to_string_lossy()
);
} else {
state.analysis_progress_controller.project_model_loaded();
}
}
}
let _ = db;
state.code_lens_controller.on_did_change(
state.db.clone(),
state.config.clone(),
state
.open_files
.iter()
.filter(|&url| is_cairo_file_path(url))
.cloned()
.map(|file| FileChange { url: file, was_deleted: false }),
);
if let Err(result) = validate_corelib(&state.db) {
notifier.notify::<CorelibVersionMismatch>(result.to_string());
}
#[cfg(feature = "testing")]
notifier.notify::<crate::lsp::ext::testing::ProjectUpdatingFinished>(());
}
fn send_request(&self, request: ProjectUpdateRequest) {
self.requests_sender
.send(request)
.expect("project controller thread should not have panicked or dropped the receiver")
}
}
pub enum ProjectUpdate {
Scarb {
crates: Vec<CrateInfo>,
workspace_dir: PathBuf,
workspace_manifest_path: PathBuf,
diagnostics: Vec<ScarbMetadataMessage>,
},
ScarbMetadataFailed {
manifest_path: PathBuf,
diagnostics: Vec<ScarbMetadataMessage>,
},
CairoProjectToml(Box<Option<ProjectConfig>>),
NoConfig(PathBuf),
}
struct ProjectControllerThread {
requests_receiver: Receiver<ProjectUpdateRequest>,
response_sender: Sender<ProjectUpdate>,
scarb_toolchain: ScarbToolchain,
notifier: Notifier,
}
impl ProjectControllerThread {
fn spawn(
requests_receiver: Receiver<ProjectUpdateRequest>,
response_sender: Sender<ProjectUpdate>,
scarb_toolchain: ScarbToolchain,
notifier: Notifier,
) -> JoinHandle {
let this = Self { requests_receiver, response_sender, scarb_toolchain, notifier };
thread::Builder::new(ThreadPriority::Worker)
.name("cairo-ls:project-controller".into())
.spawn(move || this.event_loop())
.expect("failed to spawn project controller thread")
}
fn event_loop(mut self) {
while let Ok(request) = self.requests_receiver.recv() {
let project_update = self.fetch_project_update_for_file(request);
if let Some(project_update) = project_update {
self.send_project_update(project_update);
}
}
}
#[tracing::instrument(skip_all)]
fn fetch_project_update_for_file(
&mut self,
project_update_request: ProjectUpdateRequest,
) -> Option<ProjectUpdate> {
let project_update = match ProjectManifestPath::discover(
&project_update_request.file_path,
&self.notifier,
) {
Some(ProjectManifestPath::Scarb(manifest_path)) => {
if project_update_request.loaded_manifests.contains(&manifest_path) {
debug!("scarb project is already loaded: {}", manifest_path.display());
return None;
}
let scarb_path = self.scarb_toolchain.discover()?;
let metadata = self.scarb_toolchain.metadata_raw(&manifest_path, scarb_path);
metadata
.map(|output| ProjectUpdate::Scarb {
crates: extract_crates(&output.metadata),
workspace_dir: output.metadata.workspace.root.into_std_path_buf(),
workspace_manifest_path: output
.metadata
.workspace
.manifest_path
.into_std_path_buf(),
diagnostics: manifest_diagnostics_from_ndjson(&output.stdout),
})
.unwrap_or_else(|error| {
let metadata_messages = collect_scarb_manifest_diagnostics(error);
if scarb_metadata_messages_contain_only_errors(&metadata_messages) {
self.scarb_toolchain.notify_metadata_failed();
}
ProjectUpdate::ScarbMetadataFailed {
manifest_path,
diagnostics: metadata_messages,
}
})
}
Some(ProjectManifestPath::CairoProject(config_path)) => {
assert!(config_path.is_absolute());
let maybe_project_config = ProjectConfig::from_file(&config_path)
.inspect_err(|err| {
let config_path_lossy = config_path.to_string_lossy();
error!("parsing {config_path_lossy} failed: {err:?}");
self.notifier.notify::<ShowMessage>(ShowMessageParams {
typ: MessageType::ERROR,
message: format!(
"Failed to parse: {config_path_lossy}. Project analysis will not \
be available.",
),
});
})
.ok();
ProjectUpdate::CairoProjectToml(Box::new(maybe_project_config))
}
None => ProjectUpdate::NoConfig(project_update_request.file_path),
};
Some(project_update)
}
fn send_project_update(&self, project_update: ProjectUpdate) {
self.response_sender
.send(project_update)
.expect("the receiver was expected to exist in the main event loop");
}
}
struct ProjectUpdateRequest {
file_path: PathBuf,
loaded_manifests: Snapshot<HashSet<PathBuf>>,
}
fn contains_core_from_scarb_cache(
project_config: &ProjectConfig,
scarb_toolchain: &ScarbToolchain,
) -> bool {
project_config
.content
.crate_roots
.get(&CrateIdentifier::from("core"))
.map(|p| project_config.absolute_crate_root(p))
.is_some_and(|core_root| scarb_toolchain.is_from_scarb_cache(&core_root))
}