keymap_file.rs

   1use anyhow::{Context as _, Result};
   2use collections::{BTreeMap, HashMap, IndexMap};
   3use fs::Fs;
   4use gpui::{
   5    Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
   6    KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, NoAction,
   7};
   8use schemars::{
   9    JsonSchema,
  10    r#gen::{SchemaGenerator, SchemaSettings},
  11    schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation},
  12};
  13use serde::Deserialize;
  14use serde_json::Value;
  15use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock};
  16use util::{
  17    asset_str,
  18    markdown::{MarkdownEscaped, MarkdownInlineCode, MarkdownString},
  19};
  20
  21use crate::{
  22    SettingsAssets, append_top_level_array_value_in_json_text, parse_json_with_comments,
  23    replace_top_level_array_value_in_json_text,
  24};
  25
  26pub trait KeyBindingValidator: Send + Sync {
  27    fn action_type_id(&self) -> TypeId;
  28    fn validate(&self, binding: &KeyBinding) -> Result<(), MarkdownString>;
  29}
  30
  31pub struct KeyBindingValidatorRegistration(pub fn() -> Box<dyn KeyBindingValidator>);
  32
  33inventory::collect!(KeyBindingValidatorRegistration);
  34
  35pub(crate) static KEY_BINDING_VALIDATORS: LazyLock<BTreeMap<TypeId, Box<dyn KeyBindingValidator>>> =
  36    LazyLock::new(|| {
  37        let mut validators = BTreeMap::new();
  38        for validator_registration in inventory::iter::<KeyBindingValidatorRegistration> {
  39            let validator = validator_registration.0();
  40            validators.insert(validator.action_type_id(), validator);
  41        }
  42        validators
  43    });
  44
  45// Note that the doc comments on these are shown by json-language-server when editing the keymap, so
  46// they should be considered user-facing documentation. Documentation is not handled well with
  47// schemars-0.8 - when there are newlines, it is rendered as plaintext (see
  48// https://github.com/GREsau/schemars/issues/38#issuecomment-2282883519). So for now these docs
  49// avoid newlines.
  50//
  51// TODO: Update to schemars-1.0 once it's released, and add more docs as newlines would be
  52// supported. Tracking issue is https://github.com/GREsau/schemars/issues/112.
  53
  54/// Keymap configuration consisting of sections. Each section may have a context predicate which
  55/// determines whether its bindings are used.
  56#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
  57#[serde(transparent)]
  58pub struct KeymapFile(Vec<KeymapSection>);
  59
  60/// Keymap section which binds keystrokes to actions.
  61#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
  62pub struct KeymapSection {
  63    /// Determines when these bindings are active. When just a name is provided, like `Editor` or
  64    /// `Workspace`, the bindings will be active in that context. Boolean expressions like `X && Y`,
  65    /// `X || Y`, `!X` are also supported. Some more complex logic including checking OS and the
  66    /// current file extension are also supported - see [the
  67    /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details.
  68    #[serde(default)]
  69    context: String,
  70    /// This option enables specifying keys based on their position on a QWERTY keyboard, by using
  71    /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported
  72    /// on macOS. See the documentation for more details.
  73    #[serde(default)]
  74    use_key_equivalents: bool,
  75    /// This keymap section's bindings, as a JSON object mapping keystrokes to actions. The
  76    /// keystrokes key is a string representing a sequence of keystrokes to type, where the
  77    /// keystrokes are separated by whitespace. Each keystroke is a sequence of modifiers (`ctrl`,
  78    /// `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) followed by a key, separated by `-`. The
  79    /// order of bindings does matter. When the same keystrokes are bound at the same context depth,
  80    /// the binding that occurs later in the file is preferred. For displaying keystrokes in the UI,
  81    /// the later binding for the same action is preferred.
  82    #[serde(default)]
  83    bindings: Option<IndexMap<String, KeymapAction>>,
  84    #[serde(flatten)]
  85    unrecognized_fields: IndexMap<String, Value>,
  86    // This struct intentionally uses permissive types for its fields, rather than validating during
  87    // deserialization. The purpose of this is to allow loading the portion of the keymap that doesn't
  88    // have errors. The downside of this is that the errors are not reported with line+column info.
  89    // Unfortunately the implementations of the `Spanned` types for preserving this information are
  90    // highly inconvenient (`serde_spanned`) and in some cases don't work at all here
  91    // (`json_spanned_>value`). Serde should really have builtin support for this.
  92}
  93
  94impl KeymapSection {
  95    pub fn bindings(&self) -> impl DoubleEndedIterator<Item = (&String, &KeymapAction)> {
  96        self.bindings.iter().flatten()
  97    }
  98}
  99
 100/// Keymap action as a JSON value, since it can either be null for no action, or the name of the
 101/// action, or an array of the name of the action and the action input.
 102///
 103/// Unlike the other json types involved in keymaps (including actions), this doc-comment will not
 104/// be included in the generated JSON schema, as it manually defines its `JsonSchema` impl. The
 105/// actual schema used for it is automatically generated in `KeymapFile::generate_json_schema`.
 106#[derive(Debug, Deserialize, Default, Clone)]
 107#[serde(transparent)]
 108pub struct KeymapAction(Value);
 109
 110impl std::fmt::Display for KeymapAction {
 111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 112        match &self.0 {
 113            Value::String(s) => write!(f, "{}", s),
 114            Value::Array(arr) => {
 115                let strings: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
 116                write!(f, "{}", strings.join(", "))
 117            }
 118            _ => write!(f, "{}", self.0),
 119        }
 120    }
 121}
 122
 123impl JsonSchema for KeymapAction {
 124    /// This is used when generating the JSON schema for the `KeymapAction` type, so that it can
 125    /// reference the keymap action schema.
 126    fn schema_name() -> String {
 127        "KeymapAction".into()
 128    }
 129
 130    /// This schema will be replaced with the full action schema in
 131    /// `KeymapFile::generate_json_schema`.
 132    fn json_schema(_: &mut SchemaGenerator) -> Schema {
 133        Schema::Bool(true)
 134    }
 135}
 136
 137#[derive(Debug)]
 138#[must_use]
 139pub enum KeymapFileLoadResult {
 140    Success {
 141        key_bindings: Vec<KeyBinding>,
 142    },
 143    SomeFailedToLoad {
 144        key_bindings: Vec<KeyBinding>,
 145        error_message: MarkdownString,
 146    },
 147    JsonParseFailure {
 148        error: anyhow::Error,
 149    },
 150}
 151
 152impl KeymapFile {
 153    pub fn parse(content: &str) -> anyhow::Result<Self> {
 154        parse_json_with_comments::<Self>(content)
 155    }
 156
 157    pub fn load_asset(
 158        asset_path: &str,
 159        source: Option<KeybindSource>,
 160        cx: &App,
 161    ) -> anyhow::Result<Vec<KeyBinding>> {
 162        match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
 163            KeymapFileLoadResult::Success { mut key_bindings } => match source {
 164                Some(source) => Ok({
 165                    for key_binding in &mut key_bindings {
 166                        key_binding.set_meta(source.meta());
 167                    }
 168                    key_bindings
 169                }),
 170                None => Ok(key_bindings),
 171            },
 172            KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
 173                anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",)
 174            }
 175            KeymapFileLoadResult::JsonParseFailure { error } => {
 176                anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}")
 177            }
 178        }
 179    }
 180
 181    #[cfg(feature = "test-support")]
 182    pub fn load_asset_allow_partial_failure(
 183        asset_path: &str,
 184        cx: &App,
 185    ) -> anyhow::Result<Vec<KeyBinding>> {
 186        match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
 187            KeymapFileLoadResult::SomeFailedToLoad {
 188                key_bindings,
 189                error_message,
 190                ..
 191            } if key_bindings.is_empty() => {
 192                anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",)
 193            }
 194            KeymapFileLoadResult::Success { key_bindings, .. }
 195            | KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
 196            KeymapFileLoadResult::JsonParseFailure { error } => {
 197                anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}")
 198            }
 199        }
 200    }
 201
 202    #[cfg(feature = "test-support")]
 203    pub fn load_panic_on_failure(content: &str, cx: &App) -> Vec<KeyBinding> {
 204        match Self::load(content, cx) {
 205            KeymapFileLoadResult::Success { key_bindings, .. } => key_bindings,
 206            KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
 207                panic!("{error_message}");
 208            }
 209            KeymapFileLoadResult::JsonParseFailure { error } => {
 210                panic!("JSON parse error: {error}");
 211            }
 212        }
 213    }
 214
 215    pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
 216        let key_equivalents =
 217            crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id());
 218
 219        if content.is_empty() {
 220            return KeymapFileLoadResult::Success {
 221                key_bindings: Vec::new(),
 222            };
 223        }
 224        let keymap_file = match Self::parse(content) {
 225            Ok(keymap_file) => keymap_file,
 226            Err(error) => {
 227                return KeymapFileLoadResult::JsonParseFailure { error };
 228            }
 229        };
 230
 231        // Accumulate errors in order to support partial load of user keymap in the presence of
 232        // errors in context and binding parsing.
 233        let mut errors = Vec::new();
 234        let mut key_bindings = Vec::new();
 235
 236        for KeymapSection {
 237            context,
 238            use_key_equivalents,
 239            bindings,
 240            unrecognized_fields,
 241        } in keymap_file.0.iter()
 242        {
 243            let context_predicate: Option<Rc<KeyBindingContextPredicate>> = if context.is_empty() {
 244                None
 245            } else {
 246                match KeyBindingContextPredicate::parse(context) {
 247                    Ok(context_predicate) => Some(context_predicate.into()),
 248                    Err(err) => {
 249                        // Leading space is to separate from the message indicating which section
 250                        // the error occurred in.
 251                        errors.push((
 252                            context,
 253                            format!(" Parse error in section `context` field: {}", err),
 254                        ));
 255                        continue;
 256                    }
 257                }
 258            };
 259
 260            let key_equivalents = if *use_key_equivalents {
 261                key_equivalents.as_ref()
 262            } else {
 263                None
 264            };
 265
 266            let mut section_errors = String::new();
 267
 268            if !unrecognized_fields.is_empty() {
 269                write!(
 270                    section_errors,
 271                    "\n\n - Unrecognized fields: {}",
 272                    MarkdownInlineCode(&format!("{:?}", unrecognized_fields.keys()))
 273                )
 274                .unwrap();
 275            }
 276
 277            if let Some(bindings) = bindings {
 278                for (keystrokes, action) in bindings {
 279                    let result = Self::load_keybinding(
 280                        keystrokes,
 281                        action,
 282                        context_predicate.clone(),
 283                        key_equivalents,
 284                        cx,
 285                    );
 286                    match result {
 287                        Ok(key_binding) => {
 288                            key_bindings.push(key_binding);
 289                        }
 290                        Err(err) => {
 291                            let mut lines = err.lines();
 292                            let mut indented_err = lines.next().unwrap().to_string();
 293                            for line in lines {
 294                                indented_err.push_str("  ");
 295                                indented_err.push_str(line);
 296                                indented_err.push_str("\n");
 297                            }
 298                            write!(
 299                                section_errors,
 300                                "\n\n- In binding {}, {indented_err}",
 301                                MarkdownInlineCode(&format!("\"{}\"", keystrokes))
 302                            )
 303                            .unwrap();
 304                        }
 305                    }
 306                }
 307            }
 308
 309            if !section_errors.is_empty() {
 310                errors.push((context, section_errors))
 311            }
 312        }
 313
 314        if errors.is_empty() {
 315            KeymapFileLoadResult::Success { key_bindings }
 316        } else {
 317            let mut error_message = "Errors in user keymap file.\n".to_owned();
 318            for (context, section_errors) in errors {
 319                if context.is_empty() {
 320                    let _ = write!(error_message, "\n\nIn section without context predicate:");
 321                } else {
 322                    let _ = write!(
 323                        error_message,
 324                        "\n\nIn section with {}:",
 325                        MarkdownInlineCode(&format!("context = \"{}\"", context))
 326                    );
 327                }
 328                let _ = write!(error_message, "{section_errors}");
 329            }
 330            KeymapFileLoadResult::SomeFailedToLoad {
 331                key_bindings,
 332                error_message: MarkdownString(error_message),
 333            }
 334        }
 335    }
 336
 337    fn load_keybinding(
 338        keystrokes: &str,
 339        action: &KeymapAction,
 340        context: Option<Rc<KeyBindingContextPredicate>>,
 341        key_equivalents: Option<&HashMap<char, char>>,
 342        cx: &App,
 343    ) -> std::result::Result<KeyBinding, String> {
 344        let (build_result, action_input_string) = match &action.0 {
 345            Value::Array(items) => {
 346                if items.len() != 2 {
 347                    return Err(format!(
 348                        "expected two-element array of `[name, input]`. \
 349                        Instead found {}.",
 350                        MarkdownInlineCode(&action.0.to_string())
 351                    ));
 352                }
 353                let serde_json::Value::String(ref name) = items[0] else {
 354                    return Err(format!(
 355                        "expected two-element array of `[name, input]`, \
 356                        but the first element is not a string in {}.",
 357                        MarkdownInlineCode(&action.0.to_string())
 358                    ));
 359                };
 360                let action_input = items[1].clone();
 361                let action_input_string = action_input.to_string();
 362                (
 363                    cx.build_action(&name, Some(action_input)),
 364                    Some(action_input_string),
 365                )
 366            }
 367            Value::String(name) => (cx.build_action(&name, None), None),
 368            Value::Null => (Ok(NoAction.boxed_clone()), None),
 369            _ => {
 370                return Err(format!(
 371                    "expected two-element array of `[name, input]`. \
 372                    Instead found {}.",
 373                    MarkdownInlineCode(&action.0.to_string())
 374                ));
 375            }
 376        };
 377
 378        let action = match build_result {
 379            Ok(action) => action,
 380            Err(ActionBuildError::NotFound { name }) => {
 381                return Err(format!(
 382                    "didn't find an action named {}.",
 383                    MarkdownInlineCode(&format!("\"{}\"", &name))
 384                ));
 385            }
 386            Err(ActionBuildError::BuildError { name, error }) => match action_input_string {
 387                Some(action_input_string) => {
 388                    return Err(format!(
 389                        "can't build {} action from input value {}: {}",
 390                        MarkdownInlineCode(&format!("\"{}\"", &name)),
 391                        MarkdownInlineCode(&action_input_string),
 392                        MarkdownEscaped(&error.to_string())
 393                    ));
 394                }
 395                None => {
 396                    return Err(format!(
 397                        "can't build {} action - it requires input data via [name, input]: {}",
 398                        MarkdownInlineCode(&format!("\"{}\"", &name)),
 399                        MarkdownEscaped(&error.to_string())
 400                    ));
 401                }
 402            },
 403        };
 404
 405        let key_binding = match KeyBinding::load(keystrokes, action, context, key_equivalents) {
 406            Ok(key_binding) => key_binding,
 407            Err(InvalidKeystrokeError { keystroke }) => {
 408                return Err(format!(
 409                    "invalid keystroke {}. {}",
 410                    MarkdownInlineCode(&format!("\"{}\"", &keystroke)),
 411                    KEYSTROKE_PARSE_EXPECTED_MESSAGE
 412                ));
 413            }
 414        };
 415
 416        if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) {
 417            match validator.validate(&key_binding) {
 418                Ok(()) => Ok(key_binding),
 419                Err(error) => Err(error.0),
 420            }
 421        } else {
 422            Ok(key_binding)
 423        }
 424    }
 425
 426    pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
 427        let mut generator = SchemaSettings::draft07()
 428            .with(|settings| settings.option_add_null_type = false)
 429            .into_generator();
 430
 431        let action_schemas = cx.action_schemas(&mut generator);
 432        let deprecations = cx.deprecated_actions_to_preferred_actions();
 433        let deprecation_messages = cx.action_deprecation_messages();
 434        KeymapFile::generate_json_schema(
 435            generator,
 436            action_schemas,
 437            deprecations,
 438            deprecation_messages,
 439        )
 440    }
 441
 442    fn generate_json_schema(
 443        generator: SchemaGenerator,
 444        action_schemas: Vec<(&'static str, Option<Schema>)>,
 445        deprecations: &HashMap<&'static str, &'static str>,
 446        deprecation_messages: &HashMap<&'static str, &'static str>,
 447    ) -> serde_json::Value {
 448        fn set<I, O>(input: I) -> Option<O>
 449        where
 450            I: Into<O>,
 451        {
 452            Some(input.into())
 453        }
 454
 455        fn add_deprecation(schema_object: &mut SchemaObject, message: String) {
 456            schema_object.extensions.insert(
 457                // deprecationMessage is not part of the JSON Schema spec,
 458                // but json-language-server recognizes it.
 459                "deprecationMessage".to_owned(),
 460                Value::String(message),
 461            );
 462        }
 463
 464        fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) {
 465            add_deprecation(schema_object, format!("Deprecated, use {new_name}"));
 466        }
 467
 468        fn add_description(schema_object: &mut SchemaObject, description: String) {
 469            schema_object
 470                .metadata
 471                .get_or_insert(Default::default())
 472                .description = Some(description);
 473        }
 474
 475        let empty_object: SchemaObject = SchemaObject {
 476            instance_type: set(InstanceType::Object),
 477            ..Default::default()
 478        };
 479
 480        // This is a workaround for a json-language-server issue where it matches the first
 481        // alternative that matches the value's shape and uses that for documentation.
 482        //
 483        // In the case of the array validations, it would even provide an error saying that the name
 484        // must match the name of the first alternative.
 485        let mut plain_action = SchemaObject {
 486            instance_type: set(InstanceType::String),
 487            const_value: Some(Value::String("".to_owned())),
 488            ..Default::default()
 489        };
 490        let no_action_message = "No action named this.";
 491        add_description(&mut plain_action, no_action_message.to_owned());
 492        add_deprecation(&mut plain_action, no_action_message.to_owned());
 493        let mut matches_action_name = SchemaObject {
 494            const_value: Some(Value::String("".to_owned())),
 495            ..Default::default()
 496        };
 497        let no_action_message = "No action named this that takes input.";
 498        add_description(&mut matches_action_name, no_action_message.to_owned());
 499        add_deprecation(&mut matches_action_name, no_action_message.to_owned());
 500        let action_with_input = SchemaObject {
 501            instance_type: set(InstanceType::Array),
 502            array: set(ArrayValidation {
 503                items: set(vec![
 504                    matches_action_name.into(),
 505                    // Accept any value, as we want this to be the preferred match when there is a
 506                    // typo in the name.
 507                    Schema::Bool(true),
 508                ]),
 509                min_items: Some(2),
 510                max_items: Some(2),
 511                ..Default::default()
 512            }),
 513            ..Default::default()
 514        };
 515        let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()];
 516
 517        for (name, action_schema) in action_schemas.into_iter() {
 518            let schema = if let Some(Schema::Object(schema)) = action_schema {
 519                Some(schema)
 520            } else {
 521                None
 522            };
 523
 524            let description = schema.as_ref().and_then(|schema| {
 525                schema
 526                    .metadata
 527                    .as_ref()
 528                    .and_then(|metadata| metadata.description.clone())
 529            });
 530
 531            let deprecation = if name == NoAction.name() {
 532                Some("null")
 533            } else {
 534                deprecations.get(name).copied()
 535            };
 536
 537            // Add an alternative for plain action names.
 538            let mut plain_action = SchemaObject {
 539                instance_type: set(InstanceType::String),
 540                const_value: Some(Value::String(name.to_string())),
 541                ..Default::default()
 542            };
 543            if let Some(message) = deprecation_messages.get(name) {
 544                add_deprecation(&mut plain_action, message.to_string());
 545            } else if let Some(new_name) = deprecation {
 546                add_deprecation_preferred_name(&mut plain_action, new_name);
 547            }
 548            if let Some(description) = description.clone() {
 549                add_description(&mut plain_action, description);
 550            }
 551            keymap_action_alternatives.push(plain_action.into());
 552
 553            // Add an alternative for actions with data specified as a [name, data] array.
 554            //
 555            // When a struct with no deserializable fields is added with impl_actions! /
 556            // impl_actions_as! an empty object schema is produced. The action should be invoked
 557            // without data in this case.
 558            if let Some(schema) = schema {
 559                if schema != empty_object {
 560                    let mut matches_action_name = SchemaObject {
 561                        const_value: Some(Value::String(name.to_string())),
 562                        ..Default::default()
 563                    };
 564                    if let Some(description) = description.clone() {
 565                        add_description(&mut matches_action_name, description);
 566                    }
 567                    if let Some(message) = deprecation_messages.get(name) {
 568                        add_deprecation(&mut matches_action_name, message.to_string());
 569                    } else if let Some(new_name) = deprecation {
 570                        add_deprecation_preferred_name(&mut matches_action_name, new_name);
 571                    }
 572                    let action_with_input = SchemaObject {
 573                        instance_type: set(InstanceType::Array),
 574                        array: set(ArrayValidation {
 575                            items: set(vec![matches_action_name.into(), schema.into()]),
 576                            min_items: Some(2),
 577                            max_items: Some(2),
 578                            ..Default::default()
 579                        }),
 580                        ..Default::default()
 581                    };
 582                    keymap_action_alternatives.push(action_with_input.into());
 583                }
 584            }
 585        }
 586
 587        // Placing null first causes json-language-server to default assuming actions should be
 588        // null, so place it last.
 589        keymap_action_alternatives.push(
 590            SchemaObject {
 591                instance_type: set(InstanceType::Null),
 592                ..Default::default()
 593            }
 594            .into(),
 595        );
 596
 597        let action_schema = SchemaObject {
 598            subschemas: set(SubschemaValidation {
 599                one_of: Some(keymap_action_alternatives),
 600                ..Default::default()
 601            }),
 602            ..Default::default()
 603        }
 604        .into();
 605
 606        // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so replacing
 607        // the definition of `KeymapAction` results in the full action schema being used.
 608        let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
 609        root_schema
 610            .definitions
 611            .insert(KeymapAction::schema_name(), action_schema);
 612
 613        // This and other json schemas can be viewed via `dev: open language server logs` ->
 614        // `json-language-server` -> `Server Info`.
 615        serde_json::to_value(root_schema).unwrap()
 616    }
 617
 618    pub fn sections(&self) -> impl DoubleEndedIterator<Item = &KeymapSection> {
 619        self.0.iter()
 620    }
 621
 622    pub async fn load_keymap_file(fs: &Arc<dyn Fs>) -> Result<String> {
 623        match fs.load(paths::keymap_file()).await {
 624            result @ Ok(_) => result,
 625            Err(err) => {
 626                if let Some(e) = err.downcast_ref::<std::io::Error>() {
 627                    if e.kind() == std::io::ErrorKind::NotFound {
 628                        return Ok(crate::initial_keymap_content().to_string());
 629                    }
 630                }
 631                Err(err)
 632            }
 633        }
 634    }
 635
 636    pub fn update_keybinding<'a>(
 637        mut operation: KeybindUpdateOperation<'a>,
 638        mut keymap_contents: String,
 639        tab_size: usize,
 640    ) -> Result<String> {
 641        // if trying to replace a keybinding that is not user-defined, treat it as an add operation
 642        match operation {
 643            KeybindUpdateOperation::Replace {
 644                target_source,
 645                source,
 646                ..
 647            } if target_source != KeybindSource::User => {
 648                operation = KeybindUpdateOperation::Add(source);
 649            }
 650            _ => {}
 651        }
 652
 653        // Sanity check that keymap contents are valid, even though we only use it for Replace.
 654        // We don't want to modify the file if it's invalid.
 655        let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?;
 656
 657        if let KeybindUpdateOperation::Replace { source, target, .. } = operation {
 658            let mut found_index = None;
 659            let target_action_value = target
 660                .action_value()
 661                .context("Failed to generate target action JSON value")?;
 662            let source_action_value = source
 663                .action_value()
 664                .context("Failed to generate source action JSON value")?;
 665            'sections: for (index, section) in keymap.sections().enumerate() {
 666                if section.context != target.context.unwrap_or("") {
 667                    continue;
 668                }
 669                if section.use_key_equivalents != target.use_key_equivalents {
 670                    continue;
 671                }
 672                let Some(bindings) = &section.bindings else {
 673                    continue;
 674                };
 675                for (keystrokes, action) in bindings {
 676                    if keystrokes != target.keystrokes {
 677                        continue;
 678                    }
 679                    if action.0 != target_action_value {
 680                        continue;
 681                    }
 682                    found_index = Some(index);
 683                    break 'sections;
 684                }
 685            }
 686
 687            if let Some(index) = found_index {
 688                let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
 689                    &keymap_contents,
 690                    &["bindings", target.keystrokes],
 691                    Some(&source_action_value),
 692                    Some(source.keystrokes),
 693                    index,
 694                    tab_size,
 695                )
 696                .context("Failed to replace keybinding")?;
 697                keymap_contents.replace_range(replace_range, &replace_value);
 698
 699                return Ok(keymap_contents);
 700            } else {
 701                log::warn!(
 702                    "Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead",
 703                    target.keystrokes,
 704                    target_action_value,
 705                    source.keystrokes,
 706                    source_action_value,
 707                );
 708                operation = KeybindUpdateOperation::Add(source);
 709            }
 710        }
 711
 712        if let KeybindUpdateOperation::Add(keybinding) = operation {
 713            let mut value = serde_json::Map::with_capacity(4);
 714            if let Some(context) = keybinding.context {
 715                value.insert("context".to_string(), context.into());
 716            }
 717            if keybinding.use_key_equivalents {
 718                value.insert("use_key_equivalents".to_string(), true.into());
 719            }
 720
 721            value.insert("bindings".to_string(), {
 722                let mut bindings = serde_json::Map::new();
 723                let action = keybinding.action_value()?;
 724                bindings.insert(keybinding.keystrokes.into(), action);
 725                bindings.into()
 726            });
 727
 728            let (replace_range, replace_value) = append_top_level_array_value_in_json_text(
 729                &keymap_contents,
 730                &value.into(),
 731                tab_size,
 732            )?;
 733            keymap_contents.replace_range(replace_range, &replace_value);
 734        }
 735        return Ok(keymap_contents);
 736    }
 737}
 738
 739pub enum KeybindUpdateOperation<'a> {
 740    Replace {
 741        /// Describes the keybind to create
 742        source: KeybindUpdateTarget<'a>,
 743        /// Describes the keybind to remove
 744        target: KeybindUpdateTarget<'a>,
 745        target_source: KeybindSource,
 746    },
 747    Add(KeybindUpdateTarget<'a>),
 748}
 749
 750pub struct KeybindUpdateTarget<'a> {
 751    context: Option<&'a str>,
 752    keystrokes: &'a str,
 753    action_name: &'a str,
 754    use_key_equivalents: bool,
 755    input: Option<&'a str>,
 756}
 757
 758impl<'a> KeybindUpdateTarget<'a> {
 759    fn action_value(&self) -> Result<Value> {
 760        let action_name: Value = self.action_name.into();
 761        let value = match self.input {
 762            Some(input) => {
 763                let input = serde_json::from_str::<Value>(input)
 764                    .context("Failed to parse action input as JSON")?;
 765                serde_json::json!([action_name, input])
 766            }
 767            None => action_name,
 768        };
 769        return Ok(value);
 770    }
 771}
 772
 773#[derive(Clone, Copy, PartialEq, Eq)]
 774pub enum KeybindSource {
 775    User,
 776    Default,
 777    Base,
 778    Vim,
 779}
 780
 781impl KeybindSource {
 782    const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(0);
 783    const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(1);
 784    const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(2);
 785    const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(3);
 786
 787    pub fn name(&self) -> &'static str {
 788        match self {
 789            KeybindSource::User => "User",
 790            KeybindSource::Default => "Default",
 791            KeybindSource::Base => "Base",
 792            KeybindSource::Vim => "Vim",
 793        }
 794    }
 795
 796    pub fn meta(&self) -> KeyBindingMetaIndex {
 797        match self {
 798            KeybindSource::User => Self::USER,
 799            KeybindSource::Default => Self::DEFAULT,
 800            KeybindSource::Base => Self::BASE,
 801            KeybindSource::Vim => Self::VIM,
 802        }
 803    }
 804
 805    pub fn from_meta(index: KeyBindingMetaIndex) -> Self {
 806        match index {
 807            _ if index == Self::USER => KeybindSource::User,
 808            _ if index == Self::USER => KeybindSource::Base,
 809            _ if index == Self::DEFAULT => KeybindSource::Default,
 810            _ if index == Self::VIM => KeybindSource::Vim,
 811            _ => unreachable!(),
 812        }
 813    }
 814}
 815
 816impl From<KeyBindingMetaIndex> for KeybindSource {
 817    fn from(index: KeyBindingMetaIndex) -> Self {
 818        Self::from_meta(index)
 819    }
 820}
 821
 822impl From<KeybindSource> for KeyBindingMetaIndex {
 823    fn from(source: KeybindSource) -> Self {
 824        return source.meta();
 825    }
 826}
 827
 828#[cfg(test)]
 829mod tests {
 830    use unindent::Unindent;
 831
 832    use crate::{
 833        KeybindSource, KeymapFile,
 834        keymap_file::{KeybindUpdateOperation, KeybindUpdateTarget},
 835    };
 836
 837    #[test]
 838    fn can_deserialize_keymap_with_trailing_comma() {
 839        let json = indoc::indoc! {"[
 840              // Standard macOS bindings
 841              {
 842                \"bindings\": {
 843                  \"up\": \"menu::SelectPrevious\",
 844                },
 845              },
 846            ]
 847                  "
 848        };
 849        KeymapFile::parse(json).unwrap();
 850    }
 851
 852    #[test]
 853    fn keymap_update() {
 854        zlog::init_test();
 855        #[track_caller]
 856        fn check_keymap_update(
 857            input: impl ToString,
 858            operation: KeybindUpdateOperation,
 859            expected: impl ToString,
 860        ) {
 861            let result = KeymapFile::update_keybinding(operation, input.to_string(), 4)
 862                .expect("Update succeeded");
 863            pretty_assertions::assert_eq!(expected.to_string(), result);
 864        }
 865
 866        check_keymap_update(
 867            "[]",
 868            KeybindUpdateOperation::Add(KeybindUpdateTarget {
 869                keystrokes: "ctrl-a",
 870                action_name: "zed::SomeAction",
 871                context: None,
 872                use_key_equivalents: false,
 873                input: None,
 874            }),
 875            r#"[
 876                {
 877                    "bindings": {
 878                        "ctrl-a": "zed::SomeAction"
 879                    }
 880                }
 881            ]"#
 882            .unindent(),
 883        );
 884
 885        check_keymap_update(
 886            r#"[
 887                {
 888                    "bindings": {
 889                        "ctrl-a": "zed::SomeAction"
 890                    }
 891                }
 892            ]"#
 893            .unindent(),
 894            KeybindUpdateOperation::Add(KeybindUpdateTarget {
 895                keystrokes: "ctrl-b",
 896                action_name: "zed::SomeOtherAction",
 897                context: None,
 898                use_key_equivalents: false,
 899                input: None,
 900            }),
 901            r#"[
 902                {
 903                    "bindings": {
 904                        "ctrl-a": "zed::SomeAction"
 905                    }
 906                },
 907                {
 908                    "bindings": {
 909                        "ctrl-b": "zed::SomeOtherAction"
 910                    }
 911                }
 912            ]"#
 913            .unindent(),
 914        );
 915
 916        check_keymap_update(
 917            r#"[
 918                {
 919                    "bindings": {
 920                        "ctrl-a": "zed::SomeAction"
 921                    }
 922                }
 923            ]"#
 924            .unindent(),
 925            KeybindUpdateOperation::Add(KeybindUpdateTarget {
 926                keystrokes: "ctrl-b",
 927                action_name: "zed::SomeOtherAction",
 928                context: None,
 929                use_key_equivalents: false,
 930                input: Some(r#"{"foo": "bar"}"#),
 931            }),
 932            r#"[
 933                {
 934                    "bindings": {
 935                        "ctrl-a": "zed::SomeAction"
 936                    }
 937                },
 938                {
 939                    "bindings": {
 940                        "ctrl-b": [
 941                            "zed::SomeOtherAction",
 942                            {
 943                                "foo": "bar"
 944                            }
 945                        ]
 946                    }
 947                }
 948            ]"#
 949            .unindent(),
 950        );
 951
 952        check_keymap_update(
 953            r#"[
 954                {
 955                    "bindings": {
 956                        "ctrl-a": "zed::SomeAction"
 957                    }
 958                }
 959            ]"#
 960            .unindent(),
 961            KeybindUpdateOperation::Add(KeybindUpdateTarget {
 962                keystrokes: "ctrl-b",
 963                action_name: "zed::SomeOtherAction",
 964                context: Some("Zed > Editor && some_condition = true"),
 965                use_key_equivalents: true,
 966                input: Some(r#"{"foo": "bar"}"#),
 967            }),
 968            r#"[
 969                {
 970                    "bindings": {
 971                        "ctrl-a": "zed::SomeAction"
 972                    }
 973                },
 974                {
 975                    "context": "Zed > Editor && some_condition = true",
 976                    "use_key_equivalents": true,
 977                    "bindings": {
 978                        "ctrl-b": [
 979                            "zed::SomeOtherAction",
 980                            {
 981                                "foo": "bar"
 982                            }
 983                        ]
 984                    }
 985                }
 986            ]"#
 987            .unindent(),
 988        );
 989
 990        check_keymap_update(
 991            r#"[
 992                {
 993                    "bindings": {
 994                        "ctrl-a": "zed::SomeAction"
 995                    }
 996                }
 997            ]"#
 998            .unindent(),
 999            KeybindUpdateOperation::Replace {
1000                target: KeybindUpdateTarget {
1001                    keystrokes: "ctrl-a",
1002                    action_name: "zed::SomeAction",
1003                    context: None,
1004                    use_key_equivalents: false,
1005                    input: None,
1006                },
1007                source: KeybindUpdateTarget {
1008                    keystrokes: "ctrl-b",
1009                    action_name: "zed::SomeOtherAction",
1010                    context: None,
1011                    use_key_equivalents: false,
1012                    input: Some(r#"{"foo": "bar"}"#),
1013                },
1014                target_source: KeybindSource::Base,
1015            },
1016            r#"[
1017                {
1018                    "bindings": {
1019                        "ctrl-a": "zed::SomeAction"
1020                    }
1021                },
1022                {
1023                    "bindings": {
1024                        "ctrl-b": [
1025                            "zed::SomeOtherAction",
1026                            {
1027                                "foo": "bar"
1028                            }
1029                        ]
1030                    }
1031                }
1032            ]"#
1033            .unindent(),
1034        );
1035
1036        check_keymap_update(
1037            r#"[
1038                {
1039                    "bindings": {
1040                        "ctrl-a": "zed::SomeAction"
1041                    }
1042                }
1043            ]"#
1044            .unindent(),
1045            KeybindUpdateOperation::Replace {
1046                target: KeybindUpdateTarget {
1047                    keystrokes: "ctrl-a",
1048                    action_name: "zed::SomeAction",
1049                    context: None,
1050                    use_key_equivalents: false,
1051                    input: None,
1052                },
1053                source: KeybindUpdateTarget {
1054                    keystrokes: "ctrl-b",
1055                    action_name: "zed::SomeOtherAction",
1056                    context: None,
1057                    use_key_equivalents: false,
1058                    input: Some(r#"{"foo": "bar"}"#),
1059                },
1060                target_source: KeybindSource::User,
1061            },
1062            r#"[
1063                {
1064                    "bindings": {
1065                        "ctrl-b": [
1066                            "zed::SomeOtherAction",
1067                            {
1068                                "foo": "bar"
1069                            }
1070                        ]
1071                    }
1072                }
1073            ]"#
1074            .unindent(),
1075        );
1076
1077        check_keymap_update(
1078            r#"[
1079                {
1080                    "bindings": {
1081                        "ctrl-a": "zed::SomeAction"
1082                    }
1083                }
1084            ]"#
1085            .unindent(),
1086            KeybindUpdateOperation::Replace {
1087                target: KeybindUpdateTarget {
1088                    keystrokes: "ctrl-a",
1089                    action_name: "zed::SomeNonexistentAction",
1090                    context: None,
1091                    use_key_equivalents: false,
1092                    input: None,
1093                },
1094                source: KeybindUpdateTarget {
1095                    keystrokes: "ctrl-b",
1096                    action_name: "zed::SomeOtherAction",
1097                    context: None,
1098                    use_key_equivalents: false,
1099                    input: None,
1100                },
1101                target_source: KeybindSource::User,
1102            },
1103            r#"[
1104                {
1105                    "bindings": {
1106                        "ctrl-a": "zed::SomeAction"
1107                    }
1108                },
1109                {
1110                    "bindings": {
1111                        "ctrl-b": "zed::SomeOtherAction"
1112                    }
1113                }
1114            ]"#
1115            .unindent(),
1116        );
1117
1118        check_keymap_update(
1119            r#"[
1120                {
1121                    "bindings": {
1122                        // some comment
1123                        "ctrl-a": "zed::SomeAction"
1124                        // some other comment
1125                    }
1126                }
1127            ]"#
1128            .unindent(),
1129            KeybindUpdateOperation::Replace {
1130                target: KeybindUpdateTarget {
1131                    keystrokes: "ctrl-a",
1132                    action_name: "zed::SomeAction",
1133                    context: None,
1134                    use_key_equivalents: false,
1135                    input: None,
1136                },
1137                source: KeybindUpdateTarget {
1138                    keystrokes: "ctrl-b",
1139                    action_name: "zed::SomeOtherAction",
1140                    context: None,
1141                    use_key_equivalents: false,
1142                    input: Some(r#"{"foo": "bar"}"#),
1143                },
1144                target_source: KeybindSource::User,
1145            },
1146            r#"[
1147                {
1148                    "bindings": {
1149                        // some comment
1150                        "ctrl-b": [
1151                            "zed::SomeOtherAction",
1152                            {
1153                                "foo": "bar"
1154                            }
1155                        ]
1156                        // some other comment
1157                    }
1158                }
1159            ]"#
1160            .unindent(),
1161        );
1162    }
1163}