Rework git toasts (#26420)

The notifications from git output could take up variable amounts of
screen space, and they were quite obnoxious when a git command printed
lots of output, such as fetching many new branches or verbose push
hooks.

This change makes the push/pull/fetch buttons trigger a small
notification toast, based on the output of the command that was ran. For
errors or commands with more output the user may want to see, there's an
"Open Log" button which opens a new buffer with the output of that
command.

It also uses this behavior for long error notifications for other git
commands like `commit` and `checkout`. The output of those commands can
be quite long due to arbitrary githooks running.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
Julia Ryan 2025-03-11 14:39:29 -07:00 committed by GitHub
parent e8208643bb
commit 2b94a35aaa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 415 additions and 418 deletions

View file

@ -1,12 +1,70 @@
use std::time::{Duration, Instant};
use std::{
rc::Rc,
time::{Duration, Instant},
};
use gpui::{AnyView, DismissEvent, Entity, FocusHandle, ManagedView, Subscription, Task};
use gpui::{actions, AnyView, DismissEvent, Entity, FocusHandle, ManagedView, Subscription, Task};
use ui::{animation::DefaultAnimations, prelude::*};
use crate::Workspace;
const DEFAULT_TOAST_DURATION: Duration = Duration::from_millis(2400);
const MINIMUM_RESUME_DURATION: Duration = Duration::from_millis(800);
pub trait ToastView: ManagedView {}
actions!(toast, [RunAction]);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
workspace.register_action(|_workspace, _: &RunAction, window, cx| {
let workspace = cx.entity();
let window = window.window_handle();
cx.defer(move |cx| {
let action = workspace
.read(cx)
.toast_layer
.read(cx)
.active_toast
.as_ref()
.and_then(|active_toast| active_toast.action.clone());
if let Some(on_click) = action.and_then(|action| action.on_click) {
window
.update(cx, |_, window, cx| {
on_click(window, cx);
})
.ok();
}
});
});
})
.detach();
}
pub trait ToastView: ManagedView {
fn action(&self) -> Option<ToastAction>;
}
#[derive(Clone)]
pub struct ToastAction {
pub id: ElementId,
pub label: SharedString,
pub on_click: Option<Rc<dyn Fn(&mut Window, &mut App) + 'static>>,
}
impl ToastAction {
pub fn new(
label: SharedString,
on_click: Option<Rc<dyn Fn(&mut Window, &mut App) + 'static>>,
) -> Self {
let id = ElementId::Name(label.clone());
Self {
id,
label,
on_click,
}
}
}
trait ToastViewHandle {
fn view(&self) -> AnyView;
@ -20,6 +78,7 @@ impl<V: ToastView> ToastViewHandle for Entity<V> {
pub struct ActiveToast {
toast: Box<dyn ToastViewHandle>,
action: Option<ToastAction>,
_subscriptions: [Subscription; 1],
focus_handle: FocusHandle,
}
@ -50,52 +109,43 @@ impl ToastLayer {
}
}
pub fn toggle_toast<V>(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
new_toast: Entity<V>,
) where
pub fn toggle_toast<V>(&mut self, cx: &mut Context<Self>, new_toast: Entity<V>)
where
V: ToastView,
{
if let Some(active_toast) = &self.active_toast {
let is_close = active_toast.toast.view().downcast::<V>().is_ok();
let did_close = self.hide_toast(window, cx);
let did_close = self.hide_toast(cx);
if is_close || !did_close {
return;
}
}
self.show_toast(new_toast, window, cx);
self.show_toast(new_toast, cx);
}
pub fn show_toast<V>(
&mut self,
new_toast: Entity<V>,
window: &mut Window,
cx: &mut Context<Self>,
) where
pub fn show_toast<V>(&mut self, new_toast: Entity<V>, cx: &mut Context<Self>)
where
V: ToastView,
{
let action = new_toast.read(cx).action();
let focus_handle = cx.focus_handle();
self.active_toast = Some(ActiveToast {
toast: Box::new(new_toast.clone()),
_subscriptions: [cx.subscribe_in(
&new_toast,
window,
|this, _, _: &DismissEvent, window, cx| {
this.hide_toast(window, cx);
},
)],
action,
_subscriptions: [cx.subscribe(&new_toast, |this, _, _: &DismissEvent, cx| {
this.hide_toast(cx);
})],
focus_handle,
});
self.start_dismiss_timer(DEFAULT_TOAST_DURATION, window, cx);
self.start_dismiss_timer(DEFAULT_TOAST_DURATION, cx);
cx.notify();
}
pub fn hide_toast(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> bool {
pub fn hide_toast(&mut self, cx: &mut Context<Self>) -> bool {
self.active_toast.take();
cx.notify();
true
@ -128,12 +178,7 @@ impl ToastLayer {
}
/// Starts a timer to automatically dismiss the toast after the specified duration
pub fn start_dismiss_timer(
&mut self,
duration: Duration,
_window: &mut Window,
cx: &mut Context<Self>,
) {
pub fn start_dismiss_timer(&mut self, duration: Duration, cx: &mut Context<Self>) {
self.clear_dismiss_timer(cx);
let instant_started = std::time::Instant::now();
@ -141,11 +186,7 @@ impl ToastLayer {
cx.background_executor().timer(duration).await;
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.active_toast.take();
cx.notify();
})
.ok();
this.update(&mut cx, |this, cx| this.hide_toast(cx)).ok();
}
});
@ -158,11 +199,11 @@ impl ToastLayer {
}
/// Restarts the dismiss timer with a new duration
pub fn restart_dismiss_timer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
pub fn restart_dismiss_timer(&mut self, cx: &mut Context<Self>) {
let Some(duration) = self.duration_remaining else {
return;
};
self.start_dismiss_timer(duration, window, cx);
self.start_dismiss_timer(duration, cx);
cx.notify();
}
@ -194,14 +235,14 @@ impl Render for ToastLayer {
h_flex()
.id("active-toast-container")
.occlude()
.on_hover(move |hover_start, window, cx| {
.on_hover(move |hover_start, _window, cx| {
let Some(this) = handle.upgrade() else {
return;
};
if *hover_start {
this.update(cx, |this, _| this.pause_dismiss_timer());
} else {
this.update(cx, |this, cx| this.restart_dismiss_timer(window, cx));
this.update(cx, |this, cx| this.restart_dismiss_timer(cx));
}
cx.stop_propagation();
})

View file

@ -14,7 +14,7 @@ mod toast_layer;
mod toolbar;
mod workspace_settings;
pub use toast_layer::{ToastLayer, ToastView};
pub use toast_layer::{RunAction, ToastAction, ToastLayer, ToastView};
use anyhow::{anyhow, Context as _, Result};
use call::{call_settings::CallSettings, ActiveCall};
@ -384,6 +384,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
init_settings(cx);
component::init();
theme_preview::init(cx);
toast_layer::init(cx);
cx.on_action(Workspace::close_global);
cx.on_action(reload);
@ -5016,15 +5017,9 @@ impl Workspace {
})
}
pub fn toggle_status_toast<V: ToastView>(
&mut self,
window: &mut Window,
cx: &mut App,
entity: Entity<V>,
) {
self.toast_layer.update(cx, |toast_layer, cx| {
toast_layer.toggle_toast(window, cx, entity)
})
pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
self.toast_layer
.update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
}
pub fn toggle_centered_layout(