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