Add user-visible output for remote operations (#25849)

This PR adds toasts for reporting success and errors from remote git
operations. This PR also adds a focus handle to notifications, in
anticipation of making them keyboard accessible.

Release Notes:

- N/A

---------

Co-authored-by: julia <julia@zed.dev>
This commit is contained in:
Mikayla Maki 2025-03-03 01:20:15 -08:00 committed by GitHub
parent 508b9d3b5d
commit 73ac19958a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 713 additions and 192 deletions

View file

@ -1,14 +1,34 @@
use crate::{Toast, Workspace};
use gpui::{
svg, AnyView, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
Entity, EventEmitter, PromptLevel, Render, ScrollHandle, Task,
Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle, Task,
};
use parking_lot::Mutex;
use std::ops::Deref;
use std::sync::{Arc, LazyLock};
use std::{any::TypeId, time::Duration};
use ui::{prelude::*, Tooltip};
use util::ResultExt;
#[derive(Default)]
pub struct Notifications {
notifications: Vec<(NotificationId, AnyView)>,
}
impl Deref for Notifications {
type Target = Vec<(NotificationId, AnyView)>;
fn deref(&self) -> &Self::Target {
&self.notifications
}
}
impl std::ops::DerefMut for Notifications {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.notifications
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum NotificationId {
Unique(TypeId),
@ -34,9 +54,7 @@ impl NotificationId {
}
}
pub trait Notification: EventEmitter<DismissEvent> + Render {}
impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
pub trait Notification: EventEmitter<DismissEvent> + Focusable + Render {}
impl Workspace {
#[cfg(any(test, feature = "test-support"))]
@ -89,7 +107,7 @@ impl Workspace {
E: std::fmt::Debug + std::fmt::Display,
{
self.show_notification(workspace_error_notification_id(), cx, |cx| {
cx.new(|_| ErrorMessagePrompt::new(format!("Error: {err}")))
cx.new(|cx| ErrorMessagePrompt::new(format!("Error: {err}"), cx))
});
}
@ -97,8 +115,8 @@ impl Workspace {
struct PortalError;
self.show_notification(NotificationId::unique::<PortalError>(), cx, |cx| {
cx.new(|_| {
ErrorMessagePrompt::new(err.to_string()).with_link_button(
cx.new(|cx| {
ErrorMessagePrompt::new(err.to_string(), cx).with_link_button(
"See docs",
"https://zed.dev/docs/linux#i-cant-open-any-files",
)
@ -120,14 +138,16 @@ impl Workspace {
pub fn show_toast(&mut self, toast: Toast, cx: &mut Context<Self>) {
self.dismiss_notification(&toast.id, cx);
self.show_notification(toast.id.clone(), cx, |cx| {
cx.new(|_| match toast.on_click.as_ref() {
cx.new(|cx| match toast.on_click.as_ref() {
Some((click_msg, on_click)) => {
let on_click = on_click.clone();
simple_message_notification::MessageNotification::new(toast.msg.clone())
simple_message_notification::MessageNotification::new(toast.msg.clone(), cx)
.primary_message(click_msg.clone())
.primary_on_click(move |window, cx| on_click(window, cx))
}
None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
None => {
simple_message_notification::MessageNotification::new(toast.msg.clone(), cx)
}
})
});
if toast.autohide {
@ -171,13 +191,23 @@ impl Workspace {
}
pub struct LanguageServerPrompt {
focus_handle: FocusHandle,
request: Option<project::LanguageServerPromptRequest>,
scroll_handle: ScrollHandle,
}
impl Focusable for LanguageServerPrompt {
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Notification for LanguageServerPrompt {}
impl LanguageServerPrompt {
pub fn new(request: project::LanguageServerPromptRequest) -> Self {
pub fn new(request: project::LanguageServerPromptRequest, cx: &mut App) -> Self {
Self {
focus_handle: cx.focus_handle(),
request: Some(request),
scroll_handle: ScrollHandle::new(),
}
@ -286,16 +316,18 @@ fn workspace_error_notification_id() -> NotificationId {
#[derive(Debug, Clone)]
pub struct ErrorMessagePrompt {
message: SharedString,
focus_handle: gpui::FocusHandle,
label_and_url_button: Option<(SharedString, SharedString)>,
}
impl ErrorMessagePrompt {
pub fn new<S>(message: S) -> Self
pub fn new<S>(message: S, cx: &mut App) -> Self
where
S: Into<SharedString>,
{
Self {
message: message.into(),
focus_handle: cx.focus_handle(),
label_and_url_button: None,
}
}
@ -364,17 +396,29 @@ impl Render for ErrorMessagePrompt {
}
}
impl Focusable for ErrorMessagePrompt {
fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
impl Notification for ErrorMessagePrompt {}
pub mod simple_message_notification {
use std::sync::Arc;
use gpui::{
div, AnyElement, DismissEvent, EventEmitter, ParentElement, Render, SharedString, Styled,
div, AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render,
SharedString, Styled,
};
use ui::prelude::*;
use super::Notification;
pub struct MessageNotification {
focus_handle: FocusHandle,
build_content: Box<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>,
primary_message: Option<SharedString>,
primary_icon: Option<IconName>,
@ -390,18 +434,28 @@ pub mod simple_message_notification {
title: Option<SharedString>,
}
impl Focusable for MessageNotification {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for MessageNotification {}
impl Notification for MessageNotification {}
impl MessageNotification {
pub fn new<S>(message: S) -> MessageNotification
pub fn new<S>(message: S, cx: &mut App) -> MessageNotification
where
S: Into<SharedString>,
{
let message = message.into();
Self::new_from_builder(move |_, _| Label::new(message.clone()).into_any_element())
Self::new_from_builder(cx, move |_, _| {
Label::new(message.clone()).into_any_element()
})
}
pub fn new_from_builder<F>(content: F) -> MessageNotification
pub fn new_from_builder<F>(cx: &mut App, content: F) -> MessageNotification
where
F: 'static + Fn(&mut Window, &mut Context<Self>) -> AnyElement,
{
@ -419,6 +473,7 @@ pub mod simple_message_notification {
more_info_url: None,
show_close_button: true,
title: None,
focus_handle: cx.focus_handle(),
}
}
@ -769,7 +824,7 @@ where
move |cx| {
cx.new({
let message = message.clone();
move |_cx| ErrorMessagePrompt::new(message)
move |cx| ErrorMessagePrompt::new(message, cx)
})
}
});