1use anyhow::{Result, anyhow};
2use collections::{BTreeMap, HashMap, IndexMap};
3use fs::Fs;
4use gpui::{
5 Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
6 KeyBinding, KeyBindingContextPredicate, NoAction, SharedString,
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::{asset_str, markdown::MarkdownString};
17
18use crate::{SettingsAssets, settings_store::parse_json_with_comments};
19
20pub trait KeyBindingValidator: Send + Sync {
21 fn action_type_id(&self) -> TypeId;
22 fn validate(&self, binding: &KeyBinding) -> Result<(), MarkdownString>;
23}
24
25pub struct KeyBindingValidatorRegistration(pub fn() -> Box<dyn KeyBindingValidator>);
26
27inventory::collect!(KeyBindingValidatorRegistration);
28
29pub(crate) static KEY_BINDING_VALIDATORS: LazyLock<BTreeMap<TypeId, Box<dyn KeyBindingValidator>>> =
30 LazyLock::new(|| {
31 let mut validators = BTreeMap::new();
32 for validator_registration in inventory::iter::<KeyBindingValidatorRegistration> {
33 let validator = validator_registration.0();
34 validators.insert(validator.action_type_id(), validator);
35 }
36 validators
37 });
38
39// Note that the doc comments on these are shown by json-language-server when editing the keymap, so
40// they should be considered user-facing documentation. Documentation is not handled well with
41// schemars-0.8 - when there are newlines, it is rendered as plaintext (see
42// https://github.com/GREsau/schemars/issues/38#issuecomment-2282883519). So for now these docs
43// avoid newlines.
44//
45// TODO: Update to schemars-1.0 once it's released, and add more docs as newlines would be
46// supported. Tracking issue is https://github.com/GREsau/schemars/issues/112.
47
48/// Keymap configuration consisting of sections. Each section may have a context predicate which
49/// determines whether its bindings are used.
50#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
51#[serde(transparent)]
52pub struct KeymapFile(Vec<KeymapSection>);
53
54/// Keymap section which binds keystrokes to actions.
55#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
56pub struct KeymapSection {
57 /// Determines when these bindings are active. When just a name is provided, like `Editor` or
58 /// `Workspace`, the bindings will be active in that context. Boolean expressions like `X && Y`,
59 /// `X || Y`, `!X` are also supported. Some more complex logic including checking OS and the
60 /// current file extension are also supported - see [the
61 /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details.
62 #[serde(default)]
63 context: String,
64 /// This option enables specifying keys based on their position on a QWERTY keyboard, by using
65 /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported
66 /// on macOS. See the documentation for more details.
67 #[serde(default)]
68 use_key_equivalents: bool,
69 /// This keymap section's bindings, as a JSON object mapping keystrokes to actions. The
70 /// keystrokes key is a string representing a sequence of keystrokes to type, where the
71 /// keystrokes are separated by whitespace. Each keystroke is a sequence of modifiers (`ctrl`,
72 /// `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) followed by a key, separated by `-`. The
73 /// order of bindings does matter. When the same keystrokes are bound at the same context depth,
74 /// the binding that occurs later in the file is preferred. For displaying keystrokes in the UI,
75 /// the later binding for the same action is preferred.
76 #[serde(default)]
77 bindings: Option<IndexMap<String, KeymapAction>>,
78 #[serde(flatten)]
79 unrecognized_fields: IndexMap<String, Value>,
80 // This struct intentionally uses permissive types for its fields, rather than validating during
81 // deserialization. The purpose of this is to allow loading the portion of the keymap that doesn't
82 // have errors. The downside of this is that the errors are not reported with line+column info.
83 // Unfortunately the implementations of the `Spanned` types for preserving this information are
84 // highly inconvenient (`serde_spanned`) and in some cases don't work at all here
85 // (`json_spanned_>value`). Serde should really have builtin support for this.
86}
87
88impl KeymapSection {
89 pub fn bindings(&self) -> impl DoubleEndedIterator<Item = (&String, &KeymapAction)> {
90 self.bindings.iter().flatten()
91 }
92}
93
94/// Keymap action as a JSON value, since it can either be null for no action, or the name of the
95/// action, or an array of the name of the action and the action input.
96///
97/// Unlike the other json types involved in keymaps (including actions), this doc-comment will not
98/// be included in the generated JSON schema, as it manually defines its `JsonSchema` impl. The
99/// actual schema used for it is automatically generated in `KeymapFile::generate_json_schema`.
100#[derive(Debug, Deserialize, Default, Clone)]
101#[serde(transparent)]
102pub struct KeymapAction(Value);
103
104impl std::fmt::Display for KeymapAction {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 match &self.0 {
107 Value::String(s) => write!(f, "{}", s),
108 Value::Array(arr) => {
109 let strings: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
110 write!(f, "{}", strings.join(", "))
111 }
112 _ => write!(f, "{}", self.0),
113 }
114 }
115}
116
117impl JsonSchema for KeymapAction {
118 /// This is used when generating the JSON schema for the `KeymapAction` type, so that it can
119 /// reference the keymap action schema.
120 fn schema_name() -> String {
121 "KeymapAction".into()
122 }
123
124 /// This schema will be replaced with the full action schema in
125 /// `KeymapFile::generate_json_schema`.
126 fn json_schema(_: &mut SchemaGenerator) -> Schema {
127 Schema::Bool(true)
128 }
129}
130
131#[derive(Debug)]
132#[must_use]
133pub enum KeymapFileLoadResult {
134 Success {
135 key_bindings: Vec<KeyBinding>,
136 },
137 SomeFailedToLoad {
138 key_bindings: Vec<KeyBinding>,
139 error_message: MarkdownString,
140 },
141 JsonParseFailure {
142 error: anyhow::Error,
143 },
144}
145
146impl KeymapFile {
147 pub fn parse(content: &str) -> anyhow::Result<Self> {
148 parse_json_with_comments::<Self>(content)
149 }
150
151 pub fn load_asset(asset_path: &str, cx: &App) -> anyhow::Result<Vec<KeyBinding>> {
152 match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
153 KeymapFileLoadResult::Success { key_bindings } => Ok(key_bindings),
154 KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => Err(anyhow!(
155 "Error loading built-in keymap \"{asset_path}\": {error_message}"
156 )),
157 KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
158 "JSON parse error in built-in keymap \"{asset_path}\": {error}"
159 )),
160 }
161 }
162
163 #[cfg(feature = "test-support")]
164 pub fn load_asset_allow_partial_failure(
165 asset_path: &str,
166 cx: &App,
167 ) -> anyhow::Result<Vec<KeyBinding>> {
168 match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
169 KeymapFileLoadResult::SomeFailedToLoad {
170 key_bindings,
171 error_message,
172 ..
173 } if key_bindings.is_empty() => Err(anyhow!(
174 "Error loading built-in keymap \"{asset_path}\": {error_message}"
175 )),
176 KeymapFileLoadResult::Success { key_bindings, .. }
177 | KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
178 KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
179 "JSON parse error in built-in keymap \"{asset_path}\": {error}"
180 )),
181 }
182 }
183
184 #[cfg(feature = "test-support")]
185 pub fn load_panic_on_failure(content: &str, cx: &App) -> Vec<KeyBinding> {
186 match Self::load(content, cx) {
187 KeymapFileLoadResult::Success { key_bindings, .. } => key_bindings,
188 KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
189 panic!("{error_message}");
190 }
191 KeymapFileLoadResult::JsonParseFailure { error } => {
192 panic!("JSON parse error: {error}");
193 }
194 }
195 }
196
197 pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
198 let key_equivalents =
199 crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id());
200
201 if content.is_empty() {
202 return KeymapFileLoadResult::Success {
203 key_bindings: Vec::new(),
204 };
205 }
206 let keymap_file = match parse_json_with_comments::<Self>(content) {
207 Ok(keymap_file) => keymap_file,
208 Err(error) => {
209 return KeymapFileLoadResult::JsonParseFailure { error };
210 }
211 };
212
213 // Accumulate errors in order to support partial load of user keymap in the presence of
214 // errors in context and binding parsing.
215 let mut errors = Vec::new();
216 let mut key_bindings = Vec::new();
217
218 for KeymapSection {
219 context,
220 use_key_equivalents,
221 bindings,
222 unrecognized_fields,
223 } in keymap_file.0.iter()
224 {
225 let context_predicate: Option<Rc<KeyBindingContextPredicate>> = if context.is_empty() {
226 None
227 } else {
228 match KeyBindingContextPredicate::parse(context) {
229 Ok(context_predicate) => Some(context_predicate.into()),
230 Err(err) => {
231 // Leading space is to separate from the message indicating which section
232 // the error occurred in.
233 errors.push((
234 context,
235 format!(" Parse error in section `context` field: {}", err),
236 ));
237 continue;
238 }
239 }
240 };
241
242 let key_equivalents = if *use_key_equivalents {
243 key_equivalents.as_ref()
244 } else {
245 None
246 };
247
248 let mut section_errors = String::new();
249
250 if !unrecognized_fields.is_empty() {
251 write!(
252 section_errors,
253 "\n\n - Unrecognized fields: {}",
254 MarkdownString::inline_code(&format!("{:?}", unrecognized_fields.keys()))
255 )
256 .unwrap();
257 }
258
259 if let Some(bindings) = bindings {
260 for (keystrokes, action) in bindings {
261 let result = Self::load_keybinding(
262 keystrokes,
263 action,
264 context_predicate.clone(),
265 key_equivalents,
266 cx,
267 );
268 match result {
269 Ok(key_binding) => {
270 key_bindings.push(key_binding);
271 }
272 Err(err) => {
273 let mut lines = err.lines();
274 let mut indented_err = lines.next().unwrap().to_string();
275 for line in lines {
276 indented_err.push_str(" ");
277 indented_err.push_str(line);
278 indented_err.push_str("\n");
279 }
280 write!(
281 section_errors,
282 "\n\n- In binding {}, {indented_err}",
283 inline_code_string(keystrokes),
284 )
285 .unwrap();
286 }
287 }
288 }
289 }
290
291 if !section_errors.is_empty() {
292 errors.push((context, section_errors))
293 }
294 }
295
296 if errors.is_empty() {
297 KeymapFileLoadResult::Success { key_bindings }
298 } else {
299 let mut error_message = "Errors in user keymap file.\n".to_owned();
300 for (context, section_errors) in errors {
301 if context.is_empty() {
302 write!(error_message, "\n\nIn section without context predicate:").unwrap()
303 } else {
304 write!(
305 error_message,
306 "\n\nIn section with {}:",
307 MarkdownString::inline_code(&format!("context = \"{}\"", context))
308 )
309 .unwrap()
310 }
311 write!(error_message, "{section_errors}").unwrap();
312 }
313 KeymapFileLoadResult::SomeFailedToLoad {
314 key_bindings,
315 error_message: MarkdownString(error_message),
316 }
317 }
318 }
319
320 fn load_keybinding(
321 keystrokes: &str,
322 action: &KeymapAction,
323 context: Option<Rc<KeyBindingContextPredicate>>,
324 key_equivalents: Option<&HashMap<char, char>>,
325 cx: &App,
326 ) -> std::result::Result<KeyBinding, String> {
327 let (build_result, action_input_string) = match &action.0 {
328 Value::Array(items) => {
329 if items.len() != 2 {
330 return Err(format!(
331 "expected two-element array of `[name, input]`. \
332 Instead found {}.",
333 MarkdownString::inline_code(&action.0.to_string())
334 ));
335 }
336 let serde_json::Value::String(ref name) = items[0] else {
337 return Err(format!(
338 "expected two-element array of `[name, input]`, \
339 but the first element is not a string in {}.",
340 MarkdownString::inline_code(&action.0.to_string())
341 ));
342 };
343 let action_input = items[1].clone();
344 let action_input_string = action_input.to_string();
345 (
346 cx.build_action(&name, Some(action_input)),
347 Some(action_input_string),
348 )
349 }
350 Value::String(name) => (cx.build_action(&name, None), None),
351 Value::Null => (Ok(NoAction.boxed_clone()), None),
352 _ => {
353 return Err(format!(
354 "expected two-element array of `[name, input]`. \
355 Instead found {}.",
356 MarkdownString::inline_code(&action.0.to_string())
357 ));
358 }
359 };
360
361 let action = match build_result {
362 Ok(action) => action,
363 Err(ActionBuildError::NotFound { name }) => {
364 return Err(format!(
365 "didn't find an action named {}.",
366 inline_code_string(&name)
367 ));
368 }
369 Err(ActionBuildError::BuildError { name, error }) => match action_input_string {
370 Some(action_input_string) => {
371 return Err(format!(
372 "can't build {} action from input value {}: {}",
373 inline_code_string(&name),
374 MarkdownString::inline_code(&action_input_string),
375 MarkdownString::escape(&error.to_string())
376 ));
377 }
378 None => {
379 return Err(format!(
380 "can't build {} action - it requires input data via [name, input]: {}",
381 inline_code_string(&name),
382 MarkdownString::escape(&error.to_string())
383 ));
384 }
385 },
386 };
387
388 let key_binding = match KeyBinding::load(keystrokes, action, context, key_equivalents) {
389 Ok(key_binding) => key_binding,
390 Err(InvalidKeystrokeError { keystroke }) => {
391 return Err(format!(
392 "invalid keystroke {}. {}",
393 inline_code_string(&keystroke),
394 KEYSTROKE_PARSE_EXPECTED_MESSAGE
395 ));
396 }
397 };
398
399 if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) {
400 match validator.validate(&key_binding) {
401 Ok(()) => Ok(key_binding),
402 Err(error) => Err(error.0),
403 }
404 } else {
405 Ok(key_binding)
406 }
407 }
408
409 pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
410 let mut generator = SchemaSettings::draft07()
411 .with(|settings| settings.option_add_null_type = false)
412 .into_generator();
413
414 let action_schemas = cx.action_schemas(&mut generator);
415 let deprecations = cx.action_deprecations();
416 KeymapFile::generate_json_schema(generator, action_schemas, deprecations)
417 }
418
419 fn generate_json_schema(
420 generator: SchemaGenerator,
421 action_schemas: Vec<(SharedString, Option<Schema>)>,
422 deprecations: &HashMap<SharedString, SharedString>,
423 ) -> serde_json::Value {
424 fn set<I, O>(input: I) -> Option<O>
425 where
426 I: Into<O>,
427 {
428 Some(input.into())
429 }
430
431 fn add_deprecation(schema_object: &mut SchemaObject, message: String) {
432 schema_object.extensions.insert(
433 // deprecationMessage is not part of the JSON Schema spec,
434 // but json-language-server recognizes it.
435 "deprecationMessage".to_owned(),
436 Value::String(message),
437 );
438 }
439
440 fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) {
441 add_deprecation(schema_object, format!("Deprecated, use {new_name}"));
442 }
443
444 fn add_description(schema_object: &mut SchemaObject, description: String) {
445 schema_object
446 .metadata
447 .get_or_insert(Default::default())
448 .description = Some(description);
449 }
450
451 let empty_object: SchemaObject = SchemaObject {
452 instance_type: set(InstanceType::Object),
453 ..Default::default()
454 };
455
456 // This is a workaround for a json-language-server issue where it matches the first
457 // alternative that matches the value's shape and uses that for documentation.
458 //
459 // In the case of the array validations, it would even provide an error saying that the name
460 // must match the name of the first alternative.
461 let mut plain_action = SchemaObject {
462 instance_type: set(InstanceType::String),
463 const_value: Some(Value::String("".to_owned())),
464 ..Default::default()
465 };
466 let no_action_message = "No action named this.";
467 add_description(&mut plain_action, no_action_message.to_owned());
468 add_deprecation(&mut plain_action, no_action_message.to_owned());
469 let mut matches_action_name = SchemaObject {
470 const_value: Some(Value::String("".to_owned())),
471 ..Default::default()
472 };
473 let no_action_message = "No action named this that takes input.";
474 add_description(&mut matches_action_name, no_action_message.to_owned());
475 add_deprecation(&mut matches_action_name, no_action_message.to_owned());
476 let action_with_input = SchemaObject {
477 instance_type: set(InstanceType::Array),
478 array: set(ArrayValidation {
479 items: set(vec![
480 matches_action_name.into(),
481 // Accept any value, as we want this to be the preferred match when there is a
482 // typo in the name.
483 Schema::Bool(true),
484 ]),
485 min_items: Some(2),
486 max_items: Some(2),
487 ..Default::default()
488 }),
489 ..Default::default()
490 };
491 let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()];
492
493 for (name, action_schema) in action_schemas.iter() {
494 let schema = if let Some(Schema::Object(schema)) = action_schema {
495 Some(schema.clone())
496 } else {
497 None
498 };
499
500 let description = schema.as_ref().and_then(|schema| {
501 schema
502 .metadata
503 .as_ref()
504 .and_then(|metadata| metadata.description.clone())
505 });
506
507 let deprecation = if name == NoAction.name() {
508 Some("null")
509 } else {
510 deprecations.get(name).map(|new_name| new_name.as_ref())
511 };
512
513 // Add an alternative for plain action names.
514 let mut plain_action = SchemaObject {
515 instance_type: set(InstanceType::String),
516 const_value: Some(Value::String(name.to_string())),
517 ..Default::default()
518 };
519 if let Some(new_name) = deprecation {
520 add_deprecation_preferred_name(&mut plain_action, new_name);
521 }
522 if let Some(description) = description.clone() {
523 add_description(&mut plain_action, description);
524 }
525 keymap_action_alternatives.push(plain_action.into());
526
527 // Add an alternative for actions with data specified as a [name, data] array.
528 //
529 // When a struct with no deserializable fields is added with impl_actions! /
530 // impl_actions_as! an empty object schema is produced. The action should be invoked
531 // without data in this case.
532 if let Some(schema) = schema {
533 if schema != empty_object {
534 let mut matches_action_name = SchemaObject {
535 const_value: Some(Value::String(name.to_string())),
536 ..Default::default()
537 };
538 if let Some(description) = description.clone() {
539 add_description(&mut matches_action_name, description.to_string());
540 }
541 if let Some(new_name) = deprecation {
542 add_deprecation_preferred_name(&mut matches_action_name, new_name);
543 }
544 let action_with_input = SchemaObject {
545 instance_type: set(InstanceType::Array),
546 array: set(ArrayValidation {
547 items: set(vec![matches_action_name.into(), schema.into()]),
548 min_items: Some(2),
549 max_items: Some(2),
550 ..Default::default()
551 }),
552 ..Default::default()
553 };
554 keymap_action_alternatives.push(action_with_input.into());
555 }
556 }
557 }
558
559 // Placing null first causes json-language-server to default assuming actions should be
560 // null, so place it last.
561 keymap_action_alternatives.push(
562 SchemaObject {
563 instance_type: set(InstanceType::Null),
564 ..Default::default()
565 }
566 .into(),
567 );
568
569 let action_schema = SchemaObject {
570 subschemas: set(SubschemaValidation {
571 one_of: Some(keymap_action_alternatives),
572 ..Default::default()
573 }),
574 ..Default::default()
575 }
576 .into();
577
578 // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so replacing
579 // the definition of `KeymapAction` results in the full action schema being used.
580 let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
581 root_schema
582 .definitions
583 .insert(KeymapAction::schema_name(), action_schema);
584
585 // This and other json schemas can be viewed via `debug: open language server logs` ->
586 // `json-language-server` -> `Server Info`.
587 serde_json::to_value(root_schema).unwrap()
588 }
589
590 pub fn sections(&self) -> impl DoubleEndedIterator<Item = &KeymapSection> {
591 self.0.iter()
592 }
593
594 pub async fn load_keymap_file(fs: &Arc<dyn Fs>) -> Result<String> {
595 match fs.load(paths::keymap_file()).await {
596 result @ Ok(_) => result,
597 Err(err) => {
598 if let Some(e) = err.downcast_ref::<std::io::Error>() {
599 if e.kind() == std::io::ErrorKind::NotFound {
600 return Ok(crate::initial_keymap_content().to_string());
601 }
602 }
603 Err(err)
604 }
605 }
606 }
607}
608
609// Double quotes a string and wraps it in backticks for markdown inline code..
610fn inline_code_string(text: &str) -> MarkdownString {
611 MarkdownString::inline_code(&format!("\"{}\"", text))
612}
613
614#[cfg(test)]
615mod tests {
616 use crate::KeymapFile;
617
618 #[test]
619 fn can_deserialize_keymap_with_trailing_comma() {
620 let json = indoc::indoc! {"[
621 // Standard macOS bindings
622 {
623 \"bindings\": {
624 \"up\": \"menu::SelectPrevious\",
625 },
626 },
627 ]
628 "
629 };
630 KeymapFile::parse(json).unwrap();
631 }
632}