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:
parent
e8208643bb
commit
2b94a35aaa
14 changed files with 415 additions and 418 deletions
|
@ -36,6 +36,7 @@ linkme.workspace = true
|
|||
log.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
notifications.workspace = true
|
||||
panel.workspace = true
|
||||
picker.workspace = true
|
||||
postage.workspace = true
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::askpass_modal::AskPassModal;
|
|||
use crate::commit_modal::CommitModal;
|
||||
use crate::git_panel_settings::StatusStyle;
|
||||
use crate::project_diff::Diff;
|
||||
use crate::remote_output_toast::{RemoteAction, RemoteOutputToast};
|
||||
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
||||
use crate::repository_selector::filtered_repository_entries;
|
||||
use crate::{branch_picker, render_remote_button};
|
||||
use crate::{
|
||||
|
@ -64,10 +64,11 @@ use ui::{
|
|||
use util::{maybe, post_inc, ResultExt, TryFutureExt};
|
||||
use workspace::{AppState, OpenOptions, OpenVisible};
|
||||
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, NotificationId},
|
||||
Toast, Workspace,
|
||||
notifications::DetachAndPromptErr,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
actions!(
|
||||
|
@ -948,7 +949,7 @@ impl GitPanel {
|
|||
}
|
||||
result
|
||||
.map_err(|e| {
|
||||
this.show_err_toast(e, cx);
|
||||
this.show_error_toast("checkout", e, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
|
@ -1200,7 +1201,7 @@ impl GitPanel {
|
|||
}
|
||||
result
|
||||
.map_err(|e| {
|
||||
this.show_err_toast(e, cx);
|
||||
this.show_error_toast(if stage { "add" } else { "reset" }, e, cx);
|
||||
})
|
||||
.ok();
|
||||
cx.notify();
|
||||
|
@ -1357,7 +1358,7 @@ impl GitPanel {
|
|||
this.commit_editor
|
||||
.update(cx, |editor, cx| editor.clear(window, cx));
|
||||
}
|
||||
Err(e) => this.show_err_toast(e, cx),
|
||||
Err(e) => this.show_error_toast("commit", e, cx),
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
@ -1399,7 +1400,7 @@ impl GitPanel {
|
|||
editor.set_text(prior_commit.message, window, cx)
|
||||
});
|
||||
}
|
||||
Err(e) => this.show_err_toast(e, cx),
|
||||
Err(e) => this.show_error_toast("reset", e, cx),
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
@ -1611,28 +1612,29 @@ impl GitPanel {
|
|||
telemetry::event!("Git Fetched");
|
||||
let guard = self.start_remote_operation();
|
||||
let askpass = self.askpass_delegate("git fetch", window, cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let fetch = repo.update(&mut cx, |repo, cx| repo.fetch(askpass, cx))?;
|
||||
let this = cx.weak_entity();
|
||||
window
|
||||
.spawn(cx, |mut cx| async move {
|
||||
let fetch = repo.update(&mut cx, |repo, cx| repo.fetch(askpass, cx))?;
|
||||
|
||||
let remote_message = fetch.await?;
|
||||
drop(guard);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
match remote_message {
|
||||
Ok(remote_message) => {
|
||||
this.show_remote_output(RemoteAction::Fetch, remote_message, cx);
|
||||
let remote_message = fetch.await?;
|
||||
drop(guard);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let action = RemoteAction::Fetch;
|
||||
match remote_message {
|
||||
Ok(remote_message) => this.show_remote_output(action, remote_message, cx),
|
||||
Err(e) => {
|
||||
log::error!("Error while fetching {:?}", e);
|
||||
this.show_error_toast(action.name(), e, cx)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error while fetching {:?}", e);
|
||||
this.show_err_toast(e, cx);
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.ok();
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.ok();
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub(crate) fn pull(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
|
@ -1656,7 +1658,7 @@ impl GitPanel {
|
|||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get current remote: {}", e);
|
||||
this.update(&mut cx, |this, cx| this.show_err_toast(e, cx))
|
||||
this.update(&mut cx, |this, cx| this.show_error_toast("pull", e, cx))
|
||||
.ok();
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -1677,13 +1679,12 @@ impl GitPanel {
|
|||
let remote_message = pull.await?;
|
||||
drop(guard);
|
||||
|
||||
let action = RemoteAction::Pull(remote);
|
||||
this.update(&mut cx, |this, cx| match remote_message {
|
||||
Ok(remote_message) => {
|
||||
this.show_remote_output(RemoteAction::Pull, remote_message, cx)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Error while pull {:?}", err);
|
||||
this.show_err_toast(err, cx)
|
||||
Ok(remote_message) => this.show_remote_output(action, remote_message, cx),
|
||||
Err(e) => {
|
||||
log::error!("Error while pulling {:?}", e);
|
||||
this.show_error_toast(action.name(), e, cx)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
@ -1720,7 +1721,7 @@ impl GitPanel {
|
|||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get current remote: {}", e);
|
||||
this.update(&mut cx, |this, cx| this.show_err_toast(e, cx))
|
||||
this.update(&mut cx, |this, cx| this.show_error_toast("push", e, cx))
|
||||
.ok();
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -1747,13 +1748,12 @@ impl GitPanel {
|
|||
let remote_output = push.await?;
|
||||
drop(guard);
|
||||
|
||||
let action = RemoteAction::Push(branch.name, remote);
|
||||
this.update(&mut cx, |this, cx| match remote_output {
|
||||
Ok(remote_message) => {
|
||||
this.show_remote_output(RemoteAction::Push(remote), remote_message, cx);
|
||||
}
|
||||
Ok(remote_message) => this.show_remote_output(action, remote_message, cx),
|
||||
Err(e) => {
|
||||
log::error!("Error while pushing {:?}", e);
|
||||
this.show_err_toast(e, cx);
|
||||
this.show_error_toast(action.name(), e, cx)
|
||||
}
|
||||
})?;
|
||||
|
||||
|
@ -2241,14 +2241,13 @@ impl GitPanel {
|
|||
self.conflicted_count > 0 && self.conflicted_count != self.conflicted_staged_count
|
||||
}
|
||||
|
||||
fn show_err_toast(&self, e: anyhow::Error, cx: &mut App) {
|
||||
fn show_error_toast(&self, action: impl Into<SharedString>, e: anyhow::Error, cx: &mut App) {
|
||||
let action = action.into();
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let notif_id = NotificationId::Named("git-operation-error".into());
|
||||
|
||||
let message = e.to_string().trim().to_string();
|
||||
let toast;
|
||||
if message
|
||||
.matches(git::repository::REMOTE_CANCELLED_BY_USER)
|
||||
.next()
|
||||
|
@ -2256,13 +2255,28 @@ impl GitPanel {
|
|||
{
|
||||
return; // Hide the cancelled by user message
|
||||
} else {
|
||||
toast = Toast::new(notif_id, message).on_click("Open Zed Log", |window, cx| {
|
||||
window.dispatch_action(workspace::OpenLog.boxed_clone(), cx);
|
||||
let project = self.project.clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let workspace_weak = cx.weak_entity();
|
||||
let toast =
|
||||
StatusToast::new(format!("git {} failed", action.clone()), cx, |this, _cx| {
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
.action("View Log", move |window, cx| {
|
||||
let message = message.clone();
|
||||
let project = project.clone();
|
||||
let action = action.clone();
|
||||
workspace_weak
|
||||
.update(cx, move |workspace, cx| {
|
||||
Self::open_output(
|
||||
project, action, workspace, &message, window, cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
});
|
||||
workspace.toggle_status_toast(toast, cx)
|
||||
});
|
||||
}
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(toast, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) {
|
||||
|
@ -2270,16 +2284,62 @@ impl GitPanel {
|
|||
return;
|
||||
};
|
||||
|
||||
let notification_id = NotificationId::Named("git-remote-info".into());
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_notification(notification_id.clone(), cx, |cx| {
|
||||
let workspace = cx.weak_entity();
|
||||
cx.new(|cx| RemoteOutputToast::new(action, info, notification_id, workspace, cx))
|
||||
let SuccessMessage { message, style } = remote_output::format_output(&action, info);
|
||||
let workspace_weak = cx.weak_entity();
|
||||
let operation = action.name();
|
||||
|
||||
let status_toast = StatusToast::new(message, cx, move |this, _cx| {
|
||||
use remote_output::SuccessStyle::*;
|
||||
let project = self.project.clone();
|
||||
match style {
|
||||
Toast { .. } => this,
|
||||
ToastWithLog { output } => this
|
||||
.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
|
||||
.action("View Log", move |window, cx| {
|
||||
let output = output.clone();
|
||||
let project = project.clone();
|
||||
let output =
|
||||
format!("stdout:\n{}\nstderr:\n{}", output.stdout, output.stderr);
|
||||
workspace_weak
|
||||
.update(cx, move |workspace, cx| {
|
||||
Self::open_output(
|
||||
project, operation, workspace, &output, window, cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
PushPrLink { link } => this
|
||||
.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
|
||||
.action("Open Pull Request", move |_, cx| cx.open_url(&link)),
|
||||
}
|
||||
});
|
||||
workspace.toggle_status_toast(status_toast, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn open_output(
|
||||
project: Entity<Project>,
|
||||
operation: impl Into<SharedString>,
|
||||
workspace: &mut Workspace,
|
||||
output: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let operation = operation.into();
|
||||
let buffer = cx.new(|cx| Buffer::local(output, cx));
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_title(format!("Output from git {operation}"), cx);
|
||||
});
|
||||
editor.set_read_only(true);
|
||||
editor
|
||||
});
|
||||
|
||||
workspace.add_item_to_center(Box::new(editor), window, cx);
|
||||
}
|
||||
|
||||
pub fn render_spinner(&self) -> Option<impl IntoElement> {
|
||||
(!self.pending_remote_operations.borrow().is_empty()).then(|| {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
|
|
|
@ -17,7 +17,7 @@ pub mod git_panel;
|
|||
mod git_panel_settings;
|
||||
pub mod picker_prompt;
|
||||
pub mod project_diff;
|
||||
mod remote_output_toast;
|
||||
pub(crate) mod remote_output;
|
||||
pub mod repository_selector;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
|
|
152
crates/git_ui/src/remote_output.rs
Normal file
152
crates/git_ui/src/remote_output.rs
Normal file
|
@ -0,0 +1,152 @@
|
|||
use anyhow::Context as _;
|
||||
use git::repository::{Remote, RemoteCommandOutput};
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use ui::SharedString;
|
||||
use util::ResultExt as _;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RemoteAction {
|
||||
Fetch,
|
||||
Pull(Remote),
|
||||
Push(SharedString, Remote),
|
||||
}
|
||||
|
||||
impl RemoteAction {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
RemoteAction::Fetch => "fetch",
|
||||
RemoteAction::Pull(_) => "pull",
|
||||
RemoteAction::Push(_, _) => "push",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SuccessStyle {
|
||||
Toast,
|
||||
ToastWithLog { output: RemoteCommandOutput },
|
||||
PushPrLink { link: String },
|
||||
}
|
||||
|
||||
pub struct SuccessMessage {
|
||||
pub message: String,
|
||||
pub style: SuccessStyle,
|
||||
}
|
||||
|
||||
pub fn format_output(action: &RemoteAction, output: RemoteCommandOutput) -> SuccessMessage {
|
||||
match action {
|
||||
RemoteAction::Fetch => {
|
||||
if output.stderr.is_empty() {
|
||||
SuccessMessage {
|
||||
message: "Already up to date".into(),
|
||||
style: SuccessStyle::Toast,
|
||||
}
|
||||
} else {
|
||||
SuccessMessage {
|
||||
message: "Synchronized with remotes".into(),
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoteAction::Pull(remote_ref) => {
|
||||
let get_changes = |output: &RemoteCommandOutput| -> anyhow::Result<u32> {
|
||||
let last_line = output
|
||||
.stdout
|
||||
.lines()
|
||||
.last()
|
||||
.context("Failed to get last line of output")?
|
||||
.trim();
|
||||
|
||||
let files_changed = last_line
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.context("Failed to get first word of last line")?
|
||||
.parse()?;
|
||||
|
||||
Ok(files_changed)
|
||||
};
|
||||
|
||||
if output.stderr.starts_with("Everything up to date") {
|
||||
SuccessMessage {
|
||||
message: output.stderr.trim().to_owned(),
|
||||
style: SuccessStyle::Toast,
|
||||
}
|
||||
} else if output.stdout.starts_with("Updating") {
|
||||
let files_changed = get_changes(&output).log_err();
|
||||
let message = if let Some(files_changed) = files_changed {
|
||||
format!(
|
||||
"Received {} file change{} from {}",
|
||||
files_changed,
|
||||
if files_changed == 1 { "" } else { "s" },
|
||||
remote_ref.name
|
||||
)
|
||||
} else {
|
||||
format!("Fast forwarded from {}", remote_ref.name)
|
||||
};
|
||||
SuccessMessage {
|
||||
message,
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
} else if output.stdout.starts_with("Merge") {
|
||||
let files_changed = get_changes(&output).log_err();
|
||||
let message = if let Some(files_changed) = files_changed {
|
||||
format!(
|
||||
"Merged {} file change{} from {}",
|
||||
files_changed,
|
||||
if files_changed == 1 { "" } else { "s" },
|
||||
remote_ref.name
|
||||
)
|
||||
} else {
|
||||
format!("Merged from {}", remote_ref.name)
|
||||
};
|
||||
SuccessMessage {
|
||||
message,
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
} else if output.stdout.contains("Successfully rebased") {
|
||||
SuccessMessage {
|
||||
message: format!("Successfully rebased from {}", remote_ref.name),
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
} else {
|
||||
SuccessMessage {
|
||||
message: format!("Successfully pulled from {}", remote_ref.name),
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
}
|
||||
}
|
||||
RemoteAction::Push(branch_name, remote_ref) => {
|
||||
if output.stderr.contains("* [new branch]") {
|
||||
let style = if output.stderr.contains("Create a pull request") {
|
||||
let finder = LinkFinder::new();
|
||||
let first_link = finder
|
||||
.links(&output.stderr)
|
||||
.filter(|link| *link.kind() == LinkKind::Url)
|
||||
.map(|link| link.start()..link.end())
|
||||
.next();
|
||||
if let Some(link) = first_link {
|
||||
let link = output.stderr[link].to_string();
|
||||
SuccessStyle::PushPrLink { link }
|
||||
} else {
|
||||
SuccessStyle::ToastWithLog { output }
|
||||
}
|
||||
} else {
|
||||
SuccessStyle::ToastWithLog { output }
|
||||
};
|
||||
SuccessMessage {
|
||||
message: format!("Published {} to {}", branch_name, remote_ref.name),
|
||||
style,
|
||||
}
|
||||
} else if output.stderr.starts_with("Everything up to date") {
|
||||
SuccessMessage {
|
||||
message: output.stderr.trim().to_owned(),
|
||||
style: SuccessStyle::Toast,
|
||||
}
|
||||
} else {
|
||||
SuccessMessage {
|
||||
message: "Successfully pushed new branch".to_owned(),
|
||||
style: SuccessStyle::ToastWithLog { output },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,227 +0,0 @@
|
|||
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")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue