keymap_file.rs

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