diff --git a/Cargo.lock b/Cargo.lock index bee290f2f17ffba973d432272c91344b8caa99f3..a3300a818c12f39406cc39848cae86eeb26a0a56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,7 @@ dependencies = [ "auto_update", "editor", "extension_host", + "fs", "futures 0.3.31", "gpui", "language", diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index 4e604b452122c5a8e38b2d02b54f4ee639817ab4..99ae5b5b077a14c0909737d64935220698a007c7 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -17,6 +17,7 @@ anyhow.workspace = true auto_update.workspace = true editor.workspace = true extension_host.workspace = true +fs.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 09cc2fb9568ca01748435c73fd8834efdbb50839..5cb4e1c6153154782bf10447c13c3a9017cbcce7 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -51,6 +51,7 @@ pub struct ActivityIndicator { project: Entity, auto_updater: Option>, context_menu_handle: PopoverMenuHandle, + fs_jobs: Vec, } #[derive(Debug)] @@ -99,6 +100,27 @@ impl ActivityIndicator { }) .detach(); + let fs = project.read(cx).fs().clone(); + let mut job_events = fs.subscribe_to_jobs(); + cx.spawn(async move |this, cx| { + while let Some(job_event) = job_events.next().await { + this.update(cx, |this: &mut ActivityIndicator, cx| { + match job_event { + fs::JobEvent::Started { info } => { + this.fs_jobs.retain(|j| j.id != info.id); + this.fs_jobs.push(info); + } + fs::JobEvent::Completed { id } => { + this.fs_jobs.retain(|j| j.id != id); + } + } + cx.notify(); + })?; + } + anyhow::Ok(()) + }) + .detach(); + cx.subscribe( &project.read(cx).lsp_store(), |activity_indicator, _, event, cx| { @@ -201,7 +223,8 @@ impl ActivityIndicator { statuses: Vec::new(), project: project.clone(), auto_updater, - context_menu_handle: Default::default(), + context_menu_handle: PopoverMenuHandle::default(), + fs_jobs: Vec::new(), } }); @@ -432,6 +455,23 @@ impl ActivityIndicator { }); } + // Show any long-running fs command + for fs_job in &self.fs_jobs { + if Instant::now().duration_since(fs_job.start) >= GIT_OPERATION_DELAY { + return Some(Content { + icon: Some( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Small) + .with_rotate_animation(2) + .into_any_element(), + ), + message: fs_job.message.clone().into(), + on_click: None, + tooltip_message: None, + }); + } + } + // Show any language server installation info. let mut downloading = SmallVec::<[_; 3]>::new(); let mut checking_for_update = SmallVec::<[_; 3]>::new(); diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index b8714505093f03828e3d8783204ede61bb0989b0..33cc83a7886349a537a87d4b6c8bb3f5211608fc 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -4,6 +4,10 @@ mod mac_watcher; #[cfg(not(target_os = "macos"))] pub mod fs_watcher; +use parking_lot::Mutex; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Instant; + use anyhow::{Context as _, Result, anyhow}; #[cfg(any(target_os = "linux", target_os = "freebsd"))] use ashpd::desktop::trash; @@ -12,6 +16,7 @@ use gpui::App; use gpui::BackgroundExecutor; use gpui::Global; use gpui::ReadGlobal as _; +use gpui::SharedString; use std::borrow::Cow; use util::command::new_smol_command; @@ -51,8 +56,7 @@ use git::{ repository::{RepoPath, repo_path}, status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus}, }; -#[cfg(any(test, feature = "test-support"))] -use parking_lot::Mutex; + #[cfg(any(test, feature = "test-support"))] use smol::io::AsyncReadExt; #[cfg(any(test, feature = "test-support"))] @@ -148,6 +152,7 @@ pub trait Fs: Send + Sync { async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>; fn is_fake(&self) -> bool; async fn is_case_sensitive(&self) -> Result; + fn subscribe_to_jobs(&self) -> JobEventReceiver; #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> Arc { @@ -215,6 +220,55 @@ pub struct Metadata { #[serde(transparent)] pub struct MTime(SystemTime); +pub type JobId = usize; + +#[derive(Clone, Debug)] +pub struct JobInfo { + pub start: Instant, + pub message: SharedString, + pub id: JobId, +} + +#[derive(Debug, Clone)] +pub enum JobEvent { + Started { info: JobInfo }, + Completed { id: JobId }, +} + +pub type JobEventSender = futures::channel::mpsc::UnboundedSender; +pub type JobEventReceiver = futures::channel::mpsc::UnboundedReceiver; + +struct JobTracker { + id: JobId, + subscribers: Arc>>, +} + +impl JobTracker { + fn new(info: JobInfo, subscribers: Arc>>) -> Self { + let id = info.id; + { + let mut subs = subscribers.lock(); + subs.retain(|sender| { + sender + .unbounded_send(JobEvent::Started { info: info.clone() }) + .is_ok() + }); + } + Self { id, subscribers } + } +} + +impl Drop for JobTracker { + fn drop(&mut self) { + let mut subs = self.subscribers.lock(); + subs.retain(|sender| { + sender + .unbounded_send(JobEvent::Completed { id: self.id }) + .is_ok() + }); + } +} + impl MTime { /// Conversion intended for persistence and testing. pub fn from_seconds_and_nanos(secs: u64, nanos: u32) -> Self { @@ -257,6 +311,8 @@ impl From for proto::Timestamp { pub struct RealFs { bundled_git_binary_path: Option, executor: BackgroundExecutor, + next_job_id: Arc, + job_event_subscribers: Arc>>, } pub trait FileHandle: Send + Sync + std::fmt::Debug { @@ -361,6 +417,8 @@ impl RealFs { Self { bundled_git_binary_path: git_binary_path, executor, + next_job_id: Arc::new(AtomicUsize::new(0)), + job_event_subscribers: Arc::new(Mutex::new(Vec::new())), } } } @@ -862,7 +920,6 @@ impl Fs for RealFs { Pin>>>, Arc, ) { - use parking_lot::Mutex; use util::{ResultExt as _, paths::SanitizedPath}; let (tx, rx) = smol::channel::unbounded(); @@ -959,6 +1016,15 @@ impl Fs for RealFs { } async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()> { + let job_id = self.next_job_id.fetch_add(1, Ordering::SeqCst); + let job_info = JobInfo { + id: job_id, + start: Instant::now(), + message: SharedString::from(format!("Cloning {}", repo_url)), + }; + + let _job_tracker = JobTracker::new(job_info, self.job_event_subscribers.clone()); + let output = new_smol_command("git") .current_dir(abs_work_directory) .args(&["clone", repo_url]) @@ -979,6 +1045,12 @@ impl Fs for RealFs { false } + fn subscribe_to_jobs(&self) -> JobEventReceiver { + let (sender, receiver) = futures::channel::mpsc::unbounded(); + self.job_event_subscribers.lock().push(sender); + receiver + } + /// Checks whether the file system is case sensitive by attempting to create two files /// that have the same name except for the casing. /// @@ -1049,6 +1121,7 @@ struct FakeFsState { read_dir_call_count: usize, path_write_counts: std::collections::HashMap, moves: std::collections::HashMap, + job_event_subscribers: Arc>>, } #[cfg(any(test, feature = "test-support"))] @@ -1333,6 +1406,7 @@ impl FakeFs { metadata_call_count: 0, path_write_counts: Default::default(), moves: Default::default(), + job_event_subscribers: Arc::new(Mutex::new(Vec::new())), })), }); @@ -2587,6 +2661,12 @@ impl Fs for FakeFs { Ok(true) } + fn subscribe_to_jobs(&self) -> JobEventReceiver { + let (sender, receiver) = futures::channel::mpsc::unbounded(); + self.state.lock().job_event_subscribers.lock().push(sender); + receiver + } + #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> Arc { self.this.upgrade().unwrap() @@ -3201,6 +3281,8 @@ mod tests { let fs = RealFs { bundled_git_binary_path: None, executor, + next_job_id: Arc::new(AtomicUsize::new(0)), + job_event_subscribers: Arc::new(Mutex::new(Vec::new())), }; let temp_dir = TempDir::new().unwrap(); let file_to_be_replaced = temp_dir.path().join("file.txt"); @@ -3219,6 +3301,8 @@ mod tests { let fs = RealFs { bundled_git_binary_path: None, executor, + next_job_id: Arc::new(AtomicUsize::new(0)), + job_event_subscribers: Arc::new(Mutex::new(Vec::new())), }; let temp_dir = TempDir::new().unwrap(); let file_to_be_replaced = temp_dir.path().join("file.txt");