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, Unbind, generate_list_of_all_registered_actions, register_action,
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 schemars::AllowTrailingCommas,
19};
20
21use crate::SettingsAssets;
22use settings_json::{
23 append_top_level_array_value_in_json_text, parse_json_with_comments,
24 replace_top_level_array_value_in_json_text,
25};
26
27pub trait KeyBindingValidator: Send + Sync {
28 fn action_type_id(&self) -> TypeId;
29 fn validate(&self, binding: &KeyBinding) -> Result<(), MarkdownString>;
30}
31
32pub struct KeyBindingValidatorRegistration(pub fn() -> Box<dyn KeyBindingValidator>);
33
34inventory::collect!(KeyBindingValidatorRegistration);
35
36pub(crate) static KEY_BINDING_VALIDATORS: LazyLock<BTreeMap<TypeId, Box<dyn KeyBindingValidator>>> =
37 LazyLock::new(|| {
38 let mut validators = BTreeMap::new();
39 for validator_registration in inventory::iter::<KeyBindingValidatorRegistration> {
40 let validator = validator_registration.0();
41 validators.insert(validator.action_type_id(), validator);
42 }
43 validators
44 });
45
46// Note that the doc comments on these are shown by json-language-server when editing the keymap, so
47// they should be considered user-facing documentation. Documentation is not handled well with
48// schemars-0.8 - when there are newlines, it is rendered as plaintext (see
49// https://github.com/GREsau/schemars/issues/38#issuecomment-2282883519). So for now these docs
50// avoid newlines.
51//
52// TODO: Update to schemars-1.0 once it's released, and add more docs as newlines would be
53// supported. Tracking issue is https://github.com/GREsau/schemars/issues/112.
54
55/// Keymap configuration consisting of sections. Each section may have a context predicate which
56/// determines whether its bindings are used.
57#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
58#[serde(transparent)]
59pub struct KeymapFile(Vec<KeymapSection>);
60
61/// Keymap section which binds keystrokes to actions.
62#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
63pub struct KeymapSection {
64 /// Determines when these bindings are active. When just a name is provided, like `Editor` or
65 /// `Workspace`, the bindings will be active in that context. Boolean expressions like `X && Y`,
66 /// `X || Y`, `!X` are also supported. Some more complex logic including checking OS and the
67 /// current file extension are also supported - see [the
68 /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details.
69 #[serde(default)]
70 pub context: String,
71 /// This option enables specifying keys based on their position on a QWERTY keyboard, by using
72 /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported
73 /// on macOS. See the documentation for more details.
74 #[serde(default)]
75 use_key_equivalents: bool,
76 /// This keymap section's unbindings, as a JSON object mapping keystrokes to actions. These are
77 /// parsed before `bindings`, so bindings later in the same section can still take precedence.
78 #[serde(default)]
79 unbind: Option<IndexMap<String, UnbindTargetAction>>,
80 /// This keymap section's bindings, as a JSON object mapping keystrokes to actions. The
81 /// keystrokes key is a string representing a sequence of keystrokes to type, where the
82 /// keystrokes are separated by whitespace. Each keystroke is a sequence of modifiers (`ctrl`,
83 /// `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) followed by a key, separated by `-`. The
84 /// order of bindings does matter. When the same keystrokes are bound at the same context depth,
85 /// the binding that occurs later in the file is preferred. For displaying keystrokes in the UI,
86 /// the later binding for the same action is preferred.
87 #[serde(default)]
88 bindings: Option<IndexMap<String, KeymapAction>>,
89 #[serde(flatten)]
90 unrecognized_fields: IndexMap<String, Value>,
91 // This struct intentionally uses permissive types for its fields, rather than validating during
92 // deserialization. The purpose of this is to allow loading the portion of the keymap that doesn't
93 // have errors. The downside of this is that the errors are not reported with line+column info.
94 // Unfortunately the implementations of the `Spanned` types for preserving this information are
95 // highly inconvenient (`serde_spanned`) and in some cases don't work at all here
96 // (`json_spanned_>value`). Serde should really have builtin support for this.
97}
98
99impl KeymapSection {
100 pub fn bindings(&self) -> impl DoubleEndedIterator<Item = (&String, &KeymapAction)> {
101 self.bindings.iter().flatten()
102 }
103}
104
105/// Keymap action as a JSON value, since it can either be null for no action, or the name of the
106/// action, or an array of the name of the action and the action input.
107///
108/// Unlike the other json types involved in keymaps (including actions), this doc-comment will not
109/// be included in the generated JSON schema, as it manually defines its `JsonSchema` impl. The
110/// actual schema used for it is automatically generated in `KeymapFile::generate_json_schema`.
111#[derive(Debug, Deserialize, Default, Clone)]
112#[serde(transparent)]
113pub struct KeymapAction(Value);
114
115impl std::fmt::Display for KeymapAction {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 match &self.0 {
118 Value::String(s) => write!(f, "{}", s),
119 Value::Array(arr) => {
120 let strings: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
121 write!(f, "{}", strings.join(", "))
122 }
123 _ => write!(f, "{}", self.0),
124 }
125 }
126}
127
128impl JsonSchema for KeymapAction {
129 /// This is used when generating the JSON schema for the `KeymapAction` type, so that it can
130 /// reference the keymap action schema.
131 fn schema_name() -> Cow<'static, str> {
132 "KeymapAction".into()
133 }
134
135 /// This schema will be replaced with the full action schema in
136 /// `KeymapFile::generate_json_schema`.
137 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
138 json_schema!(true)
139 }
140}
141
142#[derive(Debug, Deserialize, Default, Clone)]
143#[serde(transparent)]
144pub struct UnbindTargetAction(Value);
145
146impl JsonSchema for UnbindTargetAction {
147 fn schema_name() -> Cow<'static, str> {
148 "UnbindTargetAction".into()
149 }
150
151 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
152 json_schema!(true)
153 }
154}
155
156#[derive(Debug)]
157#[must_use]
158pub enum KeymapFileLoadResult {
159 Success {
160 key_bindings: Vec<KeyBinding>,
161 },
162 SomeFailedToLoad {
163 key_bindings: Vec<KeyBinding>,
164 error_message: MarkdownString,
165 },
166 JsonParseFailure {
167 error: anyhow::Error,
168 },
169}
170
171impl KeymapFile {
172 pub fn parse(content: &str) -> anyhow::Result<Self> {
173 if content.trim().is_empty() {
174 return Ok(Self(Vec::new()));
175 }
176 parse_json_with_comments::<Self>(content)
177 }
178
179 pub fn load_asset(
180 asset_path: &str,
181 source: Option<KeybindSource>,
182 cx: &App,
183 ) -> anyhow::Result<Vec<KeyBinding>> {
184 match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
185 KeymapFileLoadResult::Success { mut key_bindings } => match source {
186 Some(source) => Ok({
187 for key_binding in &mut key_bindings {
188 key_binding.set_meta(source.meta());
189 }
190 key_bindings
191 }),
192 None => Ok(key_bindings),
193 },
194 KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
195 anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",)
196 }
197 KeymapFileLoadResult::JsonParseFailure { error } => {
198 anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}")
199 }
200 }
201 }
202
203 pub fn load_asset_allow_partial_failure(
204 asset_path: &str,
205 cx: &App,
206 ) -> anyhow::Result<Vec<KeyBinding>> {
207 match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
208 KeymapFileLoadResult::SomeFailedToLoad {
209 key_bindings,
210 error_message,
211 ..
212 } if key_bindings.is_empty() => {
213 anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",)
214 }
215 KeymapFileLoadResult::Success { key_bindings, .. }
216 | KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
217 KeymapFileLoadResult::JsonParseFailure { error } => {
218 anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}")
219 }
220 }
221 }
222
223 #[cfg(feature = "test-support")]
224 pub fn load_panic_on_failure(content: &str, cx: &App) -> Vec<KeyBinding> {
225 match Self::load(content, cx) {
226 KeymapFileLoadResult::Success { key_bindings, .. } => key_bindings,
227 KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
228 panic!("{error_message}");
229 }
230 KeymapFileLoadResult::JsonParseFailure { error } => {
231 panic!("JSON parse error: {error}");
232 }
233 }
234 }
235
236 pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
237 let keymap_file = match Self::parse(content) {
238 Ok(keymap_file) => keymap_file,
239 Err(error) => {
240 return KeymapFileLoadResult::JsonParseFailure { error };
241 }
242 };
243
244 // Accumulate errors in order to support partial load of user keymap in the presence of
245 // errors in context and binding parsing.
246 let mut errors = Vec::new();
247 let mut key_bindings = Vec::new();
248
249 for KeymapSection {
250 context,
251 use_key_equivalents,
252 unbind,
253 bindings,
254 unrecognized_fields,
255 } in keymap_file.0.iter()
256 {
257 let context_predicate: Option<Rc<KeyBindingContextPredicate>> = if context.is_empty() {
258 None
259 } else {
260 match KeyBindingContextPredicate::parse(context) {
261 Ok(context_predicate) => Some(context_predicate.into()),
262 Err(err) => {
263 // Leading space is to separate from the message indicating which section
264 // the error occurred in.
265 errors.push((
266 context.clone(),
267 format!(" Parse error in section `context` field: {}", err),
268 ));
269 continue;
270 }
271 }
272 };
273
274 let mut section_errors = String::new();
275
276 if !unrecognized_fields.is_empty() {
277 write!(
278 section_errors,
279 "\n\n - Unrecognized fields: {}",
280 MarkdownInlineCode(&format!("{:?}", unrecognized_fields.keys()))
281 )
282 .unwrap();
283 }
284
285 if let Some(unbind) = unbind {
286 for (keystrokes, action) in unbind {
287 let result = Self::load_unbinding(
288 keystrokes,
289 action,
290 context_predicate.clone(),
291 *use_key_equivalents,
292 cx,
293 );
294 match result {
295 Ok(key_binding) => {
296 key_bindings.push(key_binding);
297 }
298 Err(err) => {
299 let mut lines = err.lines();
300 let mut indented_err = lines.next().unwrap().to_string();
301 for line in lines {
302 indented_err.push_str(" ");
303 indented_err.push_str(line);
304 indented_err.push_str("\n");
305 }
306 write!(
307 section_errors,
308 "\n\n- In unbind {}, {indented_err}",
309 MarkdownInlineCode(&format!("\"{}\"", keystrokes))
310 )
311 .unwrap();
312 }
313 }
314 }
315 }
316
317 if let Some(bindings) = bindings {
318 for (keystrokes, action) in bindings {
319 let result = Self::load_keybinding(
320 keystrokes,
321 action,
322 context_predicate.clone(),
323 *use_key_equivalents,
324 cx,
325 );
326 match result {
327 Ok(key_binding) => {
328 key_bindings.push(key_binding);
329 }
330 Err(err) => {
331 let mut lines = err.lines();
332 let mut indented_err = lines.next().unwrap().to_string();
333 for line in lines {
334 indented_err.push_str(" ");
335 indented_err.push_str(line);
336 indented_err.push_str("\n");
337 }
338 write!(
339 section_errors,
340 "\n\n- In binding {}, {indented_err}",
341 MarkdownInlineCode(&format!("\"{}\"", keystrokes))
342 )
343 .unwrap();
344 }
345 }
346 }
347 }
348
349 if !section_errors.is_empty() {
350 errors.push((context.clone(), section_errors))
351 }
352 }
353
354 if errors.is_empty() {
355 KeymapFileLoadResult::Success { key_bindings }
356 } else {
357 let mut error_message = "Errors in user keymap file.".to_owned();
358
359 for (context, section_errors) in errors {
360 if context.is_empty() {
361 let _ = write!(error_message, "\nIn section without context predicate:");
362 } else {
363 let _ = write!(
364 error_message,
365 "\nIn section with {}:",
366 MarkdownInlineCode(&format!("context = \"{}\"", context))
367 );
368 }
369 let _ = write!(error_message, "{section_errors}");
370 }
371
372 KeymapFileLoadResult::SomeFailedToLoad {
373 key_bindings,
374 error_message: MarkdownString(error_message),
375 }
376 }
377 }
378
379 fn load_keybinding(
380 keystrokes: &str,
381 action: &KeymapAction,
382 context: Option<Rc<KeyBindingContextPredicate>>,
383 use_key_equivalents: bool,
384 cx: &App,
385 ) -> std::result::Result<KeyBinding, String> {
386 Self::load_keybinding_action_value(keystrokes, &action.0, context, use_key_equivalents, cx)
387 }
388
389 fn load_keybinding_action_value(
390 keystrokes: &str,
391 action: &Value,
392 context: Option<Rc<KeyBindingContextPredicate>>,
393 use_key_equivalents: bool,
394 cx: &App,
395 ) -> std::result::Result<KeyBinding, String> {
396 let (action, action_input_string) = Self::build_keymap_action_value(action, cx)?;
397
398 let key_binding = match KeyBinding::load(
399 keystrokes,
400 action,
401 context,
402 use_key_equivalents,
403 action_input_string.map(SharedString::from),
404 cx.keyboard_mapper().as_ref(),
405 ) {
406 Ok(key_binding) => key_binding,
407 Err(InvalidKeystrokeError { keystroke }) => {
408 return Err(format!(
409 "invalid keystroke {}. {}",
410 MarkdownInlineCode(&format!("\"{}\"", &keystroke)),
411 KEYSTROKE_PARSE_EXPECTED_MESSAGE
412 ));
413 }
414 };
415
416 if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) {
417 match validator.validate(&key_binding) {
418 Ok(()) => Ok(key_binding),
419 Err(error) => Err(error.0),
420 }
421 } else {
422 Ok(key_binding)
423 }
424 }
425
426 fn load_unbinding(
427 keystrokes: &str,
428 action: &UnbindTargetAction,
429 context: Option<Rc<KeyBindingContextPredicate>>,
430 use_key_equivalents: bool,
431 cx: &App,
432 ) -> std::result::Result<KeyBinding, String> {
433 let key_binding = Self::load_keybinding_action_value(
434 keystrokes,
435 &action.0,
436 context,
437 use_key_equivalents,
438 cx,
439 )?;
440
441 if key_binding.action().partial_eq(&NoAction) {
442 return Err("expected action name string or [name, input] array.".to_string());
443 }
444
445 if key_binding.action().name() == Unbind::name_for_type() {
446 return Err(format!(
447 "can't use {} as an unbind target.",
448 MarkdownInlineCode(&format!("\"{}\"", Unbind::name_for_type()))
449 ));
450 }
451
452 KeyBinding::load(
453 keystrokes,
454 Box::new(Unbind(key_binding.action().name().into())),
455 key_binding.predicate(),
456 use_key_equivalents,
457 key_binding.action_input(),
458 cx.keyboard_mapper().as_ref(),
459 )
460 .map_err(|InvalidKeystrokeError { keystroke }| {
461 format!(
462 "invalid keystroke {}. {}",
463 MarkdownInlineCode(&format!("\"{}\"", &keystroke)),
464 KEYSTROKE_PARSE_EXPECTED_MESSAGE
465 )
466 })
467 }
468
469 pub fn parse_action(
470 action: &KeymapAction,
471 ) -> Result<Option<(&String, Option<&Value>)>, String> {
472 Self::parse_action_value(&action.0)
473 }
474
475 fn parse_action_value(action: &Value) -> Result<Option<(&String, Option<&Value>)>, String> {
476 let name_and_input = match action {
477 Value::Array(items) => {
478 if items.len() != 2 {
479 return Err(format!(
480 "expected two-element array of `[name, input]`. \
481 Instead found {}.",
482 MarkdownInlineCode(&action.to_string())
483 ));
484 }
485 let serde_json::Value::String(ref name) = items[0] else {
486 return Err(format!(
487 "expected two-element array of `[name, input]`, \
488 but the first element is not a string in {}.",
489 MarkdownInlineCode(&action.to_string())
490 ));
491 };
492 Some((name, Some(&items[1])))
493 }
494 Value::String(name) => Some((name, None)),
495 Value::Null => None,
496 _ => {
497 return Err(format!(
498 "expected two-element array of `[name, input]`. \
499 Instead found {}.",
500 MarkdownInlineCode(&action.to_string())
501 ));
502 }
503 };
504 Ok(name_and_input)
505 }
506
507 fn build_keymap_action(
508 action: &KeymapAction,
509 cx: &App,
510 ) -> std::result::Result<(Box<dyn Action>, Option<String>), String> {
511 Self::build_keymap_action_value(&action.0, cx)
512 }
513
514 fn build_keymap_action_value(
515 action: &Value,
516 cx: &App,
517 ) -> std::result::Result<(Box<dyn Action>, Option<String>), String> {
518 let (build_result, action_input_string) = match Self::parse_action_value(action)? {
519 Some((name, action_input)) if name.as_str() == ActionSequence::name_for_type() => {
520 match action_input {
521 Some(action_input) => (
522 ActionSequence::build_sequence(action_input.clone(), cx),
523 None,
524 ),
525 None => (Err(ActionSequence::expected_array_error()), None),
526 }
527 }
528 Some((name, Some(action_input))) => {
529 let action_input_string = action_input.to_string();
530 (
531 cx.build_action(name, Some(action_input.clone())),
532 Some(action_input_string),
533 )
534 }
535 Some((name, None)) => (cx.build_action(name, None), None),
536 None => (Ok(NoAction.boxed_clone()), None),
537 };
538
539 let action = match build_result {
540 Ok(action) => action,
541 Err(ActionBuildError::NotFound { name }) => {
542 return Err(format!(
543 "didn't find an action named {}.",
544 MarkdownInlineCode(&format!("\"{}\"", &name))
545 ));
546 }
547 Err(ActionBuildError::BuildError { name, error }) => match action_input_string {
548 Some(action_input_string) => {
549 return Err(format!(
550 "can't build {} action from input value {}: {}",
551 MarkdownInlineCode(&format!("\"{}\"", &name)),
552 MarkdownInlineCode(&action_input_string),
553 MarkdownEscaped(&error.to_string())
554 ));
555 }
556 None => {
557 return Err(format!(
558 "can't build {} action - it requires input data via [name, input]: {}",
559 MarkdownInlineCode(&format!("\"{}\"", &name)),
560 MarkdownEscaped(&error.to_string())
561 ));
562 }
563 },
564 };
565
566 Ok((action, action_input_string))
567 }
568
569 /// Creates a JSON schema generator, suitable for generating json schemas
570 /// for actions
571 pub fn action_schema_generator() -> schemars::SchemaGenerator {
572 schemars::generate::SchemaSettings::draft2019_09()
573 .with_transform(AllowTrailingCommas)
574 .into_generator()
575 }
576
577 pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
578 // instead of using DefaultDenyUnknownFields, actions typically use
579 // `#[serde(deny_unknown_fields)]` so that these cases are reported as parse failures. This
580 // is because the rest of the keymap will still load in these cases, whereas other settings
581 // files would not.
582 let mut generator = Self::action_schema_generator();
583
584 let action_schemas = cx.action_schemas(&mut generator);
585 let action_documentation = cx.action_documentation();
586 let deprecations = cx.deprecated_actions_to_preferred_actions();
587 let deprecation_messages = cx.action_deprecation_messages();
588 KeymapFile::generate_json_schema(
589 generator,
590 action_schemas,
591 action_documentation,
592 deprecations,
593 deprecation_messages,
594 )
595 }
596
597 pub fn generate_json_schema_from_inventory() -> Value {
598 let mut generator = Self::action_schema_generator();
599
600 let mut action_schemas = Vec::new();
601 let mut documentation = HashMap::default();
602 let mut deprecations = HashMap::default();
603 let mut deprecation_messages = HashMap::default();
604
605 for action_data in generate_list_of_all_registered_actions() {
606 let schema = (action_data.json_schema)(&mut generator);
607 action_schemas.push((action_data.name, schema));
608
609 if let Some(doc) = action_data.documentation {
610 documentation.insert(action_data.name, doc);
611 }
612 if let Some(msg) = action_data.deprecation_message {
613 deprecation_messages.insert(action_data.name, msg);
614 }
615 for &alias in action_data.deprecated_aliases {
616 deprecations.insert(alias, action_data.name);
617
618 let alias_schema = (action_data.json_schema)(&mut generator);
619 action_schemas.push((alias, alias_schema));
620 }
621 }
622
623 KeymapFile::generate_json_schema(
624 generator,
625 action_schemas,
626 &documentation,
627 &deprecations,
628 &deprecation_messages,
629 )
630 }
631
632 pub fn get_action_schema_by_name(
633 action_name: &str,
634 generator: &mut schemars::SchemaGenerator,
635 ) -> Option<schemars::Schema> {
636 for action_data in generate_list_of_all_registered_actions() {
637 if action_data.name == action_name {
638 return (action_data.json_schema)(generator);
639 }
640 for &alias in action_data.deprecated_aliases {
641 if alias == action_name {
642 return (action_data.json_schema)(generator);
643 }
644 }
645 }
646 None
647 }
648
649 pub fn generate_json_schema<'a>(
650 mut generator: schemars::SchemaGenerator,
651 action_schemas: Vec<(&'a str, Option<schemars::Schema>)>,
652 action_documentation: &HashMap<&'a str, &'a str>,
653 deprecations: &HashMap<&'a str, &'a str>,
654 deprecation_messages: &HashMap<&'a str, &'a str>,
655 ) -> serde_json::Value {
656 fn add_deprecation(schema: &mut schemars::Schema, message: String) {
657 schema.insert(
658 // deprecationMessage is not part of the JSON Schema spec, but
659 // json-language-server recognizes it.
660 "deprecationMessage".to_string(),
661 Value::String(message),
662 );
663 }
664
665 fn add_deprecation_preferred_name(schema: &mut schemars::Schema, new_name: &str) {
666 add_deprecation(schema, format!("Deprecated, use {new_name}"));
667 }
668
669 fn add_description(schema: &mut schemars::Schema, description: &str) {
670 schema.insert(
671 "description".to_string(),
672 Value::String(description.to_string()),
673 );
674 }
675
676 let empty_object = json_schema!({
677 "type": "object"
678 });
679
680 // This is a workaround for a json-language-server issue where it matches the first
681 // alternative that matches the value's shape and uses that for documentation.
682 //
683 // In the case of the array validations, it would even provide an error saying that the name
684 // must match the name of the first alternative.
685 let mut empty_action_name = json_schema!({
686 "type": "string",
687 "const": ""
688 });
689 let no_action_message = "No action named this.";
690 add_description(&mut empty_action_name, no_action_message);
691 add_deprecation(&mut empty_action_name, no_action_message.to_string());
692 let empty_action_name_with_input = json_schema!({
693 "type": "array",
694 "items": [
695 empty_action_name,
696 true
697 ],
698 "minItems": 2,
699 "maxItems": 2
700 });
701 let mut keymap_action_alternatives = vec![
702 empty_action_name.clone(),
703 empty_action_name_with_input.clone(),
704 ];
705 let mut unbind_target_action_alternatives =
706 vec![empty_action_name, empty_action_name_with_input];
707
708 let mut empty_schema_action_names = vec![];
709 let mut empty_schema_unbind_target_action_names = vec![];
710 for (name, action_schema) in action_schemas.into_iter() {
711 let deprecation = if name == NoAction.name() {
712 Some("null")
713 } else {
714 deprecations.get(name).copied()
715 };
716
717 let include_in_unbind_target_schema =
718 name != NoAction.name() && name != Unbind::name_for_type();
719
720 // Add an alternative for plain action names.
721 let mut plain_action = json_schema!({
722 "type": "string",
723 "const": name
724 });
725 if let Some(message) = deprecation_messages.get(name) {
726 add_deprecation(&mut plain_action, message.to_string());
727 } else if let Some(new_name) = deprecation {
728 add_deprecation_preferred_name(&mut plain_action, new_name);
729 }
730 let description = action_documentation.get(name);
731 if let Some(description) = &description {
732 add_description(&mut plain_action, description);
733 }
734 keymap_action_alternatives.push(plain_action.clone());
735 if include_in_unbind_target_schema {
736 unbind_target_action_alternatives.push(plain_action);
737 }
738
739 // Add an alternative for actions with data specified as a [name, data] array.
740 //
741 // When a struct with no deserializable fields is added by deriving `Action`, an empty
742 // object schema is produced. The action should be invoked without data in this case.
743 if let Some(schema) = action_schema
744 && schema != empty_object
745 {
746 let mut matches_action_name = json_schema!({
747 "const": name
748 });
749 if let Some(description) = &description {
750 add_description(&mut matches_action_name, description);
751 }
752 if let Some(message) = deprecation_messages.get(name) {
753 add_deprecation(&mut matches_action_name, message.to_string());
754 } else if let Some(new_name) = deprecation {
755 add_deprecation_preferred_name(&mut matches_action_name, new_name);
756 }
757 let action_with_input = json_schema!({
758 "type": "array",
759 "items": [matches_action_name, schema],
760 "minItems": 2,
761 "maxItems": 2
762 });
763 keymap_action_alternatives.push(action_with_input.clone());
764 if include_in_unbind_target_schema {
765 unbind_target_action_alternatives.push(action_with_input);
766 }
767 } else {
768 empty_schema_action_names.push(name);
769 if include_in_unbind_target_schema {
770 empty_schema_unbind_target_action_names.push(name);
771 }
772 }
773 }
774
775 if !empty_schema_action_names.is_empty() {
776 let action_names = json_schema!({ "enum": empty_schema_action_names });
777 let no_properties_allowed = json_schema!({
778 "type": "object",
779 "additionalProperties": false
780 });
781 let mut actions_with_empty_input = json_schema!({
782 "type": "array",
783 "items": [action_names, no_properties_allowed],
784 "minItems": 2,
785 "maxItems": 2
786 });
787 add_deprecation(
788 &mut actions_with_empty_input,
789 "This action does not take input - just the action name string should be used."
790 .to_string(),
791 );
792 keymap_action_alternatives.push(actions_with_empty_input);
793 }
794
795 if !empty_schema_unbind_target_action_names.is_empty() {
796 let action_names = json_schema!({ "enum": empty_schema_unbind_target_action_names });
797 let no_properties_allowed = json_schema!({
798 "type": "object",
799 "additionalProperties": false
800 });
801 let mut actions_with_empty_input = json_schema!({
802 "type": "array",
803 "items": [action_names, no_properties_allowed],
804 "minItems": 2,
805 "maxItems": 2
806 });
807 add_deprecation(
808 &mut actions_with_empty_input,
809 "This action does not take input - just the action name string should be used."
810 .to_string(),
811 );
812 unbind_target_action_alternatives.push(actions_with_empty_input);
813 }
814
815 // Placing null first causes json-language-server to default assuming actions should be
816 // null, so place it last.
817 keymap_action_alternatives.push(json_schema!({
818 "type": "null"
819 }));
820
821 generator.definitions_mut().insert(
822 KeymapAction::schema_name().to_string(),
823 json!({
824 "anyOf": keymap_action_alternatives
825 }),
826 );
827 generator.definitions_mut().insert(
828 UnbindTargetAction::schema_name().to_string(),
829 json!({
830 "anyOf": unbind_target_action_alternatives
831 }),
832 );
833
834 generator.root_schema_for::<KeymapFile>().to_value()
835 }
836
837 pub fn sections(&self) -> impl DoubleEndedIterator<Item = &KeymapSection> {
838 self.0.iter()
839 }
840
841 pub async fn load_keymap_file(fs: &Arc<dyn Fs>) -> Result<String> {
842 match fs.load(paths::keymap_file()).await {
843 result @ Ok(_) => result,
844 Err(err) => {
845 if let Some(e) = err.downcast_ref::<std::io::Error>()
846 && e.kind() == std::io::ErrorKind::NotFound
847 {
848 return Ok(crate::initial_keymap_content().to_string());
849 }
850 Err(err)
851 }
852 }
853 }
854
855 pub fn update_keybinding<'a>(
856 mut operation: KeybindUpdateOperation<'a>,
857 mut keymap_contents: String,
858 tab_size: usize,
859 keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
860 ) -> Result<String> {
861 // When replacing a non-user binding's keystroke, we need to also suppress the old
862 // default so it doesn't continue showing under the old keystroke.
863 let mut old_keystroke_suppression: Option<(Option<String>, String)> = None;
864
865 match operation {
866 // if trying to replace a keybinding that is not user-defined, treat it as an add operation
867 KeybindUpdateOperation::Replace {
868 target_keybind_source: target_source,
869 source,
870 target,
871 } if target_source != KeybindSource::User => {
872 if target.keystrokes_unparsed() != source.keystrokes_unparsed() {
873 old_keystroke_suppression = Some((
874 target.context.map(String::from),
875 target.keystrokes_unparsed(),
876 ));
877 }
878 operation = KeybindUpdateOperation::Add {
879 source,
880 from: Some(target),
881 };
882 }
883 // if trying to remove a keybinding that is not user-defined, treat it as creating a binding
884 // that binds it to `zed::NoAction`
885 KeybindUpdateOperation::Remove {
886 target,
887 target_keybind_source,
888 } if target_keybind_source != KeybindSource::User => {
889 let mut source = target.clone();
890 source.action_name = gpui::NoAction.name();
891 source.action_arguments.take();
892 operation = KeybindUpdateOperation::Add {
893 source,
894 from: Some(target),
895 };
896 }
897 _ => {}
898 }
899
900 // Sanity check that keymap contents are valid, even though we only use it for Replace.
901 // We don't want to modify the file if it's invalid.
902 let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?;
903
904 if let KeybindUpdateOperation::Remove { target, .. } = operation {
905 let target_action_value = target
906 .action_value()
907 .context("Failed to generate target action JSON value")?;
908 let Some((index, keystrokes_str)) =
909 find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
910 else {
911 anyhow::bail!("Failed to find keybinding to remove");
912 };
913 let is_only_binding = keymap.0[index]
914 .bindings
915 .as_ref()
916 .is_none_or(|bindings| bindings.len() == 1);
917 let key_path: &[&str] = if is_only_binding {
918 &[]
919 } else {
920 &["bindings", keystrokes_str]
921 };
922 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
923 &keymap_contents,
924 key_path,
925 None,
926 None,
927 index,
928 tab_size,
929 );
930 keymap_contents.replace_range(replace_range, &replace_value);
931 return Ok(keymap_contents);
932 }
933
934 if let KeybindUpdateOperation::Replace { source, target, .. } = operation {
935 let target_action_value = target
936 .action_value()
937 .context("Failed to generate target action JSON value")?;
938 let source_action_value = source
939 .action_value()
940 .context("Failed to generate source action JSON value")?;
941
942 if let Some((index, keystrokes_str)) =
943 find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
944 {
945 if target.context == source.context {
946 // if we are only changing the keybinding (common case)
947 // not the context, etc. Then just update the binding in place
948
949 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
950 &keymap_contents,
951 &["bindings", keystrokes_str],
952 Some(&source_action_value),
953 Some(&source.keystrokes_unparsed()),
954 index,
955 tab_size,
956 );
957 keymap_contents.replace_range(replace_range, &replace_value);
958
959 return Ok(keymap_contents);
960 } else if keymap.0[index]
961 .bindings
962 .as_ref()
963 .is_none_or(|bindings| bindings.len() == 1)
964 {
965 // if we are replacing the only binding in the section,
966 // just update the section in place, updating the context
967 // and the binding
968
969 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
970 &keymap_contents,
971 &["bindings", keystrokes_str],
972 Some(&source_action_value),
973 Some(&source.keystrokes_unparsed()),
974 index,
975 tab_size,
976 );
977 keymap_contents.replace_range(replace_range, &replace_value);
978
979 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
980 &keymap_contents,
981 &["context"],
982 source.context.map(Into::into).as_ref(),
983 None,
984 index,
985 tab_size,
986 );
987 keymap_contents.replace_range(replace_range, &replace_value);
988 return Ok(keymap_contents);
989 } else {
990 // if we are replacing one of multiple bindings in a section
991 // with a context change, remove the existing binding from the
992 // section, then treat this operation as an add operation of the
993 // new binding with the updated context.
994
995 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
996 &keymap_contents,
997 &["bindings", keystrokes_str],
998 None,
999 None,
1000 index,
1001 tab_size,
1002 );
1003 keymap_contents.replace_range(replace_range, &replace_value);
1004 operation = KeybindUpdateOperation::Add {
1005 source,
1006 from: Some(target),
1007 };
1008 }
1009 } else {
1010 log::warn!(
1011 "Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead",
1012 target.keystrokes,
1013 target_action_value,
1014 source.keystrokes,
1015 source_action_value,
1016 );
1017 operation = KeybindUpdateOperation::Add {
1018 source,
1019 from: Some(target),
1020 };
1021 }
1022 }
1023
1024 if let KeybindUpdateOperation::Add {
1025 source: keybinding,
1026 from,
1027 } = operation
1028 {
1029 let mut value = serde_json::Map::with_capacity(4);
1030 if let Some(context) = keybinding.context {
1031 value.insert("context".to_string(), context.into());
1032 }
1033 let use_key_equivalents = from.and_then(|from| {
1034 let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
1035 let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?;
1036 Some(keymap.0[index].use_key_equivalents)
1037 }).unwrap_or(false);
1038 if use_key_equivalents {
1039 value.insert("use_key_equivalents".to_string(), true.into());
1040 }
1041
1042 value.insert("bindings".to_string(), {
1043 let mut bindings = serde_json::Map::new();
1044 let action = keybinding.action_value()?;
1045 bindings.insert(keybinding.keystrokes_unparsed(), action);
1046 bindings.into()
1047 });
1048
1049 let (replace_range, replace_value) = append_top_level_array_value_in_json_text(
1050 &keymap_contents,
1051 &value.into(),
1052 tab_size,
1053 );
1054 keymap_contents.replace_range(replace_range, &replace_value);
1055 }
1056
1057 // If we converted a Replace to Add because the target was a non-user binding,
1058 // and the keystroke changed, suppress the old default keystroke with a NoAction
1059 // binding so it doesn't continue appearing under the old keystroke.
1060 if let Some((context, old_keystrokes)) = old_keystroke_suppression {
1061 let mut value = serde_json::Map::with_capacity(2);
1062 if let Some(context) = context {
1063 value.insert("context".to_string(), context.into());
1064 }
1065 value.insert("bindings".to_string(), {
1066 let mut bindings = serde_json::Map::new();
1067 bindings.insert(old_keystrokes, Value::Null);
1068 bindings.into()
1069 });
1070 let (replace_range, replace_value) = append_top_level_array_value_in_json_text(
1071 &keymap_contents,
1072 &value.into(),
1073 tab_size,
1074 );
1075 keymap_contents.replace_range(replace_range, &replace_value);
1076 }
1077
1078 return Ok(keymap_contents);
1079
1080 fn find_binding<'a, 'b>(
1081 keymap: &'b KeymapFile,
1082 target: &KeybindUpdateTarget<'a>,
1083 target_action_value: &Value,
1084 keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
1085 ) -> Option<(usize, &'b str)> {
1086 let target_context_parsed =
1087 KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
1088 for (index, section) in keymap.sections().enumerate() {
1089 let section_context_parsed =
1090 KeyBindingContextPredicate::parse(§ion.context).ok();
1091 if section_context_parsed != target_context_parsed {
1092 continue;
1093 }
1094 let Some(bindings) = §ion.bindings else {
1095 continue;
1096 };
1097 for (keystrokes_str, action) in bindings {
1098 let Ok(keystrokes) = keystrokes_str
1099 .split_whitespace()
1100 .map(|source| {
1101 let keystroke = Keystroke::parse(source)?;
1102 Ok(KeybindingKeystroke::new_with_mapper(
1103 keystroke,
1104 false,
1105 keyboard_mapper,
1106 ))
1107 })
1108 .collect::<Result<Vec<_>, InvalidKeystrokeError>>()
1109 else {
1110 continue;
1111 };
1112 if keystrokes.len() != target.keystrokes.len()
1113 || !keystrokes
1114 .iter()
1115 .zip(target.keystrokes)
1116 .all(|(a, b)| a.inner().should_match(b))
1117 {
1118 continue;
1119 }
1120 if &action.0 != target_action_value {
1121 continue;
1122 }
1123 return Some((index, keystrokes_str));
1124 }
1125 }
1126 None
1127 }
1128 }
1129}
1130
1131#[derive(Clone, Debug)]
1132pub enum KeybindUpdateOperation<'a> {
1133 Replace {
1134 /// Describes the keybind to create
1135 source: KeybindUpdateTarget<'a>,
1136 /// Describes the keybind to remove
1137 target: KeybindUpdateTarget<'a>,
1138 target_keybind_source: KeybindSource,
1139 },
1140 Add {
1141 source: KeybindUpdateTarget<'a>,
1142 from: Option<KeybindUpdateTarget<'a>>,
1143 },
1144 Remove {
1145 target: KeybindUpdateTarget<'a>,
1146 target_keybind_source: KeybindSource,
1147 },
1148}
1149
1150impl KeybindUpdateOperation<'_> {
1151 pub fn generate_telemetry(
1152 &self,
1153 ) -> (
1154 // The keybind that is created
1155 String,
1156 // The keybinding that was removed
1157 String,
1158 // The source of the keybinding
1159 String,
1160 ) {
1161 let (new_binding, removed_binding, source) = match &self {
1162 KeybindUpdateOperation::Replace {
1163 source,
1164 target,
1165 target_keybind_source,
1166 } => (Some(source), Some(target), Some(*target_keybind_source)),
1167 KeybindUpdateOperation::Add { source, .. } => (Some(source), None, None),
1168 KeybindUpdateOperation::Remove {
1169 target,
1170 target_keybind_source,
1171 } => (None, Some(target), Some(*target_keybind_source)),
1172 };
1173
1174 let new_binding = new_binding
1175 .map(KeybindUpdateTarget::telemetry_string)
1176 .unwrap_or("null".to_owned());
1177 let removed_binding = removed_binding
1178 .map(KeybindUpdateTarget::telemetry_string)
1179 .unwrap_or("null".to_owned());
1180
1181 let source = source
1182 .as_ref()
1183 .map(KeybindSource::name)
1184 .map(ToOwned::to_owned)
1185 .unwrap_or("null".to_owned());
1186
1187 (new_binding, removed_binding, source)
1188 }
1189}
1190
1191impl<'a> KeybindUpdateOperation<'a> {
1192 pub fn add(source: KeybindUpdateTarget<'a>) -> Self {
1193 Self::Add { source, from: None }
1194 }
1195}
1196
1197#[derive(Debug, Clone)]
1198pub struct KeybindUpdateTarget<'a> {
1199 pub context: Option<&'a str>,
1200 pub keystrokes: &'a [KeybindingKeystroke],
1201 pub action_name: &'a str,
1202 pub action_arguments: Option<&'a str>,
1203}
1204
1205impl<'a> KeybindUpdateTarget<'a> {
1206 fn action_value(&self) -> Result<Value> {
1207 if self.action_name == gpui::NoAction.name() {
1208 return Ok(Value::Null);
1209 }
1210 let action_name: Value = self.action_name.into();
1211 let value = match self.action_arguments {
1212 Some(args) if !args.is_empty() => {
1213 let args = serde_json::from_str::<Value>(args)
1214 .context("Failed to parse action arguments as JSON")?;
1215 serde_json::json!([action_name, args])
1216 }
1217 _ => action_name,
1218 };
1219 Ok(value)
1220 }
1221
1222 fn keystrokes_unparsed(&self) -> String {
1223 let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
1224 for keystroke in self.keystrokes {
1225 // The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()`
1226 // here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$`
1227 // by default on Windows.
1228 keystrokes.push_str(&keystroke.unparse());
1229 keystrokes.push(' ');
1230 }
1231 keystrokes.pop();
1232 keystrokes
1233 }
1234
1235 fn telemetry_string(&self) -> String {
1236 format!(
1237 "action_name: {}, context: {}, action_arguments: {}, keystrokes: {}",
1238 self.action_name,
1239 self.context.unwrap_or("global"),
1240 self.action_arguments.unwrap_or("none"),
1241 self.keystrokes_unparsed()
1242 )
1243 }
1244}
1245
1246#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
1247pub enum KeybindSource {
1248 User,
1249 Vim,
1250 Base,
1251 #[default]
1252 Default,
1253 Unknown,
1254}
1255
1256impl KeybindSource {
1257 const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Base as u32);
1258 const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Default as u32);
1259 const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Vim as u32);
1260 const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::User as u32);
1261
1262 pub fn name(&self) -> &'static str {
1263 match self {
1264 KeybindSource::User => "User",
1265 KeybindSource::Default => "Default",
1266 KeybindSource::Base => "Base",
1267 KeybindSource::Vim => "Vim",
1268 KeybindSource::Unknown => "Unknown",
1269 }
1270 }
1271
1272 pub fn meta(&self) -> KeyBindingMetaIndex {
1273 match self {
1274 KeybindSource::User => Self::USER,
1275 KeybindSource::Default => Self::DEFAULT,
1276 KeybindSource::Base => Self::BASE,
1277 KeybindSource::Vim => Self::VIM,
1278 KeybindSource::Unknown => KeyBindingMetaIndex(*self as u32),
1279 }
1280 }
1281
1282 pub fn from_meta(index: KeyBindingMetaIndex) -> Self {
1283 match index {
1284 Self::USER => KeybindSource::User,
1285 Self::BASE => KeybindSource::Base,
1286 Self::DEFAULT => KeybindSource::Default,
1287 Self::VIM => KeybindSource::Vim,
1288 _ => KeybindSource::Unknown,
1289 }
1290 }
1291}
1292
1293impl From<KeyBindingMetaIndex> for KeybindSource {
1294 fn from(index: KeyBindingMetaIndex) -> Self {
1295 Self::from_meta(index)
1296 }
1297}
1298
1299impl From<KeybindSource> for KeyBindingMetaIndex {
1300 fn from(source: KeybindSource) -> Self {
1301 source.meta()
1302 }
1303}
1304
1305/// Runs a sequence of actions. Does not wait for asynchronous actions to complete before running
1306/// the next action. Currently only works in workspace windows.
1307///
1308/// This action is special-cased in keymap parsing to allow it to access `App` while parsing, so
1309/// that it can parse its input actions.
1310pub struct ActionSequence(pub Vec<Box<dyn Action>>);
1311
1312register_action!(ActionSequence);
1313
1314impl ActionSequence {
1315 fn build_sequence(
1316 value: Value,
1317 cx: &App,
1318 ) -> std::result::Result<Box<dyn Action>, ActionBuildError> {
1319 match value {
1320 Value::Array(values) => {
1321 let actions = values
1322 .into_iter()
1323 .enumerate()
1324 .map(|(index, action)| {
1325 match KeymapFile::build_keymap_action(&KeymapAction(action), cx) {
1326 Ok((action, _)) => Ok(action),
1327 Err(err) => {
1328 return Err(ActionBuildError::BuildError {
1329 name: Self::name_for_type().to_string(),
1330 error: anyhow::anyhow!(
1331 "error at sequence index {index}: {err}"
1332 ),
1333 });
1334 }
1335 }
1336 })
1337 .collect::<Result<Vec<_>, _>>()?;
1338 Ok(Box::new(Self(actions)))
1339 }
1340 _ => Err(Self::expected_array_error()),
1341 }
1342 }
1343
1344 fn expected_array_error() -> ActionBuildError {
1345 ActionBuildError::BuildError {
1346 name: Self::name_for_type().to_string(),
1347 error: anyhow::anyhow!("expected array of actions"),
1348 }
1349 }
1350}
1351
1352impl Action for ActionSequence {
1353 fn name(&self) -> &'static str {
1354 Self::name_for_type()
1355 }
1356
1357 fn name_for_type() -> &'static str
1358 where
1359 Self: Sized,
1360 {
1361 "action::Sequence"
1362 }
1363
1364 fn partial_eq(&self, action: &dyn Action) -> bool {
1365 action
1366 .as_any()
1367 .downcast_ref::<Self>()
1368 .map_or(false, |other| {
1369 self.0.len() == other.0.len()
1370 && self
1371 .0
1372 .iter()
1373 .zip(other.0.iter())
1374 .all(|(a, b)| a.partial_eq(b.as_ref()))
1375 })
1376 }
1377
1378 fn boxed_clone(&self) -> Box<dyn Action> {
1379 Box::new(ActionSequence(
1380 self.0
1381 .iter()
1382 .map(|action| action.boxed_clone())
1383 .collect::<Vec<_>>(),
1384 ))
1385 }
1386
1387 fn build(_value: Value) -> Result<Box<dyn Action>> {
1388 Err(anyhow::anyhow!(
1389 "{} cannot be built directly",
1390 Self::name_for_type()
1391 ))
1392 }
1393
1394 fn action_json_schema(generator: &mut schemars::SchemaGenerator) -> Option<schemars::Schema> {
1395 let keymap_action_schema = generator.subschema_for::<KeymapAction>();
1396 Some(json_schema!({
1397 "type": "array",
1398 "items": keymap_action_schema
1399 }))
1400 }
1401
1402 fn deprecated_aliases() -> &'static [&'static str] {
1403 &[]
1404 }
1405
1406 fn deprecation_message() -> Option<&'static str> {
1407 None
1408 }
1409
1410 fn documentation() -> Option<&'static str> {
1411 Some(
1412 "Runs a sequence of actions.\n\n\
1413 NOTE: This does **not** wait for asynchronous actions to complete before running the next action.",
1414 )
1415 }
1416}
1417
1418#[cfg(test)]
1419mod tests {
1420 use gpui::{Action, App, DummyKeyboardMapper, KeybindingKeystroke, Keystroke, Unbind};
1421 use serde_json::Value;
1422 use unindent::Unindent;
1423
1424 use crate::{
1425 KeybindSource, KeymapFile,
1426 keymap_file::{KeybindUpdateOperation, KeybindUpdateTarget},
1427 };
1428
1429 gpui::actions!(test_keymap_file, [StringAction, InputAction]);
1430
1431 #[test]
1432 fn can_deserialize_keymap_with_trailing_comma() {
1433 let json = indoc::indoc! {"[
1434 // Standard macOS bindings
1435 {
1436 \"bindings\": {
1437 \"up\": \"menu::SelectPrevious\",
1438 },
1439 },
1440 ]
1441 "
1442 };
1443 KeymapFile::parse(json).unwrap();
1444 }
1445
1446 #[gpui::test]
1447 fn keymap_section_unbinds_are_loaded_before_bindings(cx: &mut App) {
1448 let key_bindings = match KeymapFile::load(
1449 indoc::indoc! {r#"
1450 [
1451 {
1452 "unbind": {
1453 "ctrl-a": "test_keymap_file::StringAction",
1454 "ctrl-b": ["test_keymap_file::InputAction", {}]
1455 },
1456 "bindings": {
1457 "ctrl-c": "test_keymap_file::StringAction"
1458 }
1459 }
1460 ]
1461 "#},
1462 cx,
1463 ) {
1464 crate::keymap_file::KeymapFileLoadResult::Success { key_bindings } => key_bindings,
1465 crate::keymap_file::KeymapFileLoadResult::SomeFailedToLoad {
1466 error_message, ..
1467 } => {
1468 panic!("{error_message}");
1469 }
1470 crate::keymap_file::KeymapFileLoadResult::JsonParseFailure { error } => {
1471 panic!("JSON parse error: {error}");
1472 }
1473 };
1474
1475 assert_eq!(key_bindings.len(), 3);
1476 assert!(
1477 key_bindings[0]
1478 .action()
1479 .partial_eq(&Unbind("test_keymap_file::StringAction".into()))
1480 );
1481 assert_eq!(key_bindings[0].action_input(), None);
1482 assert!(
1483 key_bindings[1]
1484 .action()
1485 .partial_eq(&Unbind("test_keymap_file::InputAction".into()))
1486 );
1487 assert_eq!(
1488 key_bindings[1]
1489 .action_input()
1490 .as_ref()
1491 .map(ToString::to_string),
1492 Some("{}".to_string())
1493 );
1494 assert_eq!(
1495 key_bindings[2].action().name(),
1496 "test_keymap_file::StringAction"
1497 );
1498 }
1499
1500 #[gpui::test]
1501 fn keymap_unbind_loads_valid_target_action_with_input(cx: &mut App) {
1502 let key_bindings = match KeymapFile::load(
1503 indoc::indoc! {r#"
1504 [
1505 {
1506 "unbind": {
1507 "ctrl-a": ["test_keymap_file::InputAction", {}]
1508 }
1509 }
1510 ]
1511 "#},
1512 cx,
1513 ) {
1514 crate::keymap_file::KeymapFileLoadResult::Success { key_bindings } => key_bindings,
1515 other => panic!("expected Success, got {other:?}"),
1516 };
1517
1518 assert_eq!(key_bindings.len(), 1);
1519 assert!(
1520 key_bindings[0]
1521 .action()
1522 .partial_eq(&Unbind("test_keymap_file::InputAction".into()))
1523 );
1524 assert_eq!(
1525 key_bindings[0]
1526 .action_input()
1527 .as_ref()
1528 .map(ToString::to_string),
1529 Some("{}".to_string())
1530 );
1531 }
1532
1533 #[gpui::test]
1534 fn keymap_unbind_rejects_null(cx: &mut App) {
1535 match KeymapFile::load(
1536 indoc::indoc! {r#"
1537 [
1538 {
1539 "unbind": {
1540 "ctrl-a": null
1541 }
1542 }
1543 ]
1544 "#},
1545 cx,
1546 ) {
1547 crate::keymap_file::KeymapFileLoadResult::SomeFailedToLoad {
1548 key_bindings,
1549 error_message,
1550 } => {
1551 assert!(key_bindings.is_empty());
1552 assert!(
1553 error_message
1554 .0
1555 .contains("expected action name string or [name, input] array.")
1556 );
1557 }
1558 other => panic!("expected SomeFailedToLoad, got {other:?}"),
1559 }
1560 }
1561
1562 #[gpui::test]
1563 fn keymap_unbind_rejects_unbind_action(cx: &mut App) {
1564 match KeymapFile::load(
1565 indoc::indoc! {r#"
1566 [
1567 {
1568 "unbind": {
1569 "ctrl-a": ["zed::Unbind", "test_keymap_file::StringAction"]
1570 }
1571 }
1572 ]
1573 "#},
1574 cx,
1575 ) {
1576 crate::keymap_file::KeymapFileLoadResult::SomeFailedToLoad {
1577 key_bindings,
1578 error_message,
1579 } => {
1580 assert!(key_bindings.is_empty());
1581 assert!(
1582 error_message
1583 .0
1584 .contains("can't use `\"zed::Unbind\"` as an unbind target.")
1585 );
1586 }
1587 other => panic!("expected SomeFailedToLoad, got {other:?}"),
1588 }
1589 }
1590
1591 #[test]
1592 fn keymap_schema_for_unbind_excludes_null_and_unbind_action() {
1593 fn schema_allows(schema: &Value, expected: &Value) -> bool {
1594 match schema {
1595 Value::Object(object) => {
1596 if object.get("const") == Some(expected) {
1597 return true;
1598 }
1599 if object.get("type") == Some(&Value::String("null".to_string()))
1600 && expected == &Value::Null
1601 {
1602 return true;
1603 }
1604 object.values().any(|value| schema_allows(value, expected))
1605 }
1606 Value::Array(items) => items.iter().any(|value| schema_allows(value, expected)),
1607 _ => false,
1608 }
1609 }
1610
1611 let schema = KeymapFile::generate_json_schema_from_inventory();
1612 let unbind_schema = schema
1613 .pointer("/$defs/UnbindTargetAction")
1614 .expect("missing UnbindTargetAction schema");
1615
1616 assert!(!schema_allows(unbind_schema, &Value::Null));
1617 assert!(!schema_allows(
1618 unbind_schema,
1619 &Value::String(Unbind::name_for_type().to_string())
1620 ));
1621 assert!(schema_allows(
1622 unbind_schema,
1623 &Value::String("test_keymap_file::StringAction".to_string())
1624 ));
1625 assert!(schema_allows(
1626 unbind_schema,
1627 &Value::String("test_keymap_file::InputAction".to_string())
1628 ));
1629 }
1630
1631 #[track_caller]
1632 fn check_keymap_update(
1633 input: impl ToString,
1634 operation: KeybindUpdateOperation,
1635 expected: impl ToString,
1636 ) {
1637 let result = KeymapFile::update_keybinding(
1638 operation,
1639 input.to_string(),
1640 4,
1641 &gpui::DummyKeyboardMapper,
1642 )
1643 .expect("Update succeeded");
1644 pretty_assertions::assert_eq!(expected.to_string(), result);
1645 }
1646
1647 #[track_caller]
1648 fn parse_keystrokes(keystrokes: &str) -> Vec<KeybindingKeystroke> {
1649 keystrokes
1650 .split(' ')
1651 .map(|s| {
1652 KeybindingKeystroke::new_with_mapper(
1653 Keystroke::parse(s).expect("Keystrokes valid"),
1654 false,
1655 &DummyKeyboardMapper,
1656 )
1657 })
1658 .collect()
1659 }
1660
1661 #[test]
1662 fn keymap_update() {
1663 zlog::init_test();
1664
1665 check_keymap_update(
1666 "[]",
1667 KeybindUpdateOperation::add(KeybindUpdateTarget {
1668 keystrokes: &parse_keystrokes("ctrl-a"),
1669 action_name: "zed::SomeAction",
1670 context: None,
1671 action_arguments: None,
1672 }),
1673 r#"[
1674 {
1675 "bindings": {
1676 "ctrl-a": "zed::SomeAction"
1677 }
1678 }
1679 ]"#
1680 .unindent(),
1681 );
1682
1683 check_keymap_update(
1684 "[]",
1685 KeybindUpdateOperation::add(KeybindUpdateTarget {
1686 keystrokes: &parse_keystrokes("\\ a"),
1687 action_name: "zed::SomeAction",
1688 context: None,
1689 action_arguments: None,
1690 }),
1691 r#"[
1692 {
1693 "bindings": {
1694 "\\ a": "zed::SomeAction"
1695 }
1696 }
1697 ]"#
1698 .unindent(),
1699 );
1700
1701 check_keymap_update(
1702 "[]",
1703 KeybindUpdateOperation::add(KeybindUpdateTarget {
1704 keystrokes: &parse_keystrokes("ctrl-a"),
1705 action_name: "zed::SomeAction",
1706 context: None,
1707 action_arguments: Some(""),
1708 }),
1709 r#"[
1710 {
1711 "bindings": {
1712 "ctrl-a": "zed::SomeAction"
1713 }
1714 }
1715 ]"#
1716 .unindent(),
1717 );
1718
1719 check_keymap_update(
1720 r#"[
1721 {
1722 "bindings": {
1723 "ctrl-a": "zed::SomeAction"
1724 }
1725 }
1726 ]"#
1727 .unindent(),
1728 KeybindUpdateOperation::add(KeybindUpdateTarget {
1729 keystrokes: &parse_keystrokes("ctrl-b"),
1730 action_name: "zed::SomeOtherAction",
1731 context: None,
1732 action_arguments: None,
1733 }),
1734 r#"[
1735 {
1736 "bindings": {
1737 "ctrl-a": "zed::SomeAction"
1738 }
1739 },
1740 {
1741 "bindings": {
1742 "ctrl-b": "zed::SomeOtherAction"
1743 }
1744 }
1745 ]"#
1746 .unindent(),
1747 );
1748
1749 check_keymap_update(
1750 r#"[
1751 {
1752 "bindings": {
1753 "ctrl-a": "zed::SomeAction"
1754 }
1755 }
1756 ]"#
1757 .unindent(),
1758 KeybindUpdateOperation::add(KeybindUpdateTarget {
1759 keystrokes: &parse_keystrokes("ctrl-b"),
1760 action_name: "zed::SomeOtherAction",
1761 context: None,
1762 action_arguments: Some(r#"{"foo": "bar"}"#),
1763 }),
1764 r#"[
1765 {
1766 "bindings": {
1767 "ctrl-a": "zed::SomeAction"
1768 }
1769 },
1770 {
1771 "bindings": {
1772 "ctrl-b": [
1773 "zed::SomeOtherAction",
1774 {
1775 "foo": "bar"
1776 }
1777 ]
1778 }
1779 }
1780 ]"#
1781 .unindent(),
1782 );
1783
1784 check_keymap_update(
1785 r#"[
1786 {
1787 "bindings": {
1788 "ctrl-a": "zed::SomeAction"
1789 }
1790 }
1791 ]"#
1792 .unindent(),
1793 KeybindUpdateOperation::add(KeybindUpdateTarget {
1794 keystrokes: &parse_keystrokes("ctrl-b"),
1795 action_name: "zed::SomeOtherAction",
1796 context: Some("Zed > Editor && some_condition = true"),
1797 action_arguments: Some(r#"{"foo": "bar"}"#),
1798 }),
1799 r#"[
1800 {
1801 "bindings": {
1802 "ctrl-a": "zed::SomeAction"
1803 }
1804 },
1805 {
1806 "context": "Zed > Editor && some_condition = true",
1807 "bindings": {
1808 "ctrl-b": [
1809 "zed::SomeOtherAction",
1810 {
1811 "foo": "bar"
1812 }
1813 ]
1814 }
1815 }
1816 ]"#
1817 .unindent(),
1818 );
1819
1820 check_keymap_update(
1821 r#"[
1822 {
1823 "bindings": {
1824 "ctrl-a": "zed::SomeAction"
1825 }
1826 }
1827 ]"#
1828 .unindent(),
1829 KeybindUpdateOperation::Replace {
1830 target: KeybindUpdateTarget {
1831 keystrokes: &parse_keystrokes("ctrl-a"),
1832 action_name: "zed::SomeAction",
1833 context: None,
1834 action_arguments: None,
1835 },
1836 source: KeybindUpdateTarget {
1837 keystrokes: &parse_keystrokes("ctrl-b"),
1838 action_name: "zed::SomeOtherAction",
1839 context: None,
1840 action_arguments: Some(r#"{"foo": "bar"}"#),
1841 },
1842 target_keybind_source: KeybindSource::Base,
1843 },
1844 r#"[
1845 {
1846 "bindings": {
1847 "ctrl-a": "zed::SomeAction"
1848 }
1849 },
1850 {
1851 "bindings": {
1852 "ctrl-b": [
1853 "zed::SomeOtherAction",
1854 {
1855 "foo": "bar"
1856 }
1857 ]
1858 }
1859 },
1860 {
1861 "bindings": {
1862 "ctrl-a": null
1863 }
1864 }
1865 ]"#
1866 .unindent(),
1867 );
1868
1869 // Replacing a non-user binding without changing the keystroke should
1870 // not produce a NoAction suppression entry.
1871 check_keymap_update(
1872 r#"[
1873 {
1874 "bindings": {
1875 "ctrl-a": "zed::SomeAction"
1876 }
1877 }
1878 ]"#
1879 .unindent(),
1880 KeybindUpdateOperation::Replace {
1881 target: KeybindUpdateTarget {
1882 keystrokes: &parse_keystrokes("ctrl-a"),
1883 action_name: "zed::SomeAction",
1884 context: None,
1885 action_arguments: None,
1886 },
1887 source: KeybindUpdateTarget {
1888 keystrokes: &parse_keystrokes("ctrl-a"),
1889 action_name: "zed::SomeOtherAction",
1890 context: None,
1891 action_arguments: None,
1892 },
1893 target_keybind_source: KeybindSource::Base,
1894 },
1895 r#"[
1896 {
1897 "bindings": {
1898 "ctrl-a": "zed::SomeAction"
1899 }
1900 },
1901 {
1902 "bindings": {
1903 "ctrl-a": "zed::SomeOtherAction"
1904 }
1905 }
1906 ]"#
1907 .unindent(),
1908 );
1909
1910 // Replacing a non-user binding with a context and a keystroke change
1911 // should produce a suppression entry that preserves the context.
1912 check_keymap_update(
1913 r#"[
1914 {
1915 "context": "SomeContext",
1916 "bindings": {
1917 "ctrl-a": "zed::SomeAction"
1918 }
1919 }
1920 ]"#
1921 .unindent(),
1922 KeybindUpdateOperation::Replace {
1923 target: KeybindUpdateTarget {
1924 keystrokes: &parse_keystrokes("ctrl-a"),
1925 action_name: "zed::SomeAction",
1926 context: Some("SomeContext"),
1927 action_arguments: None,
1928 },
1929 source: KeybindUpdateTarget {
1930 keystrokes: &parse_keystrokes("ctrl-b"),
1931 action_name: "zed::SomeOtherAction",
1932 context: Some("SomeContext"),
1933 action_arguments: None,
1934 },
1935 target_keybind_source: KeybindSource::Default,
1936 },
1937 r#"[
1938 {
1939 "context": "SomeContext",
1940 "bindings": {
1941 "ctrl-a": "zed::SomeAction"
1942 }
1943 },
1944 {
1945 "context": "SomeContext",
1946 "bindings": {
1947 "ctrl-b": "zed::SomeOtherAction"
1948 }
1949 },
1950 {
1951 "context": "SomeContext",
1952 "bindings": {
1953 "ctrl-a": null
1954 }
1955 }
1956 ]"#
1957 .unindent(),
1958 );
1959
1960 check_keymap_update(
1961 r#"[
1962 {
1963 "bindings": {
1964 "a": "zed::SomeAction"
1965 }
1966 }
1967 ]"#
1968 .unindent(),
1969 KeybindUpdateOperation::Replace {
1970 target: KeybindUpdateTarget {
1971 keystrokes: &parse_keystrokes("a"),
1972 action_name: "zed::SomeAction",
1973 context: None,
1974 action_arguments: None,
1975 },
1976 source: KeybindUpdateTarget {
1977 keystrokes: &parse_keystrokes("ctrl-b"),
1978 action_name: "zed::SomeOtherAction",
1979 context: None,
1980 action_arguments: Some(r#"{"foo": "bar"}"#),
1981 },
1982 target_keybind_source: KeybindSource::User,
1983 },
1984 r#"[
1985 {
1986 "bindings": {
1987 "ctrl-b": [
1988 "zed::SomeOtherAction",
1989 {
1990 "foo": "bar"
1991 }
1992 ]
1993 }
1994 }
1995 ]"#
1996 .unindent(),
1997 );
1998
1999 check_keymap_update(
2000 r#"[
2001 {
2002 "bindings": {
2003 "\\ a": "zed::SomeAction"
2004 }
2005 }
2006 ]"#
2007 .unindent(),
2008 KeybindUpdateOperation::Replace {
2009 target: KeybindUpdateTarget {
2010 keystrokes: &parse_keystrokes("\\ a"),
2011 action_name: "zed::SomeAction",
2012 context: None,
2013 action_arguments: None,
2014 },
2015 source: KeybindUpdateTarget {
2016 keystrokes: &parse_keystrokes("\\ b"),
2017 action_name: "zed::SomeOtherAction",
2018 context: None,
2019 action_arguments: Some(r#"{"foo": "bar"}"#),
2020 },
2021 target_keybind_source: KeybindSource::User,
2022 },
2023 r#"[
2024 {
2025 "bindings": {
2026 "\\ b": [
2027 "zed::SomeOtherAction",
2028 {
2029 "foo": "bar"
2030 }
2031 ]
2032 }
2033 }
2034 ]"#
2035 .unindent(),
2036 );
2037
2038 check_keymap_update(
2039 r#"[
2040 {
2041 "bindings": {
2042 "\\ a": "zed::SomeAction"
2043 }
2044 }
2045 ]"#
2046 .unindent(),
2047 KeybindUpdateOperation::Replace {
2048 target: KeybindUpdateTarget {
2049 keystrokes: &parse_keystrokes("\\ a"),
2050 action_name: "zed::SomeAction",
2051 context: None,
2052 action_arguments: None,
2053 },
2054 source: KeybindUpdateTarget {
2055 keystrokes: &parse_keystrokes("\\ a"),
2056 action_name: "zed::SomeAction",
2057 context: None,
2058 action_arguments: None,
2059 },
2060 target_keybind_source: KeybindSource::User,
2061 },
2062 r#"[
2063 {
2064 "bindings": {
2065 "\\ a": "zed::SomeAction"
2066 }
2067 }
2068 ]"#
2069 .unindent(),
2070 );
2071
2072 check_keymap_update(
2073 r#"[
2074 {
2075 "bindings": {
2076 "ctrl-a": "zed::SomeAction"
2077 }
2078 }
2079 ]"#
2080 .unindent(),
2081 KeybindUpdateOperation::Replace {
2082 target: KeybindUpdateTarget {
2083 keystrokes: &parse_keystrokes("ctrl-a"),
2084 action_name: "zed::SomeNonexistentAction",
2085 context: None,
2086 action_arguments: None,
2087 },
2088 source: KeybindUpdateTarget {
2089 keystrokes: &parse_keystrokes("ctrl-b"),
2090 action_name: "zed::SomeOtherAction",
2091 context: None,
2092 action_arguments: None,
2093 },
2094 target_keybind_source: KeybindSource::User,
2095 },
2096 r#"[
2097 {
2098 "bindings": {
2099 "ctrl-a": "zed::SomeAction"
2100 }
2101 },
2102 {
2103 "bindings": {
2104 "ctrl-b": "zed::SomeOtherAction"
2105 }
2106 }
2107 ]"#
2108 .unindent(),
2109 );
2110
2111 check_keymap_update(
2112 r#"[
2113 {
2114 "bindings": {
2115 // some comment
2116 "ctrl-a": "zed::SomeAction"
2117 // some other comment
2118 }
2119 }
2120 ]"#
2121 .unindent(),
2122 KeybindUpdateOperation::Replace {
2123 target: KeybindUpdateTarget {
2124 keystrokes: &parse_keystrokes("ctrl-a"),
2125 action_name: "zed::SomeAction",
2126 context: None,
2127 action_arguments: None,
2128 },
2129 source: KeybindUpdateTarget {
2130 keystrokes: &parse_keystrokes("ctrl-b"),
2131 action_name: "zed::SomeOtherAction",
2132 context: None,
2133 action_arguments: Some(r#"{"foo": "bar"}"#),
2134 },
2135 target_keybind_source: KeybindSource::User,
2136 },
2137 r#"[
2138 {
2139 "bindings": {
2140 // some comment
2141 "ctrl-b": [
2142 "zed::SomeOtherAction",
2143 {
2144 "foo": "bar"
2145 }
2146 ]
2147 // some other comment
2148 }
2149 }
2150 ]"#
2151 .unindent(),
2152 );
2153
2154 check_keymap_update(
2155 r#"[
2156 {
2157 "context": "SomeContext",
2158 "bindings": {
2159 "a": "foo::bar",
2160 "b": "baz::qux",
2161 }
2162 }
2163 ]"#
2164 .unindent(),
2165 KeybindUpdateOperation::Replace {
2166 target: KeybindUpdateTarget {
2167 keystrokes: &parse_keystrokes("a"),
2168 action_name: "foo::bar",
2169 context: Some("SomeContext"),
2170 action_arguments: None,
2171 },
2172 source: KeybindUpdateTarget {
2173 keystrokes: &parse_keystrokes("c"),
2174 action_name: "foo::baz",
2175 context: Some("SomeOtherContext"),
2176 action_arguments: None,
2177 },
2178 target_keybind_source: KeybindSource::User,
2179 },
2180 r#"[
2181 {
2182 "context": "SomeContext",
2183 "bindings": {
2184 "b": "baz::qux",
2185 }
2186 },
2187 {
2188 "context": "SomeOtherContext",
2189 "bindings": {
2190 "c": "foo::baz"
2191 }
2192 }
2193 ]"#
2194 .unindent(),
2195 );
2196
2197 check_keymap_update(
2198 r#"[
2199 {
2200 "context": "SomeContext",
2201 "bindings": {
2202 "a": "foo::bar",
2203 }
2204 }
2205 ]"#
2206 .unindent(),
2207 KeybindUpdateOperation::Replace {
2208 target: KeybindUpdateTarget {
2209 keystrokes: &parse_keystrokes("a"),
2210 action_name: "foo::bar",
2211 context: Some("SomeContext"),
2212 action_arguments: None,
2213 },
2214 source: KeybindUpdateTarget {
2215 keystrokes: &parse_keystrokes("c"),
2216 action_name: "foo::baz",
2217 context: Some("SomeOtherContext"),
2218 action_arguments: None,
2219 },
2220 target_keybind_source: KeybindSource::User,
2221 },
2222 r#"[
2223 {
2224 "context": "SomeOtherContext",
2225 "bindings": {
2226 "c": "foo::baz",
2227 }
2228 }
2229 ]"#
2230 .unindent(),
2231 );
2232
2233 check_keymap_update(
2234 r#"[
2235 {
2236 "context": "SomeContext",
2237 "bindings": {
2238 "a": "foo::bar",
2239 "c": "foo::baz",
2240 }
2241 },
2242 ]"#
2243 .unindent(),
2244 KeybindUpdateOperation::Remove {
2245 target: KeybindUpdateTarget {
2246 context: Some("SomeContext"),
2247 keystrokes: &parse_keystrokes("a"),
2248 action_name: "foo::bar",
2249 action_arguments: None,
2250 },
2251 target_keybind_source: KeybindSource::User,
2252 },
2253 r#"[
2254 {
2255 "context": "SomeContext",
2256 "bindings": {
2257 "c": "foo::baz",
2258 }
2259 },
2260 ]"#
2261 .unindent(),
2262 );
2263
2264 check_keymap_update(
2265 r#"[
2266 {
2267 "context": "SomeContext",
2268 "bindings": {
2269 "\\ a": "foo::bar",
2270 "c": "foo::baz",
2271 }
2272 },
2273 ]"#
2274 .unindent(),
2275 KeybindUpdateOperation::Remove {
2276 target: KeybindUpdateTarget {
2277 context: Some("SomeContext"),
2278 keystrokes: &parse_keystrokes("\\ a"),
2279 action_name: "foo::bar",
2280 action_arguments: None,
2281 },
2282 target_keybind_source: KeybindSource::User,
2283 },
2284 r#"[
2285 {
2286 "context": "SomeContext",
2287 "bindings": {
2288 "c": "foo::baz",
2289 }
2290 },
2291 ]"#
2292 .unindent(),
2293 );
2294
2295 check_keymap_update(
2296 r#"[
2297 {
2298 "context": "SomeContext",
2299 "bindings": {
2300 "a": ["foo::bar", true],
2301 "c": "foo::baz",
2302 }
2303 },
2304 ]"#
2305 .unindent(),
2306 KeybindUpdateOperation::Remove {
2307 target: KeybindUpdateTarget {
2308 context: Some("SomeContext"),
2309 keystrokes: &parse_keystrokes("a"),
2310 action_name: "foo::bar",
2311 action_arguments: Some("true"),
2312 },
2313 target_keybind_source: KeybindSource::User,
2314 },
2315 r#"[
2316 {
2317 "context": "SomeContext",
2318 "bindings": {
2319 "c": "foo::baz",
2320 }
2321 },
2322 ]"#
2323 .unindent(),
2324 );
2325
2326 check_keymap_update(
2327 r#"[
2328 {
2329 "context": "SomeContext",
2330 "bindings": {
2331 "b": "foo::baz",
2332 }
2333 },
2334 {
2335 "context": "SomeContext",
2336 "bindings": {
2337 "a": ["foo::bar", true],
2338 }
2339 },
2340 {
2341 "context": "SomeContext",
2342 "bindings": {
2343 "c": "foo::baz",
2344 }
2345 },
2346 ]"#
2347 .unindent(),
2348 KeybindUpdateOperation::Remove {
2349 target: KeybindUpdateTarget {
2350 context: Some("SomeContext"),
2351 keystrokes: &parse_keystrokes("a"),
2352 action_name: "foo::bar",
2353 action_arguments: Some("true"),
2354 },
2355 target_keybind_source: KeybindSource::User,
2356 },
2357 r#"[
2358 {
2359 "context": "SomeContext",
2360 "bindings": {
2361 "b": "foo::baz",
2362 }
2363 },
2364 {
2365 "context": "SomeContext",
2366 "bindings": {
2367 "c": "foo::baz",
2368 }
2369 },
2370 ]"#
2371 .unindent(),
2372 );
2373 check_keymap_update(
2374 r#"[
2375 {
2376 "context": "SomeOtherContext",
2377 "use_key_equivalents": true,
2378 "bindings": {
2379 "b": "foo::bar",
2380 }
2381 },
2382 ]"#
2383 .unindent(),
2384 KeybindUpdateOperation::Add {
2385 source: KeybindUpdateTarget {
2386 context: Some("SomeContext"),
2387 keystrokes: &parse_keystrokes("a"),
2388 action_name: "foo::baz",
2389 action_arguments: Some("true"),
2390 },
2391 from: Some(KeybindUpdateTarget {
2392 context: Some("SomeOtherContext"),
2393 keystrokes: &parse_keystrokes("b"),
2394 action_name: "foo::bar",
2395 action_arguments: None,
2396 }),
2397 },
2398 r#"[
2399 {
2400 "context": "SomeOtherContext",
2401 "use_key_equivalents": true,
2402 "bindings": {
2403 "b": "foo::bar",
2404 }
2405 },
2406 {
2407 "context": "SomeContext",
2408 "use_key_equivalents": true,
2409 "bindings": {
2410 "a": [
2411 "foo::baz",
2412 true
2413 ]
2414 }
2415 }
2416 ]"#
2417 .unindent(),
2418 );
2419
2420 check_keymap_update(
2421 r#"[
2422 {
2423 "context": "SomeOtherContext",
2424 "use_key_equivalents": true,
2425 "bindings": {
2426 "b": "foo::bar",
2427 }
2428 },
2429 ]"#
2430 .unindent(),
2431 KeybindUpdateOperation::Remove {
2432 target: KeybindUpdateTarget {
2433 context: Some("SomeContext"),
2434 keystrokes: &parse_keystrokes("a"),
2435 action_name: "foo::baz",
2436 action_arguments: Some("true"),
2437 },
2438 target_keybind_source: KeybindSource::Default,
2439 },
2440 r#"[
2441 {
2442 "context": "SomeOtherContext",
2443 "use_key_equivalents": true,
2444 "bindings": {
2445 "b": "foo::bar",
2446 }
2447 },
2448 {
2449 "context": "SomeContext",
2450 "bindings": {
2451 "a": null
2452 }
2453 }
2454 ]"#
2455 .unindent(),
2456 );
2457 }
2458
2459 #[test]
2460 fn test_keymap_remove() {
2461 zlog::init_test();
2462
2463 check_keymap_update(
2464 r#"
2465 [
2466 {
2467 "context": "Editor",
2468 "bindings": {
2469 "cmd-k cmd-u": "editor::ConvertToUpperCase",
2470 "cmd-k cmd-l": "editor::ConvertToLowerCase",
2471 "cmd-[": "pane::GoBack",
2472 }
2473 },
2474 ]
2475 "#,
2476 KeybindUpdateOperation::Remove {
2477 target: KeybindUpdateTarget {
2478 context: Some("Editor"),
2479 keystrokes: &parse_keystrokes("cmd-k cmd-l"),
2480 action_name: "editor::ConvertToLowerCase",
2481 action_arguments: None,
2482 },
2483 target_keybind_source: KeybindSource::User,
2484 },
2485 r#"
2486 [
2487 {
2488 "context": "Editor",
2489 "bindings": {
2490 "cmd-k cmd-u": "editor::ConvertToUpperCase",
2491 "cmd-[": "pane::GoBack",
2492 }
2493 },
2494 ]
2495 "#,
2496 );
2497 }
2498}