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:
@@ -112,7 +112,7 @@ impl ModalLayer {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn hide_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||
pub fn hide_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||
let Some(active_modal) = self.active_modal.as_mut() else {
|
||||
self.dismiss_on_focus_lost = false;
|
||||
return false;
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1156,11 +1156,14 @@ impl WorkspaceDb {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::persistence::model::SerializedWorkspace;
|
||||
use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
|
||||
use db::open_test_db;
|
||||
use gpui::{self};
|
||||
use gpui;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_id_stability() {
|
||||
@@ -1556,31 +1559,33 @@ mod tests {
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
|
||||
db.save_workspace(workspace_2.clone()).await;
|
||||
db.save_workspace(workspace_3.clone()).await;
|
||||
thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
|
||||
db.save_workspace(workspace_4.clone()).await;
|
||||
db.save_workspace(workspace_5.clone()).await;
|
||||
db.save_workspace(workspace_6.clone()).await;
|
||||
|
||||
let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
|
||||
assert_eq!(locations.len(), 2);
|
||||
assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
|
||||
assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"]));
|
||||
assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[0].2, Some(10));
|
||||
assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
|
||||
assert_eq!(locations[0].2, Some(20));
|
||||
assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"]));
|
||||
assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[1].2, Some(20));
|
||||
assert_eq!(locations[1].2, Some(10));
|
||||
|
||||
let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
|
||||
assert_eq!(locations.len(), 2);
|
||||
assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
|
||||
assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[0].2, Some(30));
|
||||
let empty_paths: Vec<&str> = Vec::new();
|
||||
assert_eq!(locations[1].0, LocalPaths::new(empty_paths.iter()));
|
||||
assert_eq!(locations[1].1, LocalPathsOrder::new([]));
|
||||
assert_eq!(locations[1].2, Some(50));
|
||||
assert_eq!(locations[1].3, Some(ssh_project.id.0));
|
||||
assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter()));
|
||||
assert_eq!(locations[0].1, LocalPathsOrder::new([]));
|
||||
assert_eq!(locations[0].2, Some(50));
|
||||
assert_eq!(locations[0].3, Some(ssh_project.id.0));
|
||||
assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"]));
|
||||
assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
|
||||
assert_eq!(locations[1].2, Some(30));
|
||||
|
||||
let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
|
||||
assert_eq!(locations.len(), 1);
|
||||
|
||||
@@ -47,7 +47,9 @@ use itertools::Itertools;
|
||||
use language::{LanguageRegistry, Rope};
|
||||
pub use modal_layer::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
use notifications::{simple_message_notification::MessageNotification, DetachAndPromptErr};
|
||||
use notifications::{
|
||||
simple_message_notification::MessageNotification, DetachAndPromptErr, Notifications,
|
||||
};
|
||||
pub use pane::*;
|
||||
pub use pane_group::*;
|
||||
pub use persistence::{
|
||||
@@ -815,7 +817,7 @@ pub struct Workspace {
|
||||
status_bar: Entity<StatusBar>,
|
||||
modal_layer: Entity<ModalLayer>,
|
||||
titlebar_item: Option<AnyView>,
|
||||
notifications: Vec<(NotificationId, AnyView)>,
|
||||
notifications: Notifications,
|
||||
project: Entity<Project>,
|
||||
follower_states: HashMap<PeerId, FollowerState>,
|
||||
last_leaders_by_pane: HashMap<WeakEntity<Pane>, PeerId>,
|
||||
@@ -920,7 +922,7 @@ impl Workspace {
|
||||
} => this.show_notification(
|
||||
NotificationId::named(notification_id.clone()),
|
||||
cx,
|
||||
|cx| cx.new(|_| MessageNotification::new(message.clone())),
|
||||
|cx| cx.new(|cx| MessageNotification::new(message.clone(), cx)),
|
||||
),
|
||||
|
||||
project::Event::HideToast { notification_id } => {
|
||||
@@ -937,7 +939,11 @@ impl Workspace {
|
||||
this.show_notification(
|
||||
NotificationId::composite::<LanguageServerPrompt>(id as usize),
|
||||
cx,
|
||||
|cx| cx.new(|_| notifications::LanguageServerPrompt::new(request.clone())),
|
||||
|cx| {
|
||||
cx.new(|cx| {
|
||||
notifications::LanguageServerPrompt::new(request.clone(), cx)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5223,8 +5229,8 @@ fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncA
|
||||
NotificationId::unique::<DatabaseFailedNotification>(),
|
||||
cx,
|
||||
|cx| {
|
||||
cx.new(|_| {
|
||||
MessageNotification::new("Failed to load the database file.")
|
||||
cx.new(|cx| {
|
||||
MessageNotification::new("Failed to load the database file.", cx)
|
||||
.primary_message("File an Issue")
|
||||
.primary_icon(IconName::Plus)
|
||||
.primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
|
||||
|
||||
Reference in New Issue
Block a user