keymap_file.rs

  1use std::rc::Rc;
  2
  3use crate::{settings_store::parse_json_with_comments, SettingsAssets};
  4use anyhow::anyhow;
  5use collections::{HashMap, IndexMap};
  6use gpui::{
  7    Action, ActionBuildError, App, InvalidKeystrokeError, KeyBinding, KeyBindingContextPredicate,
  8    NoAction, SharedString, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
  9};
 10use schemars::{
 11    gen::{SchemaGenerator, SchemaSettings},
 12    schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation},
 13    JsonSchema,
 14};
 15use serde::Deserialize;
 16use serde_json::Value;
 17use std::fmt::Write;
 18use util::{asset_str, markdown::MarkdownString};
 19
 20// Note that the doc comments on these are shown by json-language-server when editing the keymap, so
 21// they should be considered user-facing documentation. Documentation is not handled well with
 22// schemars-0.8 - when there are newlines, it is rendered as plaintext (see
 23// https://github.com/GREsau/schemars/issues/38#issuecomment-2282883519). So for now these docs
 24// avoid newlines.
 25//
 26// TODO: Update to schemars-1.0 once it's released, and add more docs as newlines would be
 27// supported. Tracking issue is https://github.com/GREsau/schemars/issues/112.
 28
 29/// Keymap configuration consisting of sections. Each section may have a context predicate which
 30/// determines whether its bindings are used.
 31#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
 32#[serde(transparent)]
 33pub struct KeymapFile(Vec<KeymapSection>);
 34
 35/// Keymap section which binds keystrokes to actions.
 36#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
 37pub struct KeymapSection {
 38    /// Determines when these bindings are active. When just a name is provided, like `Editor` or
 39    /// `Workspace`, the bindings will be active in that context. Boolean expressions like `X && Y`,
 40    /// `X || Y`, `!X` are also supported. Some more complex logic including checking OS and the
 41    /// current file extension are also supported - see [the
 42    /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details.
 43    #[serde(default)]
 44    context: String,
 45    /// This option enables specifying keys based on their position on a QWERTY keyboard, by using
 46    /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported
 47    /// on macOS. See the documentation for more details.
 48    #[serde(default)]
 49    use_key_equivalents: bool,
 50    /// This keymap section's bindings, as a JSON object mapping keystrokes to actions. The
 51    /// keystrokes key is a string representing a sequence of keystrokes to type, where the
 52    /// keystrokes are separated by whitespace. Each keystroke is a sequence of modifiers (`ctrl`,
 53    /// `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) followed by a key, separated by `-`. The
 54    /// order of bindings does matter. When the same keystrokes are bound at the same context depth,
 55    /// the binding that occurs later in the file is preferred. For displaying keystrokes in the UI,
 56    /// the later binding for the same action is preferred.
 57    #[serde(default)]
 58    bindings: Option<IndexMap<String, KeymapAction>>,
 59    #[serde(flatten)]
 60    unrecognized_fields: IndexMap<String, Value>,
 61    // This struct intentionally uses permissive types for its fields, rather than validating during
 62    // deserialization. The purpose of this is to allow loading the portion of the keymap that doesn't
 63    // have errors. The downside of this is that the errors are not reported with line+column info.
 64    // Unfortunately the implementations of the `Spanned` types for preserving this information are
 65    // highly inconvenient (`serde_spanned`) and in some cases don't work at all here
 66    // (`json_spanned_>value`). Serde should really have builtin support for this.
 67}
 68
 69impl KeymapSection {
 70    pub fn bindings(&self) -> impl DoubleEndedIterator<Item = (&String, &KeymapAction)> {
 71        self.bindings.iter().flatten()
 72    }
 73}
 74
 75/// Keymap action as a JSON value, since it can either be null for no action, or the name of the
 76/// action, or an array of the name of the action and the action input.
 77///
 78/// Unlike the other json types involved in keymaps (including actions), this doc-comment will not
 79/// be included in the generated JSON schema, as it manually defines its `JsonSchema` impl. The
 80/// actual schema used for it is automatically generated in `KeymapFile::generate_json_schema`.
 81#[derive(Debug, Deserialize, Default, Clone)]
 82#[serde(transparent)]
 83pub struct KeymapAction(Value);
 84
 85impl std::fmt::Display for KeymapAction {
 86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 87        match &self.0 {
 88            Value::String(s) => write!(f, "{}", s),
 89            Value::Array(arr) => {
 90                let strings: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
 91                write!(f, "{}", strings.join(", "))
 92            }
 93            _ => write!(f, "{}", self.0),
 94        }
 95    }
 96}
 97
 98impl JsonSchema for KeymapAction {
 99    /// This is used when generating the JSON schema for the `KeymapAction` type, so that it can
100    /// reference the keymap action schema.
101    fn schema_name() -> String {
102        "KeymapAction".into()
103    }
104
105    /// This schema will be replaced with the full action schema in
106    /// `KeymapFile::generate_json_schema`.
107    fn json_schema(_: &mut SchemaGenerator) -> Schema {
108        Schema::Bool(true)
109    }
110}
111
112#[derive(Debug)]
113#[must_use]
114pub enum KeymapFileLoadResult {
115    Success {
116        key_bindings: Vec<KeyBinding>,
117    },
118    SomeFailedToLoad {
119        key_bindings: Vec<KeyBinding>,
120        error_message: MarkdownString,
121    },
122    JsonParseFailure {
123        error: anyhow::Error,
124    },
125}
126
127impl KeymapFile {
128    pub fn parse(content: &str) -> anyhow::Result<Self> {
129        parse_json_with_comments::<Self>(content)
130    }
131
132    pub fn load_asset(asset_path: &str, cx: &App) -> anyhow::Result<Vec<KeyBinding>> {
133        match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
134            KeymapFileLoadResult::Success { key_bindings, .. } => Ok(key_bindings),
135            KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => Err(anyhow!(
136                "Error loading built-in keymap \"{asset_path}\": {error_message}"
137            )),
138            KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
139                "JSON parse error in built-in keymap \"{asset_path}\": {error}"
140            )),
141        }
142    }
143
144    #[cfg(feature = "test-support")]
145    pub fn load_asset_allow_partial_failure(
146        asset_path: &str,
147        cx: &App,
148    ) -> anyhow::Result<Vec<KeyBinding>> {
149        match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
150            KeymapFileLoadResult::SomeFailedToLoad {
151                key_bindings,
152                error_message,
153            } if key_bindings.is_empty() => Err(anyhow!(
154                "Error loading built-in keymap \"{asset_path}\": {error_message}"
155            )),
156            KeymapFileLoadResult::Success { key_bindings, .. }
157            | KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
158            KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
159                "JSON parse error in built-in keymap \"{asset_path}\": {error}"
160            )),
161        }
162    }
163
164    #[cfg(feature = "test-support")]
165    pub fn load_panic_on_failure(content: &str, cx: &App) -> Vec<KeyBinding> {
166        match Self::load(content, cx) {
167            KeymapFileLoadResult::Success { key_bindings } => key_bindings,
168            KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
169                panic!("{error_message}");
170            }
171            KeymapFileLoadResult::JsonParseFailure { error } => {
172                panic!("JSON parse error: {error}");
173            }
174        }
175    }
176
177    pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
178        let key_equivalents = crate::key_equivalents::get_key_equivalents(&cx.keyboard_layout());
179
180        if content.is_empty() {
181            return KeymapFileLoadResult::Success {
182                key_bindings: Vec::new(),
183            };
184        }
185        let keymap_file = match parse_json_with_comments::<Self>(content) {
186            Ok(keymap_file) => keymap_file,
187            Err(error) => {
188                return KeymapFileLoadResult::JsonParseFailure { error };
189            }
190        };
191
192        // Accumulate errors in order to support partial load of user keymap in the presence of
193        // errors in context and binding parsing.
194        let mut errors = Vec::new();
195        let mut key_bindings = Vec::new();
196
197        for KeymapSection {
198            context,
199            use_key_equivalents,
200            bindings,
201            unrecognized_fields,
202        } in keymap_file.0.iter()
203        {
204            let context_predicate: Option<Rc<KeyBindingContextPredicate>> = if context.is_empty() {
205                None
206            } else {
207                match KeyBindingContextPredicate::parse(context) {
208                    Ok(context_predicate) => Some(context_predicate.into()),
209                    Err(err) => {
210                        // Leading space is to separate from the message indicating which section
211                        // the error occurred in.
212                        errors.push((
213                            context,
214                            format!(" Parse error in section `context` field: {}", err),
215                        ));
216                        continue;
217                    }
218                }
219            };
220
221            let key_equivalents = if *use_key_equivalents {
222                key_equivalents.as_ref()
223            } else {
224                None
225            };
226
227            let mut section_errors = String::new();
228
229            if !unrecognized_fields.is_empty() {
230                write!(
231                    section_errors,
232                    "\n\n - Unrecognized fields: {}",
233                    MarkdownString::inline_code(&format!("{:?}", unrecognized_fields.keys()))
234                )
235                .unwrap();
236            }
237
238            if let Some(bindings) = bindings {
239                for (keystrokes, action) in bindings {
240                    let result = Self::load_keybinding(
241                        keystrokes,
242                        action,
243                        context_predicate.clone(),
244                        key_equivalents,
245                        cx,
246                    );
247                    match result {
248                        Ok(key_binding) => {
249                            key_bindings.push(key_binding);
250                        }
251                        Err(err) => {
252                            write!(
253                                section_errors,
254                                "\n\n - In binding {}, {err}",
255                                inline_code_string(keystrokes),
256                            )
257                            .unwrap();
258                        }
259                    }
260                }
261            }
262
263            if !section_errors.is_empty() {
264                errors.push((context, section_errors))
265            }
266        }
267
268        if errors.is_empty() {
269            KeymapFileLoadResult::Success { key_bindings }
270        } else {
271            let mut error_message = "Errors in user keymap file.\n".to_owned();
272            for (context, section_errors) in errors {
273                if context.is_empty() {
274                    write!(error_message, "\n\nIn section without context predicate:").unwrap()
275                } else {
276                    write!(
277                        error_message,
278                        "\n\nIn section with {}:",
279                        MarkdownString::inline_code(&format!("context = \"{}\"", context))
280                    )
281                    .unwrap()
282                }
283                write!(error_message, "{section_errors}").unwrap();
284            }
285            KeymapFileLoadResult::SomeFailedToLoad {
286                key_bindings,
287                error_message: MarkdownString(error_message),
288            }
289        }
290    }
291
292    fn load_keybinding(
293        keystrokes: &str,
294        action: &KeymapAction,
295        context: Option<Rc<KeyBindingContextPredicate>>,
296        key_equivalents: Option<&HashMap<char, char>>,
297        cx: &App,
298    ) -> std::result::Result<KeyBinding, String> {
299        let (build_result, action_input_string) = match &action.0 {
300            Value::Array(items) => {
301                if items.len() != 2 {
302                    return Err(format!(
303                        "expected two-element array of `[name, input]`. \
304                        Instead found {}.",
305                        MarkdownString::inline_code(&action.0.to_string())
306                    ));
307                }
308                let serde_json::Value::String(ref name) = items[0] else {
309                    return Err(format!(
310                        "expected two-element array of `[name, input]`, \
311                        but the first element is not a string in {}.",
312                        MarkdownString::inline_code(&action.0.to_string())
313                    ));
314                };
315                let action_input = items[1].clone();
316                let action_input_string = action_input.to_string();
317                (
318                    cx.build_action(&name, Some(action_input)),
319                    Some(action_input_string),
320                )
321            }
322            Value::String(name) => (cx.build_action(&name, None), None),
323            Value::Null => (Ok(NoAction.boxed_clone()), None),
324            _ => {
325                return Err(format!(
326                    "expected two-element array of `[name, input]`. \
327                    Instead found {}.",
328                    MarkdownString::inline_code(&action.0.to_string())
329                ));
330            }
331        };
332
333        let action = match build_result {
334            Ok(action) => action,
335            Err(ActionBuildError::NotFound { name }) => {
336                return Err(format!(
337                    "didn't find an action named {}.",
338                    inline_code_string(&name)
339                ))
340            }
341            Err(ActionBuildError::BuildError { name, error }) => match action_input_string {
342                Some(action_input_string) => {
343                    return Err(format!(
344                        "can't build {} action from input value {}: {}",
345                        inline_code_string(&name),
346                        MarkdownString::inline_code(&action_input_string),
347                        MarkdownString::escape(&error.to_string())
348                    ))
349                }
350                None => {
351                    return Err(format!(
352                        "can't build {} action - it requires input data via [name, input]: {}",
353                        inline_code_string(&name),
354                        MarkdownString::escape(&error.to_string())
355                    ))
356                }
357            },
358        };
359
360        match KeyBinding::load(keystrokes, action, context, key_equivalents) {
361            Ok(binding) => Ok(binding),
362            Err(InvalidKeystrokeError { keystroke }) => Err(format!(
363                "invalid keystroke {}. {}",
364                inline_code_string(&keystroke),
365                KEYSTROKE_PARSE_EXPECTED_MESSAGE
366            )),
367        }
368    }
369
370    pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
371        let mut generator = SchemaSettings::draft07()
372            .with(|settings| settings.option_add_null_type = false)
373            .into_generator();
374
375        let action_schemas = cx.action_schemas(&mut generator);
376        let deprecations = cx.action_deprecations();
377        KeymapFile::generate_json_schema(generator, action_schemas, deprecations)
378    }
379
380    fn generate_json_schema(
381        generator: SchemaGenerator,
382        action_schemas: Vec<(SharedString, Option<Schema>)>,
383        deprecations: &HashMap<SharedString, SharedString>,
384    ) -> serde_json::Value {
385        fn set<I, O>(input: I) -> Option<O>
386        where
387            I: Into<O>,
388        {
389            Some(input.into())
390        }
391
392        fn add_deprecation(schema_object: &mut SchemaObject, message: String) {
393            schema_object.extensions.insert(
394                // deprecationMessage is not part of the JSON Schema spec,
395                // but json-language-server recognizes it.
396                "deprecationMessage".to_owned(),
397                Value::String(message),
398            );
399        }
400
401        fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) {
402            add_deprecation(schema_object, format!("Deprecated, use {new_name}"));
403        }
404
405        fn add_description(schema_object: &mut SchemaObject, description: String) {
406            schema_object
407                .metadata
408                .get_or_insert(Default::default())
409                .description = Some(description);
410        }
411
412        let empty_object: SchemaObject = SchemaObject {
413            instance_type: set(InstanceType::Object),
414            ..Default::default()
415        };
416
417        // This is a workaround for a json-language-server issue where it matches the first
418        // alternative that matches the value's shape and uses that for documentation.
419        //
420        // In the case of the array validations, it would even provide an error saying that the name
421        // must match the name of the first alternative.
422        let mut plain_action = SchemaObject {
423            instance_type: set(InstanceType::String),
424            const_value: Some(Value::String("".to_owned())),
425            ..Default::default()
426        };
427        let no_action_message = "No action named this.";
428        add_description(&mut plain_action, no_action_message.to_owned());
429        add_deprecation(&mut plain_action, no_action_message.to_owned());
430        let mut matches_action_name = SchemaObject {
431            const_value: Some(Value::String("".to_owned())),
432            ..Default::default()
433        };
434        let no_action_message = "No action named this that takes input.";
435        add_description(&mut matches_action_name, no_action_message.to_owned());
436        add_deprecation(&mut matches_action_name, no_action_message.to_owned());
437        let action_with_input = SchemaObject {
438            instance_type: set(InstanceType::Array),
439            array: set(ArrayValidation {
440                items: set(vec![
441                    matches_action_name.into(),
442                    // Accept any value, as we want this to be the preferred match when there is a
443                    // typo in the name.
444                    Schema::Bool(true),
445                ]),
446                min_items: Some(2),
447                max_items: Some(2),
448                ..Default::default()
449            }),
450            ..Default::default()
451        };
452        let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()];
453
454        for (name, action_schema) in action_schemas.iter() {
455            let schema = if let Some(Schema::Object(schema)) = action_schema {
456                Some(schema.clone())
457            } else {
458                None
459            };
460
461            let description = schema.as_ref().and_then(|schema| {
462                schema
463                    .metadata
464                    .as_ref()
465                    .and_then(|metadata| metadata.description.clone())
466            });
467
468            let deprecation = if name == NoAction.name() {
469                Some("null")
470            } else {
471                deprecations.get(name).map(|new_name| new_name.as_ref())
472            };
473
474            // Add an alternative for plain action names.
475            let mut plain_action = SchemaObject {
476                instance_type: set(InstanceType::String),
477                const_value: Some(Value::String(name.to_string())),
478                ..Default::default()
479            };
480            if let Some(new_name) = deprecation {
481                add_deprecation_preferred_name(&mut plain_action, new_name);
482            }
483            if let Some(description) = description.clone() {
484                add_description(&mut plain_action, description);
485            }
486            keymap_action_alternatives.push(plain_action.into());
487
488            // Add an alternative for actions with data specified as a [name, data] array.
489            //
490            // When a struct with no deserializable fields is added with impl_actions! /
491            // impl_actions_as! an empty object schema is produced. The action should be invoked
492            // without data in this case.
493            if let Some(schema) = schema {
494                if schema != empty_object {
495                    let mut matches_action_name = SchemaObject {
496                        const_value: Some(Value::String(name.to_string())),
497                        ..Default::default()
498                    };
499                    if let Some(description) = description.clone() {
500                        add_description(&mut matches_action_name, description.to_string());
501                    }
502                    if let Some(new_name) = deprecation {
503                        add_deprecation_preferred_name(&mut matches_action_name, new_name);
504                    }
505                    let action_with_input = SchemaObject {
506                        instance_type: set(InstanceType::Array),
507                        array: set(ArrayValidation {
508                            items: set(vec![matches_action_name.into(), schema.into()]),
509                            min_items: Some(2),
510                            max_items: Some(2),
511                            ..Default::default()
512                        }),
513                        ..Default::default()
514                    };
515                    keymap_action_alternatives.push(action_with_input.into());
516                }
517            }
518        }
519
520        // Placing null first causes json-language-server to default assuming actions should be
521        // null, so place it last.
522        keymap_action_alternatives.push(
523            SchemaObject {
524                instance_type: set(InstanceType::Null),
525                ..Default::default()
526            }
527            .into(),
528        );
529
530        let action_schema = SchemaObject {
531            subschemas: set(SubschemaValidation {
532                one_of: Some(keymap_action_alternatives),
533                ..Default::default()
534            }),
535            ..Default::default()
536        }
537        .into();
538
539        // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so replacing
540        // the definition of `KeymapAction` results in the full action schema being used.
541        let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
542        root_schema
543            .definitions
544            .insert(KeymapAction::schema_name(), action_schema);
545
546        // This and other json schemas can be viewed via `debug: open language server logs` ->
547        // `json-language-server` -> `Server Info`.
548        serde_json::to_value(root_schema).unwrap()
549    }
550
551    pub fn sections(&self) -> impl DoubleEndedIterator<Item = &KeymapSection> {
552        self.0.iter()
553    }
554}
555
556// Double quotes a string and wraps it in backticks for markdown inline code..
557fn inline_code_string(text: &str) -> MarkdownString {
558    MarkdownString::inline_code(&format!("\"{}\"", text))
559}
560
561#[cfg(test)]
562mod tests {
563    use crate::KeymapFile;
564
565    #[test]
566    fn can_deserialize_keymap_with_trailing_comma() {
567        let json = indoc::indoc! {"[
568              // Standard macOS bindings
569              {
570                \"bindings\": {
571                  \"up\": \"menu::SelectPrev\",
572                },
573              },
574            ]
575                  "
576        };
577        KeymapFile::parse(json).unwrap();
578    }
579}