Detailed changes
@@ -2959,6 +2959,7 @@ impl InlineAssistant {
cx.prompt(
PromptLevel::Info,
prompt_text.as_str(),
+ None,
&["Continue", "Cancel"],
)
})?;
@@ -130,7 +130,8 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
} else {
drop(cx.prompt(
gpui::PromptLevel::Info,
- "Auto-updates disabled for non-bundled app.",
+ "Could not check for updates",
+ Some("Auto-updates disabled for non-bundled app."),
&["Ok"],
));
}
@@ -22,7 +22,10 @@ use gpui::{
};
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
use project::{Fs, Project};
-use rpc::proto::{self, PeerId};
+use rpc::{
+ proto::{self, PeerId},
+ ErrorCode, ErrorExt,
+};
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
use smallvec::SmallVec;
@@ -35,7 +38,7 @@ use ui::{
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
- notifications::{NotifyResultExt, NotifyTaskExt},
+ notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
Workspace,
};
@@ -879,7 +882,7 @@ impl CollabPanel {
.update(cx, |workspace, cx| {
let app_state = workspace.app_state().clone();
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
- .detach_and_log_err(cx);
+ .detach_and_prompt_err("Failed to join project", cx, |_, _| None);
})
.ok();
}))
@@ -1017,7 +1020,12 @@ impl CollabPanel {
)
})
})
- .detach_and_notify_err(cx)
+ .detach_and_prompt_err("Failed to grant write access", cx, |e, _| {
+ match e.error_code() {
+ ErrorCode::NeedsCla => Some("This user has not yet signed the CLA at https://zed.dev/cla.".into()),
+ _ => None,
+ }
+ })
}),
)
} else if role == proto::ChannelRole::Member {
@@ -1038,7 +1046,7 @@ impl CollabPanel {
)
})
})
- .detach_and_notify_err(cx)
+ .detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None)
}),
)
} else {
@@ -1258,7 +1266,11 @@ impl CollabPanel {
app_state,
cx,
)
- .detach_and_log_err(cx);
+ .detach_and_prompt_err(
+ "Failed to join project",
+ cx,
+ |_, _| None,
+ );
}
}
ListEntry::ParticipantScreen { peer_id, .. } => {
@@ -1432,7 +1444,7 @@ impl CollabPanel {
fn leave_call(cx: &mut WindowContext) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
- .detach_and_log_err(cx);
+ .detach_and_prompt_err("Failed to hang up", cx, |_, _| None);
}
fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
@@ -1534,11 +1546,11 @@ impl CollabPanel {
cx: &mut ViewContext<CollabPanel>,
) {
if let Some(clipboard) = self.channel_clipboard.take() {
- self.channel_store.update(cx, |channel_store, cx| {
- channel_store
- .move_channel(clipboard.channel_id, Some(to_channel_id), cx)
- .detach_and_log_err(cx)
- })
+ self.channel_store
+ .update(cx, |channel_store, cx| {
+ channel_store.move_channel(clipboard.channel_id, Some(to_channel_id), cx)
+ })
+ .detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
}
}
@@ -1610,7 +1622,12 @@ impl CollabPanel {
"Are you sure you want to remove the channel \"{}\"?",
channel.name
);
- let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+ let answer = cx.prompt(
+ PromptLevel::Warning,
+ &prompt_message,
+ None,
+ &["Remove", "Cancel"],
+ );
cx.spawn(|this, mut cx| async move {
if answer.await? == 0 {
channel_store
@@ -1631,7 +1648,12 @@ impl CollabPanel {
"Are you sure you want to remove \"{}\" from your contacts?",
github_login
);
- let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
+ let answer = cx.prompt(
+ PromptLevel::Warning,
+ &prompt_message,
+ None,
+ &["Remove", "Cancel"],
+ );
cx.spawn(|_, mut cx| async move {
if answer.await? == 0 {
user_store
@@ -1641,7 +1663,7 @@ impl CollabPanel {
}
anyhow::Ok(())
})
- .detach_and_log_err(cx);
+ .detach_and_prompt_err("Failed to remove contact", cx, |_, _| None);
}
fn respond_to_contact_request(
@@ -1654,7 +1676,7 @@ impl CollabPanel {
.update(cx, |store, cx| {
store.respond_to_contact_request(user_id, accept, cx)
})
- .detach_and_log_err(cx);
+ .detach_and_prompt_err("Failed to respond to contact request", cx, |_, _| None);
}
fn respond_to_channel_invite(
@@ -1675,7 +1697,7 @@ impl CollabPanel {
.update(cx, |call, cx| {
call.invite(recipient_user_id, Some(self.project.clone()), cx)
})
- .detach_and_log_err(cx);
+ .detach_and_prompt_err("Call failed", cx, |_, _| None);
}
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
@@ -1691,7 +1713,7 @@ impl CollabPanel {
Some(handle),
cx,
)
- .detach_and_log_err(cx)
+ .detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
}
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
@@ -1704,7 +1726,7 @@ impl CollabPanel {
panel.update(cx, |panel, cx| {
panel
.select_channel(channel_id, None, cx)
- .detach_and_log_err(cx);
+ .detach_and_notify_err(cx);
});
}
});
@@ -1981,7 +2003,7 @@ impl CollabPanel {
.update(cx, |channel_store, cx| {
channel_store.move_channel(dragged_channel.id, None, cx)
})
- .detach_and_log_err(cx)
+ .detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
}))
})
}
@@ -2257,7 +2279,7 @@ impl CollabPanel {
.update(cx, |channel_store, cx| {
channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
})
- .detach_and_log_err(cx)
+ .detach_and_prompt_err("Failed to move channel", cx, |_, _| None)
}))
.child(
ListItem::new(channel_id as usize)
@@ -14,7 +14,7 @@ use rpc::proto::channel_member;
use std::sync::Arc;
use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
use util::TryFutureExt;
-use workspace::{notifications::NotifyTaskExt, ModalView};
+use workspace::{notifications::DetachAndPromptErr, ModalView};
actions!(
channel_modal,
@@ -498,7 +498,7 @@ impl ChannelModalDelegate {
cx.notify();
})
})
- .detach_and_notify_err(cx);
+ .detach_and_prompt_err("Failed to update role", cx, |_, _| None);
Some(())
}
@@ -530,7 +530,7 @@ impl ChannelModalDelegate {
cx.notify();
})
})
- .detach_and_notify_err(cx);
+ .detach_and_prompt_err("Failed to remove member", cx, |_, _| None);
Some(())
}
@@ -556,7 +556,7 @@ impl ChannelModalDelegate {
cx.notify();
})
})
- .detach_and_notify_err(cx);
+ .detach_and_prompt_err("Failed to invite member", cx, |_, _| None);
}
fn show_context_menu(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
@@ -31,7 +31,8 @@ pub fn init(cx: &mut AppContext) {
let prompt = cx.prompt(
PromptLevel::Info,
- &format!("Copied into clipboard:\n\n{specs}"),
+ "Copied into clipboard",
+ Some(&specs),
&["OK"],
);
cx.spawn(|_, _cx| async move {
@@ -97,7 +97,7 @@ impl ModalView for FeedbackModal {
return true;
}
- let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", &["Yes", "No"]);
+ let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", None, &["Yes", "No"]);
cx.spawn(move |this, mut cx| async move {
if answer.await.ok() == Some(0) {
@@ -222,6 +222,7 @@ impl FeedbackModal {
let answer = cx.prompt(
PromptLevel::Info,
"Ready to submit your feedback?",
+ None,
&["Yes, Submit!", "No"],
);
let client = cx.global::<Arc<Client>>().clone();
@@ -255,6 +256,7 @@ impl FeedbackModal {
let prompt = cx.prompt(
PromptLevel::Critical,
FEEDBACK_SUBMISSION_ERROR_TEXT,
+ None,
&["OK"],
);
cx.spawn(|_, _cx| async move {
@@ -150,7 +150,13 @@ pub(crate) trait PlatformWindow {
fn as_any_mut(&mut self) -> &mut dyn Any;
fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
- fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
+ fn prompt(
+ &self,
+ level: PromptLevel,
+ msg: &str,
+ detail: Option<&str>,
+ answers: &[&str],
+ ) -> oneshot::Receiver<usize>;
fn activate(&self);
fn set_title(&mut self, title: &str);
fn set_edited(&mut self, edited: bool);
@@ -772,7 +772,13 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().input_handler.take()
}
- fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize> {
+ fn prompt(
+ &self,
+ level: PromptLevel,
+ msg: &str,
+ detail: Option<&str>,
+ answers: &[&str],
+ ) -> oneshot::Receiver<usize> {
// macOs applies overrides to modal window buttons after they are added.
// Two most important for this logic are:
// * Buttons with "Cancel" title will be displayed as the last buttons in the modal
@@ -808,6 +814,9 @@ impl PlatformWindow for MacWindow {
};
let _: () = msg_send![alert, setAlertStyle: alert_style];
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
+ if let Some(detail) = detail {
+ let _: () = msg_send![alert, setInformativeText: ns_string(detail)];
+ }
for (ix, answer) in answers
.iter()
@@ -185,6 +185,7 @@ impl PlatformWindow for TestWindow {
&self,
_level: crate::PromptLevel,
_msg: &str,
+ _detail: Option<&str>,
_answers: &[&str],
) -> futures::channel::oneshot::Receiver<usize> {
self.0
@@ -1478,9 +1478,12 @@ impl<'a> WindowContext<'a> {
&self,
level: PromptLevel,
message: &str,
+ detail: Option<&str>,
answers: &[&str],
) -> oneshot::Receiver<usize> {
- self.window.platform_window.prompt(level, message, answers)
+ self.window
+ .platform_window
+ .prompt(level, message, detail, answers)
}
/// Returns all available actions for the focused element.
@@ -778,6 +778,7 @@ impl ProjectPanel {
let answer = cx.prompt(
PromptLevel::Info,
&format!("Delete {file_name:?}?"),
+ None,
&["Delete", "Cancel"],
);
@@ -209,6 +209,7 @@ enum ErrorCode {
UpgradeRequired = 4;
Forbidden = 5;
WrongReleaseChannel = 6;
+ NeedsCla = 7;
}
message Test {
@@ -746,6 +746,7 @@ impl ProjectSearchView {
cx.prompt(
PromptLevel::Info,
prompt_text.as_str(),
+ None,
&["Continue", "Cancel"],
)
})?;
@@ -1,8 +1,8 @@
use crate::{Toast, Workspace};
use collections::HashMap;
use gpui::{
- AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
- Task, View, ViewContext, VisualContext, WindowContext,
+ AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter,
+ PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext,
};
use std::{any::TypeId, ops::DerefMut};
@@ -299,7 +299,7 @@ pub trait NotifyTaskExt {
impl<R, E> NotifyTaskExt for Task<Result<R, E>>
where
- E: std::fmt::Debug + 'static,
+ E: std::fmt::Debug + Sized + 'static,
R: 'static,
{
fn detach_and_notify_err(self, cx: &mut WindowContext) {
@@ -307,3 +307,39 @@ where
.detach();
}
}
+
+pub trait DetachAndPromptErr {
+ fn detach_and_prompt_err(
+ self,
+ msg: &str,
+ cx: &mut WindowContext,
+ f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
+ );
+}
+
+impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
+where
+ R: 'static,
+{
+ fn detach_and_prompt_err(
+ self,
+ msg: &str,
+ cx: &mut WindowContext,
+ f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
+ ) {
+ let msg = msg.to_owned();
+ cx.spawn(|mut cx| async move {
+ if let Err(err) = self.await {
+ log::error!("{err:?}");
+ if let Ok(prompt) = cx.update(|cx| {
+ let detail = f(&err, cx)
+ .unwrap_or_else(|| format!("{err:?}. Please try again.", err = err));
+ cx.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"])
+ }) {
+ prompt.await.ok();
+ }
+ }
+ })
+ .detach();
+ }
+}
@@ -870,7 +870,7 @@ impl Pane {
items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
all_dirty_items: usize,
cx: &AppContext,
- ) -> String {
+ ) -> (String, String) {
/// Quantity of item paths displayed in prompt prior to cutoff..
const FILE_NAMES_CUTOFF_POINT: usize = 10;
let mut file_names: Vec<_> = items
@@ -894,10 +894,12 @@ impl Pane {
file_names.push(format!(".. {} files not shown", not_shown_files).into());
}
}
- let file_names = file_names.join("\n");
- format!(
- "Do you want to save changes to the following {} files?\n{file_names}",
- all_dirty_items
+ (
+ format!(
+ "Do you want to save changes to the following {} files?",
+ all_dirty_items
+ ),
+ file_names.join("\n"),
)
}
@@ -929,11 +931,12 @@ impl Pane {
cx.spawn(|pane, mut cx| async move {
if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
let answer = pane.update(&mut cx, |_, cx| {
- let prompt =
+ let (prompt, detail) =
Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
cx.prompt(
PromptLevel::Warning,
&prompt,
+ Some(&detail),
&["Save all", "Discard all", "Cancel"],
)
})?;
@@ -1131,6 +1134,7 @@ impl Pane {
cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
+ None,
&["Overwrite", "Discard", "Cancel"],
)
})?;
@@ -1154,6 +1158,7 @@ impl Pane {
cx.prompt(
PromptLevel::Warning,
&prompt,
+ None,
&["Save", "Don't Save", "Cancel"],
)
})?;
@@ -30,8 +30,8 @@ use gpui::{
DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
- Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
- WindowBounds, WindowContext, WindowHandle, WindowOptions,
+ Render, SharedString, Size, Styled, Subscription, Task, View, ViewContext, VisualContext,
+ WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
@@ -1159,6 +1159,7 @@ impl Workspace {
cx.prompt(
PromptLevel::Warning,
"Do you want to leave the current call?",
+ None,
&["Close window and hang up", "Cancel"],
)
})?;
@@ -1214,7 +1215,7 @@ impl Workspace {
// Override save mode and display "Save all files" prompt
if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
let answer = workspace.update(&mut cx, |_, cx| {
- let prompt = Pane::file_names_for_prompt(
+ let (prompt, detail) = Pane::file_names_for_prompt(
&mut dirty_items.iter().map(|(_, handle)| handle),
dirty_items.len(),
cx,
@@ -1222,6 +1223,7 @@ impl Workspace {
cx.prompt(
PromptLevel::Warning,
&prompt,
+ Some(&detail),
&["Save all", "Discard all", "Cancel"],
)
})?;
@@ -3887,13 +3889,16 @@ async fn join_channel_internal(
if should_prompt {
if let Some(workspace) = requesting_window {
- let answer = workspace.update(cx, |_, cx| {
- cx.prompt(
- PromptLevel::Warning,
- "Leaving this call will unshare your current project.\nDo you want to switch channels?",
- &["Yes, Join Channel", "Cancel"],
- )
- })?.await;
+ let answer = workspace
+ .update(cx, |_, cx| {
+ cx.prompt(
+ PromptLevel::Warning,
+ "Do you want to switch channels?",
+ Some("Leaving this call will unshare your current project."),
+ &["Yes, Join Channel", "Cancel"],
+ )
+ })?
+ .await;
if answer == Ok(1) {
return Ok(false);
@@ -3995,23 +4000,27 @@ pub fn join_channel(
if let Some(active_window) = active_window {
active_window
.update(&mut cx, |_, cx| {
- let message:SharedString = match err.error_code() {
+ let detail: SharedString = match err.error_code() {
ErrorCode::SignedOut => {
- "Failed to join channel\n\nPlease sign in to continue.".into()
+ "Please sign in to continue.".into()
},
ErrorCode::UpgradeRequired => {
- "Failed to join channel\n\nPlease update to the latest version of Zed to continue.".into()
+ "Your are running an unsupported version of Zed. Please update to continue.".into()
},
ErrorCode::NoSuchChannel => {
- "Failed to find channel\n\nPlease check the link and try again.".into()
+ "No matching channel was found. Please check the link and try again.".into()
+ },
+ ErrorCode::Forbidden => {
+ "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
},
- ErrorCode::Disconnected => "Failed to join channel\n\nPlease check your internet connection and try again.".into(),
- ErrorCode::WrongReleaseChannel => format!("Failed to join channel\n\nOther people in the channel are using the {} release of Zed, please switch to that release instead.", err.error_tag("required").unwrap_or("other")).into(),
- _ => format!("Failed to join channel\n\n{}\n\nPlease try again.", err).into(),
+ ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
+ ErrorCode::WrongReleaseChannel => format!("Others in the channel are using the {} release of Zed. Please switch to join this call.", err.error_tag("required").unwrap_or("other")).into(),
+ _ => format!("{}\n\nPlease try again.", err).into(),
};
cx.prompt(
PromptLevel::Critical,
- &message,
+ "Failed to join channel",
+ Some(&detail),
&["Ok"],
)
})?
@@ -4238,6 +4247,7 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
cx.prompt(
PromptLevel::Info,
"Are you sure you want to restart?",
+ None,
&["Restart", "Cancel"],
)
})
@@ -370,16 +370,12 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewCo
}
fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
- use std::fmt::Write as _;
-
let app_name = cx.global::<ReleaseChannel>().display_name();
let version = env!("CARGO_PKG_VERSION");
- let mut message = format!("{app_name} {version}");
- if let Some(sha) = cx.try_global::<AppCommitSha>() {
- write!(&mut message, "\n\n{}", sha.0).unwrap();
- }
+ let message = format!("{app_name} {version}");
+ let detail = cx.try_global::<AppCommitSha>().map(|sha| sha.0.as_ref());
- let prompt = cx.prompt(PromptLevel::Info, &message, &["OK"]);
+ let prompt = cx.prompt(PromptLevel::Info, &message, detail, &["OK"]);
cx.foreground_executor()
.spawn(async {
prompt.await.ok();
@@ -410,6 +406,7 @@ fn quit(_: &Quit, cx: &mut AppContext) {
cx.prompt(
PromptLevel::Info,
"Are you sure you want to quit?",
+ None,
&["Quit", "Cancel"],
)
})