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"],
));
}
@@ -689,12 +689,7 @@ impl Client {
Ok(())
}
Err(error) => {
- client.respond_with_error(
- receipt,
- proto::Error {
- message: format!("{:?}", error),
- },
- )?;
+ client.respond_with_error(receipt, error.to_proto())?;
Err(error)
}
}
@@ -1,5 +1,5 @@
use super::*;
-use rpc::proto::channel_member::Kind;
+use rpc::{proto::channel_member::Kind, ErrorCode, ErrorCodeExt};
use sea_orm::TryGetableMany;
impl Database {
@@ -166,7 +166,7 @@ impl Database {
}
if role.is_none() || role == Some(ChannelRole::Banned) {
- Err(anyhow!("not allowed"))?
+ Err(ErrorCode::Forbidden.anyhow())?
}
let role = role.unwrap();
@@ -1201,7 +1201,7 @@ impl Database {
Ok(channel::Entity::find_by_id(channel_id)
.one(&*tx)
.await?
- .ok_or_else(|| anyhow!("no such channel"))?)
+ .ok_or_else(|| proto::ErrorCode::NoSuchChannel.anyhow())?)
}
pub(crate) async fn get_or_create_channel_room(
@@ -1219,7 +1219,9 @@ impl Database {
let room_id = if let Some(room) = room {
if let Some(env) = room.environment {
if &env != environment {
- Err(anyhow!("must join using the {} release", env))?;
+ Err(ErrorCode::WrongReleaseChannel
+ .with_tag("required", &env)
+ .anyhow())?;
}
}
room.id
@@ -10,7 +10,7 @@ use crate::{
User, UserId,
},
executor::Executor,
- AppState, Result,
+ AppState, Error, Result,
};
use anyhow::anyhow;
use async_tungstenite::tungstenite::{
@@ -44,7 +44,7 @@ use rpc::{
self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
},
- Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
+ Connection, ConnectionId, ErrorCode, ErrorCodeExt, ErrorExt, Peer, Receipt, TypedEnvelope,
};
use serde::{Serialize, Serializer};
use std::{
@@ -543,12 +543,11 @@ impl Server {
}
}
Err(error) => {
- peer.respond_with_error(
- receipt,
- proto::Error {
- message: error.to_string(),
- },
- )?;
+ let proto_err = match &error {
+ Error::Internal(err) => err.to_proto(),
+ _ => ErrorCode::Internal.message(format!("{}", error)).to_proto(),
+ };
+ peer.respond_with_error(receipt, proto_err)?;
Err(error)
}
}
@@ -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"],
);
@@ -197,6 +197,19 @@ message Ack {}
message Error {
string message = 1;
+ ErrorCode code = 2;
+ repeated string tags = 3;
+}
+
+enum ErrorCode {
+ Internal = 0;
+ NoSuchChannel = 1;
+ Disconnected = 2;
+ SignedOut = 3;
+ UpgradeRequired = 4;
+ Forbidden = 5;
+ WrongReleaseChannel = 6;
+ NeedsCla = 7;
}
message Test {
@@ -0,0 +1,223 @@
+/// Some helpers for structured error handling.
+///
+/// The helpers defined here allow you to pass type-safe error codes from
+/// the collab server to the client; and provide a mechanism for additional
+/// structured data alongside the message.
+///
+/// When returning an error, it can be as simple as:
+///
+/// `return Err(Error::Forbidden.into())`
+///
+/// If you'd like to log more context, you can set a message. These messages
+/// show up in our logs, but are not shown visibly to users.
+///
+/// `return Err(Error::Forbidden.message("not an admin").into())`
+///
+/// If you'd like to provide enough context that the UI can render a good error
+/// message (or would be helpful to see in a structured format in the logs), you
+/// can use .with_tag():
+///
+/// `return Err(Error::WrongReleaseChannel.with_tag("required", "stable").into())`
+///
+/// When handling an error you can use .error_code() to match which error it was
+/// and .error_tag() to read any tags.
+///
+/// ```
+/// match err.error_code() {
+/// ErrorCode::Forbidden => alert("I'm sorry I can't do that.")
+/// ErrorCode::WrongReleaseChannel =>
+/// alert(format!("You need to be on the {} release channel.", err.error_tag("required").unwrap()))
+/// ErrorCode::Internal => alert("Sorry, something went wrong")
+/// }
+/// ```
+///
+use crate::proto;
+pub use proto::ErrorCode;
+
+/// ErrorCodeExt provides some helpers for structured error handling.
+///
+/// The primary implementation is on the proto::ErrorCode to easily convert
+/// that into an anyhow::Error, which we use pervasively.
+///
+/// The RpcError struct provides support for further metadata if needed.
+pub trait ErrorCodeExt {
+ /// Return an anyhow::Error containing this.
+ /// (useful in places where .into() doesn't have enough type information)
+ fn anyhow(self) -> anyhow::Error;
+
+ /// Add a message to the error (by default the error code is used)
+ fn message(self, msg: String) -> RpcError;
+
+ /// Add a tag to the error. Tags are key value pairs that can be used
+ /// to send semi-structured data along with the error.
+ fn with_tag(self, k: &str, v: &str) -> RpcError;
+}
+
+impl ErrorCodeExt for proto::ErrorCode {
+ fn anyhow(self) -> anyhow::Error {
+ self.into()
+ }
+
+ fn message(self, msg: String) -> RpcError {
+ let err: RpcError = self.into();
+ err.message(msg)
+ }
+
+ fn with_tag(self, k: &str, v: &str) -> RpcError {
+ let err: RpcError = self.into();
+ err.with_tag(k, v)
+ }
+}
+
+/// ErrorExt provides helpers for structured error handling.
+///
+/// The primary implementation is on the anyhow::Error, which is
+/// what we use throughout our codebase. Though under the hood this
+pub trait ErrorExt {
+ /// error_code() returns the ErrorCode (or ErrorCode::Internal if there is none)
+ fn error_code(&self) -> proto::ErrorCode;
+ /// error_tag() returns the value of the tag with the given key, if any.
+ fn error_tag(&self, k: &str) -> Option<&str>;
+ /// to_proto() converts the error into a proto::Error
+ fn to_proto(&self) -> proto::Error;
+}
+
+impl ErrorExt for anyhow::Error {
+ fn error_code(&self) -> proto::ErrorCode {
+ if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
+ rpc_error.code
+ } else {
+ proto::ErrorCode::Internal
+ }
+ }
+
+ fn error_tag(&self, k: &str) -> Option<&str> {
+ if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
+ rpc_error.error_tag(k)
+ } else {
+ None
+ }
+ }
+
+ fn to_proto(&self) -> proto::Error {
+ if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
+ rpc_error.to_proto()
+ } else {
+ ErrorCode::Internal.message(format!("{}", self)).to_proto()
+ }
+ }
+}
+
+impl From<proto::ErrorCode> for anyhow::Error {
+ fn from(value: proto::ErrorCode) -> Self {
+ RpcError {
+ request: None,
+ code: value,
+ msg: format!("{:?}", value).to_string(),
+ tags: Default::default(),
+ }
+ .into()
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct RpcError {
+ request: Option<String>,
+ msg: String,
+ code: proto::ErrorCode,
+ tags: Vec<String>,
+}
+
+/// RpcError is a structured error type that is returned by the collab server.
+/// In addition to a message, it lets you set a specific ErrorCode, and attach
+/// small amounts of metadata to help the client handle the error appropriately.
+///
+/// This struct is not typically used directly, as we pass anyhow::Error around
+/// in the app; however it is useful for chaining .message() and .with_tag() on
+/// ErrorCode.
+impl RpcError {
+ /// from_proto converts a proto::Error into an anyhow::Error containing
+ /// an RpcError.
+ pub fn from_proto(error: &proto::Error, request: &str) -> anyhow::Error {
+ RpcError {
+ request: Some(request.to_string()),
+ code: error.code(),
+ msg: error.message.clone(),
+ tags: error.tags.clone(),
+ }
+ .into()
+ }
+}
+
+impl ErrorCodeExt for RpcError {
+ fn message(mut self, msg: String) -> RpcError {
+ self.msg = msg;
+ self
+ }
+
+ fn with_tag(mut self, k: &str, v: &str) -> RpcError {
+ self.tags.push(format!("{}={}", k, v));
+ self
+ }
+
+ fn anyhow(self) -> anyhow::Error {
+ self.into()
+ }
+}
+
+impl ErrorExt for RpcError {
+ fn error_tag(&self, k: &str) -> Option<&str> {
+ for tag in &self.tags {
+ let mut parts = tag.split('=');
+ if let Some(key) = parts.next() {
+ if key == k {
+ return parts.next();
+ }
+ }
+ }
+ None
+ }
+
+ fn error_code(&self) -> proto::ErrorCode {
+ self.code
+ }
+
+ fn to_proto(&self) -> proto::Error {
+ proto::Error {
+ code: self.code as i32,
+ message: self.msg.clone(),
+ tags: self.tags.clone(),
+ }
+ }
+}
+
+impl std::error::Error for RpcError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ None
+ }
+}
+
+impl std::fmt::Display for RpcError {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ if let Some(request) = &self.request {
+ write!(f, "RPC request {} failed: {}", request, self.msg)?
+ } else {
+ write!(f, "{}", self.msg)?
+ }
+ for tag in &self.tags {
+ write!(f, " {}", tag)?
+ }
+ Ok(())
+ }
+}
+
+impl From<proto::ErrorCode> for RpcError {
+ fn from(code: proto::ErrorCode) -> Self {
+ RpcError {
+ request: None,
+ code,
+ msg: format!("{:?}", code).to_string(),
+ tags: Default::default(),
+ }
+ }
+}
@@ -1,3 +1,5 @@
+use crate::{ErrorCode, ErrorCodeExt, ErrorExt, RpcError};
+
use super::{
proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, PeerId, RequestMessage},
Connection,
@@ -423,11 +425,7 @@ impl Peer {
let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
- Err(anyhow!(
- "RPC request {} failed - {}",
- T::NAME,
- error.message
- ))
+ Err(RpcError::from_proto(&error, T::NAME))
} else {
Ok(TypedEnvelope {
message_id: response.id,
@@ -516,9 +514,12 @@ impl Peer {
envelope: Box<dyn AnyTypedEnvelope>,
) -> Result<()> {
let connection = self.connection_state(envelope.sender_id())?;
- let response = proto::Error {
- message: format!("message {} was not handled", envelope.payload_type_name()),
- };
+ let response = ErrorCode::Internal
+ .message(format!(
+ "message {} was not handled",
+ envelope.payload_type_name()
+ ))
+ .to_proto();
let message_id = connection
.next_message_id
.fetch_add(1, atomic::Ordering::SeqCst);
@@ -692,17 +693,17 @@ mod tests {
server
.send(
server_to_client_conn_id,
- proto::Error {
- message: "message 1".to_string(),
- },
+ ErrorCode::Internal
+ .message("message 1".to_string())
+ .to_proto(),
)
.unwrap();
server
.send(
server_to_client_conn_id,
- proto::Error {
- message: "message 2".to_string(),
- },
+ ErrorCode::Internal
+ .message("message 2".to_string())
+ .to_proto(),
)
.unwrap();
server.respond(request.receipt(), proto::Ack {}).unwrap();
@@ -797,17 +798,17 @@ mod tests {
server
.send(
server_to_client_conn_id,
- proto::Error {
- message: "message 1".to_string(),
- },
+ ErrorCode::Internal
+ .message("message 1".to_string())
+ .to_proto(),
)
.unwrap();
server
.send(
server_to_client_conn_id,
- proto::Error {
- message: "message 2".to_string(),
- },
+ ErrorCode::Internal
+ .message("message 2".to_string())
+ .to_proto(),
)
.unwrap();
server.respond(request1.receipt(), proto::Ack {}).unwrap();
@@ -1,10 +1,12 @@
pub mod auth;
mod conn;
+mod error;
mod notification;
mod peer;
pub mod proto;
pub use conn::Connection;
+pub use error::*;
pub use notification::*;
pub use peer::*;
mod macros;
@@ -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"],
)
})?;
@@ -14,8 +14,8 @@ mod workspace_settings;
use anyhow::{anyhow, Context as _, Result};
use call::ActiveCall;
use client::{
- proto::{self, PeerId},
- Client, Status, TypedEnvelope, UserStore,
+ proto::{self, ErrorCode, PeerId},
+ Client, ErrorExt, Status, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
@@ -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);
@@ -3919,10 +3924,10 @@ async fn join_channel_internal(
| Status::Reconnecting
| Status::Reauthenticating => continue,
Status::Connected { .. } => break 'outer,
- Status::SignedOut => return Err(anyhow!("not signed in")),
- Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
+ Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
+ Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
- return Err(anyhow!("zed is offline"))
+ return Err(ErrorCode::Disconnected.into())
}
}
}
@@ -3995,9 +4000,27 @@ pub fn join_channel(
if let Some(active_window) = active_window {
active_window
.update(&mut cx, |_, cx| {
+ let detail: SharedString = match err.error_code() {
+ ErrorCode::SignedOut => {
+ "Please sign in to continue.".into()
+ },
+ ErrorCode::UpgradeRequired => {
+ "Your are running an unsupported version of Zed. Please update to continue.".into()
+ },
+ ErrorCode::NoSuchChannel => {
+ "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 => "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,
- &format!("Failed to join channel: {}", err),
+ "Failed to join channel",
+ Some(&detail),
&["Ok"],
)
})?
@@ -4224,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"],
)
})