1use anyhow::{anyhow, Result};
2use collections::{BTreeMap, HashMap, IndexMap};
3use fs::Fs;
4use gpui::{
5 Action, ActionBuildError, App, InvalidKeystrokeError, KeyBinding, KeyBindingContextPredicate,
6 NoAction, SharedString, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
7};
8use schemars::{
9 gen::{SchemaGenerator, SchemaSettings},
10 schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation},
11 JsonSchema,
12};
13use serde::{Deserialize, Serialize};
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::{settings_store::parse_json_with_comments, SettingsAssets};
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, Serialize)]
51#[serde(transparent)]
52pub struct KeymapFile(Vec<KeymapSection>);
53
54/// Keymap section which binds keystrokes to actions.
55#[derive(Debug, Deserialize, Default, Clone, JsonSchema, Serialize)]
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, Serialize)]
101#[serde(transparent)]
102pub struct KeymapAction(pub(crate) 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 keymap_file: KeymapFile,
137 },
138 SomeFailedToLoad {
139 key_bindings: Vec<KeyBinding>,
140 keymap_file: KeymapFile,
141 error_message: MarkdownString,
142 },
143 JsonParseFailure {
144 error: anyhow::Error,
145 },
146}
147
148impl KeymapFile {
149 pub fn parse(content: &str) -> anyhow::Result<Self> {
150 parse_json_with_comments::<Self>(content)
151 }
152
153 pub fn load_asset(asset_path: &str, cx: &App) -> anyhow::Result<Vec<KeyBinding>> {
154 match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
155 KeymapFileLoadResult::Success { key_bindings, .. } => Ok(key_bindings),
156 KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => Err(anyhow!(
157 "Error loading built-in keymap \"{asset_path}\": {error_message}"
158 )),
159 KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
160 "JSON parse error in built-in keymap \"{asset_path}\": {error}"
161 )),
162 }
163 }
164
165 #[cfg(feature = "test-support")]
166 pub fn load_asset_allow_partial_failure(
167 asset_path: &str,
168 cx: &App,
169 ) -> anyhow::Result<Vec<KeyBinding>> {
170 match Self::load(asset_str::<SettingsAssets>(asset_path).as_ref(), cx) {
171 KeymapFileLoadResult::SomeFailedToLoad {
172 key_bindings,
173 error_message,
174 ..
175 } if key_bindings.is_empty() => Err(anyhow!(
176 "Error loading built-in keymap \"{asset_path}\": {error_message}"
177 )),
178 KeymapFileLoadResult::Success { key_bindings, .. }
179 | KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings),
180 KeymapFileLoadResult::JsonParseFailure { error } => Err(anyhow!(
181 "JSON parse error in built-in keymap \"{asset_path}\": {error}"
182 )),
183 }
184 }
185
186 #[cfg(feature = "test-support")]
187 pub fn load_panic_on_failure(content: &str, cx: &App) -> Vec<KeyBinding> {
188 match Self::load(content, cx) {
189 KeymapFileLoadResult::Success { key_bindings, .. } => key_bindings,
190 KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => {
191 panic!("{error_message}");
192 }
193 KeymapFileLoadResult::JsonParseFailure { error } => {
194 panic!("JSON parse error: {error}");
195 }
196 }
197 }
198
199 pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
200 let key_equivalents = crate::key_equivalents::get_key_equivalents(&cx.keyboard_layout());
201
202 if content.is_empty() {
203 return KeymapFileLoadResult::Success {
204 key_bindings: Vec::new(),
205 keymap_file: KeymapFile(Vec::new()),
206 };
207 }
208 let keymap_file = match parse_json_with_comments::<Self>(content) {
209 Ok(keymap_file) => keymap_file,
210 Err(error) => {
211 return KeymapFileLoadResult::JsonParseFailure { error };
212 }
213 };
214
215 // Accumulate errors in order to support partial load of user keymap in the presence of
216 // errors in context and binding parsing.
217 let mut errors = Vec::new();
218 let mut key_bindings = Vec::new();
219
220 for KeymapSection {
221 context,
222 use_key_equivalents,
223 bindings,
224 unrecognized_fields,
225 } in keymap_file.0.iter()
226 {
227 let context_predicate: Option<Rc<KeyBindingContextPredicate>> = if context.is_empty() {
228 None
229 } else {
230 match KeyBindingContextPredicate::parse(context) {
231 Ok(context_predicate) => Some(context_predicate.into()),
232 Err(err) => {
233 // Leading space is to separate from the message indicating which section
234 // the error occurred in.
235 errors.push((
236 context,
237 format!(" Parse error in section `context` field: {}", err),
238 ));
239 continue;
240 }
241 }
242 };
243
244 let key_equivalents = if *use_key_equivalents {
245 key_equivalents.as_ref()
246 } else {
247 None
248 };
249
250 let mut section_errors = String::new();
251
252 if !unrecognized_fields.is_empty() {
253 write!(
254 section_errors,
255 "\n\n - Unrecognized fields: {}",
256 MarkdownString::inline_code(&format!("{:?}", unrecognized_fields.keys()))
257 )
258 .unwrap();
259 }
260
261 if let Some(bindings) = bindings {
262 for (keystrokes, action) in bindings {
263 let result = Self::load_keybinding(
264 keystrokes,
265 action,
266 context_predicate.clone(),
267 key_equivalents,
268 cx,
269 );
270 match result {
271 Ok(key_binding) => {
272 key_bindings.push(key_binding);
273 }
274 Err(err) => {
275 let mut lines = err.lines();
276 let mut indented_err = lines.next().unwrap().to_string();
277 for line in lines {
278 indented_err.push_str(" ");
279 indented_err.push_str(line);
280 indented_err.push_str("\n");
281 }
282 write!(
283 section_errors,
284 "\n\n- In binding {}, {indented_err}",
285 inline_code_string(keystrokes),
286 )
287 .unwrap();
288 }
289 }
290 }
291 }
292
293 if !section_errors.is_empty() {
294 errors.push((context, section_errors))
295 }
296 }
297
298 if errors.is_empty() {
299 KeymapFileLoadResult::Success {
300 key_bindings,
301 keymap_file,
302 }
303 } else {
304 let mut error_message = "Errors in user keymap file.\n".to_owned();
305 for (context, section_errors) in errors {
306 if context.is_empty() {
307 write!(error_message, "\n\nIn section without context predicate:").unwrap()
308 } else {
309 write!(
310 error_message,
311 "\n\nIn section with {}:",
312 MarkdownString::inline_code(&format!("context = \"{}\"", context))
313 )
314 .unwrap()
315 }
316 write!(error_message, "{section_errors}").unwrap();
317 }
318 KeymapFileLoadResult::SomeFailedToLoad {
319 key_bindings,
320 keymap_file,
321 error_message: MarkdownString(error_message),
322 }
323 }
324 }
325
326 fn load_keybinding(
327 keystrokes: &str,
328 action: &KeymapAction,
329 context: Option<Rc<KeyBindingContextPredicate>>,
330 key_equivalents: Option<&HashMap<char, char>>,
331 cx: &App,
332 ) -> std::result::Result<KeyBinding, String> {
333 let (build_result, action_input_string) = match &action.0 {
334 Value::Array(items) => {
335 if items.len() != 2 {
336 return Err(format!(
337 "expected two-element array of `[name, input]`. \
338 Instead found {}.",
339 MarkdownString::inline_code(&action.0.to_string())
340 ));
341 }
342 let serde_json::Value::String(ref name) = items[0] else {
343 return Err(format!(
344 "expected two-element array of `[name, input]`, \
345 but the first element is not a string in {}.",
346 MarkdownString::inline_code(&action.0.to_string())
347 ));
348 };
349 let action_input = items[1].clone();
350 let action_input_string = action_input.to_string();
351 (
352 cx.build_action(&name, Some(action_input)),
353 Some(action_input_string),
354 )
355 }
356 Value::String(name) => (cx.build_action(&name, None), None),
357 Value::Null => (Ok(NoAction.boxed_clone()), None),
358 _ => {
359 return Err(format!(
360 "expected two-element array of `[name, input]`. \
361 Instead found {}.",
362 MarkdownString::inline_code(&action.0.to_string())
363 ));
364 }
365 };
366
367 let action = match build_result {
368 Ok(action) => action,
369 Err(ActionBuildError::NotFound { name }) => {
370 return Err(format!(
371 "didn't find an action named {}.",
372 inline_code_string(&name)
373 ))
374 }
375 Err(ActionBuildError::BuildError { name, error }) => match action_input_string {
376 Some(action_input_string) => {
377 return Err(format!(
378 "can't build {} action from input value {}: {}",
379 inline_code_string(&name),
380 MarkdownString::inline_code(&action_input_string),
381 MarkdownString::escape(&error.to_string())
382 ))
383 }
384 None => {
385 return Err(format!(
386 "can't build {} action - it requires input data via [name, input]: {}",
387 inline_code_string(&name),
388 MarkdownString::escape(&error.to_string())
389 ))
390 }
391 },
392 };
393
394 let key_binding = match KeyBinding::load(keystrokes, action, context, key_equivalents) {
395 Ok(key_binding) => key_binding,
396 Err(InvalidKeystrokeError { keystroke }) => {
397 return Err(format!(
398 "invalid keystroke {}. {}",
399 inline_code_string(&keystroke),
400 KEYSTROKE_PARSE_EXPECTED_MESSAGE
401 ))
402 }
403 };
404
405 if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) {
406 match validator.validate(&key_binding) {
407 Ok(()) => Ok(key_binding),
408 Err(error) => Err(error.0),
409 }
410 } else {
411 Ok(key_binding)
412 }
413 }
414
415 pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
416 let mut generator = SchemaSettings::draft07()
417 .with(|settings| settings.option_add_null_type = false)
418 .into_generator();
419
420 let action_schemas = cx.action_schemas(&mut generator);
421 let deprecations = cx.action_deprecations();
422 KeymapFile::generate_json_schema(generator, action_schemas, deprecations)
423 }
424
425 fn generate_json_schema(
426 generator: SchemaGenerator,
427 action_schemas: Vec<(SharedString, Option<Schema>)>,
428 deprecations: &HashMap<SharedString, SharedString>,
429 ) -> serde_json::Value {
430 fn set<I, O>(input: I) -> Option<O>
431 where
432 I: Into<O>,
433 {
434 Some(input.into())
435 }
436
437 fn add_deprecation(schema_object: &mut SchemaObject, message: String) {
438 schema_object.extensions.insert(
439 // deprecationMessage is not part of the JSON Schema spec,
440 // but json-language-server recognizes it.
441 "deprecationMessage".to_owned(),
442 Value::String(message),
443 );
444 }
445
446 fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) {
447 add_deprecation(schema_object, format!("Deprecated, use {new_name}"));
448 }
449
450 fn add_description(schema_object: &mut SchemaObject, description: String) {
451 schema_object
452 .metadata
453 .get_or_insert(Default::default())
454 .description = Some(description);
455 }
456
457 let empty_object: SchemaObject = SchemaObject {
458 instance_type: set(InstanceType::Object),
459 ..Default::default()
460 };
461
462 // This is a workaround for a json-language-server issue where it matches the first
463 // alternative that matches the value's shape and uses that for documentation.
464 //
465 // In the case of the array validations, it would even provide an error saying that the name
466 // must match the name of the first alternative.
467 let mut plain_action = SchemaObject {
468 instance_type: set(InstanceType::String),
469 const_value: Some(Value::String("".to_owned())),
470 ..Default::default()
471 };
472 let no_action_message = "No action named this.";
473 add_description(&mut plain_action, no_action_message.to_owned());
474 add_deprecation(&mut plain_action, no_action_message.to_owned());
475 let mut matches_action_name = SchemaObject {
476 const_value: Some(Value::String("".to_owned())),
477 ..Default::default()
478 };
479 let no_action_message = "No action named this that takes input.";
480 add_description(&mut matches_action_name, no_action_message.to_owned());
481 add_deprecation(&mut matches_action_name, no_action_message.to_owned());
482 let action_with_input = SchemaObject {
483 instance_type: set(InstanceType::Array),
484 array: set(ArrayValidation {
485 items: set(vec![
486 matches_action_name.into(),
487 // Accept any value, as we want this to be the preferred match when there is a
488 // typo in the name.
489 Schema::Bool(true),
490 ]),
491 min_items: Some(2),
492 max_items: Some(2),
493 ..Default::default()
494 }),
495 ..Default::default()
496 };
497 let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()];
498
499 for (name, action_schema) in action_schemas.iter() {
500 let schema = if let Some(Schema::Object(schema)) = action_schema {
501 Some(schema.clone())
502 } else {
503 None
504 };
505
506 let description = schema.as_ref().and_then(|schema| {
507 schema
508 .metadata
509 .as_ref()
510 .and_then(|metadata| metadata.description.clone())
511 });
512
513 let deprecation = if name == NoAction.name() {
514 Some("null")
515 } else {
516 deprecations.get(name).map(|new_name| new_name.as_ref())
517 };
518
519 // Add an alternative for plain action names.
520 let mut plain_action = SchemaObject {
521 instance_type: set(InstanceType::String),
522 const_value: Some(Value::String(name.to_string())),
523 ..Default::default()
524 };
525 if let Some(new_name) = deprecation {
526 add_deprecation_preferred_name(&mut plain_action, new_name);
527 }
528 if let Some(description) = description.clone() {
529 add_description(&mut plain_action, description);
530 }
531 keymap_action_alternatives.push(plain_action.into());
532
533 // Add an alternative for actions with data specified as a [name, data] array.
534 //
535 // When a struct with no deserializable fields is added with impl_actions! /
536 // impl_actions_as! an empty object schema is produced. The action should be invoked
537 // without data in this case.
538 if let Some(schema) = schema {
539 if schema != empty_object {
540 let mut matches_action_name = SchemaObject {
541 const_value: Some(Value::String(name.to_string())),
542 ..Default::default()
543 };
544 if let Some(description) = description.clone() {
545 add_description(&mut matches_action_name, description.to_string());
546 }
547 if let Some(new_name) = deprecation {
548 add_deprecation_preferred_name(&mut matches_action_name, new_name);
549 }
550 let action_with_input = SchemaObject {
551 instance_type: set(InstanceType::Array),
552 array: set(ArrayValidation {
553 items: set(vec![matches_action_name.into(), schema.into()]),
554 min_items: Some(2),
555 max_items: Some(2),
556 ..Default::default()
557 }),
558 ..Default::default()
559 };
560 keymap_action_alternatives.push(action_with_input.into());
561 }
562 }
563 }
564
565 // Placing null first causes json-language-server to default assuming actions should be
566 // null, so place it last.
567 keymap_action_alternatives.push(
568 SchemaObject {
569 instance_type: set(InstanceType::Null),
570 ..Default::default()
571 }
572 .into(),
573 );
574
575 let action_schema = SchemaObject {
576 subschemas: set(SubschemaValidation {
577 one_of: Some(keymap_action_alternatives),
578 ..Default::default()
579 }),
580 ..Default::default()
581 }
582 .into();
583
584 // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so replacing
585 // the definition of `KeymapAction` results in the full action schema being used.
586 let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
587 root_schema
588 .definitions
589 .insert(KeymapAction::schema_name(), action_schema);
590
591 // This and other json schemas can be viewed via `debug: open language server logs` ->
592 // `json-language-server` -> `Server Info`.
593 serde_json::to_value(root_schema).unwrap()
594 }
595
596 pub fn sections(&self) -> impl DoubleEndedIterator<Item = &KeymapSection> {
597 self.0.iter()
598 }
599
600 pub async fn load_keymap_file(fs: &Arc<dyn Fs>) -> Result<String> {
601 match fs.load(paths::keymap_file()).await {
602 result @ Ok(_) => result,
603 Err(err) => {
604 if let Some(e) = err.downcast_ref::<std::io::Error>() {
605 if e.kind() == std::io::ErrorKind::NotFound {
606 return Ok(crate::initial_keymap_content().to_string());
607 }
608 }
609 Err(err)
610 }
611 }
612 }
613}
614
615// Double quotes a string and wraps it in backticks for markdown inline code..
616fn inline_code_string(text: &str) -> MarkdownString {
617 MarkdownString::inline_code(&format!("\"{}\"", text))
618}
619
620#[cfg(test)]
621mod tests {
622 use super::KeymapFile;
623
624 #[test]
625 fn can_deserialize_keymap_with_trailing_comma() {
626 let json = indoc::indoc! {"[
627 // Standard macOS bindings
628 {
629 \"bindings\": {
630 \"up\": \"menu::SelectPrev\",
631 },
632 },
633 ]
634 "
635 };
636 KeymapFile::parse(json).unwrap();
637 }
638}