Load all key bindings that parse and use markdown in error notifications (#23113)
* Collects and reports all parse errors * Shares parsed `KeyBindingContextPredicate` among the actions. * Updates gpui keybinding and action parsing to return structured errors. * Renames "block" to "section" to match the docs, as types like `KeymapSection` are shown in `json-language-server` hovers. * Removes wrapping of `context` and `use_key_equivalents` fields so that `json-language-server` auto-inserts `""` and `false` instead of `null`. * Updates `add_to_cx` to take `&self`, so that the user keymap doesn't get unnecessarily cloned. In retrospect I wish I'd just switched to using TreeSitter to do the parsing and provide proper diagnostics. This is tracked in #23333 Release Notes: - Improved handling of errors within the user keymap file. Parse errors within context, keystrokes, or actions no longer prevent loading the key bindings that do parse.
This commit is contained in:
parent
c929533e00
commit
711dc21eb2
18 changed files with 552 additions and 196 deletions
|
@ -291,7 +291,7 @@ fn main() {
|
|||
}
|
||||
settings::init(cx);
|
||||
handle_settings_file_changes(user_settings_file_rx, cx, handle_settings_changed);
|
||||
handle_keymap_file_changes(user_keymap_file_rx, cx, handle_keymap_changed);
|
||||
handle_keymap_file_changes(user_keymap_file_rx, cx);
|
||||
client::init_settings(cx);
|
||||
let user_agent = format!(
|
||||
"Zed/{} ({}; {})",
|
||||
|
@ -609,31 +609,6 @@ fn main() {
|
|||
});
|
||||
}
|
||||
|
||||
fn handle_keymap_changed(error: Option<anyhow::Error>, cx: &mut AppContext) {
|
||||
struct KeymapParseErrorNotification;
|
||||
let id = NotificationId::unique::<KeymapParseErrorNotification>();
|
||||
|
||||
for workspace in workspace::local_workspace_windows(cx) {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| match &error {
|
||||
Some(error) => {
|
||||
workspace.show_notification(id.clone(), cx, |cx| {
|
||||
cx.new_view(|_| {
|
||||
MessageNotification::new(format!("Invalid keymap file\n{error}"))
|
||||
.with_click_message("Open keymap file")
|
||||
.on_click(|cx| {
|
||||
cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
None => workspace.dismiss_notification(&id, cx),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut AppContext) {
|
||||
struct SettingsParseErrorNotification;
|
||||
let id = NotificationId::unique::<SettingsParseErrorNotification>();
|
||||
|
|
|
@ -22,9 +22,10 @@ use feature_flags::FeatureFlagAppExt;
|
|||
use futures::FutureExt;
|
||||
use futures::{channel::mpsc, select_biased, StreamExt};
|
||||
use gpui::{
|
||||
actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, MenuItem,
|
||||
PathPromptOptions, PromptLevel, ReadGlobal, Task, TitlebarOptions, View, ViewContext,
|
||||
VisualContext, WindowKind, WindowOptions,
|
||||
actions, point, px, Action, AppContext, AsyncAppContext, Context, DismissEvent, Element,
|
||||
FocusableView, KeyBinding, MenuItem, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal,
|
||||
SharedString, Styled, Task, TitlebarOptions, View, ViewContext, VisualContext, WindowKind,
|
||||
WindowOptions,
|
||||
};
|
||||
pub use open_listener::*;
|
||||
use outline_panel::OutlinePanel;
|
||||
|
@ -39,18 +40,20 @@ use rope::Rope;
|
|||
use search::project_search::ProjectSearchBar;
|
||||
use settings::{
|
||||
initial_project_settings_content, initial_tasks_content, update_settings_file, KeymapFile,
|
||||
Settings, SettingsStore, DEFAULT_KEYMAP_PATH, VIM_KEYMAP_PATH,
|
||||
KeymapFileLoadResult, Settings, SettingsStore, DEFAULT_KEYMAP_PATH, VIM_KEYMAP_PATH,
|
||||
};
|
||||
use std::any::TypeId;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
|
||||
use terminal_view::terminal_panel::{self, TerminalPanel};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use util::markdown::MarkdownString;
|
||||
use util::{asset_str, ResultExt};
|
||||
use uuid::Uuid;
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use welcome::{BaseKeymap, MultibufferHint};
|
||||
use workspace::notifications::NotificationId;
|
||||
use workspace::notifications::{dismiss_app_notification, show_app_notification, NotificationId};
|
||||
use workspace::CloseIntent;
|
||||
use workspace::{
|
||||
create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
|
||||
|
@ -983,7 +986,6 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
|||
pub fn handle_keymap_file_changes(
|
||||
mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
|
||||
cx: &mut AppContext,
|
||||
keymap_changed: impl Fn(Option<anyhow::Error>, &mut AppContext) + 'static,
|
||||
) {
|
||||
BaseKeymap::register(cx);
|
||||
VimModeSetting::register(cx);
|
||||
|
@ -1016,36 +1018,122 @@ pub fn handle_keymap_file_changes(
|
|||
|
||||
load_default_keymap(cx);
|
||||
|
||||
struct KeymapParseErrorNotification;
|
||||
let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();
|
||||
|
||||
cx.spawn(move |cx| async move {
|
||||
let mut user_keymap = KeymapFile::default();
|
||||
let mut user_key_bindings = Vec::new();
|
||||
loop {
|
||||
select_biased! {
|
||||
_ = base_keymap_rx.next() => {}
|
||||
_ = keyboard_layout_rx.next() => {}
|
||||
user_keymap_content = user_keymap_file_rx.next() => {
|
||||
if let Some(user_keymap_content) = user_keymap_content {
|
||||
match KeymapFile::parse(&user_keymap_content) {
|
||||
Ok(keymap_content) => {
|
||||
cx.update(|cx| keymap_changed(None, cx)).log_err();
|
||||
user_keymap = keymap_content;
|
||||
}
|
||||
Err(error) => {
|
||||
cx.update(|cx| keymap_changed(Some(error), cx)).log_err();
|
||||
}
|
||||
}
|
||||
cx.update(|cx| {
|
||||
let load_result = KeymapFile::load(&user_keymap_content, cx);
|
||||
match load_result {
|
||||
KeymapFileLoadResult::Success { key_bindings } => {
|
||||
user_key_bindings = key_bindings;
|
||||
dismiss_app_notification(¬ification_id, cx);
|
||||
}
|
||||
KeymapFileLoadResult::SomeFailedToLoad {
|
||||
key_bindings,
|
||||
error_message
|
||||
} => {
|
||||
user_key_bindings = key_bindings;
|
||||
show_keymap_file_load_error(notification_id.clone(), error_message, cx);
|
||||
}
|
||||
KeymapFileLoadResult::AllFailedToLoad {
|
||||
error_message
|
||||
} => {
|
||||
show_keymap_file_load_error(notification_id.clone(), error_message, cx);
|
||||
}
|
||||
KeymapFileLoadResult::JsonParseFailure { error } => {
|
||||
show_keymap_file_json_error(notification_id.clone(), error, cx);
|
||||
}
|
||||
};
|
||||
}).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.update(|cx| reload_keymaps(cx, &user_keymap)).ok();
|
||||
cx.update(|cx| reload_keymaps(cx, user_key_bindings.clone()))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
|
||||
fn show_keymap_file_json_error(
|
||||
notification_id: NotificationId,
|
||||
error: anyhow::Error,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let message: SharedString =
|
||||
format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
|
||||
show_app_notification(notification_id, cx, move |cx| {
|
||||
cx.new_view(|_cx| {
|
||||
MessageNotification::new(message.clone())
|
||||
.with_click_message("Open keymap file")
|
||||
.on_click(|cx| {
|
||||
cx.dispatch_action(zed_actions::OpenKeymap.boxed_clone());
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
})
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn show_keymap_file_load_error(
|
||||
notification_id: NotificationId,
|
||||
markdown_error_message: MarkdownString,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let parsed_markdown = cx.background_executor().spawn(async move {
|
||||
let file_location_directory = None;
|
||||
let language_registry = None;
|
||||
markdown_preview::markdown_parser::parse_markdown(
|
||||
&markdown_error_message.0,
|
||||
file_location_directory,
|
||||
language_registry,
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
cx.spawn(move |cx| async move {
|
||||
let parsed_markdown = Rc::new(parsed_markdown.await);
|
||||
cx.update(|cx| {
|
||||
show_app_notification(notification_id, cx, move |cx| {
|
||||
let workspace_handle = cx.view().downgrade();
|
||||
let parsed_markdown = parsed_markdown.clone();
|
||||
cx.new_view(move |_cx| {
|
||||
MessageNotification::new_from_builder(move |cx| {
|
||||
gpui::div()
|
||||
.text_xs()
|
||||
.child(markdown_preview::markdown_renderer::render_parsed_markdown(
|
||||
&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);
|
||||
})
|
||||
})
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn reload_keymaps(cx: &mut AppContext, user_key_bindings: Vec<KeyBinding>) {
|
||||
cx.clear_key_bindings();
|
||||
load_default_keymap(cx);
|
||||
keymap_content.clone().add_to_cx(cx).log_err();
|
||||
cx.bind_keys(user_key_bindings);
|
||||
cx.set_menus(app_menus());
|
||||
cx.set_dock_menu(vec![MenuItem::action("New Window", workspace::NewWindow)]);
|
||||
}
|
||||
|
@ -1056,13 +1144,13 @@ pub fn load_default_keymap(cx: &mut AppContext) {
|
|||
return;
|
||||
}
|
||||
|
||||
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
|
||||
cx.bind_keys(KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap());
|
||||
if VimModeSetting::get_global(cx).0 {
|
||||
KeymapFile::load_asset(VIM_KEYMAP_PATH, cx).unwrap();
|
||||
cx.bind_keys(KeymapFile::load_asset(VIM_KEYMAP_PATH, cx).unwrap());
|
||||
}
|
||||
|
||||
if let Some(asset_path) = base_keymap.asset_path() {
|
||||
KeymapFile::load_asset(asset_path, cx).unwrap();
|
||||
cx.bind_keys(KeymapFile::load_asset(asset_path, cx).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3375,7 +3463,7 @@ mod tests {
|
|||
PathBuf::from("/keymap.json"),
|
||||
);
|
||||
handle_settings_file_changes(settings_rx, cx, |_, _| {});
|
||||
handle_keymap_file_changes(keymap_rx, cx, |_, _| {});
|
||||
handle_keymap_file_changes(keymap_rx, cx);
|
||||
});
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
|
@ -3488,7 +3576,7 @@ mod tests {
|
|||
);
|
||||
|
||||
handle_settings_file_changes(settings_rx, cx, |_, _| {});
|
||||
handle_keymap_file_changes(keymap_rx, cx, |_, _| {});
|
||||
handle_keymap_file_changes(keymap_rx, cx);
|
||||
});
|
||||
|
||||
cx.background_executor.run_until_parked();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue