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