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, KeybindingKeystroke, Keystroke,
7 NoAction, SharedString,
8};
9use schemars::{JsonSchema, json_schema};
10use serde::Deserialize;
11use serde_json::{Value, json};
12use std::borrow::Cow;
13use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock};
14use util::ResultExt as _;
15use util::{
16 asset_str,
17 markdown::{MarkdownEscaped, MarkdownInlineCode, MarkdownString},
18};
19
20use crate::{
21 SettingsAssets, append_top_level_array_value_in_json_text, parse_json_with_comments,
22 replace_top_level_array_value_in_json_text,
23};
24
25pub trait KeyBindingValidator: Send + Sync {
26 fn action_type_id(&self) -> TypeId;
27 fn validate(&self, binding: &KeyBinding) -> Result<(), MarkdownString>;
28}
29
30pub struct KeyBindingValidatorRegistration(pub fn() -> Box<dyn KeyBindingValidator>);
31
32inventory::collect!(KeyBindingValidatorRegistration);
33
34pub(crate) static KEY_BINDING_VALIDATORS: LazyLock<BTreeMap<TypeId, Box<dyn KeyBindingValidator>>> =
35 LazyLock::new(|| {
36 let mut validators = BTreeMap::new();
37 for validator_registration in inventory::iter::<KeyBindingValidatorRegistration> {
38 let validator = validator_registration.0();
39 validators.insert(validator.action_type_id(), validator);
40 }
41 validators
42 });
43
44// Note that the doc comments on these are shown by json-language-server when editing the keymap, so
45// they should be considered user-facing documentation. Documentation is not handled well with
46// schemars-0.8 - when there are newlines, it is rendered as plaintext (see
47// https://github.com/GREsau/schemars/issues/38#issuecomment-2282883519). So for now these docs
48// avoid newlines.
49//
50// TODO: Update to schemars-1.0 once it's released, and add more docs as newlines would be
51// supported. Tracking issue is https://github.com/GREsau/schemars/issues/112.
52
53/// Keymap configuration consisting of sections. Each section may have a context predicate which
54/// determines whether its bindings are used.
55#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
56#[serde(transparent)]
57pub struct KeymapFile(Vec<KeymapSection>);
58
59/// Keymap section which binds keystrokes to actions.
60#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
61pub struct KeymapSection {
62 /// Determines when these bindings are active. When just a name is provided, like `Editor` or
63 /// `Workspace`, the bindings will be active in that context. Boolean expressions like `X && Y`,
64 /// `X || Y`, `!X` are also supported. Some more complex logic including checking OS and the
65 /// current file extension are also supported - see [the
66 /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details.
67 #[serde(default)]
68 pub context: String,
69 /// This option enables specifying keys based on their position on a QWERTY keyboard, by using
70 /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported
71 /// on macOS. See the documentation for more details.
72 #[serde(default)]
73 use_key_equivalents: bool,
74 /// This keymap section's bindings, as a JSON object mapping keystrokes to actions. The
75 /// keystrokes key is a string representing a sequence of keystrokes to type, where the
76 /// keystrokes are separated by whitespace. Each keystroke is a sequence of modifiers (`ctrl`,
77 /// `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) followed by a key, separated by `-`. The
78 /// order of bindings does matter. When the same keystrokes are bound at the same context depth,
79 /// the binding that occurs later in the file is preferred. For displaying keystrokes in the UI,
80 /// the later binding for the same action is preferred.
81 #[serde(default)]
82 bindings: Option<IndexMap<String, KeymapAction>>,
83 #[serde(flatten)]
84 unrecognized_fields: IndexMap<String, Value>,
85 // This struct intentionally uses permissive types for its fields, rather than validating during
86 // deserialization. The purpose of this is to allow loading the portion of the keymap that doesn't
87 // have errors. The downside of this is that the errors are not reported with line+column info.
88 // Unfortunately the implementations of the `Spanned` types for preserving this information are
89 // highly inconvenient (`serde_spanned`) and in some cases don't work at all here
90 // (`json_spanned_>value`). Serde should really have builtin support for this.
91}
92
93impl KeymapSection {
94 pub fn bindings(&self) -> impl DoubleEndedIterator<Item = (&String, &KeymapAction)> {
95 self.bindings.iter().flatten()
96 }
97}
98
99/// Keymap action as a JSON value, since it can either be null for no action, or the name of the
100/// action, or an array of the name of the action and the action input.
101///
102/// Unlike the other json types involved in keymaps (including actions), this doc-comment will not
103/// be included in the generated JSON schema, as it manually defines its `JsonSchema` impl. The
104/// actual schema used for it is automatically generated in `KeymapFile::generate_json_schema`.
105#[derive(Debug, Deserialize, Default, Clone)]
106#[serde(transparent)]
107pub struct KeymapAction(Value);
108
109impl std::fmt::Display for KeymapAction {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match &self.0 {
112 Value::String(s) => write!(f, "{}", s),
113 Value::Array(arr) => {
114 let strings: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
115 write!(f, "{}", strings.join(", "))
116 }
117 _ => write!(f, "{}", self.0),
118 }
119 }
120}
121
122impl JsonSchema for KeymapAction {
123 /// This is used when generating the JSON schema for the `KeymapAction` type, so that it can
124 /// reference the keymap action schema.
125 fn schema_name() -> Cow<'static, str> {
126 "KeymapAction".into()
127 }
128
129 /// This schema will be replaced with the full action schema in
130 /// `KeymapFile::generate_json_schema`.
131 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
132 json_schema!(true)
133 }
134}
135
136#[derive(Debug)]
137#[must_use]
138pub enum KeymapFileLoadResult {
139 Success {
140 key_bindings: Vec<KeyBinding>,
141 },
142 SomeFailedToLoad {
143 key_bindings: Vec<KeyBinding>,
144 error_message: MarkdownString,
145 },
146 JsonParseFailure {
147 error: anyhow::Error,
148 },
149}
150
151impl KeymapFile {
152 pub fn parse(content: &str) -> anyhow::Result<Self> {
153 parse_json_with_comments::<Self>(content)
154 }
155
156 pub fn load_asset(
157 asset_path: &str,
158 source: Option<KeybindSource>,
159 cx: &App,
160 ) -> anyhow::Result<Vec<KeyBinding>> {
161 match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
162 KeymapFileLoadResult::Success { mut key_bindings } => match source {
163 Some(source) => Ok({
164 for key_binding in &mut key_bindings {
165 key_binding.set_meta(source.meta());
166 }
167 key_bindings
168 }),
169 None => Ok(key_bindings),
170 },
171 KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
172 anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",)
173 }
174 KeymapFileLoadResult::JsonParseFailure { error } => {
175 anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}")
176 }
177 }
178 }
179
180 pub fn load_asset_allow_partial_failure(
181 asset_path: &str,
182 cx: &App,
183 ) -> anyhow::Result<Vec<KeyBinding>> {
184 match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
185 KeymapFileLoadResult::SomeFailedToLoad {
186 key_bindings,
187 error_message,
188 ..
189 } if key_bindings.is_empty() => {
190 anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",)
191 }
192 KeymapFileLoadResult::Success { key_bindings, .. }
193 | KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
194 KeymapFileLoadResult::JsonParseFailure { error } => {
195 anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}")
196 }
197 }
198 }
199
200 #[cfg(feature = "test-support")]
201 pub fn load_panic_on_failure(content: &str, cx: &App) -> Vec<KeyBinding> {
202 match Self::load(content, cx) {
203 KeymapFileLoadResult::Success { key_bindings, .. } => key_bindings,
204 KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
205 panic!("{error_message}");
206 }
207 KeymapFileLoadResult::JsonParseFailure { error } => {
208 panic!("JSON parse error: {error}");
209 }
210 }
211 }
212
213 pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
214 if content.is_empty() {
215 return KeymapFileLoadResult::Success {
216 key_bindings: Vec::new(),
217 };
218 }
219 let keymap_file = match Self::parse(content) {
220 Ok(keymap_file) => keymap_file,
221 Err(error) => {
222 return KeymapFileLoadResult::JsonParseFailure { error };
223 }
224 };
225
226 // Accumulate errors in order to support partial load of user keymap in the presence of
227 // errors in context and binding parsing.
228 let mut errors = Vec::new();
229 let mut key_bindings = Vec::new();
230
231 for KeymapSection {
232 context,
233 use_key_equivalents,
234 bindings,
235 unrecognized_fields,
236 } in keymap_file.0.iter()
237 {
238 let context_predicate: Option<Rc<KeyBindingContextPredicate>> = if context.is_empty() {
239 None
240 } else {
241 match KeyBindingContextPredicate::parse(context) {
242 Ok(context_predicate) => Some(context_predicate.into()),
243 Err(err) => {
244 // Leading space is to separate from the message indicating which section
245 // the error occurred in.
246 errors.push((
247 context,
248 format!(" Parse error in section `context` field: {}", err),
249 ));
250 continue;
251 }
252 }
253 };
254
255 let mut section_errors = String::new();
256
257 if !unrecognized_fields.is_empty() {
258 write!(
259 section_errors,
260 "\n\n - Unrecognized fields: {}",
261 MarkdownInlineCode(&format!("{:?}", unrecognized_fields.keys()))
262 )
263 .unwrap();
264 }
265
266 if let Some(bindings) = bindings {
267 for (keystrokes, action) in bindings {
268 let result = Self::load_keybinding(
269 keystrokes,
270 action,
271 context_predicate.clone(),
272 *use_key_equivalents,
273 cx,
274 );
275 match result {
276 Ok(key_binding) => {
277 key_bindings.push(key_binding);
278 }
279 Err(err) => {
280 let mut lines = err.lines();
281 let mut indented_err = lines.next().unwrap().to_string();
282 for line in lines {
283 indented_err.push_str(" ");
284 indented_err.push_str(line);
285 indented_err.push_str("\n");
286 }
287 write!(
288 section_errors,
289 "\n\n- In binding {}, {indented_err}",
290 MarkdownInlineCode(&format!("\"{}\"", keystrokes))
291 )
292 .unwrap();
293 }
294 }
295 }
296 }
297
298 if !section_errors.is_empty() {
299 errors.push((context, section_errors))
300 }
301 }
302
303 if errors.is_empty() {
304 KeymapFileLoadResult::Success { key_bindings }
305 } else {
306 let mut error_message = "Errors in user keymap file.\n".to_owned();
307 for (context, section_errors) in errors {
308 if context.is_empty() {
309 let _ = write!(error_message, "\n\nIn section without context predicate:");
310 } else {
311 let _ = write!(
312 error_message,
313 "\n\nIn section with {}:",
314 MarkdownInlineCode(&format!("context = \"{}\"", context))
315 );
316 }
317 let _ = write!(error_message, "{section_errors}");
318 }
319 KeymapFileLoadResult::SomeFailedToLoad {
320 key_bindings,
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 use_key_equivalents: bool,
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 MarkdownInlineCode(&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 MarkdownInlineCode(&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 MarkdownInlineCode(&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 MarkdownInlineCode(&format!("\"{}\"", &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 MarkdownInlineCode(&format!("\"{}\"", &name)),
380 MarkdownInlineCode(&action_input_string),
381 MarkdownEscaped(&error.to_string())
382 ));
383 }
384 None => {
385 return Err(format!(
386 "can't build {} action - it requires input data via [name, input]: {}",
387 MarkdownInlineCode(&format!("\"{}\"", &name)),
388 MarkdownEscaped(&error.to_string())
389 ));
390 }
391 },
392 };
393
394 let key_binding = match KeyBinding::load(
395 keystrokes,
396 action,
397 context,
398 use_key_equivalents,
399 action_input_string.map(SharedString::from),
400 cx.keyboard_mapper().as_ref(),
401 ) {
402 Ok(key_binding) => key_binding,
403 Err(InvalidKeystrokeError { keystroke }) => {
404 return Err(format!(
405 "invalid keystroke {}. {}",
406 MarkdownInlineCode(&format!("\"{}\"", &keystroke)),
407 KEYSTROKE_PARSE_EXPECTED_MESSAGE
408 ));
409 }
410 };
411
412 if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) {
413 match validator.validate(&key_binding) {
414 Ok(()) => Ok(key_binding),
415 Err(error) => Err(error.0),
416 }
417 } else {
418 Ok(key_binding)
419 }
420 }
421
422 /// Creates a JSON schema generator, suitable for generating json schemas
423 /// for actions
424 pub fn action_schema_generator() -> schemars::SchemaGenerator {
425 schemars::generate::SchemaSettings::draft2019_09().into_generator()
426 }
427
428 pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
429 // instead of using DefaultDenyUnknownFields, actions typically use
430 // `#[serde(deny_unknown_fields)]` so that these cases are reported as parse failures. This
431 // is because the rest of the keymap will still load in these cases, whereas other settings
432 // files would not.
433 let mut generator = Self::action_schema_generator();
434
435 let action_schemas = cx.action_schemas(&mut generator);
436 let action_documentation = cx.action_documentation();
437 let deprecations = cx.deprecated_actions_to_preferred_actions();
438 let deprecation_messages = cx.action_deprecation_messages();
439 KeymapFile::generate_json_schema(
440 generator,
441 action_schemas,
442 action_documentation,
443 deprecations,
444 deprecation_messages,
445 )
446 }
447
448 fn generate_json_schema(
449 mut generator: schemars::SchemaGenerator,
450 action_schemas: Vec<(&'static str, Option<schemars::Schema>)>,
451 action_documentation: &HashMap<&'static str, &'static str>,
452 deprecations: &HashMap<&'static str, &'static str>,
453 deprecation_messages: &HashMap<&'static str, &'static str>,
454 ) -> serde_json::Value {
455 fn add_deprecation(schema: &mut schemars::Schema, message: String) {
456 schema.insert(
457 // deprecationMessage is not part of the JSON Schema spec, but
458 // json-language-server recognizes it.
459 "deprecationMessage".to_string(),
460 Value::String(message),
461 );
462 }
463
464 fn add_deprecation_preferred_name(schema: &mut schemars::Schema, new_name: &str) {
465 add_deprecation(schema, format!("Deprecated, use {new_name}"));
466 }
467
468 fn add_description(schema: &mut schemars::Schema, description: &str) {
469 schema.insert(
470 "description".to_string(),
471 Value::String(description.to_string()),
472 );
473 }
474
475 let empty_object = json_schema!({
476 "type": "object"
477 });
478
479 // This is a workaround for a json-language-server issue where it matches the first
480 // alternative that matches the value's shape and uses that for documentation.
481 //
482 // In the case of the array validations, it would even provide an error saying that the name
483 // must match the name of the first alternative.
484 let mut empty_action_name = json_schema!({
485 "type": "string",
486 "const": ""
487 });
488 let no_action_message = "No action named this.";
489 add_description(&mut empty_action_name, no_action_message);
490 add_deprecation(&mut empty_action_name, no_action_message.to_string());
491 let empty_action_name_with_input = json_schema!({
492 "type": "array",
493 "items": [
494 empty_action_name,
495 true
496 ],
497 "minItems": 2,
498 "maxItems": 2
499 });
500 let mut keymap_action_alternatives = vec![empty_action_name, empty_action_name_with_input];
501
502 let mut empty_schema_action_names = vec![];
503 for (name, action_schema) in action_schemas.into_iter() {
504 let deprecation = if name == NoAction.name() {
505 Some("null")
506 } else {
507 deprecations.get(name).copied()
508 };
509
510 // Add an alternative for plain action names.
511 let mut plain_action = json_schema!({
512 "type": "string",
513 "const": name
514 });
515 if let Some(message) = deprecation_messages.get(name) {
516 add_deprecation(&mut plain_action, message.to_string());
517 } else if let Some(new_name) = deprecation {
518 add_deprecation_preferred_name(&mut plain_action, new_name);
519 }
520 let description = action_documentation.get(name);
521 if let Some(description) = &description {
522 add_description(&mut plain_action, description);
523 }
524 keymap_action_alternatives.push(plain_action);
525
526 // Add an alternative for actions with data specified as a [name, data] array.
527 //
528 // When a struct with no deserializable fields is added by deriving `Action`, an empty
529 // object schema is produced. The action should be invoked without data in this case.
530 if let Some(schema) = action_schema
531 && schema != empty_object
532 {
533 let mut matches_action_name = json_schema!({
534 "const": name
535 });
536 if let Some(description) = &description {
537 add_description(&mut matches_action_name, description);
538 }
539 if let Some(message) = deprecation_messages.get(name) {
540 add_deprecation(&mut matches_action_name, message.to_string());
541 } else if let Some(new_name) = deprecation {
542 add_deprecation_preferred_name(&mut matches_action_name, new_name);
543 }
544 let action_with_input = json_schema!({
545 "type": "array",
546 "items": [matches_action_name, schema],
547 "minItems": 2,
548 "maxItems": 2
549 });
550 keymap_action_alternatives.push(action_with_input);
551 } else {
552 empty_schema_action_names.push(name);
553 }
554 }
555
556 if !empty_schema_action_names.is_empty() {
557 let action_names = json_schema!({ "enum": empty_schema_action_names });
558 let no_properties_allowed = json_schema!({
559 "type": "object",
560 "additionalProperties": false
561 });
562 let mut actions_with_empty_input = json_schema!({
563 "type": "array",
564 "items": [action_names, no_properties_allowed],
565 "minItems": 2,
566 "maxItems": 2
567 });
568 add_deprecation(
569 &mut actions_with_empty_input,
570 "This action does not take input - just the action name string should be used."
571 .to_string(),
572 );
573 keymap_action_alternatives.push(actions_with_empty_input);
574 }
575
576 // Placing null first causes json-language-server to default assuming actions should be
577 // null, so place it last.
578 keymap_action_alternatives.push(json_schema!({
579 "type": "null"
580 }));
581
582 // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so setting
583 // the definition of `KeymapAction` results in the full action schema being used.
584 generator.definitions_mut().insert(
585 KeymapAction::schema_name().to_string(),
586 json!({
587 "oneOf": keymap_action_alternatives
588 }),
589 );
590
591 generator.root_schema_for::<KeymapFile>().to_value()
592 }
593
594 pub fn sections(&self) -> impl DoubleEndedIterator<Item = &KeymapSection> {
595 self.0.iter()
596 }
597
598 pub async fn load_keymap_file(fs: &Arc<dyn Fs>) -> Result<String> {
599 match fs.load(paths::keymap_file()).await {
600 result @ Ok(_) => result,
601 Err(err) => {
602 if let Some(e) = err.downcast_ref::<std::io::Error>()
603 && e.kind() == std::io::ErrorKind::NotFound
604 {
605 return Ok(crate::initial_keymap_content().to_string());
606 }
607 Err(err)
608 }
609 }
610 }
611
612 pub fn update_keybinding<'a>(
613 mut operation: KeybindUpdateOperation<'a>,
614 mut keymap_contents: String,
615 tab_size: usize,
616 keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
617 ) -> Result<String> {
618 match operation {
619 // if trying to replace a keybinding that is not user-defined, treat it as an add operation
620 KeybindUpdateOperation::Replace {
621 target_keybind_source: target_source,
622 source,
623 target,
624 } if target_source != KeybindSource::User => {
625 operation = KeybindUpdateOperation::Add {
626 source,
627 from: Some(target),
628 };
629 }
630 // if trying to remove a keybinding that is not user-defined, treat it as creating a binding
631 // that binds it to `zed::NoAction`
632 KeybindUpdateOperation::Remove {
633 target,
634 target_keybind_source,
635 } if target_keybind_source != KeybindSource::User => {
636 let mut source = target.clone();
637 source.action_name = gpui::NoAction.name();
638 source.action_arguments.take();
639 operation = KeybindUpdateOperation::Add {
640 source,
641 from: Some(target),
642 };
643 }
644 _ => {}
645 }
646
647 // Sanity check that keymap contents are valid, even though we only use it for Replace.
648 // We don't want to modify the file if it's invalid.
649 let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?;
650
651 if let KeybindUpdateOperation::Remove { target, .. } = operation {
652 let target_action_value = target
653 .action_value()
654 .context("Failed to generate target action JSON value")?;
655 let Some((index, keystrokes_str)) =
656 find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
657 else {
658 anyhow::bail!("Failed to find keybinding to remove");
659 };
660 let is_only_binding = keymap.0[index]
661 .bindings
662 .as_ref()
663 .is_none_or(|bindings| bindings.len() == 1);
664 let key_path: &[&str] = if is_only_binding {
665 &[]
666 } else {
667 &["bindings", keystrokes_str]
668 };
669 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
670 &keymap_contents,
671 key_path,
672 None,
673 None,
674 index,
675 tab_size,
676 );
677 keymap_contents.replace_range(replace_range, &replace_value);
678 return Ok(keymap_contents);
679 }
680
681 if let KeybindUpdateOperation::Replace { source, target, .. } = operation {
682 let target_action_value = target
683 .action_value()
684 .context("Failed to generate target action JSON value")?;
685 let source_action_value = source
686 .action_value()
687 .context("Failed to generate source action JSON value")?;
688
689 if let Some((index, keystrokes_str)) =
690 find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
691 {
692 if target.context == source.context {
693 // if we are only changing the keybinding (common case)
694 // not the context, etc. Then just update the binding in place
695
696 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
697 &keymap_contents,
698 &["bindings", keystrokes_str],
699 Some(&source_action_value),
700 Some(&source.keystrokes_unparsed()),
701 index,
702 tab_size,
703 );
704 keymap_contents.replace_range(replace_range, &replace_value);
705
706 return Ok(keymap_contents);
707 } else if keymap.0[index]
708 .bindings
709 .as_ref()
710 .is_none_or(|bindings| bindings.len() == 1)
711 {
712 // if we are replacing the only binding in the section,
713 // just update the section in place, updating the context
714 // and the binding
715
716 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
717 &keymap_contents,
718 &["bindings", keystrokes_str],
719 Some(&source_action_value),
720 Some(&source.keystrokes_unparsed()),
721 index,
722 tab_size,
723 );
724 keymap_contents.replace_range(replace_range, &replace_value);
725
726 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
727 &keymap_contents,
728 &["context"],
729 source.context.map(Into::into).as_ref(),
730 None,
731 index,
732 tab_size,
733 );
734 keymap_contents.replace_range(replace_range, &replace_value);
735 return Ok(keymap_contents);
736 } else {
737 // if we are replacing one of multiple bindings in a section
738 // with a context change, remove the existing binding from the
739 // section, then treat this operation as an add operation of the
740 // new binding with the updated context.
741
742 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
743 &keymap_contents,
744 &["bindings", keystrokes_str],
745 None,
746 None,
747 index,
748 tab_size,
749 );
750 keymap_contents.replace_range(replace_range, &replace_value);
751 operation = KeybindUpdateOperation::Add {
752 source,
753 from: Some(target),
754 };
755 }
756 } else {
757 log::warn!(
758 "Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead",
759 target.keystrokes,
760 target_action_value,
761 source.keystrokes,
762 source_action_value,
763 );
764 operation = KeybindUpdateOperation::Add {
765 source,
766 from: Some(target),
767 };
768 }
769 }
770
771 if let KeybindUpdateOperation::Add {
772 source: keybinding,
773 from,
774 } = operation
775 {
776 let mut value = serde_json::Map::with_capacity(4);
777 if let Some(context) = keybinding.context {
778 value.insert("context".to_string(), context.into());
779 }
780 let use_key_equivalents = from.and_then(|from| {
781 let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
782 let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?;
783 Some(keymap.0[index].use_key_equivalents)
784 }).unwrap_or(false);
785 if use_key_equivalents {
786 value.insert("use_key_equivalents".to_string(), true.into());
787 }
788
789 value.insert("bindings".to_string(), {
790 let mut bindings = serde_json::Map::new();
791 let action = keybinding.action_value()?;
792 bindings.insert(keybinding.keystrokes_unparsed(), action);
793 bindings.into()
794 });
795
796 let (replace_range, replace_value) = append_top_level_array_value_in_json_text(
797 &keymap_contents,
798 &value.into(),
799 tab_size,
800 );
801 keymap_contents.replace_range(replace_range, &replace_value);
802 }
803 return Ok(keymap_contents);
804
805 fn find_binding<'a, 'b>(
806 keymap: &'b KeymapFile,
807 target: &KeybindUpdateTarget<'a>,
808 target_action_value: &Value,
809 keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
810 ) -> Option<(usize, &'b str)> {
811 let target_context_parsed =
812 KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
813 for (index, section) in keymap.sections().enumerate() {
814 let section_context_parsed =
815 KeyBindingContextPredicate::parse(§ion.context).ok();
816 if section_context_parsed != target_context_parsed {
817 continue;
818 }
819 let Some(bindings) = §ion.bindings else {
820 continue;
821 };
822 for (keystrokes_str, action) in bindings {
823 let Ok(keystrokes) = keystrokes_str
824 .split_whitespace()
825 .map(|source| {
826 let keystroke = Keystroke::parse(source)?;
827 Ok(KeybindingKeystroke::new_with_mapper(
828 keystroke,
829 false,
830 keyboard_mapper,
831 ))
832 })
833 .collect::<Result<Vec<_>, InvalidKeystrokeError>>()
834 else {
835 continue;
836 };
837 if keystrokes.len() != target.keystrokes.len()
838 || !keystrokes
839 .iter()
840 .zip(target.keystrokes)
841 .all(|(a, b)| a.inner().should_match(b))
842 {
843 continue;
844 }
845 if &action.0 != target_action_value {
846 continue;
847 }
848 return Some((index, keystrokes_str));
849 }
850 }
851 None
852 }
853 }
854}
855
856#[derive(Clone, Debug)]
857pub enum KeybindUpdateOperation<'a> {
858 Replace {
859 /// Describes the keybind to create
860 source: KeybindUpdateTarget<'a>,
861 /// Describes the keybind to remove
862 target: KeybindUpdateTarget<'a>,
863 target_keybind_source: KeybindSource,
864 },
865 Add {
866 source: KeybindUpdateTarget<'a>,
867 from: Option<KeybindUpdateTarget<'a>>,
868 },
869 Remove {
870 target: KeybindUpdateTarget<'a>,
871 target_keybind_source: KeybindSource,
872 },
873}
874
875impl KeybindUpdateOperation<'_> {
876 pub fn generate_telemetry(
877 &self,
878 ) -> (
879 // The keybind that is created
880 String,
881 // The keybinding that was removed
882 String,
883 // The source of the keybinding
884 String,
885 ) {
886 let (new_binding, removed_binding, source) = match &self {
887 KeybindUpdateOperation::Replace {
888 source,
889 target,
890 target_keybind_source,
891 } => (Some(source), Some(target), Some(*target_keybind_source)),
892 KeybindUpdateOperation::Add { source, .. } => (Some(source), None, None),
893 KeybindUpdateOperation::Remove {
894 target,
895 target_keybind_source,
896 } => (None, Some(target), Some(*target_keybind_source)),
897 };
898
899 let new_binding = new_binding
900 .map(KeybindUpdateTarget::telemetry_string)
901 .unwrap_or("null".to_owned());
902 let removed_binding = removed_binding
903 .map(KeybindUpdateTarget::telemetry_string)
904 .unwrap_or("null".to_owned());
905
906 let source = source
907 .as_ref()
908 .map(KeybindSource::name)
909 .map(ToOwned::to_owned)
910 .unwrap_or("null".to_owned());
911
912 (new_binding, removed_binding, source)
913 }
914}
915
916impl<'a> KeybindUpdateOperation<'a> {
917 pub fn add(source: KeybindUpdateTarget<'a>) -> Self {
918 Self::Add { source, from: None }
919 }
920}
921
922#[derive(Debug, Clone)]
923pub struct KeybindUpdateTarget<'a> {
924 pub context: Option<&'a str>,
925 pub keystrokes: &'a [KeybindingKeystroke],
926 pub action_name: &'a str,
927 pub action_arguments: Option<&'a str>,
928}
929
930impl<'a> KeybindUpdateTarget<'a> {
931 fn action_value(&self) -> Result<Value> {
932 if self.action_name == gpui::NoAction.name() {
933 return Ok(Value::Null);
934 }
935 let action_name: Value = self.action_name.into();
936 let value = match self.action_arguments {
937 Some(args) if !args.is_empty() => {
938 let args = serde_json::from_str::<Value>(args)
939 .context("Failed to parse action arguments as JSON")?;
940 serde_json::json!([action_name, args])
941 }
942 _ => action_name,
943 };
944 Ok(value)
945 }
946
947 fn keystrokes_unparsed(&self) -> String {
948 let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
949 for keystroke in self.keystrokes {
950 // The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()`
951 // here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$`
952 // by default on Windows.
953 keystrokes.push_str(&keystroke.unparse());
954 keystrokes.push(' ');
955 }
956 keystrokes.pop();
957 keystrokes
958 }
959
960 fn telemetry_string(&self) -> String {
961 format!(
962 "action_name: {}, context: {}, action_arguments: {}, keystrokes: {}",
963 self.action_name,
964 self.context.unwrap_or("global"),
965 self.action_arguments.unwrap_or("none"),
966 self.keystrokes_unparsed()
967 )
968 }
969}
970
971#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
972pub enum KeybindSource {
973 User,
974 Vim,
975 Base,
976 #[default]
977 Default,
978 Unknown,
979}
980
981impl KeybindSource {
982 const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Base as u32);
983 const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Default as u32);
984 const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Vim as u32);
985 const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::User as u32);
986
987 pub fn name(&self) -> &'static str {
988 match self {
989 KeybindSource::User => "User",
990 KeybindSource::Default => "Default",
991 KeybindSource::Base => "Base",
992 KeybindSource::Vim => "Vim",
993 KeybindSource::Unknown => "Unknown",
994 }
995 }
996
997 pub fn meta(&self) -> KeyBindingMetaIndex {
998 match self {
999 KeybindSource::User => Self::USER,
1000 KeybindSource::Default => Self::DEFAULT,
1001 KeybindSource::Base => Self::BASE,
1002 KeybindSource::Vim => Self::VIM,
1003 KeybindSource::Unknown => KeyBindingMetaIndex(*self as u32),
1004 }
1005 }
1006
1007 pub fn from_meta(index: KeyBindingMetaIndex) -> Self {
1008 match index {
1009 Self::USER => KeybindSource::User,
1010 Self::BASE => KeybindSource::Base,
1011 Self::DEFAULT => KeybindSource::Default,
1012 Self::VIM => KeybindSource::Vim,
1013 _ => KeybindSource::Unknown,
1014 }
1015 }
1016}
1017
1018impl From<KeyBindingMetaIndex> for KeybindSource {
1019 fn from(index: KeyBindingMetaIndex) -> Self {
1020 Self::from_meta(index)
1021 }
1022}
1023
1024impl From<KeybindSource> for KeyBindingMetaIndex {
1025 fn from(source: KeybindSource) -> Self {
1026 source.meta()
1027 }
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032 use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke};
1033 use unindent::Unindent;
1034
1035 use crate::{
1036 KeybindSource, KeymapFile,
1037 keymap_file::{KeybindUpdateOperation, KeybindUpdateTarget},
1038 };
1039
1040 #[test]
1041 fn can_deserialize_keymap_with_trailing_comma() {
1042 let json = indoc::indoc! {"[
1043 // Standard macOS bindings
1044 {
1045 \"bindings\": {
1046 \"up\": \"menu::SelectPrevious\",
1047 },
1048 },
1049 ]
1050 "
1051 };
1052 KeymapFile::parse(json).unwrap();
1053 }
1054
1055 #[track_caller]
1056 fn check_keymap_update(
1057 input: impl ToString,
1058 operation: KeybindUpdateOperation,
1059 expected: impl ToString,
1060 ) {
1061 let result = KeymapFile::update_keybinding(
1062 operation,
1063 input.to_string(),
1064 4,
1065 &gpui::DummyKeyboardMapper,
1066 )
1067 .expect("Update succeeded");
1068 pretty_assertions::assert_eq!(expected.to_string(), result);
1069 }
1070
1071 #[track_caller]
1072 fn parse_keystrokes(keystrokes: &str) -> Vec<KeybindingKeystroke> {
1073 keystrokes
1074 .split(' ')
1075 .map(|s| {
1076 KeybindingKeystroke::new_with_mapper(
1077 Keystroke::parse(s).expect("Keystrokes valid"),
1078 false,
1079 &DummyKeyboardMapper,
1080 )
1081 })
1082 .collect()
1083 }
1084
1085 #[test]
1086 fn keymap_update() {
1087 zlog::init_test();
1088
1089 check_keymap_update(
1090 "[]",
1091 KeybindUpdateOperation::add(KeybindUpdateTarget {
1092 keystrokes: &parse_keystrokes("ctrl-a"),
1093 action_name: "zed::SomeAction",
1094 context: None,
1095 action_arguments: None,
1096 }),
1097 r#"[
1098 {
1099 "bindings": {
1100 "ctrl-a": "zed::SomeAction"
1101 }
1102 }
1103 ]"#
1104 .unindent(),
1105 );
1106
1107 check_keymap_update(
1108 "[]",
1109 KeybindUpdateOperation::add(KeybindUpdateTarget {
1110 keystrokes: &parse_keystrokes("\\ a"),
1111 action_name: "zed::SomeAction",
1112 context: None,
1113 action_arguments: None,
1114 }),
1115 r#"[
1116 {
1117 "bindings": {
1118 "\\ a": "zed::SomeAction"
1119 }
1120 }
1121 ]"#
1122 .unindent(),
1123 );
1124
1125 check_keymap_update(
1126 "[]",
1127 KeybindUpdateOperation::add(KeybindUpdateTarget {
1128 keystrokes: &parse_keystrokes("ctrl-a"),
1129 action_name: "zed::SomeAction",
1130 context: None,
1131 action_arguments: Some(""),
1132 }),
1133 r#"[
1134 {
1135 "bindings": {
1136 "ctrl-a": "zed::SomeAction"
1137 }
1138 }
1139 ]"#
1140 .unindent(),
1141 );
1142
1143 check_keymap_update(
1144 r#"[
1145 {
1146 "bindings": {
1147 "ctrl-a": "zed::SomeAction"
1148 }
1149 }
1150 ]"#
1151 .unindent(),
1152 KeybindUpdateOperation::add(KeybindUpdateTarget {
1153 keystrokes: &parse_keystrokes("ctrl-b"),
1154 action_name: "zed::SomeOtherAction",
1155 context: None,
1156 action_arguments: None,
1157 }),
1158 r#"[
1159 {
1160 "bindings": {
1161 "ctrl-a": "zed::SomeAction"
1162 }
1163 },
1164 {
1165 "bindings": {
1166 "ctrl-b": "zed::SomeOtherAction"
1167 }
1168 }
1169 ]"#
1170 .unindent(),
1171 );
1172
1173 check_keymap_update(
1174 r#"[
1175 {
1176 "bindings": {
1177 "ctrl-a": "zed::SomeAction"
1178 }
1179 }
1180 ]"#
1181 .unindent(),
1182 KeybindUpdateOperation::add(KeybindUpdateTarget {
1183 keystrokes: &parse_keystrokes("ctrl-b"),
1184 action_name: "zed::SomeOtherAction",
1185 context: None,
1186 action_arguments: Some(r#"{"foo": "bar"}"#),
1187 }),
1188 r#"[
1189 {
1190 "bindings": {
1191 "ctrl-a": "zed::SomeAction"
1192 }
1193 },
1194 {
1195 "bindings": {
1196 "ctrl-b": [
1197 "zed::SomeOtherAction",
1198 {
1199 "foo": "bar"
1200 }
1201 ]
1202 }
1203 }
1204 ]"#
1205 .unindent(),
1206 );
1207
1208 check_keymap_update(
1209 r#"[
1210 {
1211 "bindings": {
1212 "ctrl-a": "zed::SomeAction"
1213 }
1214 }
1215 ]"#
1216 .unindent(),
1217 KeybindUpdateOperation::add(KeybindUpdateTarget {
1218 keystrokes: &parse_keystrokes("ctrl-b"),
1219 action_name: "zed::SomeOtherAction",
1220 context: Some("Zed > Editor && some_condition = true"),
1221 action_arguments: Some(r#"{"foo": "bar"}"#),
1222 }),
1223 r#"[
1224 {
1225 "bindings": {
1226 "ctrl-a": "zed::SomeAction"
1227 }
1228 },
1229 {
1230 "context": "Zed > Editor && some_condition = true",
1231 "bindings": {
1232 "ctrl-b": [
1233 "zed::SomeOtherAction",
1234 {
1235 "foo": "bar"
1236 }
1237 ]
1238 }
1239 }
1240 ]"#
1241 .unindent(),
1242 );
1243
1244 check_keymap_update(
1245 r#"[
1246 {
1247 "bindings": {
1248 "ctrl-a": "zed::SomeAction"
1249 }
1250 }
1251 ]"#
1252 .unindent(),
1253 KeybindUpdateOperation::Replace {
1254 target: KeybindUpdateTarget {
1255 keystrokes: &parse_keystrokes("ctrl-a"),
1256 action_name: "zed::SomeAction",
1257 context: None,
1258 action_arguments: None,
1259 },
1260 source: KeybindUpdateTarget {
1261 keystrokes: &parse_keystrokes("ctrl-b"),
1262 action_name: "zed::SomeOtherAction",
1263 context: None,
1264 action_arguments: Some(r#"{"foo": "bar"}"#),
1265 },
1266 target_keybind_source: KeybindSource::Base,
1267 },
1268 r#"[
1269 {
1270 "bindings": {
1271 "ctrl-a": "zed::SomeAction"
1272 }
1273 },
1274 {
1275 "bindings": {
1276 "ctrl-b": [
1277 "zed::SomeOtherAction",
1278 {
1279 "foo": "bar"
1280 }
1281 ]
1282 }
1283 }
1284 ]"#
1285 .unindent(),
1286 );
1287
1288 check_keymap_update(
1289 r#"[
1290 {
1291 "bindings": {
1292 "a": "zed::SomeAction"
1293 }
1294 }
1295 ]"#
1296 .unindent(),
1297 KeybindUpdateOperation::Replace {
1298 target: KeybindUpdateTarget {
1299 keystrokes: &parse_keystrokes("a"),
1300 action_name: "zed::SomeAction",
1301 context: None,
1302 action_arguments: None,
1303 },
1304 source: KeybindUpdateTarget {
1305 keystrokes: &parse_keystrokes("ctrl-b"),
1306 action_name: "zed::SomeOtherAction",
1307 context: None,
1308 action_arguments: Some(r#"{"foo": "bar"}"#),
1309 },
1310 target_keybind_source: KeybindSource::User,
1311 },
1312 r#"[
1313 {
1314 "bindings": {
1315 "ctrl-b": [
1316 "zed::SomeOtherAction",
1317 {
1318 "foo": "bar"
1319 }
1320 ]
1321 }
1322 }
1323 ]"#
1324 .unindent(),
1325 );
1326
1327 check_keymap_update(
1328 r#"[
1329 {
1330 "bindings": {
1331 "\\ a": "zed::SomeAction"
1332 }
1333 }
1334 ]"#
1335 .unindent(),
1336 KeybindUpdateOperation::Replace {
1337 target: KeybindUpdateTarget {
1338 keystrokes: &parse_keystrokes("\\ a"),
1339 action_name: "zed::SomeAction",
1340 context: None,
1341 action_arguments: None,
1342 },
1343 source: KeybindUpdateTarget {
1344 keystrokes: &parse_keystrokes("\\ b"),
1345 action_name: "zed::SomeOtherAction",
1346 context: None,
1347 action_arguments: Some(r#"{"foo": "bar"}"#),
1348 },
1349 target_keybind_source: KeybindSource::User,
1350 },
1351 r#"[
1352 {
1353 "bindings": {
1354 "\\ b": [
1355 "zed::SomeOtherAction",
1356 {
1357 "foo": "bar"
1358 }
1359 ]
1360 }
1361 }
1362 ]"#
1363 .unindent(),
1364 );
1365
1366 check_keymap_update(
1367 r#"[
1368 {
1369 "bindings": {
1370 "\\ a": "zed::SomeAction"
1371 }
1372 }
1373 ]"#
1374 .unindent(),
1375 KeybindUpdateOperation::Replace {
1376 target: KeybindUpdateTarget {
1377 keystrokes: &parse_keystrokes("\\ a"),
1378 action_name: "zed::SomeAction",
1379 context: None,
1380 action_arguments: None,
1381 },
1382 source: KeybindUpdateTarget {
1383 keystrokes: &parse_keystrokes("\\ a"),
1384 action_name: "zed::SomeAction",
1385 context: None,
1386 action_arguments: None,
1387 },
1388 target_keybind_source: KeybindSource::User,
1389 },
1390 r#"[
1391 {
1392 "bindings": {
1393 "\\ a": "zed::SomeAction"
1394 }
1395 }
1396 ]"#
1397 .unindent(),
1398 );
1399
1400 check_keymap_update(
1401 r#"[
1402 {
1403 "bindings": {
1404 "ctrl-a": "zed::SomeAction"
1405 }
1406 }
1407 ]"#
1408 .unindent(),
1409 KeybindUpdateOperation::Replace {
1410 target: KeybindUpdateTarget {
1411 keystrokes: &parse_keystrokes("ctrl-a"),
1412 action_name: "zed::SomeNonexistentAction",
1413 context: None,
1414 action_arguments: None,
1415 },
1416 source: KeybindUpdateTarget {
1417 keystrokes: &parse_keystrokes("ctrl-b"),
1418 action_name: "zed::SomeOtherAction",
1419 context: None,
1420 action_arguments: None,
1421 },
1422 target_keybind_source: KeybindSource::User,
1423 },
1424 r#"[
1425 {
1426 "bindings": {
1427 "ctrl-a": "zed::SomeAction"
1428 }
1429 },
1430 {
1431 "bindings": {
1432 "ctrl-b": "zed::SomeOtherAction"
1433 }
1434 }
1435 ]"#
1436 .unindent(),
1437 );
1438
1439 check_keymap_update(
1440 r#"[
1441 {
1442 "bindings": {
1443 // some comment
1444 "ctrl-a": "zed::SomeAction"
1445 // some other comment
1446 }
1447 }
1448 ]"#
1449 .unindent(),
1450 KeybindUpdateOperation::Replace {
1451 target: KeybindUpdateTarget {
1452 keystrokes: &parse_keystrokes("ctrl-a"),
1453 action_name: "zed::SomeAction",
1454 context: None,
1455 action_arguments: None,
1456 },
1457 source: KeybindUpdateTarget {
1458 keystrokes: &parse_keystrokes("ctrl-b"),
1459 action_name: "zed::SomeOtherAction",
1460 context: None,
1461 action_arguments: Some(r#"{"foo": "bar"}"#),
1462 },
1463 target_keybind_source: KeybindSource::User,
1464 },
1465 r#"[
1466 {
1467 "bindings": {
1468 // some comment
1469 "ctrl-b": [
1470 "zed::SomeOtherAction",
1471 {
1472 "foo": "bar"
1473 }
1474 ]
1475 // some other comment
1476 }
1477 }
1478 ]"#
1479 .unindent(),
1480 );
1481
1482 check_keymap_update(
1483 r#"[
1484 {
1485 "context": "SomeContext",
1486 "bindings": {
1487 "a": "foo::bar",
1488 "b": "baz::qux",
1489 }
1490 }
1491 ]"#
1492 .unindent(),
1493 KeybindUpdateOperation::Replace {
1494 target: KeybindUpdateTarget {
1495 keystrokes: &parse_keystrokes("a"),
1496 action_name: "foo::bar",
1497 context: Some("SomeContext"),
1498 action_arguments: None,
1499 },
1500 source: KeybindUpdateTarget {
1501 keystrokes: &parse_keystrokes("c"),
1502 action_name: "foo::baz",
1503 context: Some("SomeOtherContext"),
1504 action_arguments: None,
1505 },
1506 target_keybind_source: KeybindSource::User,
1507 },
1508 r#"[
1509 {
1510 "context": "SomeContext",
1511 "bindings": {
1512 "b": "baz::qux",
1513 }
1514 },
1515 {
1516 "context": "SomeOtherContext",
1517 "bindings": {
1518 "c": "foo::baz"
1519 }
1520 }
1521 ]"#
1522 .unindent(),
1523 );
1524
1525 check_keymap_update(
1526 r#"[
1527 {
1528 "context": "SomeContext",
1529 "bindings": {
1530 "a": "foo::bar",
1531 }
1532 }
1533 ]"#
1534 .unindent(),
1535 KeybindUpdateOperation::Replace {
1536 target: KeybindUpdateTarget {
1537 keystrokes: &parse_keystrokes("a"),
1538 action_name: "foo::bar",
1539 context: Some("SomeContext"),
1540 action_arguments: None,
1541 },
1542 source: KeybindUpdateTarget {
1543 keystrokes: &parse_keystrokes("c"),
1544 action_name: "foo::baz",
1545 context: Some("SomeOtherContext"),
1546 action_arguments: None,
1547 },
1548 target_keybind_source: KeybindSource::User,
1549 },
1550 r#"[
1551 {
1552 "context": "SomeOtherContext",
1553 "bindings": {
1554 "c": "foo::baz",
1555 }
1556 }
1557 ]"#
1558 .unindent(),
1559 );
1560
1561 check_keymap_update(
1562 r#"[
1563 {
1564 "context": "SomeContext",
1565 "bindings": {
1566 "a": "foo::bar",
1567 "c": "foo::baz",
1568 }
1569 },
1570 ]"#
1571 .unindent(),
1572 KeybindUpdateOperation::Remove {
1573 target: KeybindUpdateTarget {
1574 context: Some("SomeContext"),
1575 keystrokes: &parse_keystrokes("a"),
1576 action_name: "foo::bar",
1577 action_arguments: None,
1578 },
1579 target_keybind_source: KeybindSource::User,
1580 },
1581 r#"[
1582 {
1583 "context": "SomeContext",
1584 "bindings": {
1585 "c": "foo::baz",
1586 }
1587 },
1588 ]"#
1589 .unindent(),
1590 );
1591
1592 check_keymap_update(
1593 r#"[
1594 {
1595 "context": "SomeContext",
1596 "bindings": {
1597 "\\ a": "foo::bar",
1598 "c": "foo::baz",
1599 }
1600 },
1601 ]"#
1602 .unindent(),
1603 KeybindUpdateOperation::Remove {
1604 target: KeybindUpdateTarget {
1605 context: Some("SomeContext"),
1606 keystrokes: &parse_keystrokes("\\ a"),
1607 action_name: "foo::bar",
1608 action_arguments: None,
1609 },
1610 target_keybind_source: KeybindSource::User,
1611 },
1612 r#"[
1613 {
1614 "context": "SomeContext",
1615 "bindings": {
1616 "c": "foo::baz",
1617 }
1618 },
1619 ]"#
1620 .unindent(),
1621 );
1622
1623 check_keymap_update(
1624 r#"[
1625 {
1626 "context": "SomeContext",
1627 "bindings": {
1628 "a": ["foo::bar", true],
1629 "c": "foo::baz",
1630 }
1631 },
1632 ]"#
1633 .unindent(),
1634 KeybindUpdateOperation::Remove {
1635 target: KeybindUpdateTarget {
1636 context: Some("SomeContext"),
1637 keystrokes: &parse_keystrokes("a"),
1638 action_name: "foo::bar",
1639 action_arguments: Some("true"),
1640 },
1641 target_keybind_source: KeybindSource::User,
1642 },
1643 r#"[
1644 {
1645 "context": "SomeContext",
1646 "bindings": {
1647 "c": "foo::baz",
1648 }
1649 },
1650 ]"#
1651 .unindent(),
1652 );
1653
1654 check_keymap_update(
1655 r#"[
1656 {
1657 "context": "SomeContext",
1658 "bindings": {
1659 "b": "foo::baz",
1660 }
1661 },
1662 {
1663 "context": "SomeContext",
1664 "bindings": {
1665 "a": ["foo::bar", true],
1666 }
1667 },
1668 {
1669 "context": "SomeContext",
1670 "bindings": {
1671 "c": "foo::baz",
1672 }
1673 },
1674 ]"#
1675 .unindent(),
1676 KeybindUpdateOperation::Remove {
1677 target: KeybindUpdateTarget {
1678 context: Some("SomeContext"),
1679 keystrokes: &parse_keystrokes("a"),
1680 action_name: "foo::bar",
1681 action_arguments: Some("true"),
1682 },
1683 target_keybind_source: KeybindSource::User,
1684 },
1685 r#"[
1686 {
1687 "context": "SomeContext",
1688 "bindings": {
1689 "b": "foo::baz",
1690 }
1691 },
1692 {
1693 "context": "SomeContext",
1694 "bindings": {
1695 "c": "foo::baz",
1696 }
1697 },
1698 ]"#
1699 .unindent(),
1700 );
1701 check_keymap_update(
1702 r#"[
1703 {
1704 "context": "SomeOtherContext",
1705 "use_key_equivalents": true,
1706 "bindings": {
1707 "b": "foo::bar",
1708 }
1709 },
1710 ]"#
1711 .unindent(),
1712 KeybindUpdateOperation::Add {
1713 source: KeybindUpdateTarget {
1714 context: Some("SomeContext"),
1715 keystrokes: &parse_keystrokes("a"),
1716 action_name: "foo::baz",
1717 action_arguments: Some("true"),
1718 },
1719 from: Some(KeybindUpdateTarget {
1720 context: Some("SomeOtherContext"),
1721 keystrokes: &parse_keystrokes("b"),
1722 action_name: "foo::bar",
1723 action_arguments: None,
1724 }),
1725 },
1726 r#"[
1727 {
1728 "context": "SomeOtherContext",
1729 "use_key_equivalents": true,
1730 "bindings": {
1731 "b": "foo::bar",
1732 }
1733 },
1734 {
1735 "context": "SomeContext",
1736 "use_key_equivalents": true,
1737 "bindings": {
1738 "a": [
1739 "foo::baz",
1740 true
1741 ]
1742 }
1743 }
1744 ]"#
1745 .unindent(),
1746 );
1747
1748 check_keymap_update(
1749 r#"[
1750 {
1751 "context": "SomeOtherContext",
1752 "use_key_equivalents": true,
1753 "bindings": {
1754 "b": "foo::bar",
1755 }
1756 },
1757 ]"#
1758 .unindent(),
1759 KeybindUpdateOperation::Remove {
1760 target: KeybindUpdateTarget {
1761 context: Some("SomeContext"),
1762 keystrokes: &parse_keystrokes("a"),
1763 action_name: "foo::baz",
1764 action_arguments: Some("true"),
1765 },
1766 target_keybind_source: KeybindSource::Default,
1767 },
1768 r#"[
1769 {
1770 "context": "SomeOtherContext",
1771 "use_key_equivalents": true,
1772 "bindings": {
1773 "b": "foo::bar",
1774 }
1775 },
1776 {
1777 "context": "SomeContext",
1778 "bindings": {
1779 "a": null
1780 }
1781 }
1782 ]"#
1783 .unindent(),
1784 );
1785 }
1786
1787 #[test]
1788 fn test_keymap_remove() {
1789 zlog::init_test();
1790
1791 check_keymap_update(
1792 r#"
1793 [
1794 {
1795 "context": "Editor",
1796 "bindings": {
1797 "cmd-k cmd-u": "editor::ConvertToUpperCase",
1798 "cmd-k cmd-l": "editor::ConvertToLowerCase",
1799 "cmd-[": "pane::GoBack",
1800 }
1801 },
1802 ]
1803 "#,
1804 KeybindUpdateOperation::Remove {
1805 target: KeybindUpdateTarget {
1806 context: Some("Editor"),
1807 keystrokes: &parse_keystrokes("cmd-k cmd-l"),
1808 action_name: "editor::ConvertToLowerCase",
1809 action_arguments: None,
1810 },
1811 target_keybind_source: KeybindSource::User,
1812 },
1813 r#"
1814 [
1815 {
1816 "context": "Editor",
1817 "bindings": {
1818 "cmd-k cmd-u": "editor::ConvertToUpperCase",
1819 "cmd-[": "pane::GoBack",
1820 }
1821 },
1822 ]
1823 "#,
1824 );
1825 }
1826}