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, NoAction,
7};
8use schemars::{
9 JsonSchema,
10 r#gen::{SchemaGenerator, SchemaSettings},
11 schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation},
12};
13use serde::Deserialize;
14use serde_json::Value;
15use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock};
16use util::{
17 asset_str,
18 markdown::{MarkdownEscaped, MarkdownInlineCode, MarkdownString},
19};
20
21use crate::{
22 SettingsAssets, append_top_level_array_value_in_json_text, parse_json_with_comments,
23 replace_top_level_array_value_in_json_text,
24};
25
26pub trait KeyBindingValidator: Send + Sync {
27 fn action_type_id(&self) -> TypeId;
28 fn validate(&self, binding: &KeyBinding) -> Result<(), MarkdownString>;
29}
30
31pub struct KeyBindingValidatorRegistration(pub fn() -> Box<dyn KeyBindingValidator>);
32
33inventory::collect!(KeyBindingValidatorRegistration);
34
35pub(crate) static KEY_BINDING_VALIDATORS: LazyLock<BTreeMap<TypeId, Box<dyn KeyBindingValidator>>> =
36 LazyLock::new(|| {
37 let mut validators = BTreeMap::new();
38 for validator_registration in inventory::iter::<KeyBindingValidatorRegistration> {
39 let validator = validator_registration.0();
40 validators.insert(validator.action_type_id(), validator);
41 }
42 validators
43 });
44
45// Note that the doc comments on these are shown by json-language-server when editing the keymap, so
46// they should be considered user-facing documentation. Documentation is not handled well with
47// schemars-0.8 - when there are newlines, it is rendered as plaintext (see
48// https://github.com/GREsau/schemars/issues/38#issuecomment-2282883519). So for now these docs
49// avoid newlines.
50//
51// TODO: Update to schemars-1.0 once it's released, and add more docs as newlines would be
52// supported. Tracking issue is https://github.com/GREsau/schemars/issues/112.
53
54/// Keymap configuration consisting of sections. Each section may have a context predicate which
55/// determines whether its bindings are used.
56#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
57#[serde(transparent)]
58pub struct KeymapFile(Vec<KeymapSection>);
59
60/// Keymap section which binds keystrokes to actions.
61#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
62pub struct KeymapSection {
63 /// Determines when these bindings are active. When just a name is provided, like `Editor` or
64 /// `Workspace`, the bindings will be active in that context. Boolean expressions like `X && Y`,
65 /// `X || Y`, `!X` are also supported. Some more complex logic including checking OS and the
66 /// current file extension are also supported - see [the
67 /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details.
68 #[serde(default)]
69 context: String,
70 /// This option enables specifying keys based on their position on a QWERTY keyboard, by using
71 /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported
72 /// on macOS. See the documentation for more details.
73 #[serde(default)]
74 use_key_equivalents: bool,
75 /// This keymap section's bindings, as a JSON object mapping keystrokes to actions. The
76 /// keystrokes key is a string representing a sequence of keystrokes to type, where the
77 /// keystrokes are separated by whitespace. Each keystroke is a sequence of modifiers (`ctrl`,
78 /// `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) followed by a key, separated by `-`. The
79 /// order of bindings does matter. When the same keystrokes are bound at the same context depth,
80 /// the binding that occurs later in the file is preferred. For displaying keystrokes in the UI,
81 /// the later binding for the same action is preferred.
82 #[serde(default)]
83 bindings: Option<IndexMap<String, KeymapAction>>,
84 #[serde(flatten)]
85 unrecognized_fields: IndexMap<String, Value>,
86 // This struct intentionally uses permissive types for its fields, rather than validating during
87 // deserialization. The purpose of this is to allow loading the portion of the keymap that doesn't
88 // have errors. The downside of this is that the errors are not reported with line+column info.
89 // Unfortunately the implementations of the `Spanned` types for preserving this information are
90 // highly inconvenient (`serde_spanned`) and in some cases don't work at all here
91 // (`json_spanned_>value`). Serde should really have builtin support for this.
92}
93
94impl KeymapSection {
95 pub fn bindings(&self) -> impl DoubleEndedIterator<Item = (&String, &KeymapAction)> {
96 self.bindings.iter().flatten()
97 }
98}
99
100/// Keymap action as a JSON value, since it can either be null for no action, or the name of the
101/// action, or an array of the name of the action and the action input.
102///
103/// Unlike the other json types involved in keymaps (including actions), this doc-comment will not
104/// be included in the generated JSON schema, as it manually defines its `JsonSchema` impl. The
105/// actual schema used for it is automatically generated in `KeymapFile::generate_json_schema`.
106#[derive(Debug, Deserialize, Default, Clone)]
107#[serde(transparent)]
108pub struct KeymapAction(Value);
109
110impl std::fmt::Display for KeymapAction {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 match &self.0 {
113 Value::String(s) => write!(f, "{}", s),
114 Value::Array(arr) => {
115 let strings: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
116 write!(f, "{}", strings.join(", "))
117 }
118 _ => write!(f, "{}", self.0),
119 }
120 }
121}
122
123impl JsonSchema for KeymapAction {
124 /// This is used when generating the JSON schema for the `KeymapAction` type, so that it can
125 /// reference the keymap action schema.
126 fn schema_name() -> String {
127 "KeymapAction".into()
128 }
129
130 /// This schema will be replaced with the full action schema in
131 /// `KeymapFile::generate_json_schema`.
132 fn json_schema(_: &mut SchemaGenerator) -> Schema {
133 Schema::Bool(true)
134 }
135}
136
137#[derive(Debug)]
138#[must_use]
139pub enum KeymapFileLoadResult {
140 Success {
141 key_bindings: Vec<KeyBinding>,
142 },
143 SomeFailedToLoad {
144 key_bindings: Vec<KeyBinding>,
145 error_message: MarkdownString,
146 },
147 JsonParseFailure {
148 error: anyhow::Error,
149 },
150}
151
152impl KeymapFile {
153 pub fn parse(content: &str) -> anyhow::Result<Self> {
154 parse_json_with_comments::<Self>(content)
155 }
156
157 pub fn load_asset(
158 asset_path: &str,
159 source: Option<KeybindSource>,
160 cx: &App,
161 ) -> anyhow::Result<Vec<KeyBinding>> {
162 match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
163 KeymapFileLoadResult::Success { mut key_bindings } => match source {
164 Some(source) => Ok({
165 for key_binding in &mut key_bindings {
166 key_binding.set_meta(source.meta());
167 }
168 key_bindings
169 }),
170 None => Ok(key_bindings),
171 },
172 KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
173 anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",)
174 }
175 KeymapFileLoadResult::JsonParseFailure { error } => {
176 anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}")
177 }
178 }
179 }
180
181 #[cfg(feature = "test-support")]
182 pub fn load_asset_allow_partial_failure(
183 asset_path: &str,
184 cx: &App,
185 ) -> anyhow::Result<Vec<KeyBinding>> {
186 match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
187 KeymapFileLoadResult::SomeFailedToLoad {
188 key_bindings,
189 error_message,
190 ..
191 } if key_bindings.is_empty() => {
192 anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",)
193 }
194 KeymapFileLoadResult::Success { key_bindings, .. }
195 | KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
196 KeymapFileLoadResult::JsonParseFailure { error } => {
197 anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}")
198 }
199 }
200 }
201
202 #[cfg(feature = "test-support")]
203 pub fn load_panic_on_failure(content: &str, cx: &App) -> Vec<KeyBinding> {
204 match Self::load(content, cx) {
205 KeymapFileLoadResult::Success { key_bindings, .. } => key_bindings,
206 KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
207 panic!("{error_message}");
208 }
209 KeymapFileLoadResult::JsonParseFailure { error } => {
210 panic!("JSON parse error: {error}");
211 }
212 }
213 }
214
215 pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
216 let key_equivalents =
217 crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id());
218
219 if content.is_empty() {
220 return KeymapFileLoadResult::Success {
221 key_bindings: Vec::new(),
222 };
223 }
224 let keymap_file = match Self::parse(content) {
225 Ok(keymap_file) => keymap_file,
226 Err(error) => {
227 return KeymapFileLoadResult::JsonParseFailure { error };
228 }
229 };
230
231 // Accumulate errors in order to support partial load of user keymap in the presence of
232 // errors in context and binding parsing.
233 let mut errors = Vec::new();
234 let mut key_bindings = Vec::new();
235
236 for KeymapSection {
237 context,
238 use_key_equivalents,
239 bindings,
240 unrecognized_fields,
241 } in keymap_file.0.iter()
242 {
243 let context_predicate: Option<Rc<KeyBindingContextPredicate>> = if context.is_empty() {
244 None
245 } else {
246 match KeyBindingContextPredicate::parse(context) {
247 Ok(context_predicate) => Some(context_predicate.into()),
248 Err(err) => {
249 // Leading space is to separate from the message indicating which section
250 // the error occurred in.
251 errors.push((
252 context,
253 format!(" Parse error in section `context` field: {}", err),
254 ));
255 continue;
256 }
257 }
258 };
259
260 let key_equivalents = if *use_key_equivalents {
261 key_equivalents.as_ref()
262 } else {
263 None
264 };
265
266 let mut section_errors = String::new();
267
268 if !unrecognized_fields.is_empty() {
269 write!(
270 section_errors,
271 "\n\n - Unrecognized fields: {}",
272 MarkdownInlineCode(&format!("{:?}", unrecognized_fields.keys()))
273 )
274 .unwrap();
275 }
276
277 if let Some(bindings) = bindings {
278 for (keystrokes, action) in bindings {
279 let result = Self::load_keybinding(
280 keystrokes,
281 action,
282 context_predicate.clone(),
283 key_equivalents,
284 cx,
285 );
286 match result {
287 Ok(key_binding) => {
288 key_bindings.push(key_binding);
289 }
290 Err(err) => {
291 let mut lines = err.lines();
292 let mut indented_err = lines.next().unwrap().to_string();
293 for line in lines {
294 indented_err.push_str(" ");
295 indented_err.push_str(line);
296 indented_err.push_str("\n");
297 }
298 write!(
299 section_errors,
300 "\n\n- In binding {}, {indented_err}",
301 MarkdownInlineCode(&format!("\"{}\"", keystrokes))
302 )
303 .unwrap();
304 }
305 }
306 }
307 }
308
309 if !section_errors.is_empty() {
310 errors.push((context, section_errors))
311 }
312 }
313
314 if errors.is_empty() {
315 KeymapFileLoadResult::Success { key_bindings }
316 } else {
317 let mut error_message = "Errors in user keymap file.\n".to_owned();
318 for (context, section_errors) in errors {
319 if context.is_empty() {
320 let _ = write!(error_message, "\n\nIn section without context predicate:");
321 } else {
322 let _ = write!(
323 error_message,
324 "\n\nIn section with {}:",
325 MarkdownInlineCode(&format!("context = \"{}\"", context))
326 );
327 }
328 let _ = write!(error_message, "{section_errors}");
329 }
330 KeymapFileLoadResult::SomeFailedToLoad {
331 key_bindings,
332 error_message: MarkdownString(error_message),
333 }
334 }
335 }
336
337 fn load_keybinding(
338 keystrokes: &str,
339 action: &KeymapAction,
340 context: Option<Rc<KeyBindingContextPredicate>>,
341 key_equivalents: Option<&HashMap<char, char>>,
342 cx: &App,
343 ) -> std::result::Result<KeyBinding, String> {
344 let (build_result, action_input_string) = match &action.0 {
345 Value::Array(items) => {
346 if items.len() != 2 {
347 return Err(format!(
348 "expected two-element array of `[name, input]`. \
349 Instead found {}.",
350 MarkdownInlineCode(&action.0.to_string())
351 ));
352 }
353 let serde_json::Value::String(ref name) = items[0] else {
354 return Err(format!(
355 "expected two-element array of `[name, input]`, \
356 but the first element is not a string in {}.",
357 MarkdownInlineCode(&action.0.to_string())
358 ));
359 };
360 let action_input = items[1].clone();
361 let action_input_string = action_input.to_string();
362 (
363 cx.build_action(&name, Some(action_input)),
364 Some(action_input_string),
365 )
366 }
367 Value::String(name) => (cx.build_action(&name, None), None),
368 Value::Null => (Ok(NoAction.boxed_clone()), None),
369 _ => {
370 return Err(format!(
371 "expected two-element array of `[name, input]`. \
372 Instead found {}.",
373 MarkdownInlineCode(&action.0.to_string())
374 ));
375 }
376 };
377
378 let action = match build_result {
379 Ok(action) => action,
380 Err(ActionBuildError::NotFound { name }) => {
381 return Err(format!(
382 "didn't find an action named {}.",
383 MarkdownInlineCode(&format!("\"{}\"", &name))
384 ));
385 }
386 Err(ActionBuildError::BuildError { name, error }) => match action_input_string {
387 Some(action_input_string) => {
388 return Err(format!(
389 "can't build {} action from input value {}: {}",
390 MarkdownInlineCode(&format!("\"{}\"", &name)),
391 MarkdownInlineCode(&action_input_string),
392 MarkdownEscaped(&error.to_string())
393 ));
394 }
395 None => {
396 return Err(format!(
397 "can't build {} action - it requires input data via [name, input]: {}",
398 MarkdownInlineCode(&format!("\"{}\"", &name)),
399 MarkdownEscaped(&error.to_string())
400 ));
401 }
402 },
403 };
404
405 let key_binding = match KeyBinding::load(keystrokes, action, context, key_equivalents) {
406 Ok(key_binding) => key_binding,
407 Err(InvalidKeystrokeError { keystroke }) => {
408 return Err(format!(
409 "invalid keystroke {}. {}",
410 MarkdownInlineCode(&format!("\"{}\"", &keystroke)),
411 KEYSTROKE_PARSE_EXPECTED_MESSAGE
412 ));
413 }
414 };
415
416 if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) {
417 match validator.validate(&key_binding) {
418 Ok(()) => Ok(key_binding),
419 Err(error) => Err(error.0),
420 }
421 } else {
422 Ok(key_binding)
423 }
424 }
425
426 pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
427 let mut generator = SchemaSettings::draft07()
428 .with(|settings| settings.option_add_null_type = false)
429 .into_generator();
430
431 let action_schemas = cx.action_schemas(&mut generator);
432 let deprecations = cx.deprecated_actions_to_preferred_actions();
433 let deprecation_messages = cx.action_deprecation_messages();
434 KeymapFile::generate_json_schema(
435 generator,
436 action_schemas,
437 deprecations,
438 deprecation_messages,
439 )
440 }
441
442 fn generate_json_schema(
443 generator: SchemaGenerator,
444 action_schemas: Vec<(&'static str, Option<Schema>)>,
445 deprecations: &HashMap<&'static str, &'static str>,
446 deprecation_messages: &HashMap<&'static str, &'static str>,
447 ) -> serde_json::Value {
448 fn set<I, O>(input: I) -> Option<O>
449 where
450 I: Into<O>,
451 {
452 Some(input.into())
453 }
454
455 fn add_deprecation(schema_object: &mut SchemaObject, message: String) {
456 schema_object.extensions.insert(
457 // deprecationMessage is not part of the JSON Schema spec,
458 // but json-language-server recognizes it.
459 "deprecationMessage".to_owned(),
460 Value::String(message),
461 );
462 }
463
464 fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) {
465 add_deprecation(schema_object, format!("Deprecated, use {new_name}"));
466 }
467
468 fn add_description(schema_object: &mut SchemaObject, description: String) {
469 schema_object
470 .metadata
471 .get_or_insert(Default::default())
472 .description = Some(description);
473 }
474
475 let empty_object: SchemaObject = SchemaObject {
476 instance_type: set(InstanceType::Object),
477 ..Default::default()
478 };
479
480 // This is a workaround for a json-language-server issue where it matches the first
481 // alternative that matches the value's shape and uses that for documentation.
482 //
483 // In the case of the array validations, it would even provide an error saying that the name
484 // must match the name of the first alternative.
485 let mut plain_action = SchemaObject {
486 instance_type: set(InstanceType::String),
487 const_value: Some(Value::String("".to_owned())),
488 ..Default::default()
489 };
490 let no_action_message = "No action named this.";
491 add_description(&mut plain_action, no_action_message.to_owned());
492 add_deprecation(&mut plain_action, no_action_message.to_owned());
493 let mut matches_action_name = SchemaObject {
494 const_value: Some(Value::String("".to_owned())),
495 ..Default::default()
496 };
497 let no_action_message = "No action named this that takes input.";
498 add_description(&mut matches_action_name, no_action_message.to_owned());
499 add_deprecation(&mut matches_action_name, no_action_message.to_owned());
500 let action_with_input = SchemaObject {
501 instance_type: set(InstanceType::Array),
502 array: set(ArrayValidation {
503 items: set(vec![
504 matches_action_name.into(),
505 // Accept any value, as we want this to be the preferred match when there is a
506 // typo in the name.
507 Schema::Bool(true),
508 ]),
509 min_items: Some(2),
510 max_items: Some(2),
511 ..Default::default()
512 }),
513 ..Default::default()
514 };
515 let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()];
516
517 for (name, action_schema) in action_schemas.into_iter() {
518 let schema = if let Some(Schema::Object(schema)) = action_schema {
519 Some(schema)
520 } else {
521 None
522 };
523
524 let description = schema.as_ref().and_then(|schema| {
525 schema
526 .metadata
527 .as_ref()
528 .and_then(|metadata| metadata.description.clone())
529 });
530
531 let deprecation = if name == NoAction.name() {
532 Some("null")
533 } else {
534 deprecations.get(name).copied()
535 };
536
537 // Add an alternative for plain action names.
538 let mut plain_action = SchemaObject {
539 instance_type: set(InstanceType::String),
540 const_value: Some(Value::String(name.to_string())),
541 ..Default::default()
542 };
543 if let Some(message) = deprecation_messages.get(name) {
544 add_deprecation(&mut plain_action, message.to_string());
545 } else if let Some(new_name) = deprecation {
546 add_deprecation_preferred_name(&mut plain_action, new_name);
547 }
548 if let Some(description) = description.clone() {
549 add_description(&mut plain_action, description);
550 }
551 keymap_action_alternatives.push(plain_action.into());
552
553 // Add an alternative for actions with data specified as a [name, data] array.
554 //
555 // When a struct with no deserializable fields is added with impl_actions! /
556 // impl_actions_as! an empty object schema is produced. The action should be invoked
557 // without data in this case.
558 if let Some(schema) = schema {
559 if schema != empty_object {
560 let mut matches_action_name = SchemaObject {
561 const_value: Some(Value::String(name.to_string())),
562 ..Default::default()
563 };
564 if let Some(description) = description.clone() {
565 add_description(&mut matches_action_name, description);
566 }
567 if let Some(message) = deprecation_messages.get(name) {
568 add_deprecation(&mut matches_action_name, message.to_string());
569 } else if let Some(new_name) = deprecation {
570 add_deprecation_preferred_name(&mut matches_action_name, new_name);
571 }
572 let action_with_input = SchemaObject {
573 instance_type: set(InstanceType::Array),
574 array: set(ArrayValidation {
575 items: set(vec![matches_action_name.into(), schema.into()]),
576 min_items: Some(2),
577 max_items: Some(2),
578 ..Default::default()
579 }),
580 ..Default::default()
581 };
582 keymap_action_alternatives.push(action_with_input.into());
583 }
584 }
585 }
586
587 // Placing null first causes json-language-server to default assuming actions should be
588 // null, so place it last.
589 keymap_action_alternatives.push(
590 SchemaObject {
591 instance_type: set(InstanceType::Null),
592 ..Default::default()
593 }
594 .into(),
595 );
596
597 let action_schema = SchemaObject {
598 subschemas: set(SubschemaValidation {
599 one_of: Some(keymap_action_alternatives),
600 ..Default::default()
601 }),
602 ..Default::default()
603 }
604 .into();
605
606 // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so replacing
607 // the definition of `KeymapAction` results in the full action schema being used.
608 let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
609 root_schema
610 .definitions
611 .insert(KeymapAction::schema_name(), action_schema);
612
613 // This and other json schemas can be viewed via `dev: open language server logs` ->
614 // `json-language-server` -> `Server Info`.
615 serde_json::to_value(root_schema).unwrap()
616 }
617
618 pub fn sections(&self) -> impl DoubleEndedIterator<Item = &KeymapSection> {
619 self.0.iter()
620 }
621
622 pub async fn load_keymap_file(fs: &Arc<dyn Fs>) -> Result<String> {
623 match fs.load(paths::keymap_file()).await {
624 result @ Ok(_) => result,
625 Err(err) => {
626 if let Some(e) = err.downcast_ref::<std::io::Error>() {
627 if e.kind() == std::io::ErrorKind::NotFound {
628 return Ok(crate::initial_keymap_content().to_string());
629 }
630 }
631 Err(err)
632 }
633 }
634 }
635
636 pub fn update_keybinding<'a>(
637 mut operation: KeybindUpdateOperation<'a>,
638 mut keymap_contents: String,
639 tab_size: usize,
640 ) -> Result<String> {
641 // if trying to replace a keybinding that is not user-defined, treat it as an add operation
642 match operation {
643 KeybindUpdateOperation::Replace {
644 target_source,
645 source,
646 ..
647 } if target_source != KeybindSource::User => {
648 operation = KeybindUpdateOperation::Add(source);
649 }
650 _ => {}
651 }
652
653 // Sanity check that keymap contents are valid, even though we only use it for Replace.
654 // We don't want to modify the file if it's invalid.
655 let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?;
656
657 if let KeybindUpdateOperation::Replace { source, target, .. } = operation {
658 let mut found_index = None;
659 let target_action_value = target
660 .action_value()
661 .context("Failed to generate target action JSON value")?;
662 let source_action_value = source
663 .action_value()
664 .context("Failed to generate source action JSON value")?;
665 'sections: for (index, section) in keymap.sections().enumerate() {
666 if section.context != target.context.unwrap_or("") {
667 continue;
668 }
669 if section.use_key_equivalents != target.use_key_equivalents {
670 continue;
671 }
672 let Some(bindings) = §ion.bindings else {
673 continue;
674 };
675 for (keystrokes, action) in bindings {
676 if keystrokes != target.keystrokes {
677 continue;
678 }
679 if action.0 != target_action_value {
680 continue;
681 }
682 found_index = Some(index);
683 break 'sections;
684 }
685 }
686
687 if let Some(index) = found_index {
688 let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
689 &keymap_contents,
690 &["bindings", target.keystrokes],
691 Some(&source_action_value),
692 Some(source.keystrokes),
693 index,
694 tab_size,
695 )
696 .context("Failed to replace keybinding")?;
697 keymap_contents.replace_range(replace_range, &replace_value);
698
699 return Ok(keymap_contents);
700 } else {
701 log::warn!(
702 "Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead",
703 target.keystrokes,
704 target_action_value,
705 source.keystrokes,
706 source_action_value,
707 );
708 operation = KeybindUpdateOperation::Add(source);
709 }
710 }
711
712 if let KeybindUpdateOperation::Add(keybinding) = operation {
713 let mut value = serde_json::Map::with_capacity(4);
714 if let Some(context) = keybinding.context {
715 value.insert("context".to_string(), context.into());
716 }
717 if keybinding.use_key_equivalents {
718 value.insert("use_key_equivalents".to_string(), true.into());
719 }
720
721 value.insert("bindings".to_string(), {
722 let mut bindings = serde_json::Map::new();
723 let action = keybinding.action_value()?;
724 bindings.insert(keybinding.keystrokes.into(), action);
725 bindings.into()
726 });
727
728 let (replace_range, replace_value) = append_top_level_array_value_in_json_text(
729 &keymap_contents,
730 &value.into(),
731 tab_size,
732 )?;
733 keymap_contents.replace_range(replace_range, &replace_value);
734 }
735 return Ok(keymap_contents);
736 }
737}
738
739pub enum KeybindUpdateOperation<'a> {
740 Replace {
741 /// Describes the keybind to create
742 source: KeybindUpdateTarget<'a>,
743 /// Describes the keybind to remove
744 target: KeybindUpdateTarget<'a>,
745 target_source: KeybindSource,
746 },
747 Add(KeybindUpdateTarget<'a>),
748}
749
750pub struct KeybindUpdateTarget<'a> {
751 context: Option<&'a str>,
752 keystrokes: &'a str,
753 action_name: &'a str,
754 use_key_equivalents: bool,
755 input: Option<&'a str>,
756}
757
758impl<'a> KeybindUpdateTarget<'a> {
759 fn action_value(&self) -> Result<Value> {
760 let action_name: Value = self.action_name.into();
761 let value = match self.input {
762 Some(input) => {
763 let input = serde_json::from_str::<Value>(input)
764 .context("Failed to parse action input as JSON")?;
765 serde_json::json!([action_name, input])
766 }
767 None => action_name,
768 };
769 return Ok(value);
770 }
771}
772
773#[derive(Clone, Copy, PartialEq, Eq)]
774pub enum KeybindSource {
775 User,
776 Default,
777 Base,
778 Vim,
779}
780
781impl KeybindSource {
782 const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(0);
783 const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(1);
784 const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(2);
785 const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(3);
786
787 pub fn name(&self) -> &'static str {
788 match self {
789 KeybindSource::User => "User",
790 KeybindSource::Default => "Default",
791 KeybindSource::Base => "Base",
792 KeybindSource::Vim => "Vim",
793 }
794 }
795
796 pub fn meta(&self) -> KeyBindingMetaIndex {
797 match self {
798 KeybindSource::User => Self::USER,
799 KeybindSource::Default => Self::DEFAULT,
800 KeybindSource::Base => Self::BASE,
801 KeybindSource::Vim => Self::VIM,
802 }
803 }
804
805 pub fn from_meta(index: KeyBindingMetaIndex) -> Self {
806 match index {
807 _ if index == Self::USER => KeybindSource::User,
808 _ if index == Self::USER => KeybindSource::Base,
809 _ if index == Self::DEFAULT => KeybindSource::Default,
810 _ if index == Self::VIM => KeybindSource::Vim,
811 _ => unreachable!(),
812 }
813 }
814}
815
816impl From<KeyBindingMetaIndex> for KeybindSource {
817 fn from(index: KeyBindingMetaIndex) -> Self {
818 Self::from_meta(index)
819 }
820}
821
822impl From<KeybindSource> for KeyBindingMetaIndex {
823 fn from(source: KeybindSource) -> Self {
824 return source.meta();
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use unindent::Unindent;
831
832 use crate::{
833 KeybindSource, KeymapFile,
834 keymap_file::{KeybindUpdateOperation, KeybindUpdateTarget},
835 };
836
837 #[test]
838 fn can_deserialize_keymap_with_trailing_comma() {
839 let json = indoc::indoc! {"[
840 // Standard macOS bindings
841 {
842 \"bindings\": {
843 \"up\": \"menu::SelectPrevious\",
844 },
845 },
846 ]
847 "
848 };
849 KeymapFile::parse(json).unwrap();
850 }
851
852 #[test]
853 fn keymap_update() {
854 zlog::init_test();
855 #[track_caller]
856 fn check_keymap_update(
857 input: impl ToString,
858 operation: KeybindUpdateOperation,
859 expected: impl ToString,
860 ) {
861 let result = KeymapFile::update_keybinding(operation, input.to_string(), 4)
862 .expect("Update succeeded");
863 pretty_assertions::assert_eq!(expected.to_string(), result);
864 }
865
866 check_keymap_update(
867 "[]",
868 KeybindUpdateOperation::Add(KeybindUpdateTarget {
869 keystrokes: "ctrl-a",
870 action_name: "zed::SomeAction",
871 context: None,
872 use_key_equivalents: false,
873 input: None,
874 }),
875 r#"[
876 {
877 "bindings": {
878 "ctrl-a": "zed::SomeAction"
879 }
880 }
881 ]"#
882 .unindent(),
883 );
884
885 check_keymap_update(
886 r#"[
887 {
888 "bindings": {
889 "ctrl-a": "zed::SomeAction"
890 }
891 }
892 ]"#
893 .unindent(),
894 KeybindUpdateOperation::Add(KeybindUpdateTarget {
895 keystrokes: "ctrl-b",
896 action_name: "zed::SomeOtherAction",
897 context: None,
898 use_key_equivalents: false,
899 input: None,
900 }),
901 r#"[
902 {
903 "bindings": {
904 "ctrl-a": "zed::SomeAction"
905 }
906 },
907 {
908 "bindings": {
909 "ctrl-b": "zed::SomeOtherAction"
910 }
911 }
912 ]"#
913 .unindent(),
914 );
915
916 check_keymap_update(
917 r#"[
918 {
919 "bindings": {
920 "ctrl-a": "zed::SomeAction"
921 }
922 }
923 ]"#
924 .unindent(),
925 KeybindUpdateOperation::Add(KeybindUpdateTarget {
926 keystrokes: "ctrl-b",
927 action_name: "zed::SomeOtherAction",
928 context: None,
929 use_key_equivalents: false,
930 input: Some(r#"{"foo": "bar"}"#),
931 }),
932 r#"[
933 {
934 "bindings": {
935 "ctrl-a": "zed::SomeAction"
936 }
937 },
938 {
939 "bindings": {
940 "ctrl-b": [
941 "zed::SomeOtherAction",
942 {
943 "foo": "bar"
944 }
945 ]
946 }
947 }
948 ]"#
949 .unindent(),
950 );
951
952 check_keymap_update(
953 r#"[
954 {
955 "bindings": {
956 "ctrl-a": "zed::SomeAction"
957 }
958 }
959 ]"#
960 .unindent(),
961 KeybindUpdateOperation::Add(KeybindUpdateTarget {
962 keystrokes: "ctrl-b",
963 action_name: "zed::SomeOtherAction",
964 context: Some("Zed > Editor && some_condition = true"),
965 use_key_equivalents: true,
966 input: Some(r#"{"foo": "bar"}"#),
967 }),
968 r#"[
969 {
970 "bindings": {
971 "ctrl-a": "zed::SomeAction"
972 }
973 },
974 {
975 "context": "Zed > Editor && some_condition = true",
976 "use_key_equivalents": true,
977 "bindings": {
978 "ctrl-b": [
979 "zed::SomeOtherAction",
980 {
981 "foo": "bar"
982 }
983 ]
984 }
985 }
986 ]"#
987 .unindent(),
988 );
989
990 check_keymap_update(
991 r#"[
992 {
993 "bindings": {
994 "ctrl-a": "zed::SomeAction"
995 }
996 }
997 ]"#
998 .unindent(),
999 KeybindUpdateOperation::Replace {
1000 target: KeybindUpdateTarget {
1001 keystrokes: "ctrl-a",
1002 action_name: "zed::SomeAction",
1003 context: None,
1004 use_key_equivalents: false,
1005 input: None,
1006 },
1007 source: KeybindUpdateTarget {
1008 keystrokes: "ctrl-b",
1009 action_name: "zed::SomeOtherAction",
1010 context: None,
1011 use_key_equivalents: false,
1012 input: Some(r#"{"foo": "bar"}"#),
1013 },
1014 target_source: KeybindSource::Base,
1015 },
1016 r#"[
1017 {
1018 "bindings": {
1019 "ctrl-a": "zed::SomeAction"
1020 }
1021 },
1022 {
1023 "bindings": {
1024 "ctrl-b": [
1025 "zed::SomeOtherAction",
1026 {
1027 "foo": "bar"
1028 }
1029 ]
1030 }
1031 }
1032 ]"#
1033 .unindent(),
1034 );
1035
1036 check_keymap_update(
1037 r#"[
1038 {
1039 "bindings": {
1040 "ctrl-a": "zed::SomeAction"
1041 }
1042 }
1043 ]"#
1044 .unindent(),
1045 KeybindUpdateOperation::Replace {
1046 target: KeybindUpdateTarget {
1047 keystrokes: "ctrl-a",
1048 action_name: "zed::SomeAction",
1049 context: None,
1050 use_key_equivalents: false,
1051 input: None,
1052 },
1053 source: KeybindUpdateTarget {
1054 keystrokes: "ctrl-b",
1055 action_name: "zed::SomeOtherAction",
1056 context: None,
1057 use_key_equivalents: false,
1058 input: Some(r#"{"foo": "bar"}"#),
1059 },
1060 target_source: KeybindSource::User,
1061 },
1062 r#"[
1063 {
1064 "bindings": {
1065 "ctrl-b": [
1066 "zed::SomeOtherAction",
1067 {
1068 "foo": "bar"
1069 }
1070 ]
1071 }
1072 }
1073 ]"#
1074 .unindent(),
1075 );
1076
1077 check_keymap_update(
1078 r#"[
1079 {
1080 "bindings": {
1081 "ctrl-a": "zed::SomeAction"
1082 }
1083 }
1084 ]"#
1085 .unindent(),
1086 KeybindUpdateOperation::Replace {
1087 target: KeybindUpdateTarget {
1088 keystrokes: "ctrl-a",
1089 action_name: "zed::SomeNonexistentAction",
1090 context: None,
1091 use_key_equivalents: false,
1092 input: None,
1093 },
1094 source: KeybindUpdateTarget {
1095 keystrokes: "ctrl-b",
1096 action_name: "zed::SomeOtherAction",
1097 context: None,
1098 use_key_equivalents: false,
1099 input: None,
1100 },
1101 target_source: KeybindSource::User,
1102 },
1103 r#"[
1104 {
1105 "bindings": {
1106 "ctrl-a": "zed::SomeAction"
1107 }
1108 },
1109 {
1110 "bindings": {
1111 "ctrl-b": "zed::SomeOtherAction"
1112 }
1113 }
1114 ]"#
1115 .unindent(),
1116 );
1117
1118 check_keymap_update(
1119 r#"[
1120 {
1121 "bindings": {
1122 // some comment
1123 "ctrl-a": "zed::SomeAction"
1124 // some other comment
1125 }
1126 }
1127 ]"#
1128 .unindent(),
1129 KeybindUpdateOperation::Replace {
1130 target: KeybindUpdateTarget {
1131 keystrokes: "ctrl-a",
1132 action_name: "zed::SomeAction",
1133 context: None,
1134 use_key_equivalents: false,
1135 input: None,
1136 },
1137 source: KeybindUpdateTarget {
1138 keystrokes: "ctrl-b",
1139 action_name: "zed::SomeOtherAction",
1140 context: None,
1141 use_key_equivalents: false,
1142 input: Some(r#"{"foo": "bar"}"#),
1143 },
1144 target_source: KeybindSource::User,
1145 },
1146 r#"[
1147 {
1148 "bindings": {
1149 // some comment
1150 "ctrl-b": [
1151 "zed::SomeOtherAction",
1152 {
1153 "foo": "bar"
1154 }
1155 ]
1156 // some other comment
1157 }
1158 }
1159 ]"#
1160 .unindent(),
1161 );
1162 }
1163}