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