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,15 +1,8 @@
use std::sync::Arc;
use std::rc::Rc;
use gpui::{ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement};
use ui::prelude::*;
use workspace::ToastView;
#[derive(Clone)]
pub struct ToastAction {
id: ElementId,
label: SharedString,
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement};
use ui::{prelude::*, Tooltip};
use workspace::{ToastAction, ToastView};
#[derive(Clone, Copy)]
pub struct ToastIcon {
@ -40,49 +33,33 @@ impl From<IconName> for ToastIcon {
}
}
impl ToastAction {
pub fn new(
label: SharedString,
on_click: Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
) -> Self {
let id = ElementId::Name(label.clone());
Self {
id,
label,
on_click,
}
}
}
#[derive(IntoComponent)]
#[component(scope = "Notification")]
pub struct StatusToast {
icon: Option<ToastIcon>,
text: SharedString,
action: Option<ToastAction>,
this_handle: Entity<Self>,
focus_handle: FocusHandle,
}
impl StatusToast {
pub fn new(
text: impl Into<SharedString>,
window: &mut Window,
cx: &mut App,
f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
f: impl FnOnce(Self, &mut Context<Self>) -> Self,
) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
window.refresh();
f(
Self {
text: text.into(),
icon: None,
action: None,
this_handle: cx.entity(),
focus_handle,
},
window,
cx,
)
})
@ -96,9 +73,18 @@ impl StatusToast {
pub fn action(
mut self,
label: impl Into<SharedString>,
f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
f: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
self.action = Some(ToastAction::new(label.into(), Some(Arc::new(f))));
let this_handle = self.this_handle.clone();
self.action = Some(ToastAction::new(
label.into(),
Some(Rc::new(move |window, cx| {
this_handle.update(cx, |_, cx| {
cx.emit(DismissEvent);
});
f(window, cx);
})),
));
self
}
}
@ -122,18 +108,24 @@ impl Render for StatusToast {
.when_some(self.action.as_ref(), |this, action| {
this.child(
Button::new(action.id.clone(), action.label.clone())
.tooltip(Tooltip::for_action_title(
action.label.clone(),
&workspace::RunAction,
))
.color(Color::Muted)
.when_some(action.on_click.clone(), |el, handler| {
el.on_click(move |click_event, window, cx| {
handler(click_event, window, cx)
})
el.on_click(move |_click_event, window, cx| handler(window, cx))
}),
)
})
}
}
impl ToastView for StatusToast {}
impl ToastView for StatusToast {
fn action(&self) -> Option<ToastAction> {
self.action.clone()
}
}
impl Focusable for StatusToast {
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
@ -144,56 +136,44 @@ impl Focusable for StatusToast {
impl EventEmitter<DismissEvent> for StatusToast {}
impl ComponentPreview for StatusToast {
fn preview(window: &mut Window, cx: &mut App) -> AnyElement {
let text_example = StatusToast::new("Operation completed", window, cx, |this, _, _| this);
fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
let text_example = StatusToast::new("Operation completed", cx, |this, _| this);
let action_example =
StatusToast::new("Update ready to install", window, cx, |this, _, cx| {
this.action("Restart", cx.listener(|_, _, _, _| {}))
});
let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| {
this.action("Restart", |_, _| {})
});
let icon_example = StatusToast::new(
"Nathan Sobo accepted your contact request",
window,
cx,
|this, _, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)),
|this, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)),
);
let success_example = StatusToast::new(
"Pushed 4 changes to `zed/main`",
window,
cx,
|this, _, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Success)),
);
let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| {
this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
});
let error_example = StatusToast::new(
"git push: Couldn't find remote origin `iamnbutler/zed`",
window,
cx,
|this, _, cx| {
|this, _cx| {
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
.action("More Info", cx.listener(|_, _, _, _| {}))
.action("More Info", |_, _| {})
},
);
let warning_example =
StatusToast::new("You have outdated settings", window, cx, |this, _, cx| {
this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
.action("More Info", cx.listener(|_, _, _, _| {}))
});
let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| {
this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
.action("More Info", |_, _| {})
});
let pr_example = StatusToast::new(
"`zed/new-notification-system` created!",
window,
cx,
|this, _, cx| {
let pr_example =
StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
.action(
"Open Pull Request",
cx.listener(|_, _, _, cx| cx.open_url("https://github.com/")),
)
},
);
.action("Open Pull Request", |_, cx| {
cx.open_url("https://github.com/")
})
});
v_flex()
.gap_6()