Display keymap errors on initial load (#23394)

Also fixes issue introduced in #23113 where changes to keyboard layout
would not cause reload of keymap configuration.

Closes #20531

Release Notes:

- N/A
This commit is contained in:
Michael Sloan 2025-01-21 00:14:46 -07:00 committed by GitHub
parent 04c04e8406
commit cc1af7d96b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 147 additions and 82 deletions

View file

@ -119,9 +119,6 @@ pub enum KeymapFileLoadResult {
key_bindings: Vec<KeyBinding>, key_bindings: Vec<KeyBinding>,
error_message: MarkdownString, error_message: MarkdownString,
}, },
AllFailedToLoad {
error_message: MarkdownString,
},
JsonParseFailure { JsonParseFailure {
error: anyhow::Error, error: anyhow::Error,
}, },
@ -135,8 +132,7 @@ impl KeymapFile {
pub fn load_asset(asset_path: &str, cx: &AppContext) -> anyhow::Result<Vec<KeyBinding>> { pub fn load_asset(asset_path: &str, cx: &AppContext) -> anyhow::Result<Vec<KeyBinding>> {
match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) { match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
KeymapFileLoadResult::Success { key_bindings, .. } => Ok(key_bindings), KeymapFileLoadResult::Success { key_bindings, .. } => Ok(key_bindings),
KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => Err(anyhow!(
| KeymapFileLoadResult::AllFailedToLoad { error_message } => Err(anyhow!(
"Error loading built-in keymap \"{asset_path}\": {error_message}" "Error loading built-in keymap \"{asset_path}\": {error_message}"
)), )),
KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!( KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
@ -151,11 +147,14 @@ impl KeymapFile {
cx: &AppContext, cx: &AppContext,
) -> anyhow::Result<Vec<KeyBinding>> { ) -> anyhow::Result<Vec<KeyBinding>> {
match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) { match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
KeymapFileLoadResult::Success { key_bindings, .. } KeymapFileLoadResult::SomeFailedToLoad {
| KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings), key_bindings,
KeymapFileLoadResult::AllFailedToLoad { error_message } => Err(anyhow!( error_message,
} if key_bindings.is_empty() => Err(anyhow!(
"Error loading built-in keymap \"{asset_path}\": {error_message}" "Error loading built-in keymap \"{asset_path}\": {error_message}"
)), )),
KeymapFileLoadResult::Success { key_bindings, .. }
| KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!( KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
"JSON parse error in built-in keymap \"{asset_path}\": {error}" "JSON parse error in built-in keymap \"{asset_path}\": {error}"
)), )),
@ -166,8 +165,7 @@ impl KeymapFile {
pub fn load_panic_on_failure(content: &str, cx: &AppContext) -> Vec<KeyBinding> { pub fn load_panic_on_failure(content: &str, cx: &AppContext) -> Vec<KeyBinding> {
match Self::load(content, cx) { match Self::load(content, cx) {
KeymapFileLoadResult::Success { key_bindings } => key_bindings, KeymapFileLoadResult::Success { key_bindings } => key_bindings,
KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
| KeymapFileLoadResult::AllFailedToLoad { error_message, .. } => {
panic!("{error_message}"); panic!("{error_message}");
} }
KeymapFileLoadResult::JsonParseFailure { error } => { KeymapFileLoadResult::JsonParseFailure { error } => {

View file

@ -1,7 +1,7 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
/// Markdown text. /// Markdown text.
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct MarkdownString(pub String); pub struct MarkdownString(pub String);
impl Display for MarkdownString { impl Display for MarkdownString {

View file

@ -22,10 +22,10 @@ use feature_flags::FeatureFlagAppExt;
use futures::FutureExt; use futures::FutureExt;
use futures::{channel::mpsc, select_biased, StreamExt}; use futures::{channel::mpsc, select_biased, StreamExt};
use gpui::{ use gpui::{
actions, point, px, Action, AppContext, AsyncAppContext, Context, DismissEvent, Element, actions, point, px, Action, AnyWindowHandle, AppContext, AsyncAppContext, Context,
FocusableView, KeyBinding, MenuItem, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, DismissEvent, Element, FocusableView, KeyBinding, MenuItem, ParentElement, PathPromptOptions,
SharedString, Styled, Task, TitlebarOptions, View, ViewContext, VisualContext, WindowKind, PromptLevel, ReadGlobal, SharedString, Styled, Task, TitlebarOptions, View, ViewContext,
WindowOptions, VisualContext, WindowKind, WindowOptions,
}; };
pub use open_listener::*; pub use open_listener::*;
use outline_panel::OutlinePanel; use outline_panel::OutlinePanel;
@ -1017,6 +1017,16 @@ pub fn handle_keymap_file_changes(
}) })
.detach(); .detach();
// Need to notify about keymap load errors when new workspaces are created, so that initial
// keymap load errors are shown to the user.
let (new_workspace_window_tx, mut new_workspace_window_rx) = mpsc::unbounded();
cx.observe_new_views(move |_: &mut Workspace, cx| {
new_workspace_window_tx
.unbounded_send(cx.window_handle())
.unwrap();
})
.detach();
let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout()); let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout());
cx.on_keyboard_layout_change(move |cx| { cx.on_keyboard_layout_change(move |cx| {
let next_mapping = settings::get_key_equivalents(cx.keyboard_layout()); let next_mapping = settings::get_key_equivalents(cx.keyboard_layout());
@ -1033,68 +1043,102 @@ pub fn handle_keymap_file_changes(
let notification_id = NotificationId::unique::<KeymapParseErrorNotification>(); let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();
cx.spawn(move |cx| async move { cx.spawn(move |cx| async move {
let mut user_key_bindings = Vec::new(); let mut user_keymap_content = String::new();
enum LastError {
None,
JsonError(anyhow::Error),
LoadError(MarkdownString),
}
let mut last_load_error = LastError::None;
loop { loop {
select_biased! { let new_workspace_window = select_biased! {
_ = base_keymap_rx.next() => {} _ = base_keymap_rx.next() => None,
_ = keyboard_layout_rx.next() => {} _ = keyboard_layout_rx.next() => None,
user_keymap_content = user_keymap_file_rx.next() => { workspace = new_workspace_window_rx.next() => workspace,
if let Some(user_keymap_content) = user_keymap_content { content = user_keymap_file_rx.next() => {
cx.update(|cx| { if let Some(content) = content {
let load_result = KeymapFile::load(&user_keymap_content, cx); user_keymap_content = content;
match load_result { }
KeymapFileLoadResult::Success { key_bindings } => { None
user_key_bindings = key_bindings; }
dismiss_app_notification(&notification_id, cx); };
} cx.update(|cx| {
KeymapFileLoadResult::SomeFailedToLoad { // No need to reload keymaps when a new workspace is added, just need to send the notification to it.
key_bindings, if new_workspace_window.is_none() {
error_message let load_result = KeymapFile::load(&user_keymap_content, cx);
} => { match load_result {
user_key_bindings = key_bindings; KeymapFileLoadResult::Success { key_bindings } => {
show_keymap_file_load_error(notification_id.clone(), error_message, cx); reload_keymaps(cx, key_bindings);
} dismiss_app_notification(&notification_id, cx);
KeymapFileLoadResult::AllFailedToLoad { last_load_error = LastError::None;
error_message }
} => { KeymapFileLoadResult::SomeFailedToLoad {
show_keymap_file_load_error(notification_id.clone(), error_message, cx); key_bindings,
} error_message,
KeymapFileLoadResult::JsonParseFailure { error } => { } => {
show_keymap_file_json_error(notification_id.clone(), error, cx); if !key_bindings.is_empty() {
} reload_keymaps(cx, key_bindings);
}; }
}).ok(); last_load_error = LastError::LoadError(error_message);
}
KeymapFileLoadResult::JsonParseFailure { error } => {
last_load_error = LastError::JsonError(error);
}
} }
} }
} match &last_load_error {
cx.update(|cx| reload_keymaps(cx, user_key_bindings.clone())) LastError::None => {}
.ok(); LastError::JsonError(err) => {
show_keymap_file_json_error(
new_workspace_window,
notification_id.clone(),
err,
cx,
);
}
LastError::LoadError(message) => {
show_keymap_file_load_error(
new_workspace_window,
notification_id.clone(),
message.clone(),
cx,
);
}
}
})
.ok();
} }
}) })
.detach(); .detach();
} }
fn show_keymap_file_json_error( fn show_keymap_file_json_error(
new_workspace_window: Option<AnyWindowHandle>,
notification_id: NotificationId, notification_id: NotificationId,
error: anyhow::Error, error: &anyhow::Error,
cx: &mut AppContext, cx: &mut AppContext,
) { ) {
let message: SharedString = let message: SharedString =
format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into(); format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
show_app_notification(notification_id, cx, move |cx| { show_notification_to_specific_workspace_or_all_workspaces(
cx.new_view(|_cx| { new_workspace_window,
MessageNotification::new(message.clone()) notification_id,
.with_click_message("Open keymap file") cx,
.on_click(|cx| { move |cx| {
cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone()); cx.new_view(|_cx| {
cx.emit(DismissEvent); MessageNotification::new(message.clone())
}) .with_click_message("Open keymap file")
}) .on_click(|cx| {
}) cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
.log_err(); cx.emit(DismissEvent);
})
})
},
);
} }
fn show_keymap_file_load_error( fn show_keymap_file_load_error(
new_workspace_window: Option<AnyWindowHandle>,
notification_id: NotificationId, notification_id: NotificationId,
markdown_error_message: MarkdownString, markdown_error_message: MarkdownString,
cx: &mut AppContext, cx: &mut AppContext,
@ -1113,34 +1157,57 @@ fn show_keymap_file_load_error(
cx.spawn(move |cx| async move { cx.spawn(move |cx| async move {
let parsed_markdown = Rc::new(parsed_markdown.await); let parsed_markdown = Rc::new(parsed_markdown.await);
cx.update(|cx| { cx.update(|cx| {
show_app_notification(notification_id, cx, move |cx| { show_notification_to_specific_workspace_or_all_workspaces(
let workspace_handle = cx.view().downgrade(); new_workspace_window,
let parsed_markdown = parsed_markdown.clone(); notification_id,
cx.new_view(move |_cx| { cx,
MessageNotification::new_from_builder(move |cx| { move |cx| {
gpui::div() let workspace_handle = cx.view().downgrade();
.text_xs() let parsed_markdown = parsed_markdown.clone();
.child(markdown_preview::markdown_renderer::render_parsed_markdown( cx.new_view(move |_cx| {
&parsed_markdown.clone(), MessageNotification::new_from_builder(move |cx| {
Some(workspace_handle.clone()), gpui::div()
cx, .text_xs()
)) .child(markdown_preview::markdown_renderer::render_parsed_markdown(
.into_any() &parsed_markdown.clone(),
Some(workspace_handle.clone()),
cx,
))
.into_any()
})
.with_click_message("Open keymap file")
.on_click(|cx| {
cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
cx.emit(DismissEvent);
})
}) })
.with_click_message("Open keymap file") },
.on_click(|cx| { )
cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
cx.emit(DismissEvent);
})
})
})
.log_err();
}) })
.log_err(); .ok();
}) })
.detach(); .detach();
} }
fn show_notification_to_specific_workspace_or_all_workspaces<V>(
new_workspace_window: Option<AnyWindowHandle>,
notification_id: NotificationId,
cx: &mut AppContext,
build_notification: impl Fn(&mut ViewContext<Workspace>) -> View<V>,
) where
V: workspace::notifications::Notification,
{
if let Some(workspace_window) = new_workspace_window.and_then(|w| w.downcast::<Workspace>()) {
workspace_window
.update(cx, |workspace, cx| {
workspace.show_notification(notification_id, cx, build_notification);
})
.ok();
} else {
show_app_notification(notification_id, cx, build_notification).ok();
}
}
fn reload_keymaps(cx: &mut AppContext, user_key_bindings: Vec<KeyBinding>) { fn reload_keymaps(cx: &mut AppContext, user_key_bindings: Vec<KeyBinding>) {
cx.clear_key_bindings(); cx.clear_key_bindings();
load_default_keymap(cx); load_default_keymap(cx);