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
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -14333,6 +14333,7 @@ name = "vim"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assets",
|
||||
"async-compat",
|
||||
"async-trait",
|
||||
"collections",
|
||||
|
@ -14350,6 +14351,7 @@ dependencies = [
|
|||
"multi_buffer",
|
||||
"nvim-rs",
|
||||
"parking_lot",
|
||||
"project_panel",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
|
|
|
@ -304,7 +304,9 @@ impl TestServer {
|
|||
collab_ui::init(&app_state, cx);
|
||||
file_finder::init(cx);
|
||||
menu::init();
|
||||
settings::KeymapFile::load_asset(os_keymap, cx).unwrap();
|
||||
cx.bind_keys(
|
||||
settings::KeymapFile::load_asset_allow_partial_failure(os_keymap, cx).unwrap(),
|
||||
);
|
||||
language_model::LanguageModelRegistry::test(cx);
|
||||
assistant::context_store::init(&client.clone().into());
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ use gpui::{
|
|||
ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
||||
use postage::{sink::Sink, stream::Stream};
|
||||
use settings::Settings;
|
||||
use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
|
||||
|
@ -635,7 +634,7 @@ mod tests {
|
|||
workspace::init(app_state.clone(), cx);
|
||||
init(cx);
|
||||
Project::init_settings(cx);
|
||||
KeymapFile::parse(
|
||||
cx.bind_keys(KeymapFile::load_panic_on_failure(
|
||||
r#"[
|
||||
{
|
||||
"bindings": {
|
||||
|
@ -645,10 +644,8 @@ mod tests {
|
|||
}
|
||||
}
|
||||
]"#,
|
||||
)
|
||||
.unwrap()
|
||||
.add_to_cx(cx)
|
||||
.unwrap();
|
||||
cx,
|
||||
));
|
||||
app_state
|
||||
})
|
||||
}
|
||||
|
|
|
@ -32,8 +32,8 @@ impl PreprocessorContext {
|
|||
_ => return None,
|
||||
};
|
||||
|
||||
keymap.blocks().iter().find_map(|block| {
|
||||
block.bindings().iter().find_map(|(keystroke, a)| {
|
||||
keymap.sections().find_map(|section| {
|
||||
section.bindings().find_map(|(keystroke, a)| {
|
||||
if a.to_string() == action {
|
||||
Some(keystroke.to_string())
|
||||
} else {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
use crate::SharedString;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
pub use no_action::{is_no_action, NoAction};
|
||||
use serde_json::json;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
fmt::Display,
|
||||
};
|
||||
|
||||
/// Actions are used to implement keyboard-driven UI.
|
||||
/// When you declare an action, you can bind keys to the action in the keymap and
|
||||
|
@ -97,6 +100,47 @@ impl dyn Action {
|
|||
}
|
||||
}
|
||||
|
||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||
/// markdown to display it.
|
||||
#[derive(Debug)]
|
||||
pub enum ActionBuildError {
|
||||
/// Indicates that an action with this name has not been registered.
|
||||
NotFound {
|
||||
/// Name of the action that was not found.
|
||||
name: String,
|
||||
},
|
||||
/// Indicates that an error occurred while building the action, typically a JSON deserialization
|
||||
/// error.
|
||||
BuildError {
|
||||
/// Name of the action that was attempting to be built.
|
||||
name: String,
|
||||
/// Error that occurred while building the action.
|
||||
error: anyhow::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::error::Error for ActionBuildError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
ActionBuildError::NotFound { .. } => None,
|
||||
ActionBuildError::BuildError { error, .. } => error.source(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ActionBuildError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ActionBuildError::NotFound { name } => {
|
||||
write!(f, "Didn't find an action named \"{name}\"")
|
||||
}
|
||||
ActionBuildError::BuildError { name, error } => {
|
||||
write!(f, "Error while building action \"{name}\": {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
|
||||
|
||||
pub(crate) struct ActionRegistry {
|
||||
|
@ -201,7 +245,7 @@ impl ActionRegistry {
|
|||
.ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
|
||||
.clone();
|
||||
|
||||
self.build_action(&name, None)
|
||||
Ok(self.build_action(&name, None)?)
|
||||
}
|
||||
|
||||
/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
|
||||
|
@ -209,14 +253,20 @@ impl ActionRegistry {
|
|||
&self,
|
||||
name: &str,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<Box<dyn Action>> {
|
||||
) -> std::result::Result<Box<dyn Action>, ActionBuildError> {
|
||||
let build_action = self
|
||||
.by_name
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow!("No action type registered for {}", name))?
|
||||
.ok_or_else(|| ActionBuildError::NotFound {
|
||||
name: name.to_owned(),
|
||||
})?
|
||||
.build;
|
||||
(build_action)(params.unwrap_or_else(|| json!({})))
|
||||
.with_context(|| format!("Attempting to build action {}", name))
|
||||
(build_action)(params.unwrap_or_else(|| json!({}))).map_err(|e| {
|
||||
ActionBuildError::BuildError {
|
||||
name: name.to_owned(),
|
||||
error: e,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn all_action_names(&self) -> &[SharedString] {
|
||||
|
|
|
@ -32,11 +32,11 @@ pub use test_context::*;
|
|||
use util::ResultExt;
|
||||
|
||||
use crate::{
|
||||
current_platform, hash, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
|
||||
Asset, AssetSource, BackgroundExecutor, Bounds, ClipboardItem, Context, DispatchPhase,
|
||||
DisplayId, Entity, EventEmitter, FocusHandle, FocusId, ForegroundExecutor, Global, KeyBinding,
|
||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, Point, PromptBuilder, PromptHandle, PromptLevel, Render,
|
||||
current_platform, hash, init_app_menus, Action, ActionBuildError, ActionRegistry, Any, AnyView,
|
||||
AnyWindowHandle, Asset, AssetSource, BackgroundExecutor, Bounds, ClipboardItem, Context,
|
||||
DispatchPhase, DisplayId, Entity, EventEmitter, FocusHandle, FocusId, ForegroundExecutor,
|
||||
Global, KeyBinding, Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||
Pixels, Platform, PlatformDisplay, Point, PromptBuilder, PromptHandle, PromptLevel, Render,
|
||||
RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
|
||||
Subscription, SvgRenderer, Task, TextSystem, View, ViewContext, Window, WindowAppearance,
|
||||
WindowContext, WindowHandle, WindowId,
|
||||
|
@ -1220,7 +1220,7 @@ impl AppContext {
|
|||
&self,
|
||||
name: &str,
|
||||
data: Option<serde_json::Value>,
|
||||
) -> Result<Box<dyn Action>> {
|
||||
) -> std::result::Result<Box<dyn Action>, ActionBuildError> {
|
||||
self.actions.build_action(name, data)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{Action, KeyBindingContextPredicate, Keystroke};
|
||||
use anyhow::Result;
|
||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A keybinding and its associated metadata, from the keymap.
|
||||
pub struct KeyBinding {
|
||||
pub(crate) action: Box<dyn Action>,
|
||||
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||
pub(crate) context_predicate: Option<KeyBindingContextPredicate>,
|
||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
}
|
||||
|
||||
impl Clone for KeyBinding {
|
||||
|
@ -22,8 +23,13 @@ impl Clone for KeyBinding {
|
|||
}
|
||||
|
||||
impl KeyBinding {
|
||||
/// Construct a new keybinding from the given data.
|
||||
pub fn new<A: Action>(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self {
|
||||
/// Construct a new keybinding from the given data. Panics on parse error.
|
||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||
let context_predicate = if let Some(context) = context {
|
||||
Some(KeyBindingContextPredicate::parse(context).unwrap().into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
|
||||
}
|
||||
|
||||
|
@ -31,19 +37,13 @@ impl KeyBinding {
|
|||
pub fn load(
|
||||
keystrokes: &str,
|
||||
action: Box<dyn Action>,
|
||||
context: Option<&str>,
|
||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
) -> Result<Self> {
|
||||
let context = if let Some(context) = context {
|
||||
Some(KeyBindingContextPredicate::parse(context)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||
.split_whitespace()
|
||||
.map(Keystroke::parse)
|
||||
.collect::<Result<_>>()?;
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
|
||||
if let Some(equivalents) = key_equivalents {
|
||||
for keystroke in keystrokes.iter_mut() {
|
||||
|
@ -58,7 +58,7 @@ impl KeyBinding {
|
|||
Ok(Self {
|
||||
keystrokes,
|
||||
action,
|
||||
context_predicate: context,
|
||||
context_predicate,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -88,8 +88,8 @@ impl KeyBinding {
|
|||
}
|
||||
|
||||
/// Get the predicate used to match this binding
|
||||
pub fn predicate(&self) -> Option<&KeyBindingContextPredicate> {
|
||||
self.context_predicate.as_ref()
|
||||
pub fn predicate(&self) -> Option<Rc<KeyBindingContextPredicate>> {
|
||||
self.context_predicate.as_ref().map(|rc| rc.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -244,7 +244,7 @@ impl KeyBindingContextPredicate {
|
|||
let source = skip_whitespace(source);
|
||||
let (predicate, rest) = Self::parse_expr(source, 0)?;
|
||||
if let Some(next) = rest.chars().next() {
|
||||
Err(anyhow!("unexpected character {next:?}"))
|
||||
Err(anyhow!("unexpected character '{next:?}'"))
|
||||
} else {
|
||||
Ok(predicate)
|
||||
}
|
||||
|
@ -333,7 +333,7 @@ impl KeyBindingContextPredicate {
|
|||
let next = source
|
||||
.chars()
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("unexpected eof"))?;
|
||||
.ok_or_else(|| anyhow!("unexpected end"))?;
|
||||
match next {
|
||||
'(' => {
|
||||
source = skip_whitespace(&source[1..]);
|
||||
|
@ -369,7 +369,7 @@ impl KeyBindingContextPredicate {
|
|||
source,
|
||||
))
|
||||
}
|
||||
_ => Err(anyhow!("unexpected character {next:?}")),
|
||||
_ => Err(anyhow!("unexpected character '{next:?}'")),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -389,7 +389,7 @@ impl KeyBindingContextPredicate {
|
|||
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
|
||||
Ok(Self::Equal(left, right))
|
||||
} else {
|
||||
Err(anyhow!("operands must be identifiers"))
|
||||
Err(anyhow!("operands of == must be identifiers"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -397,7 +397,7 @@ impl KeyBindingContextPredicate {
|
|||
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
|
||||
Ok(Self::NotEqual(left, right))
|
||||
} else {
|
||||
Err(anyhow!("operands must be identifiers"))
|
||||
Err(anyhow!("operands of != must be identifiers"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -504,7 +504,7 @@ mod tests {
|
|||
KeyBindingContextPredicate::parse("c == !d")
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"operands must be identifiers"
|
||||
"operands of == must be identifiers"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use anyhow::anyhow;
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Write;
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Write},
|
||||
};
|
||||
|
||||
/// A keystroke and associated metadata generated by the platform
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||
|
@ -18,6 +20,31 @@ pub struct Keystroke {
|
|||
pub key_char: Option<String>,
|
||||
}
|
||||
|
||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||
/// markdown to display it.
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidKeystrokeError {
|
||||
/// The invalid keystroke.
|
||||
pub keystroke: String,
|
||||
}
|
||||
|
||||
impl Error for InvalidKeystrokeError {}
|
||||
|
||||
impl Display for InvalidKeystrokeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Invalid keystroke \"{}\". {}",
|
||||
self.keystroke, KEYSTROKE_PARSE_EXPECTED_MESSAGE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sentence explaining what keystroke parser expects, starting with "Expected ..."
|
||||
pub const KEYSTROKE_PARSE_EXPECTED_MESSAGE: &str = "Expected a sequence of modifiers \
|
||||
(`ctrl`, `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) \
|
||||
followed by a key, separated by `-`.";
|
||||
|
||||
impl Keystroke {
|
||||
/// When matching a key we cannot know whether the user intended to type
|
||||
/// the key_char or the key itself. On some non-US keyboards keys we use in our
|
||||
|
@ -51,7 +78,7 @@ impl Keystroke {
|
|||
/// [ctrl-][alt-][shift-][cmd-][fn-]key[->key_char]
|
||||
/// key_char syntax is only used for generating test events,
|
||||
/// when matching a key with an key_char set will be matched without it.
|
||||
pub fn parse(source: &str) -> anyhow::Result<Self> {
|
||||
pub fn parse(source: &str) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut control = false;
|
||||
let mut alt = false;
|
||||
let mut shift = false;
|
||||
|
@ -78,7 +105,9 @@ impl Keystroke {
|
|||
key_char = Some(String::from(&next[1..]));
|
||||
components.next();
|
||||
} else {
|
||||
return Err(anyhow!("Invalid keystroke `{}`", source));
|
||||
return Err(InvalidKeystrokeError {
|
||||
keystroke: source.to_owned(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
key = Some(String::from(component));
|
||||
|
@ -87,8 +116,8 @@ impl Keystroke {
|
|||
}
|
||||
}
|
||||
|
||||
//Allow for the user to specify a keystroke modifier as the key itself
|
||||
//This sets the `key` to the modifier, and disables the modifier
|
||||
// Allow for the user to specify a keystroke modifier as the key itself
|
||||
// This sets the `key` to the modifier, and disables the modifier
|
||||
if key.is_none() {
|
||||
if shift {
|
||||
key = Some("shift".to_string());
|
||||
|
@ -108,7 +137,9 @@ impl Keystroke {
|
|||
}
|
||||
}
|
||||
|
||||
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
|
||||
let key = key.ok_or_else(|| InvalidKeystrokeError {
|
||||
keystroke: source.to_owned(),
|
||||
})?;
|
||||
|
||||
Ok(Keystroke {
|
||||
modifiers: Modifiers {
|
||||
|
|
|
@ -52,7 +52,7 @@ impl KeyContextView {
|
|||
.into_iter()
|
||||
.map(|binding| {
|
||||
let match_state = if let Some(predicate) = binding.predicate() {
|
||||
if this.matches(predicate) {
|
||||
if this.matches(&predicate) {
|
||||
if this.action_matches(&e.action, binding.action()) {
|
||||
Some(true)
|
||||
} else {
|
||||
|
|
|
@ -110,15 +110,15 @@ pub fn render_parsed_markdown(
|
|||
parsed: &ParsedMarkdown,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &WindowContext,
|
||||
) -> Vec<AnyElement> {
|
||||
) -> Div {
|
||||
let mut cx = RenderContext::new(workspace, cx);
|
||||
let mut elements = Vec::new();
|
||||
|
||||
for child in &parsed.children {
|
||||
elements.push(render_markdown_block(child, &mut cx));
|
||||
}
|
||||
|
||||
elements
|
||||
v_flex().gap_3().children(
|
||||
parsed
|
||||
.children
|
||||
.iter()
|
||||
.map(|block| render_markdown_block(block, &mut cx)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderContext) -> AnyElement {
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use gpui::{Action, AppContext, KeyBinding, NoAction, SharedString};
|
||||
use anyhow::anyhow;
|
||||
use collections::{BTreeMap, HashMap, IndexMap};
|
||||
use gpui::{
|
||||
Action, ActionBuildError, AppContext, InvalidKeystrokeError, KeyBinding,
|
||||
KeyBindingContextPredicate, NoAction, SharedString, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
|
||||
};
|
||||
use schemars::{
|
||||
gen::{SchemaGenerator, SchemaSettings},
|
||||
schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation},
|
||||
|
@ -9,31 +14,37 @@ use schemars::{
|
|||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use util::{asset_str, ResultExt};
|
||||
use std::fmt::Write;
|
||||
use util::{asset_str, markdown::MarkdownString};
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
pub struct KeymapFile(Vec<KeymapBlock>);
|
||||
pub struct KeymapFile(Vec<KeymapSection>);
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct KeymapBlock {
|
||||
pub struct KeymapSection {
|
||||
#[serde(default)]
|
||||
context: Option<String>,
|
||||
context: String,
|
||||
#[serde(default)]
|
||||
use_key_equivalents: Option<bool>,
|
||||
bindings: BTreeMap<String, KeymapAction>,
|
||||
use_key_equivalents: bool,
|
||||
#[serde(default)]
|
||||
bindings: Option<BTreeMap<String, KeymapAction>>,
|
||||
#[serde(flatten)]
|
||||
unrecognized_fields: IndexMap<String, Value>,
|
||||
}
|
||||
|
||||
impl KeymapBlock {
|
||||
pub fn context(&self) -> Option<&str> {
|
||||
self.context.as_deref()
|
||||
}
|
||||
|
||||
pub fn bindings(&self) -> &BTreeMap<String, KeymapAction> {
|
||||
&self.bindings
|
||||
impl KeymapSection {
|
||||
pub fn bindings(&self) -> impl Iterator<Item = (&String, &KeymapAction)> {
|
||||
self.bindings.iter().flatten()
|
||||
}
|
||||
}
|
||||
|
||||
/// Keymap action as a JSON value, since it can either be null for no action, or the name of the
|
||||
/// action, or an array of the name of the action and the action input.
|
||||
///
|
||||
/// Unlike the other deserializable types here, this doc-comment will not be included in the
|
||||
/// generated JSON schema, as it manually defines its `JsonSchema` impl. The actual schema used for
|
||||
/// it is automatically generated in `KeymapFile::generate_json_schema`.
|
||||
#[derive(Debug, Deserialize, Default, Clone)]
|
||||
#[serde(transparent)]
|
||||
pub struct KeymapAction(Value);
|
||||
|
@ -52,91 +63,278 @@ impl std::fmt::Display for KeymapAction {
|
|||
}
|
||||
|
||||
impl JsonSchema for KeymapAction {
|
||||
/// This is used when generating the JSON schema for the `KeymapAction` type, so that it can
|
||||
/// reference the keymap action schema.
|
||||
fn schema_name() -> String {
|
||||
"KeymapAction".into()
|
||||
}
|
||||
|
||||
/// This schema will be replaced with the full action schema in
|
||||
/// `KeymapFile::generate_json_schema`.
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Bool(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[must_use]
|
||||
pub enum KeymapFileLoadResult {
|
||||
Success {
|
||||
key_bindings: Vec<KeyBinding>,
|
||||
},
|
||||
SomeFailedToLoad {
|
||||
key_bindings: Vec<KeyBinding>,
|
||||
error_message: MarkdownString,
|
||||
},
|
||||
AllFailedToLoad {
|
||||
error_message: MarkdownString,
|
||||
},
|
||||
JsonParseFailure {
|
||||
error: anyhow::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl KeymapFile {
|
||||
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
|
||||
let content = asset_str::<SettingsAssets>(asset_path);
|
||||
|
||||
Self::parse(content.as_ref())?.add_to_cx(cx)
|
||||
}
|
||||
|
||||
pub fn parse(content: &str) -> Result<Self> {
|
||||
if content.is_empty() {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
pub fn parse(content: &str) -> anyhow::Result<Self> {
|
||||
parse_json_with_comments::<Self>(content)
|
||||
}
|
||||
|
||||
pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {
|
||||
pub fn load_asset(asset_path: &str, cx: &AppContext) -> anyhow::Result<Vec<KeyBinding>> {
|
||||
match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
|
||||
KeymapFileLoadResult::Success { key_bindings, .. } => Ok(key_bindings),
|
||||
KeymapFileLoadResult::SomeFailedToLoad { error_message, .. }
|
||||
| KeymapFileLoadResult::AllFailedToLoad { error_message } => Err(anyhow!(
|
||||
"Error loading built-in keymap \"{asset_path}\": {error_message}"
|
||||
)),
|
||||
KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
|
||||
"JSON parse error in built-in keymap \"{asset_path}\": {error}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn load_asset_allow_partial_failure(
|
||||
asset_path: &str,
|
||||
cx: &AppContext,
|
||||
) -> anyhow::Result<Vec<KeyBinding>> {
|
||||
match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
|
||||
KeymapFileLoadResult::Success { key_bindings, .. }
|
||||
| KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
|
||||
KeymapFileLoadResult::AllFailedToLoad { error_message } => Err(anyhow!(
|
||||
"Error loading built-in keymap \"{asset_path}\": {error_message}"
|
||||
)),
|
||||
KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
|
||||
"JSON parse error in built-in keymap \"{asset_path}\": {error}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn load_panic_on_failure(content: &str, cx: &AppContext) -> Vec<KeyBinding> {
|
||||
match Self::load(content, cx) {
|
||||
KeymapFileLoadResult::Success { key_bindings } => key_bindings,
|
||||
KeymapFileLoadResult::SomeFailedToLoad { error_message, .. }
|
||||
| KeymapFileLoadResult::AllFailedToLoad { error_message, .. } => {
|
||||
panic!("{error_message}");
|
||||
}
|
||||
KeymapFileLoadResult::JsonParseFailure { error } => {
|
||||
panic!("JSON parse error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(content: &str, cx: &AppContext) -> KeymapFileLoadResult {
|
||||
let key_equivalents = crate::key_equivalents::get_key_equivalents(&cx.keyboard_layout());
|
||||
|
||||
for KeymapBlock {
|
||||
if content.is_empty() {
|
||||
return KeymapFileLoadResult::Success {
|
||||
key_bindings: Vec::new(),
|
||||
};
|
||||
}
|
||||
let keymap_file = match parse_json_with_comments::<Self>(content) {
|
||||
Ok(keymap_file) => keymap_file,
|
||||
Err(error) => {
|
||||
return KeymapFileLoadResult::JsonParseFailure { error };
|
||||
}
|
||||
};
|
||||
|
||||
// Accumulate errors in order to support partial load of user keymap in the presence of
|
||||
// errors in context and binding parsing.
|
||||
let mut errors = Vec::new();
|
||||
let mut key_bindings = Vec::new();
|
||||
|
||||
for KeymapSection {
|
||||
context,
|
||||
use_key_equivalents,
|
||||
bindings,
|
||||
} in self.0
|
||||
unrecognized_fields,
|
||||
} in keymap_file.0.iter()
|
||||
{
|
||||
let bindings = bindings
|
||||
.into_iter()
|
||||
.filter_map(|(keystroke, action)| {
|
||||
let action = action.0;
|
||||
let context_predicate: Option<Rc<KeyBindingContextPredicate>> = if context.is_empty() {
|
||||
None
|
||||
} else {
|
||||
match KeyBindingContextPredicate::parse(context) {
|
||||
Ok(context_predicate) => Some(context_predicate.into()),
|
||||
Err(err) => {
|
||||
// Leading space is to separate from the message indicating which section
|
||||
// the error occurred in.
|
||||
errors.push((
|
||||
context,
|
||||
format!(" Parse error in section `context` field: {}", err),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// This is a workaround for a limitation in serde: serde-rs/json#497
|
||||
// We want to deserialize the action data as a `RawValue` so that we can
|
||||
// deserialize the action itself dynamically directly from the JSON
|
||||
// string. But `RawValue` currently does not work inside of an untagged enum.
|
||||
match action {
|
||||
Value::Array(items) => {
|
||||
let Ok([name, data]): Result<[serde_json::Value; 2], _> =
|
||||
items.try_into()
|
||||
else {
|
||||
return Some(Err(anyhow!("Expected array of length 2")));
|
||||
};
|
||||
let serde_json::Value::String(name) = name else {
|
||||
return Some(Err(anyhow!(
|
||||
"Expected first item in array to be a string."
|
||||
)));
|
||||
};
|
||||
cx.build_action(&name, Some(data))
|
||||
let key_equivalents = if *use_key_equivalents {
|
||||
key_equivalents.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut section_errors = String::new();
|
||||
|
||||
if !unrecognized_fields.is_empty() {
|
||||
write!(
|
||||
section_errors,
|
||||
"\n\n - Unrecognized fields: {}",
|
||||
MarkdownString::inline_code(&format!("{:?}", unrecognized_fields.keys()))
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if let Some(bindings) = bindings {
|
||||
for binding in bindings {
|
||||
let (keystrokes, action) = binding;
|
||||
let result = Self::load_keybinding(
|
||||
keystrokes,
|
||||
action,
|
||||
context_predicate.clone(),
|
||||
key_equivalents,
|
||||
cx,
|
||||
);
|
||||
match result {
|
||||
Ok(key_binding) => {
|
||||
key_bindings.push(key_binding);
|
||||
}
|
||||
Value::String(name) => cx.build_action(&name, None),
|
||||
Value::Null => Ok(no_action()),
|
||||
_ => {
|
||||
return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
|
||||
Err(err) => {
|
||||
write!(
|
||||
section_errors,
|
||||
"\n\n - In binding {}, {err}",
|
||||
inline_code_string(keystrokes),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"invalid binding value for keystroke {keystroke}, context {context:?}"
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
.map(|action| {
|
||||
KeyBinding::load(
|
||||
&keystroke,
|
||||
action,
|
||||
context.as_deref(),
|
||||
if use_key_equivalents.unwrap_or_default() {
|
||||
key_equivalents.as_ref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
}
|
||||
}
|
||||
|
||||
cx.bind_keys(bindings);
|
||||
if !section_errors.is_empty() {
|
||||
errors.push((context, section_errors))
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
KeymapFileLoadResult::Success { key_bindings }
|
||||
} else {
|
||||
let mut error_message = "Errors in user keymap file.\n".to_owned();
|
||||
for (context, section_errors) in errors {
|
||||
if context.is_empty() {
|
||||
write!(error_message, "\n\nIn section without context predicate:").unwrap()
|
||||
} else {
|
||||
write!(
|
||||
error_message,
|
||||
"\n\nIn section with {}:",
|
||||
MarkdownString::inline_code(&format!("context = \"{}\"", context))
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
write!(error_message, "{section_errors}").unwrap();
|
||||
}
|
||||
KeymapFileLoadResult::SomeFailedToLoad {
|
||||
key_bindings,
|
||||
error_message: MarkdownString(error_message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_keybinding(
|
||||
keystrokes: &str,
|
||||
action: &KeymapAction,
|
||||
context: Option<Rc<KeyBindingContextPredicate>>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
cx: &AppContext,
|
||||
) -> std::result::Result<KeyBinding, String> {
|
||||
let (build_result, action_input_string) = match &action.0 {
|
||||
Value::Array(items) => {
|
||||
if items.len() != 2 {
|
||||
return Err(format!(
|
||||
"expected two-element array of `[name, input]`. \
|
||||
Instead found {}.",
|
||||
MarkdownString::inline_code(&action.0.to_string())
|
||||
));
|
||||
}
|
||||
let serde_json::Value::String(ref name) = items[0] else {
|
||||
return Err(format!(
|
||||
"expected two-element array of `[name, input]`, \
|
||||
but the first element is not a string in {}.",
|
||||
MarkdownString::inline_code(&action.0.to_string())
|
||||
));
|
||||
};
|
||||
let action_input = items[1].clone();
|
||||
let action_input_string = action_input.to_string();
|
||||
(
|
||||
cx.build_action(&name, Some(action_input)),
|
||||
Some(action_input_string),
|
||||
)
|
||||
}
|
||||
Value::String(name) => (cx.build_action(&name, None), None),
|
||||
Value::Null => (Ok(NoAction.boxed_clone()), None),
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"expected two-element array of `[name, input]`. \
|
||||
Instead found {}.",
|
||||
MarkdownString::inline_code(&action.0.to_string())
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let action = match build_result {
|
||||
Ok(action) => action,
|
||||
Err(ActionBuildError::NotFound { name }) => {
|
||||
return Err(format!(
|
||||
"didn't find an action named {}.",
|
||||
inline_code_string(&name)
|
||||
))
|
||||
}
|
||||
Err(ActionBuildError::BuildError { name, error }) => match action_input_string {
|
||||
Some(action_input_string) => {
|
||||
return Err(format!(
|
||||
"can't build {} action from input value {}: {}",
|
||||
inline_code_string(&name),
|
||||
MarkdownString::inline_code(&action_input_string),
|
||||
MarkdownString::escape(&error.to_string())
|
||||
))
|
||||
}
|
||||
None => {
|
||||
return Err(format!(
|
||||
"can't build {} action - it requires input data via [name, input]: {}",
|
||||
inline_code_string(&name),
|
||||
MarkdownString::escape(&error.to_string())
|
||||
))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
match KeyBinding::load(keystrokes, action, context, key_equivalents) {
|
||||
Ok(binding) => Ok(binding),
|
||||
Err(InvalidKeystrokeError { keystroke }) => Err(format!(
|
||||
"invalid keystroke {}. {}",
|
||||
inline_code_string(&keystroke),
|
||||
KEYSTROKE_PARSE_EXPECTED_MESSAGE
|
||||
)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_json_schema_for_registered_actions(cx: &mut AppContext) -> Value {
|
||||
|
@ -308,23 +506,26 @@ impl KeymapFile {
|
|||
}
|
||||
.into();
|
||||
|
||||
// The `KeymapSection` schema will reference the `KeymapAction` schema by name, so replacing
|
||||
// the definition of `KeymapAction` results in the full action schema being used.
|
||||
let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
|
||||
root_schema
|
||||
.definitions
|
||||
.insert("KeymapAction".to_owned(), action_schema);
|
||||
.insert(KeymapAction::schema_name(), action_schema);
|
||||
|
||||
// This and other json schemas can be viewed via `debug: open language server logs` ->
|
||||
// `json-language-server` -> `Server Info`.
|
||||
serde_json::to_value(root_schema).unwrap()
|
||||
}
|
||||
|
||||
pub fn blocks(&self) -> &[KeymapBlock] {
|
||||
&self.0
|
||||
pub fn sections(&self) -> impl Iterator<Item = &KeymapSection> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
fn no_action() -> Box<dyn gpui::Action> {
|
||||
gpui::NoAction.boxed_clone()
|
||||
// Double quotes a string and wraps it in backticks for markdown inline code..
|
||||
fn inline_code_string(text: &str) -> MarkdownString {
|
||||
MarkdownString::inline_code(&format!("\"{}\"", text))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -342,7 +543,6 @@ mod tests {
|
|||
},
|
||||
]
|
||||
"
|
||||
|
||||
};
|
||||
KeymapFile::parse(json).unwrap();
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use util::asset_str;
|
|||
pub use editable_setting_control::*;
|
||||
pub use json_schema::*;
|
||||
pub use key_equivalents::*;
|
||||
pub use keymap_file::KeymapFile;
|
||||
pub use keymap_file::{KeymapFile, KeymapFileLoadResult};
|
||||
pub use settings_file::*;
|
||||
pub use settings_store::{
|
||||
parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
|
||||
|
|
|
@ -145,7 +145,7 @@ fn load_embedded_fonts(cx: &AppContext) -> gpui::Result<()> {
|
|||
}
|
||||
|
||||
fn load_storybook_keymap(cx: &mut AppContext) {
|
||||
KeymapFile::load_asset("keymaps/storybook.json", cx).unwrap();
|
||||
cx.bind_keys(KeymapFile::load_asset("keymaps/storybook.json", cx).unwrap());
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
|
|
|
@ -48,6 +48,7 @@ workspace.workspace = true
|
|||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assets.workspace = true
|
||||
command_palette.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
@ -55,6 +56,7 @@ indoc.workspace = true
|
|||
language = { workspace = true, features = ["test-support"] }
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
parking_lot.workspace = true
|
||||
project_panel.workspace = true
|
||||
release_channel.workspace = true
|
||||
settings.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use assets::Assets;
|
||||
use editor::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use gpui::{Context, SemanticVersion, UpdateGlobal, View, VisualContext};
|
||||
use search::{project_search::ProjectSearchBar, BufferSearchBar};
|
||||
|
@ -20,6 +21,7 @@ impl VimTestContext {
|
|||
cx.set_global(settings);
|
||||
release_channel::init(SemanticVersion::default(), cx);
|
||||
command_palette::init(cx);
|
||||
project_panel::init(Assets, cx);
|
||||
crate::init(cx);
|
||||
search::init(cx);
|
||||
});
|
||||
|
@ -59,9 +61,16 @@ impl VimTestContext {
|
|||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
|
||||
});
|
||||
settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap();
|
||||
let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
|
||||
"keymaps/default-macos.json",
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
cx.bind_keys(default_key_bindings);
|
||||
if enabled {
|
||||
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
|
||||
let vim_key_bindings =
|
||||
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
|
||||
cx.bind_keys(vim_key_bindings);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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