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