git: Add notification to git clone (#41712)

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
This commit is contained in:
Alvaro Parker
2025-11-11 01:02:13 -05:00
committed by GitHub
parent 9e717c7711
commit f90d0789fb
4 changed files with 130 additions and 4 deletions
Generated
+1
View File
@@ -96,6 +96,7 @@ dependencies = [
"auto_update",
"editor",
"extension_host",
"fs",
"futures 0.3.31",
"gpui",
"language",
+1
View File
@@ -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();
+87 -3
View File
@@ -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");