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