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, AppContext, InvalidKeystrokeError, KeyBinding,
  8    KeyBindingContextPredicate, 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    AllFailedToLoad {
123        error_message: MarkdownString,
124    },
125    JsonParseFailure {
126        error: anyhow::Error,
127    },
128}
129
130impl KeymapFile {
131    pub fn parse(content: &str) -> anyhow::Result<Self> {
132        parse_json_with_comments::<Self>(content)
133    }
134
135    pub fn load_asset(asset_path: &str, cx: &AppContext) -> anyhow::Result<Vec<KeyBinding>> {
136        match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
137            KeymapFileLoadResult::Success { key_bindings, .. } => Ok(key_bindings),
138            KeymapFileLoadResult::SomeFailedToLoad { error_message, .. }
139            | KeymapFileLoadResult::AllFailedToLoad { error_message } => Err(anyhow!(
140                "Error loading built-in keymap \"{asset_path}\": {error_message}"
141            )),
142            KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
143                "JSON parse error in built-in keymap \"{asset_path}\": {error}"
144            )),
145        }
146    }
147
148    #[cfg(feature = "test-support")]
149    pub fn load_asset_allow_partial_failure(
150        asset_path: &str,
151        cx: &AppContext,
152    ) -> anyhow::Result<Vec<KeyBinding>> {
153        match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
154            KeymapFileLoadResult::Success { key_bindings, .. }
155            | KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
156            KeymapFileLoadResult::AllFailedToLoad { error_message } => Err(anyhow!(
157                "Error loading built-in keymap \"{asset_path}\": {error_message}"
158            )),
159            KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
160                "JSON parse error in built-in keymap \"{asset_path}\": {error}"
161            )),
162        }
163    }
164
165    #[cfg(feature = "test-support")]
166    pub fn load_panic_on_failure(content: &str, cx: &AppContext) -> Vec<KeyBinding> {
167        match Self::load(content, cx) {
168            KeymapFileLoadResult::Success { key_bindings } => key_bindings,
169            KeymapFileLoadResult::SomeFailedToLoad { error_message, .. }
170            | KeymapFileLoadResult::AllFailedToLoad { error_message, .. } => {
171                panic!("{error_message}");
172            }
173            KeymapFileLoadResult::JsonParseFailure { error } => {
174                panic!("JSON parse error: {error}");
175            }
176        }
177    }
178
179    pub fn load(content: &str, cx: &AppContext) -> KeymapFileLoadResult {
180        let key_equivalents = crate::key_equivalents::get_key_equivalents(&cx.keyboard_layout());
181
182        if content.is_empty() {
183            return KeymapFileLoadResult::Success {
184                key_bindings: Vec::new(),
185            };
186        }
187        let keymap_file = match parse_json_with_comments::<Self>(content) {
188            Ok(keymap_file) => keymap_file,
189            Err(error) => {
190                return KeymapFileLoadResult::JsonParseFailure { error };
191            }
192        };
193
194        // Accumulate errors in order to support partial load of user keymap in the presence of
195        // errors in context and binding parsing.
196        let mut errors = Vec::new();
197        let mut key_bindings = Vec::new();
198
199        for KeymapSection {
200            context,
201            use_key_equivalents,
202            bindings,
203            unrecognized_fields,
204        } in keymap_file.0.iter()
205        {
206            let context_predicate: Option<Rc<KeyBindingContextPredicate>> = if context.is_empty() {
207                None
208            } else {
209                match KeyBindingContextPredicate::parse(context) {
210                    Ok(context_predicate) => Some(context_predicate.into()),
211                    Err(err) => {
212                        // Leading space is to separate from the message indicating which section
213                        // the error occurred in.
214                        errors.push((
215                            context,
216                            format!(" Parse error in section `context` field: {}", err),
217                        ));
218                        continue;
219                    }
220                }
221            };
222
223            let key_equivalents = if *use_key_equivalents {
224                key_equivalents.as_ref()
225            } else {
226                None
227            };
228
229            let mut section_errors = String::new();
230
231            if !unrecognized_fields.is_empty() {
232                write!(
233                    section_errors,
234                    "\n\n - Unrecognized fields: {}",
235                    MarkdownString::inline_code(&format!("{:?}", unrecognized_fields.keys()))
236                )
237                .unwrap();
238            }
239
240            if let Some(bindings) = bindings {
241                for (keystrokes, action) in bindings {
242                    let result = Self::load_keybinding(
243                        keystrokes,
244                        action,
245                        context_predicate.clone(),
246                        key_equivalents,
247                        cx,
248                    );
249                    match result {
250                        Ok(key_binding) => {
251                            key_bindings.push(key_binding);
252                        }
253                        Err(err) => {
254                            write!(
255                                section_errors,
256                                "\n\n - In binding {}, {err}",
257                                inline_code_string(keystrokes),
258                            )
259                            .unwrap();
260                        }
261                    }
262                }
263            }
264
265            if !section_errors.is_empty() {
266                errors.push((context, section_errors))
267            }
268        }
269
270        if errors.is_empty() {
271            KeymapFileLoadResult::Success { key_bindings }
272        } else {
273            let mut error_message = "Errors in user keymap file.\n".to_owned();
274            for (context, section_errors) in errors {
275                if context.is_empty() {
276                    write!(error_message, "\n\nIn section without context predicate:").unwrap()
277                } else {
278                    write!(
279                        error_message,
280                        "\n\nIn section with {}:",
281                        MarkdownString::inline_code(&format!("context = \"{}\"", context))
282                    )
283                    .unwrap()
284                }
285                write!(error_message, "{section_errors}").unwrap();
286            }
287            KeymapFileLoadResult::SomeFailedToLoad {
288                key_bindings,
289                error_message: MarkdownString(error_message),
290            }
291        }
292    }
293
294    fn load_keybinding(
295        keystrokes: &str,
296        action: &KeymapAction,
297        context: Option<Rc<KeyBindingContextPredicate>>,
298        key_equivalents: Option<&HashMap<char, char>>,
299        cx: &AppContext,
300    ) -> std::result::Result<KeyBinding, String> {
301        let (build_result, action_input_string) = match &action.0 {
302            Value::Array(items) => {
303                if items.len() != 2 {
304                    return Err(format!(
305                        "expected two-element array of `[name, input]`. \
306                        Instead found {}.",
307                        MarkdownString::inline_code(&action.0.to_string())
308                    ));
309                }
310                let serde_json::Value::String(ref name) = items[0] else {
311                    return Err(format!(
312                        "expected two-element array of `[name, input]`, \
313                        but the first element is not a string in {}.",
314                        MarkdownString::inline_code(&action.0.to_string())
315                    ));
316                };
317                let action_input = items[1].clone();
318                let action_input_string = action_input.to_string();
319                (
320                    cx.build_action(&name, Some(action_input)),
321                    Some(action_input_string),
322                )
323            }
324            Value::String(name) => (cx.build_action(&name, None), None),
325            Value::Null => (Ok(NoAction.boxed_clone()), None),
326            _ => {
327                return Err(format!(
328                    "expected two-element array of `[name, input]`. \
329                    Instead found {}.",
330                    MarkdownString::inline_code(&action.0.to_string())
331                ));
332            }
333        };
334
335        let action = match build_result {
336            Ok(action) => action,
337            Err(ActionBuildError::NotFound { name }) => {
338                return Err(format!(
339                    "didn't find an action named {}.",
340                    inline_code_string(&name)
341                ))
342            }
343            Err(ActionBuildError::BuildError { name, error }) => match action_input_string {
344                Some(action_input_string) => {
345                    return Err(format!(
346                        "can't build {} action from input value {}: {}",
347                        inline_code_string(&name),
348                        MarkdownString::inline_code(&action_input_string),
349                        MarkdownString::escape(&error.to_string())
350                    ))
351                }
352                None => {
353                    return Err(format!(
354                        "can't build {} action - it requires input data via [name, input]: {}",
355                        inline_code_string(&name),
356                        MarkdownString::escape(&error.to_string())
357                    ))
358                }
359            },
360        };
361
362        match KeyBinding::load(keystrokes, action, context, key_equivalents) {
363            Ok(binding) => Ok(binding),
364            Err(InvalidKeystrokeError { keystroke }) => Err(format!(
365                "invalid keystroke {}. {}",
366                inline_code_string(&keystroke),
367                KEYSTROKE_PARSE_EXPECTED_MESSAGE
368            )),
369        }
370    }
371
372    pub fn generate_json_schema_for_registered_actions(cx: &mut AppContext) -> Value {
373        let mut generator = SchemaSettings::draft07()
374            .with(|settings| settings.option_add_null_type = false)
375            .into_generator();
376
377        let action_schemas = cx.action_schemas(&mut generator);
378        let deprecations = cx.action_deprecations();
379        KeymapFile::generate_json_schema(generator, action_schemas, deprecations)
380    }
381
382    fn generate_json_schema(
383        generator: SchemaGenerator,
384        action_schemas: Vec<(SharedString, Option<Schema>)>,
385        deprecations: &HashMap<SharedString, SharedString>,
386    ) -> serde_json::Value {
387        fn set<I, O>(input: I) -> Option<O>
388        where
389            I: Into<O>,
390        {
391            Some(input.into())
392        }
393
394        fn add_deprecation(schema_object: &mut SchemaObject, message: String) {
395            schema_object.extensions.insert(
396                // deprecationMessage is not part of the JSON Schema spec,
397                // but json-language-server recognizes it.
398                "deprecationMessage".to_owned(),
399                Value::String(message),
400            );
401        }
402
403        fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) {
404            add_deprecation(schema_object, format!("Deprecated, use {new_name}"));
405        }
406
407        fn add_description(schema_object: &mut SchemaObject, description: String) {
408            schema_object
409                .metadata
410                .get_or_insert(Default::default())
411                .description = Some(description);
412        }
413
414        let empty_object: SchemaObject = SchemaObject {
415            instance_type: set(InstanceType::Object),
416            ..Default::default()
417        };
418
419        // This is a workaround for a json-language-server issue where it matches the first
420        // alternative that matches the value's shape and uses that for documentation.
421        //
422        // In the case of the array validations, it would even provide an error saying that the name
423        // must match the name of the first alternative.
424        let mut plain_action = SchemaObject {
425            instance_type: set(InstanceType::String),
426            const_value: Some(Value::String("".to_owned())),
427            ..Default::default()
428        };
429        let no_action_message = "No action named this.";
430        add_description(&mut plain_action, no_action_message.to_owned());
431        add_deprecation(&mut plain_action, no_action_message.to_owned());
432        let mut matches_action_name = SchemaObject {
433            const_value: Some(Value::String("".to_owned())),
434            ..Default::default()
435        };
436        let no_action_message = "No action named this that takes input.";
437        add_description(&mut matches_action_name, no_action_message.to_owned());
438        add_deprecation(&mut matches_action_name, no_action_message.to_owned());
439        let action_with_input = SchemaObject {
440            instance_type: set(InstanceType::Array),
441            array: set(ArrayValidation {
442                items: set(vec![
443                    matches_action_name.into(),
444                    // Accept any value, as we want this to be the preferred match when there is a
445                    // typo in the name.
446                    Schema::Bool(true),
447                ]),
448                min_items: Some(2),
449                max_items: Some(2),
450                ..Default::default()
451            }),
452            ..Default::default()
453        };
454        let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()];
455
456        for (name, action_schema) in action_schemas.iter() {
457            let schema = if let Some(Schema::Object(schema)) = action_schema {
458                Some(schema.clone())
459            } else {
460                None
461            };
462
463            let description = schema.as_ref().and_then(|schema| {
464                schema
465                    .metadata
466                    .as_ref()
467                    .and_then(|metadata| metadata.description.clone())
468            });
469
470            let deprecation = if name == NoAction.name() {
471                Some("null")
472            } else {
473                deprecations.get(name).map(|new_name| new_name.as_ref())
474            };
475
476            // Add an alternative for plain action names.
477            let mut plain_action = SchemaObject {
478                instance_type: set(InstanceType::String),
479                const_value: Some(Value::String(name.to_string())),
480                ..Default::default()
481            };
482            if let Some(new_name) = deprecation {
483                add_deprecation_preferred_name(&mut plain_action, new_name);
484            }
485            if let Some(description) = description.clone() {
486                add_description(&mut plain_action, description);
487            }
488            keymap_action_alternatives.push(plain_action.into());
489
490            // Add an alternative for actions with data specified as a [name, data] array.
491            //
492            // When a struct with no deserializable fields is added with impl_actions! /
493            // impl_actions_as! an empty object schema is produced. The action should be invoked
494            // without data in this case.
495            if let Some(schema) = schema {
496                if schema != empty_object {
497                    let mut matches_action_name = SchemaObject {
498                        const_value: Some(Value::String(name.to_string())),
499                        ..Default::default()
500                    };
501                    if let Some(description) = description.clone() {
502                        add_description(&mut matches_action_name, description.to_string());
503                    }
504                    if let Some(new_name) = deprecation {
505                        add_deprecation_preferred_name(&mut matches_action_name, new_name);
506                    }
507                    let action_with_input = SchemaObject {
508                        instance_type: set(InstanceType::Array),
509                        array: set(ArrayValidation {
510                            items: set(vec![matches_action_name.into(), schema.into()]),
511                            min_items: Some(2),
512                            max_items: Some(2),
513                            ..Default::default()
514                        }),
515                        ..Default::default()
516                    };
517                    keymap_action_alternatives.push(action_with_input.into());
518                }
519            }
520        }
521
522        // Placing null first causes json-language-server to default assuming actions should be
523        // null, so place it last.
524        keymap_action_alternatives.push(
525            SchemaObject {
526                instance_type: set(InstanceType::Null),
527                ..Default::default()
528            }
529            .into(),
530        );
531
532        let action_schema = SchemaObject {
533            subschemas: set(SubschemaValidation {
534                one_of: Some(keymap_action_alternatives),
535                ..Default::default()
536            }),
537            ..Default::default()
538        }
539        .into();
540
541        // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so replacing
542        // the definition of `KeymapAction` results in the full action schema being used.
543        let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
544        root_schema
545            .definitions
546            .insert(KeymapAction::schema_name(), action_schema);
547
548        // This and other json schemas can be viewed via `debug: open language server logs` ->
549        // `json-language-server` -> `Server Info`.
550        serde_json::to_value(root_schema).unwrap()
551    }
552
553    pub fn sections(&self) -> impl DoubleEndedIterator<Item = &KeymapSection> {
554        self.0.iter()
555    }
556}
557
558// Double quotes a string and wraps it in backticks for markdown inline code..
559fn inline_code_string(text: &str) -> MarkdownString {
560    MarkdownString::inline_code(&format!("\"{}\"", text))
561}
562
563#[cfg(test)]
564mod tests {
565    use crate::KeymapFile;
566
567    #[test]
568    fn can_deserialize_keymap_with_trailing_comma() {
569        let json = indoc::indoc! {"[
570              // Standard macOS bindings
571              {
572                \"bindings\": {
573                  \"up\": \"menu::SelectPrev\",
574                },
575              },
576            ]
577                  "
578        };
579        KeymapFile::parse(json).unwrap();
580    }
581}