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