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