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