Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LS: Send project model updates through channel #6734

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 3 additions & 24 deletions crates/cairo-lang-language-server/src/lang/db/swapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ use cairo_lang_utils::{Intern, LookupIntern};
use lsp_types::Url;
use tracing::{error, warn};

use crate::config::Config;
use crate::lang::db::AnalysisDatabase;
use crate::lang::lsp::LsProtoGroup;
use crate::lang::proc_macros::db::ProcMacroGroup;
use crate::project::ProjectController;
use crate::server::client::Notifier;
use crate::toolchain::scarb::ScarbToolchain;
use crate::{Tricks, env_config};

Expand Down Expand Up @@ -57,9 +55,7 @@ impl AnalysisDatabaseSwapper {
&mut self,
db: &mut AnalysisDatabase,
open_files: &HashSet<Url>,
config: &Config,
tricks: &Tricks,
notifier: &Notifier,
project_controller: &mut ProjectController,
) {
let Ok(elapsed) = self.last_replace.elapsed() else {
Expand All @@ -76,7 +72,7 @@ impl AnalysisDatabaseSwapper {
return;
}

self.swap(db, open_files, config, tricks, notifier, project_controller)
self.swap(db, open_files, tricks, project_controller)
}

/// Swaps the database.
Expand All @@ -85,9 +81,7 @@ impl AnalysisDatabaseSwapper {
&mut self,
db: &mut AnalysisDatabase,
open_files: &HashSet<Url>,
config: &Config,
tricks: &Tricks,
notifier: &Notifier,
project_controller: &mut ProjectController,
) {
let Ok(new_db) = catch_unwind(AssertUnwindSafe(|| {
Expand All @@ -96,13 +90,7 @@ impl AnalysisDatabaseSwapper {
let mut new_db = AnalysisDatabase::new(tricks);
self.migrate_proc_macro_state(&mut new_db, db);
self.migrate_file_overrides(&mut new_db, db, open_files);
self.detect_crates_for_open_files(
&mut new_db,
open_files,
config,
notifier,
project_controller,
);
self.detect_crates_for_open_files(open_files, project_controller);
new_db
})) else {
error!("caught panic when preparing new db for swap");
Expand Down Expand Up @@ -156,10 +144,7 @@ impl AnalysisDatabaseSwapper {
/// Ensures all open files have their crates detected to regenerate crates state.
fn detect_crates_for_open_files(
&self,
new_db: &mut AnalysisDatabase,
open_files: &HashSet<Url>,
config: &Config,
notifier: &Notifier,
project_controller: &mut ProjectController,
) {
for uri in open_files {
Expand All @@ -168,13 +153,7 @@ impl AnalysisDatabaseSwapper {
continue;
};

project_controller.detect_crate_for(
new_db,
&self.scarb_toolchain,
config,
&file_path,
notifier,
);
project_controller.update_project_for(&self.scarb_toolchain, &file_path);
}
}
}
40 changes: 22 additions & 18 deletions crates/cairo-lang-language-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use anyhow::Result;
use cairo_lang_filesystem::db::FilesGroup;
use cairo_lang_filesystem::ids::FileLongId;
use cairo_lang_semantic::plugin::PluginSuite;
use crossbeam::select;
use crossbeam::channel::{Receiver, select_biased};
use lsp_server::Message;
use lsp_types::RegistrationParams;
use salsa::{Database, Durability};
Expand All @@ -60,6 +60,7 @@ use crate::lsp::capabilities::server::{
collect_dynamic_registrations, collect_server_capabilities,
};
use crate::lsp::result::LSPResult;
use crate::project::{ProjectController, ProjectUpdate};
use crate::server::client::{Notifier, Requester, Responder};
use crate::server::connection::{Connection, ConnectionInitializer};
use crate::server::panic::is_cancelled;
Expand Down Expand Up @@ -273,6 +274,7 @@ impl Backend {
state.proc_macro_controller.initialize_once(&mut state.db);
let proc_macro_channels = state.proc_macro_controller.init_channels();

let project_updates_receiver = state.project_controller.init_channel();
let mut scheduler = Scheduler::new(&mut state, connection.make_sender());

Self::dispatch_setup_tasks(&mut scheduler);
Expand All @@ -287,7 +289,12 @@ impl Backend {
// we basically never hit such a case in CairoLS in happy paths.
scheduler.on_sync_task(Self::refresh_diagnostics);

let result = Self::event_loop(&connection, proc_macro_channels, scheduler);
let result = Self::event_loop(
&connection,
proc_macro_channels,
scheduler,
project_updates_receiver,
);

// Trigger cancellation in any background tasks that might still be running.
state.db.salsa_runtime_mut().synthetic_write(Durability::LOW);
Expand Down Expand Up @@ -343,11 +350,20 @@ impl Backend {
connection: &Connection,
proc_macro_channels: ProcMacroChannelsReceivers,
mut scheduler: Scheduler<'_>,
project_updates_receiver: Receiver<ProjectUpdate>,
) -> Result<()> {
let incoming = connection.incoming();

loop {
select! {
select_biased! {
// Project updates may significantly change the state, therefore
// they should be handled first in case of multiple operations being ready at once.
// To ensure it, keep project updates channel in the first arm of `select_biased!`.
recv(project_updates_receiver) -> project_update => {
let Ok(project_update) = project_update else { break };

scheduler.local(move |state, notifier, _, _| ProjectController::handle_update(state, notifier, project_update));
}
recv(incoming) -> msg => {
let Ok(msg) = msg else { break };

Expand Down Expand Up @@ -390,13 +406,11 @@ impl Backend {
}

/// Calls [`lang::db::AnalysisDatabaseSwapper::maybe_swap`] to do its work.
fn maybe_swap_database(state: &mut State, notifier: Notifier) {
fn maybe_swap_database(state: &mut State, _notifier: Notifier) {
state.db_swapper.maybe_swap(
&mut state.db,
&state.open_files,
&state.config,
&state.tricks,
&notifier,
&mut state.project_controller,
);
}
Expand All @@ -407,24 +421,14 @@ impl Backend {
}

/// Reload crate detection for all open files.
fn reload(
state: &mut State,
notifier: &Notifier,
requester: &mut Requester<'_>,
) -> LSPResult<()> {
fn reload(state: &mut State, requester: &mut Requester<'_>) -> LSPResult<()> {
state.project_controller.clear_loaded_workspaces();
state.config.reload(requester, &state.client_capabilities)?;

for uri in state.open_files.iter() {
let Some(file_id) = state.db.file_for_url(uri) else { continue };
if let FileLongId::OnDisk(file_path) = state.db.lookup_intern_file(file_id) {
state.project_controller.detect_crate_for(
&mut state.db,
&state.scarb_toolchain,
&state.config,
&file_path,
notifier,
);
state.project_controller.update_project_for(&state.scarb_toolchain, &file_path);
}
}

Expand Down
113 changes: 75 additions & 38 deletions crates/cairo-lang-language-server/src/project/mod.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,49 @@
use std::cell::OnceCell;
use std::collections::HashSet;
use std::path::{Path, PathBuf};

use anyhow::Context;
use cairo_lang_compiler::db::validate_corelib;
use cairo_lang_compiler::project::{setup_project, update_crate_roots_from_project_config};
use cairo_lang_project::ProjectConfig;
use tracing::{error, trace, warn};
use crossbeam::channel::{Receiver, Sender};
use tracing::{debug, error, trace, warn};

pub use self::crate_data::Crate;
pub use self::project_manifest_path::*;
use crate::config::Config;
use crate::lang::db::AnalysisDatabase;
use crate::lsp::ext::{CorelibVersionMismatch, ScarbMetadataFailed};
use crate::project::scarb::{get_workspace_members_manifests, update_crate_roots};
use crate::lsp::ext::CorelibVersionMismatch;
use crate::project::scarb::{extract_crates, get_workspace_members_manifests};
use crate::project::unmanaged_core_crate::try_to_init_unmanaged_core;
use crate::server::client::Notifier;
use crate::state::Owned;
use crate::state::{Owned, State};
use crate::toolchain::scarb::ScarbToolchain;

mod crate_data;
mod project_manifest_path;
// TODO(mkaput): These two are `pub` temporarily.
pub(crate) mod scarb;
pub(crate) mod unmanaged_core_crate;
mod scarb;
mod unmanaged_core_crate;

pub struct ProjectController {
loaded_scarb_manifests: Owned<HashSet<PathBuf>>,
sender: OnceCell<Sender<ProjectUpdate>>,
}

impl ProjectController {
pub fn new() -> Self {
ProjectController { loaded_scarb_manifests: Default::default() }
ProjectController { loaded_scarb_manifests: Default::default(), sender: Default::default() }
}

/// Tries to detect the crate root the config that contains a cairo file, and add it to the
/// system.
pub fn init_channel(&mut self) -> Receiver<ProjectUpdate> {
let (sender, receiver) = crossbeam::channel::unbounded();

self.sender.set(sender).expect("failed to set sender once cell");
receiver
}

/// Tries to fetch changes to the project model that are necessary for the file analysis.
#[tracing::instrument(skip_all)]
pub fn detect_crate_for(
&mut self,
db: &mut AnalysisDatabase,
scarb_toolchain: &ScarbToolchain,
config: &Config,
file_path: &Path,
notifier: &Notifier,
) {
match ProjectManifestPath::discover(file_path) {
pub fn update_project_for(&mut self, scarb_toolchain: &ScarbToolchain, file_path: &Path) {
let project_update = match ProjectManifestPath::discover(file_path) {
Some(ProjectManifestPath::Scarb(manifest_path)) => {
if self.loaded_scarb_manifests.contains(&manifest_path) {
trace!("scarb project is already loaded: {}", manifest_path.display());
Expand All @@ -57,48 +56,86 @@ impl ProjectController {
format!("failed to refresh scarb workspace: {}", manifest_path.display())
})
.inspect_err(|err| {
warn!("{err:?}");
notifier.notify::<ScarbMetadataFailed>(());
error!("{err:?}");
})
.ok();

if let Some(metadata) = metadata {
let maybe_crates = if let Some(metadata) = metadata {
self.loaded_scarb_manifests.extend(get_workspace_members_manifests(&metadata));
update_crate_roots(&metadata, db);
Some(extract_crates(&metadata))
} else {
// Try to set up a corelib at least.
try_to_init_unmanaged_core(db, config, scarb_toolchain);
}
None
};

if let Err(result) = validate_corelib(db) {
notifier.notify::<CorelibVersionMismatch>(result.to_string());
}
ProjectUpdate::Scarb(maybe_crates)
}

Some(ProjectManifestPath::CairoProject(config_path)) => {
// The base path of ProjectConfig must be absolute to ensure that all paths in Salsa
// DB will also be absolute.
assert!(config_path.is_absolute());

try_to_init_unmanaged_core(db, config, scarb_toolchain);
let maybe_project_config = ProjectConfig::from_file(&config_path)
// TODO: send failure notification
.inspect_err(|err| error!("{err:?}"))
.ok();
ProjectUpdate::CairoProjectToml(maybe_project_config)
}

None => ProjectUpdate::NoConfig(file_path.to_path_buf()),
};

if let Ok(config) = ProjectConfig::from_file(&config_path) {
update_crate_roots_from_project_config(db, &config);
};
self.sender
.get()
.expect("failed to get sender once cell")
.send(project_update)
.expect("the receiver was expected to exist in the main event loop");
}

pub fn handle_update(state: &mut State, notifier: Notifier, project_update: ProjectUpdate) {
let db = &mut state.db;
match project_update {
ProjectUpdate::Scarb(crates) => {
if let Some(crates) = crates {
debug!("updating crate roots from scarb metadata: {crates:#?}");

for cr in crates {
cr.apply(db);
}
} else {
// Try to set up a corelib at least.
try_to_init_unmanaged_core(db, &state.config, &state.scarb_toolchain);
}
}
ProjectUpdate::CairoProjectToml(maybe_project_config) => {
try_to_init_unmanaged_core(db, &state.config, &state.scarb_toolchain);

None => {
try_to_init_unmanaged_core(db, config, scarb_toolchain);
if let Some(project_config) = maybe_project_config {
update_crate_roots_from_project_config(db, &project_config);
}
}
ProjectUpdate::NoConfig(file_path) => {
try_to_init_unmanaged_core(db, &state.config, &state.scarb_toolchain);

if let Err(err) = setup_project(&mut *db, file_path) {
if let Err(err) = setup_project(&mut *db, &file_path) {
let file_path_s = file_path.to_string_lossy();
error!("error loading file {file_path_s} as a single crate: {err}");
}
}
}

if let Err(result) = validate_corelib(db) {
notifier.notify::<CorelibVersionMismatch>(result.to_string());
}
}

pub fn clear_loaded_workspaces(&mut self) {
self.loaded_scarb_manifests.clear()
}
}

pub enum ProjectUpdate {
Scarb(Option<Vec<Crate>>),
CairoProjectToml(Option<ProjectConfig>),
NoConfig(PathBuf),
}
Loading