diff --git a/crates/ide-host/src/lib.rs b/crates/ide-host/src/lib.rs index 2685d7bf1..70fcfb9b4 100644 --- a/crates/ide-host/src/lib.rs +++ b/crates/ide-host/src/lib.rs @@ -86,6 +86,8 @@ pub struct Host { project_requests: RwLock>>, /// Shutdowns of projects closed with [`Host::close_project_detached`]. closing: parking_lot::Mutex>>, + /// One open at a time per window (see [`Host::opening`]). + open_locks: parking_lot::Mutex>>>, } /// How long exit waits for project shutdown before giving up. @@ -135,6 +137,7 @@ impl Host { requests: Default::default(), project_requests: RwLock::default(), closing: Default::default(), + open_locks: Default::default(), }; host.apply_proxy(); host @@ -401,6 +404,13 @@ impl Host { self.requests.cancel(id); } + /// The lock an open of `window` holds from its check for the current project to its + /// insert: two opens racing (a double click, a launch path and a restore) would each start + /// an index, language servers and watchers, and the second would replace the first unclosed. + pub(crate) fn opening(&self, window: &str) -> Arc> { + self.open_locks.lock().entry(window.to_owned()).or_default().clone() + } + pub(crate) fn insert_project(&self, window: &str, project: Arc) { self.projects.write().insert(window.to_owned(), project); } diff --git a/crates/ide-host/src/project.rs b/crates/ide-host/src/project.rs index 6b935077d..0bdd984c9 100644 --- a/crates/ide-host/src/project.rs +++ b/crates/ide-host/src/project.rs @@ -77,6 +77,8 @@ impl Host { lap = std::time::Instant::now(); }; let root = dunce::canonicalize(root)?; + let opening = self.opening(window); + let _opening = opening.lock(); // Reject a damaged manifest before replacing the currently open project. ide_workspace::workspace::Workspace::load(&root)?; // The window reloaded (or asked again) for the project it already has: keep @@ -569,6 +571,8 @@ impl Host { return Err(IdeError::invalid(format!("{} is a folder", file.display()))); } let dir = file.parent().map(Path::to_path_buf).ok_or_else(|| IdeError::invalid("no parent folder"))?; + let opening = self.opening(window); + let _opening = opening.lock(); if let Ok(current) = self.project(window) && current.light { diff --git a/crates/ide-host/tests/open_project.rs b/crates/ide-host/tests/open_project.rs new file mode 100644 index 000000000..37ec7c417 --- /dev/null +++ b/crates/ide-host/tests/open_project.rs @@ -0,0 +1,46 @@ +//! Opening the project a window is already opening reuses it: racing opens (a double +//! click, a launch path and a restore) must not start a second index and language servers. + +use ide_core::NullSink; +use ide_host::{Host, HostConfig}; +use std::sync::{Arc, Barrier}; + +#[test] +fn racing_opens_of_one_window_share_one_project() { + let tmp = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap().join("project"); + std::fs::create_dir_all(root.join("src")).unwrap(); + std::fs::write(root.join("src/Main.java"), "class Main {}\n").unwrap(); + let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap(); + let _guard = runtime.enter(); + let host = Arc::new(Host::new(HostConfig { + config_dir: tmp.path().join("config"), + data_dir: tmp.path().join("data"), + local_data_dir: tmp.path().join("data"), + languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()), + syntax: Arc::default(), + sink_for: Box::new(|_| Arc::new(NullSink)), + owner: |w| w, + runtime: runtime.handle().clone(), + project_environment: false, + })); + host.trust.decide(&root, true, false).unwrap(); + + let start = Arc::new(Barrier::new(4)); + let opens: Vec<_> = (0..4) + .map(|_| { + let (host, root, start) = (host.clone(), root.clone(), start.clone()); + std::thread::spawn(move || { + start.wait(); + host.open_project("main", &root).unwrap() + }) + }) + .collect(); + let projects: Vec<_> = opens.into_iter().map(|t| t.join().unwrap()).collect(); + + let current = host.project("main").unwrap(); + for project in &projects { + assert!(Arc::ptr_eq(project, ¤t), "every open returns the window's one project"); + } + host.close_project("main"); +}