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