Cargo.lock 🔗
@@ -96,6 +96,7 @@ dependencies = [
"auto_update",
"editor",
"extension_host",
+ "fs",
"futures 0.3.31",
"gpui",
"language",
Alvaro Parker created
Adds a simple notification when cloning a repo using the integrated git
clone on Zed. Before this, the user had no feedback after starting the
cloning action.
Demo:
https://github.com/user-attachments/assets/72fcdf1b-fc99-4fe5-8db2-7c30b170f12f
Not sure about that icon I'm using for the animation, but that can be
easily changed.
Release Notes:
- Added notification when cloning a repo from zed
Cargo.lock | 1
crates/activity_indicator/Cargo.toml | 1
crates/activity_indicator/src/activity_indicator.rs | 42 ++++++
crates/fs/src/fs.rs | 90 ++++++++++++++
4 files changed, 130 insertions(+), 4 deletions(-)
@@ -96,6 +96,7 @@ dependencies = [
"auto_update",
"editor",
"extension_host",
+ "fs",
"futures 0.3.31",
"gpui",
"language",
@@ -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
@@ -51,6 +51,7 @@ pub struct ActivityIndicator {
project: Entity<Project>,
auto_updater: Option<Entity<AutoUpdater>>,
context_menu_handle: PopoverMenuHandle<ContextMenu>,
+ fs_jobs: Vec<fs::JobInfo>,
}
#[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();
@@ -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<bool>;
+ fn subscribe_to_jobs(&self) -> JobEventReceiver;
#[cfg(any(test, feature = "test-support"))]
fn as_fake(&self) -> Arc<FakeFs> {
@@ -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<JobEvent>;
+pub type JobEventReceiver = futures::channel::mpsc::UnboundedReceiver<JobEvent>;
+
+struct JobTracker {
+ id: JobId,
+ subscribers: Arc<Mutex<Vec<JobEventSender>>>,
+}
+
+impl JobTracker {
+ fn new(info: JobInfo, subscribers: Arc<Mutex<Vec<JobEventSender>>>) -> 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<MTime> for proto::Timestamp {
pub struct RealFs {
bundled_git_binary_path: Option<PathBuf>,
executor: BackgroundExecutor,
+ next_job_id: Arc<AtomicUsize>,
+ job_event_subscribers: Arc<Mutex<Vec<JobEventSender>>>,
}
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<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>,
Arc<dyn Watcher>,
) {
- 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<PathBuf, usize>,
moves: std::collections::HashMap<u64, PathBuf>,
+ job_event_subscribers: Arc<Mutex<Vec<JobEventSender>>>,
}
#[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<FakeFs> {
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");