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