keymap_file.rs

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