ZIm/crates/git_ui/src/remote_output_toast.rs
Mikayla Maki 74fc52d5ce
Git Beta: Fix a few cases of empty toasts showing up (#25985)
Improve parsing of git remote outputs

Release Notes:

- N/A
2025-03-04 02:16:50 +00:00

227 lines
8.5 KiB
Rust

use std::{ops::Range, time::Duration};
use git::repository::{Remote, RemoteCommandOutput};
use gpui::{
DismissEvent, EventEmitter, FocusHandle, Focusable, HighlightStyle, InteractiveText,
StyledText, Task, UnderlineStyle, WeakEntity,
};
use itertools::Itertools;
use linkify::{LinkFinder, LinkKind};
use ui::{
div, h_flex, px, v_flex, vh, Clickable, Color, Context, FluentBuilder, Icon, IconButton,
IconName, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
Render, SharedString, Styled, StyledExt, Window,
};
use workspace::{
notifications::{Notification, NotificationId},
Workspace,
};
pub enum RemoteAction {
Fetch,
Pull,
Push(Remote),
}
struct InfoFromRemote {
name: SharedString,
remote_text: SharedString,
links: Vec<Range<usize>>,
}
pub struct RemoteOutputToast {
_workspace: WeakEntity<Workspace>,
_id: NotificationId,
message: SharedString,
remote_info: Option<InfoFromRemote>,
_dismiss_task: Task<()>,
focus_handle: FocusHandle,
}
impl Focusable for RemoteOutputToast {
fn focus_handle(&self, _cx: &ui::App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Notification for RemoteOutputToast {}
const REMOTE_OUTPUT_TOAST_SECONDS: u64 = 5;
impl RemoteOutputToast {
pub fn new(
action: RemoteAction,
output: RemoteCommandOutput,
id: NotificationId,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
let task = cx.spawn({
let workspace = workspace.clone();
let id = id.clone();
|_, mut cx| async move {
cx.background_executor()
.timer(Duration::from_secs(REMOTE_OUTPUT_TOAST_SECONDS))
.await;
workspace
.update(&mut cx, |workspace, cx| {
workspace.dismiss_notification(&id, cx);
})
.ok();
}
});
let mut message: SharedString;
let remote;
match action {
RemoteAction::Fetch | RemoteAction::Pull => {
if output.is_empty() {
message = "Up to date".into();
} else {
message = output.stderr.into();
}
remote = None;
}
RemoteAction::Push(remote_ref) => {
message = output.stdout.trim().to_string().into();
if message.is_empty() {
message = output.stderr.trim().to_string().into();
if message.is_empty() {
message = "Push Successful".into();
}
remote = None;
} else {
let remote_message = get_remote_lines(&output.stderr);
remote = if remote_message.is_empty() {
None
} else {
let finder = LinkFinder::new();
let links = finder
.links(&remote_message)
.filter(|link| *link.kind() == LinkKind::Url)
.map(|link| link.start()..link.end())
.collect_vec();
Some(InfoFromRemote {
name: remote_ref.name,
remote_text: remote_message.into(),
links,
})
}
}
}
}
Self {
_workspace: workspace,
_id: id,
message,
remote_info: remote,
_dismiss_task: task,
focus_handle: cx.focus_handle(),
}
}
}
impl Render for RemoteOutputToast {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
div()
.occlude()
.w_full()
.max_h(vh(0.8, window))
.elevation_3(cx)
.child(
v_flex()
.p_3()
.overflow_hidden()
.child(
h_flex()
.justify_between()
.items_start()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::GitBranch).color(Color::Default))
.child(Label::new("Git")),
)
.child(h_flex().child(
IconButton::new("close", IconName::Close).on_click(
cx.listener(|_, _, _, cx| cx.emit(gpui::DismissEvent)),
),
)),
)
.child(Label::new(self.message.clone()).size(LabelSize::Default))
.when_some(self.remote_info.as_ref(), |this, remote_info| {
this.child(
div()
.border_1()
.border_color(Color::Muted.color(cx))
.rounded_lg()
.text_sm()
.mt_1()
.p_1()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::Cloud).color(Color::Default))
.child(
Label::new(remote_info.name.clone())
.size(LabelSize::Default),
),
)
.map(|div| {
let styled_text =
StyledText::new(remote_info.remote_text.clone())
.with_highlights(remote_info.links.iter().map(
|link| {
(
link.clone(),
HighlightStyle {
underline: Some(UnderlineStyle {
thickness: px(1.0),
..Default::default()
}),
..Default::default()
},
)
},
));
let this = cx.weak_entity();
let text = InteractiveText::new("remote-message", styled_text)
.on_click(
remote_info.links.clone(),
move |ix, _window, cx| {
this.update(cx, |this, cx| {
if let Some(remote_info) = &this.remote_info {
cx.open_url(
&remote_info.remote_text
[remote_info.links[ix].clone()],
)
}
})
.ok();
},
);
div.child(text)
}),
)
}),
)
}
}
impl EventEmitter<DismissEvent> for RemoteOutputToast {}
fn get_remote_lines(output: &str) -> String {
output
.lines()
.filter_map(|line| line.strip_prefix("remote:"))
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("\n")
}