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