Typed workspace errors (#57649)
This migrates the `Workspace::show_error` method to take a generic type that implements `WorkspaceError` instead. That trait containts methods to show a proper error message but most importantly means to help with providing actions given certain errors. The intention for this is rather easy: Whenever we show an error to the user on the workspace level, that error should be - actionable to a degree, even if that is only opening logs or whatnot - show a message that is understandable to the user - for not-too-critical errors, should hide after a certain delay and not force the user to dismiss it using their mouse. For now, there exist trait implementations for string error types and anyhow errors. The long term goal here is to get rid of these altogether and migrate everything to be strongly typed and especially actionable instead. Self-Review Checklist: - [ ] I've reviewed my own diff for quality, security, and reliability - [ ] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [X] Performance impact has been considered and is acceptable Release Notes: - N/A --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev> Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
1 parent
bac8897c06
commit
1b7e58bce8
17 files changed
+1132
-447
No files matched your search
@@ -2030,7 +2030,7 @@ impl AgentPanel {
|
|||||||
Err(error) => {
|
Err(error) => {
|
||||||
log::error!("failed to spawn agent panel terminal: {error:#}");
|
log::error!("failed to spawn agent panel terminal: {error:#}");
|
||||||
workspace
|
workspace
|
||||||
.update(cx, |workspace, cx| workspace.show_error(&error, cx))
|
.update(cx, |workspace, cx| workspace.show_error(error, cx))
|
||||||
.log_err();
|
.log_err();
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
if this.pending_terminal_spawn == Some(terminal_id) {
|
if this.pending_terminal_spawn == Some(terminal_id) {
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ use util::{ResultExt as _, maybe};
|
|||||||
use workspace::{
|
use workspace::{
|
||||||
Workspace,
|
Workspace,
|
||||||
notifications::{
|
notifications::{
|
||||||
ErrorMessagePrompt, Notification, NotificationId, SuppressEvent, show_app_notification,
|
Notification, NotificationId, SuppressEvent, show_app_notification,
|
||||||
simple_message_notification::MessageNotification,
|
simple_message_notification::MessageNotification,
|
||||||
},
|
},
|
||||||
|
workspace_error::{ErrorAction, ErrorSeverity, WorkspaceError},
|
||||||
};
|
};
|
||||||
use zed_actions::ShowUpdateNotification;
|
use zed_actions::ShowUpdateNotification;
|
||||||
|
|
||||||
@@ -64,21 +65,28 @@ fn notify_release_notes_failed_to_show(
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Workspace>,
|
cx: &mut Context<Workspace>,
|
||||||
) {
|
) {
|
||||||
struct ViewReleaseNotesError;
|
let url = release_notes_url(cx);
|
||||||
workspace.show_notification(
|
|
||||||
NotificationId::unique::<ViewReleaseNotesError>(),
|
struct ReleaseNotesError {
|
||||||
cx,
|
url: Option<String>,
|
||||||
|cx| {
|
}
|
||||||
cx.new(move |cx| {
|
|
||||||
let url = release_notes_url(cx);
|
impl WorkspaceError for ReleaseNotesError {
|
||||||
let mut prompt = ErrorMessagePrompt::new("Couldn't load release notes", cx);
|
fn primary_message(&self) -> SharedString {
|
||||||
if let Some(url) = url {
|
"Couldn't load release notes".into()
|
||||||
prompt = prompt.with_link_button("View in Browser".to_string(), url);
|
}
|
||||||
}
|
fn severity(&self) -> ErrorSeverity {
|
||||||
prompt
|
ErrorSeverity::Error
|
||||||
})
|
}
|
||||||
},
|
fn primary_action(&self) -> ErrorAction {
|
||||||
);
|
self.url
|
||||||
|
.clone()
|
||||||
|
.map(|url| ErrorAction::link("View in Browser", url))
|
||||||
|
.unwrap_or_else(ErrorAction::dismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.show_error(ReleaseNotesError { url }, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_release_notes_locally(
|
fn view_release_notes_locally(
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ pub fn init(cx: &mut App) {
|
|||||||
})
|
})
|
||||||
.detach_and_notify_err(workspace_handle, window, cx);
|
.detach_and_notify_err(workspace_handle, window, cx);
|
||||||
} else {
|
} else {
|
||||||
workspace.show_error(&"There’s no active call; join one first.", cx);
|
workspace.show_error("There’s no active call; join one first.", cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
workspace.register_action(|workspace, _: &ShareProject, window, cx| {
|
workspace.register_action(|workspace, _: &ShareProject, window, cx| {
|
||||||
|
|||||||
@@ -488,7 +488,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
|||||||
CommandInterceptResult {
|
CommandInterceptResult {
|
||||||
results: vec![CommandInterceptItem {
|
results: vec![CommandInterceptItem {
|
||||||
action: OpenZedUrl {
|
action: OpenZedUrl {
|
||||||
url: query_for_link.clone(),
|
url: query_for_link.clone().into(),
|
||||||
}
|
}
|
||||||
.boxed_clone(),
|
.boxed_clone(),
|
||||||
string: query_for_link,
|
string: query_for_link,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ pub fn initiate_sign_out(copilot: Entity<Copilot>, window: &mut Window, cx: &mut
|
|||||||
Err(err) => cx.update(|window, cx| {
|
Err(err) => cx.update(|window, cx| {
|
||||||
if let Some(workspace) = Workspace::for_window(window, cx) {
|
if let Some(workspace) = Workspace::for_window(window, cx) {
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
workspace.show_error(&err, cx);
|
workspace.show_error(format!("Error: {err}"), cx);
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
log::error!("{:?}", err);
|
log::error!("{:?}", err);
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ use language::{
|
|||||||
use release_channel::AppVersion;
|
use release_channel::AppVersion;
|
||||||
use text::{Anchor, Bias, Point};
|
use text::{Anchor, Bias, Point};
|
||||||
use ui::SharedString;
|
use ui::SharedString;
|
||||||
use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
|
use workspace::notifications::simple_message_notification::MessageNotification;
|
||||||
|
use workspace::notifications::{NotificationId, show_app_notification};
|
||||||
|
use workspace::workspace_error::{ErrorAction, ErrorSeverity, WorkspaceError};
|
||||||
use zeta_prompt::{ParsedOutput, ZetaPromptInput};
|
use zeta_prompt::{ParsedOutput, ZetaPromptInput};
|
||||||
|
|
||||||
use std::{ops::Range, path::Path, sync::Arc};
|
use std::{ops::Range, path::Path, sync::Arc};
|
||||||
@@ -503,14 +505,33 @@ fn handle_api_response<T>(
|
|||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let error_message: SharedString = err.to_string().into();
|
let message: SharedString = err.to_string().into();
|
||||||
|
|
||||||
|
struct UpdateRequiredError {
|
||||||
|
message: SharedString,
|
||||||
|
}
|
||||||
|
impl WorkspaceError for UpdateRequiredError {
|
||||||
|
fn primary_message(&self) -> SharedString {
|
||||||
|
self.message.clone()
|
||||||
|
}
|
||||||
|
fn severity(&self) -> ErrorSeverity {
|
||||||
|
ErrorSeverity::Critical
|
||||||
|
}
|
||||||
|
fn primary_action(&self) -> ErrorAction {
|
||||||
|
ErrorAction::link("Update Zed", "https://zed.dev/releases")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
show_app_notification(
|
show_app_notification(
|
||||||
NotificationId::unique::<ZedUpdateRequiredError>(),
|
NotificationId::unique::<ZedUpdateRequiredError>(),
|
||||||
cx,
|
cx,
|
||||||
move |cx| {
|
move |cx| {
|
||||||
cx.new(|cx| {
|
cx.new({
|
||||||
ErrorMessagePrompt::new(error_message.clone(), cx)
|
let message = message.clone();
|
||||||
.with_link_button("Update Zed", "https://zed.dev/releases")
|
move |cx| {
|
||||||
|
let error = UpdateRequiredError { message };
|
||||||
|
MessageNotification::from_workspace_error(error, cx)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1118,7 +1118,7 @@ impl EditPredictionButton {
|
|||||||
.link_with_handler(
|
.link_with_handler(
|
||||||
"Learn More",
|
"Learn More",
|
||||||
OpenBrowser {
|
OpenBrowser {
|
||||||
url: zed_urls::edit_prediction_docs(cx),
|
url: zed_urls::edit_prediction_docs(cx).into(),
|
||||||
}
|
}
|
||||||
.boxed_clone(),
|
.boxed_clone(),
|
||||||
|_window, _cx| {
|
|_window, _cx| {
|
||||||
@@ -1631,12 +1631,10 @@ fn emit_edit_prediction_menu_opened(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copilot_settings_url(enterprise_uri: Option<&str>) -> String {
|
fn copilot_settings_url(enterprise_uri: Option<&str>) -> Arc<str> {
|
||||||
match enterprise_uri {
|
match enterprise_uri {
|
||||||
Some(uri) => {
|
Some(uri) => format!("{}{}", uri.trim_end_matches('/'), COPILOT_SETTINGS_PATH).into(),
|
||||||
format!("{}{}", uri.trim_end_matches('/'), COPILOT_SETTINGS_PATH)
|
None => COPILOT_SETTINGS_URL.into(),
|
||||||
}
|
|
||||||
None => COPILOT_SETTINGS_URL.to_string(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1672,7 +1670,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(url, "https://my-company.ghe.com/settings/copilot");
|
assert_eq!(url.as_ref(), "https://my-company.ghe.com/settings/copilot");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@@ -1702,7 +1700,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(url, "https://my-company.ghe.com/settings/copilot");
|
assert_eq!(url.as_ref(), "https://my-company.ghe.com/settings/copilot");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
@@ -1723,6 +1721,6 @@ mod tests {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(url, "https://github.com/settings/copilot");
|
assert_eq!(url.as_ref(), "https://github.com/settings/copilot");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1082,7 +1082,10 @@ impl Editor {
|
|||||||
if let Some(url) = url {
|
if let Some(url) = url {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
if parse_zed_link(&url, cx).is_some() {
|
if parse_zed_link(&url, cx).is_some() {
|
||||||
window.dispatch_action(Box::new(zed_actions::OpenZedUrl { url }), cx);
|
window.dispatch_action(
|
||||||
|
Box::new(zed_actions::OpenZedUrl { url: url.into() }),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
cx.open_url(&url);
|
cx.open_url(&url);
|
||||||
}
|
}
|
||||||
@@ -1727,8 +1730,10 @@ impl Editor {
|
|||||||
Some(Either::Left(url)) => {
|
Some(Either::Left(url)) => {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
if parse_zed_link(&url, cx).is_some() {
|
if parse_zed_link(&url, cx).is_some() {
|
||||||
window
|
window.dispatch_action(
|
||||||
.dispatch_action(Box::new(zed_actions::OpenZedUrl { url }), cx);
|
Box::new(zed_actions::OpenZedUrl { url: url.into() }),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
cx.open_url(&url);
|
cx.open_url(&url);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,12 @@ pub fn init(cx: &mut App) {
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
workspace_handle
|
workspace_handle
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
workspace.show_portal_error(err.to_string(), cx);
|
workspace.show_error(
|
||||||
|
workspace::workspace_error::PortalError::new(
|
||||||
|
err.to_string(),
|
||||||
|
),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
return None;
|
return None;
|
||||||
@@ -149,10 +154,10 @@ pub fn init(cx: &mut App) {
|
|||||||
log::error!("Failed to install dev extension: {:?}", err);
|
log::error!("Failed to install dev extension: {:?}", err);
|
||||||
workspace_handle
|
workspace_handle
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
|
// NOTE: using `anyhow::context` here ends up not printing
|
||||||
|
// the error
|
||||||
workspace.show_error(
|
workspace.show_error(
|
||||||
// NOTE: using `anyhow::context` here ends up not printing
|
format!("Failed to install dev extension: {}", err),
|
||||||
// the error
|
|
||||||
&format!("Failed to install dev extension: {}", err),
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ use workspace::SERIALIZATION_THROTTLE_TIME;
|
|||||||
use workspace::{
|
use workspace::{
|
||||||
Item, Workspace,
|
Item, Workspace,
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyTaskExt},
|
notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt},
|
||||||
};
|
};
|
||||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||||
|
|
||||||
@@ -826,7 +826,7 @@ impl GitPanel {
|
|||||||
GitStoreEvent::IndexWriteError(error) => {
|
GitStoreEvent::IndexWriteError(error) => {
|
||||||
this.workspace
|
this.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
workspace.show_error(error, cx);
|
workspace.show_error(format!("{error}"), cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -4080,16 +4080,7 @@ impl GitPanel {
|
|||||||
{
|
{
|
||||||
if let Ok(Some(workspace)) = weak_this.update(cx, |this, _cx| this.workspace.upgrade()) {
|
if let Ok(Some(workspace)) = weak_this.update(cx, |this, _cx| this.workspace.upgrade()) {
|
||||||
let _ = workspace.update(cx, |workspace, cx| {
|
let _ = workspace.update(cx, |workspace, cx| {
|
||||||
struct CommitMessageError;
|
workspace.show_error(format!("Failed to generate commit message: {err}"), cx);
|
||||||
let notification_id = NotificationId::unique::<CommitMessageError>();
|
|
||||||
workspace.show_notification(notification_id, cx, |cx| {
|
|
||||||
cx.new(|cx| {
|
|
||||||
ErrorMessagePrompt::new(
|
|
||||||
format!("Failed to generate commit message: {err}"),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ pub async fn open_remote_project(
|
|||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
for item in open_results.iter().flatten() {
|
for item in open_results.iter().flatten() {
|
||||||
if let Err(e) = item {
|
if let Err(e) = item {
|
||||||
workspace.show_error(&e, cx);
|
workspace.show_error(format!("{e}"), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -4057,10 +4057,7 @@ impl Pane {
|
|||||||
.workspace
|
.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
if workspace.project().read(cx).is_via_collab() {
|
if workspace.project().read(cx).is_via_collab() {
|
||||||
workspace.show_error(
|
workspace.show_error("Cannot drop files on a remote project", cx);
|
||||||
&anyhow::anyhow!("Cannot drop files on a remote project"),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@@ -4116,7 +4113,7 @@ impl Pane {
|
|||||||
_ = workspace.update_in(cx, |workspace, window, cx| {
|
_ = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
for item in opened_items.into_iter().flatten() {
|
for item in opened_items.into_iter().flatten() {
|
||||||
if let Err(e) = item {
|
if let Err(e) = item {
|
||||||
workspace.show_error(&e, cx);
|
workspace.show_error(format!("Error: {e}"), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if to_pane.read(cx).items_len() == 0 {
|
if to_pane.read(cx).items_len() == 0 {
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ mod theme_preview;
|
|||||||
mod toast_layer;
|
mod toast_layer;
|
||||||
mod toolbar;
|
mod toolbar;
|
||||||
pub mod welcome;
|
pub mod welcome;
|
||||||
|
pub mod workspace_error;
|
||||||
mod workspace_settings;
|
mod workspace_settings;
|
||||||
|
|
||||||
pub use crate::notifications::NotificationFrame;
|
|
||||||
pub use dock::Panel;
|
pub use dock::Panel;
|
||||||
pub use multi_workspace::{
|
pub use multi_workspace::{
|
||||||
CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MoveProjectToNewWindow,
|
CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MoveProjectToNewWindow,
|
||||||
@@ -577,16 +577,16 @@ pub enum CloseIntent {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Toast {
|
pub struct Toast {
|
||||||
id: NotificationId,
|
id: NotificationId,
|
||||||
msg: Cow<'static, str>,
|
message: Cow<'static, str>,
|
||||||
autohide: bool,
|
autohide: bool,
|
||||||
on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
|
on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Toast {
|
impl Toast {
|
||||||
pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
|
pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, message: I) -> Self {
|
||||||
Toast {
|
Toast {
|
||||||
id,
|
id,
|
||||||
msg: msg.into(),
|
message: message.into(),
|
||||||
on_click: None,
|
on_click: None,
|
||||||
autohide: false,
|
autohide: false,
|
||||||
}
|
}
|
||||||
@@ -610,7 +610,7 @@ impl Toast {
|
|||||||
impl PartialEq for Toast {
|
impl PartialEq for Toast {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.id == other.id
|
self.id == other.id
|
||||||
&& self.msg == other.msg
|
&& self.message == other.message
|
||||||
&& self.on_click.is_some() == other.on_click.is_some()
|
&& self.on_click.is_some() == other.on_click.is_some()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3012,7 +3012,8 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let rx = workspace.update_in(cx, |workspace, window, cx| {
|
let rx = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
workspace.show_portal_error(err.to_string(), cx);
|
workspace
|
||||||
|
.show_error(workspace_error::PortalError::new(err.to_string()), cx);
|
||||||
let prompt = workspace.on_prompt_for_open_path.take().unwrap();
|
let prompt = workspace.on_prompt_for_open_path.take().unwrap();
|
||||||
let rx = prompt(workspace, lister, window, cx);
|
let rx = prompt(workspace, lister, window, cx);
|
||||||
workspace.on_prompt_for_open_path = Some(prompt);
|
workspace.on_prompt_for_open_path = Some(prompt);
|
||||||
@@ -3068,7 +3069,8 @@ impl Workspace {
|
|||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let rx = workspace.update_in(cx, |workspace, window, cx| {
|
let rx = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
workspace.show_portal_error(err.to_string(), cx);
|
workspace
|
||||||
|
.show_error(workspace_error::PortalError::new(err.to_string()), cx);
|
||||||
|
|
||||||
let prompt = workspace.on_prompt_for_new_path.take().unwrap();
|
let prompt = workspace.on_prompt_for_new_path.take().unwrap();
|
||||||
let rx = prompt(workspace, lister, suggested_name, window, cx);
|
let rx = prompt(workspace, lister, suggested_name, window, cx);
|
||||||
@@ -3851,10 +3853,7 @@ impl Workspace {
|
|||||||
) {
|
) {
|
||||||
let project = self.project.read(cx);
|
let project = self.project.read(cx);
|
||||||
if project.is_via_collab() {
|
if project.is_via_collab() {
|
||||||
self.show_error(
|
self.show_error("You cannot add folders to someone else's project", cx);
|
||||||
&anyhow!("You cannot add folders to someone else's project"),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let paths = self.prompt_for_open_path(
|
let paths = self.prompt_for_open_path(
|
||||||
@@ -10133,7 +10132,7 @@ pub fn open_paths(
|
|||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
for item in open_task.iter().flatten() {
|
for item in open_task.iter().flatten() {
|
||||||
if let Err(e) = item {
|
if let Err(e) = item {
|
||||||
workspace.show_error(&e, cx);
|
workspace.show_error(format!("Error: {e}"), cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -10468,10 +10467,10 @@ async fn open_remote_project_inner(
|
|||||||
for error in project_path_errors {
|
for error in project_path_errors {
|
||||||
if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
|
if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
|
||||||
if let Some(path) = error.error_tag("path") {
|
if let Some(path) = error.error_tag("path") {
|
||||||
workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
|
workspace.show_error(format!("'{path}' does not exist"), cx)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
workspace.show_error(&error, cx)
|
workspace.show_error(format!("{error}"), cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use gpui::{Action, SharedString};
|
||||||
|
use ui::{IconName, IconPosition};
|
||||||
|
use zed_actions::OpenBrowser;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ErrorSeverity {
|
||||||
|
Critical,
|
||||||
|
Error,
|
||||||
|
Warning,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorSeverity {
|
||||||
|
pub fn auto_dismiss_delay(&self) -> Option<Duration> {
|
||||||
|
match self {
|
||||||
|
ErrorSeverity::Critical => None,
|
||||||
|
ErrorSeverity::Error => Some(Duration::from_secs(20)),
|
||||||
|
ErrorSeverity::Warning => Some(Duration::from_secs(10)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The behavior triggered when the user invokes an [`ErrorAction`].
|
||||||
|
pub enum ErrorActionHandler {
|
||||||
|
/// Run the provided callback when the action is invoked.
|
||||||
|
/// The notification is still dismissed afterwards by the button's click handler.
|
||||||
|
Action(Box<dyn Action>),
|
||||||
|
/// Dismiss the notification without running any extra logic.
|
||||||
|
Dismiss,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An icon to display on an action button, with its position relative to the label.
|
||||||
|
///
|
||||||
|
/// Bundled together so the position is only carried around when an icon is actually set
|
||||||
|
/// — there's no meaningful "position" without an icon to position.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct ActionIcon {
|
||||||
|
pub name: IconName,
|
||||||
|
pub position: IconPosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionIcon {
|
||||||
|
/// Show `name` at the start (left) of the action button label.
|
||||||
|
pub fn start(name: IconName) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
position: IconPosition::Start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show `name` at the end (right) of the action button label.
|
||||||
|
pub fn end(name: IconName) -> Self {
|
||||||
|
Self {
|
||||||
|
name,
|
||||||
|
position: IconPosition::End,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ErrorAction {
|
||||||
|
pub label: SharedString,
|
||||||
|
pub icon: Option<ActionIcon>,
|
||||||
|
pub tooltip: Option<SharedString>,
|
||||||
|
pub handler: ErrorActionHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorAction {
|
||||||
|
pub fn new<A: Action + 'static>(label: impl Into<SharedString>, handler: A) -> Self {
|
||||||
|
Self {
|
||||||
|
label: label.into(),
|
||||||
|
icon: None,
|
||||||
|
tooltip: None,
|
||||||
|
handler: ErrorActionHandler::Action(Box::new(handler)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a dismiss-only action labelled "Dismiss".
|
||||||
|
///
|
||||||
|
/// Useful as a sensible default for [`WorkspaceError::primary_action`] when the error has no
|
||||||
|
/// recovery affordance beyond closing the notification.
|
||||||
|
pub fn dismiss() -> Self {
|
||||||
|
Self {
|
||||||
|
label: "Dismiss".into(),
|
||||||
|
icon: None,
|
||||||
|
tooltip: None,
|
||||||
|
handler: ErrorActionHandler::Dismiss,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show `icon` at the start (left) of the action button label.
|
||||||
|
pub fn with_icon(mut self, icon: IconName) -> Self {
|
||||||
|
self.icon = Some(ActionIcon::start(icon));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show `icon` at the end (right) of the action button label.
|
||||||
|
///
|
||||||
|
/// Useful for actions that navigate the user elsewhere — for example the trailing
|
||||||
|
/// `⇗` produced by [`Self::link`].
|
||||||
|
pub fn with_end_icon(mut self, icon: IconName) -> Self {
|
||||||
|
self.icon = Some(ActionIcon::end(icon));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
|
||||||
|
self.tooltip = Some(tooltip.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn link(label: impl Into<SharedString>, url: impl Into<Arc<str>>) -> Self {
|
||||||
|
Self::new(label, OpenBrowser { url: url.into() }).with_end_icon(IconName::ArrowUpRight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WorkspaceError {
|
||||||
|
fn primary_message(&self) -> SharedString;
|
||||||
|
|
||||||
|
fn secondary_message(&self) -> Option<SharedString> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The primary action shown in the error notification.
|
||||||
|
///
|
||||||
|
/// If in doubt, use [`ErrorAction::dismiss`].
|
||||||
|
fn primary_action(&self) -> ErrorAction;
|
||||||
|
|
||||||
|
fn secondary_action(&self) -> Option<ErrorAction> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn severity(&self) -> ErrorSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkspaceError for &'static str {
|
||||||
|
fn primary_message(&self) -> SharedString {
|
||||||
|
self.to_string().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_action(&self) -> ErrorAction {
|
||||||
|
ErrorAction::dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn severity(&self) -> ErrorSeverity {
|
||||||
|
ErrorSeverity::Critical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkspaceError for String {
|
||||||
|
fn primary_message(&self) -> SharedString {
|
||||||
|
self.clone().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_action(&self) -> ErrorAction {
|
||||||
|
ErrorAction::dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn severity(&self) -> ErrorSeverity {
|
||||||
|
ErrorSeverity::Critical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkspaceError for anyhow::Error {
|
||||||
|
fn primary_message(&self) -> SharedString {
|
||||||
|
format!("{self}").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_action(&self) -> ErrorAction {
|
||||||
|
ErrorAction::dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn severity(&self) -> ErrorSeverity {
|
||||||
|
ErrorSeverity::Critical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PortalError {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PortalError {
|
||||||
|
pub fn new(message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkspaceError for PortalError {
|
||||||
|
fn primary_message(&self) -> SharedString {
|
||||||
|
self.message.clone().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn severity(&self) -> ErrorSeverity {
|
||||||
|
ErrorSeverity::Critical
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_action(&self) -> ErrorAction {
|
||||||
|
ErrorAction::link(
|
||||||
|
"See docs",
|
||||||
|
"https://zed.dev/docs/linux#i-cant-open-any-files",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
-2
@@ -71,6 +71,8 @@ use settings::{
|
|||||||
update_settings_file,
|
update_settings_file,
|
||||||
};
|
};
|
||||||
use sidebar::Sidebar;
|
use sidebar::Sidebar;
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
use workspace::workspace_error::{ErrorAction, ErrorSeverity, WorkspaceError};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
@@ -156,6 +158,15 @@ actions!(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
actions!(
|
||||||
|
dev,
|
||||||
|
[
|
||||||
|
/// Show an error on the workspace level.
|
||||||
|
ShowWorkspaceError
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
cx.on_action(|_: &Hide, cx| cx.hide());
|
cx.on_action(|_: &Hide, cx| cx.hide());
|
||||||
@@ -891,7 +902,7 @@ fn register_actions(
|
|||||||
})
|
})
|
||||||
.register_action(|_, action: &OpenZedUrl, _, cx| {
|
.register_action(|_, action: &OpenZedUrl, _, cx| {
|
||||||
OpenListener::global(cx).open(RawOpenRequest {
|
OpenListener::global(cx).open(RawOpenRequest {
|
||||||
urls: vec![action.url.clone()],
|
urls: vec![String::from(&*action.url)],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -909,7 +920,7 @@ fn register_actions(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
workspace.show_error(
|
workspace.show_error(
|
||||||
&anyhow::anyhow!(
|
format!(
|
||||||
"Opening this URL in a browser failed because the URL is invalid: {}\n\nError was: {e}",
|
"Opening this URL in a browser failed because the URL is invalid: {}\n\nError was: {e}",
|
||||||
action.url
|
action.url
|
||||||
),
|
),
|
||||||
@@ -1293,6 +1304,43 @@ fn register_actions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
workspace.register_action(sidebar::dump_workspace_info);
|
workspace.register_action(sidebar::dump_workspace_info);
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
workspace.register_action(|workspace, _: &ShowWorkspaceError, _, cx| {
|
||||||
|
struct DebugError;
|
||||||
|
struct SecondDebugError;
|
||||||
|
|
||||||
|
impl WorkspaceError for DebugError {
|
||||||
|
fn primary_message(&self) -> SharedString {
|
||||||
|
SharedString::new_static("This is an error.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn severity(&self) -> ErrorSeverity {
|
||||||
|
ErrorSeverity::Warning
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_action(&self) -> ErrorAction {
|
||||||
|
ErrorAction::dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkspaceError for SecondDebugError {
|
||||||
|
fn primary_message(&self) -> SharedString {
|
||||||
|
SharedString::new_static("This is some error to ignore.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn severity(&self) -> ErrorSeverity {
|
||||||
|
ErrorSeverity::Error
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_action(&self) -> ErrorAction {
|
||||||
|
ErrorAction::dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.show_error(DebugError, cx);
|
||||||
|
workspace.show_error(SecondDebugError, cx);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize_pane(
|
fn initialize_pane(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use gpui::{Action, actions};
|
use gpui::{Action, actions};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
// If the zed binary doesn't use anything in this crate, it will be optimized away
|
// If the zed binary doesn't use anything in this crate, it will be optimized away
|
||||||
// and the actions won't initialize. So we just provide an empty initialization function
|
// and the actions won't initialize. So we just provide an empty initialization function
|
||||||
@@ -17,7 +17,7 @@ pub fn init() {}
|
|||||||
#[action(namespace = zed)]
|
#[action(namespace = zed)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct OpenBrowser {
|
pub struct OpenBrowser {
|
||||||
pub url: String,
|
pub url: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a zed:// URL within the application.
|
/// Opens a zed:// URL within the application.
|
||||||
@@ -25,7 +25,7 @@ pub struct OpenBrowser {
|
|||||||
#[action(namespace = zed)]
|
#[action(namespace = zed)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct OpenZedUrl {
|
pub struct OpenZedUrl {
|
||||||
pub url: String,
|
pub url: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the keymap to either add a keybinding or change an existing one
|
/// Opens the keymap to either add a keybinding or change an existing one
|
||||||
|
|||||||
Reference in new issue
Block a user