1use crate::{settings_store::parse_json_with_comments, SettingsAssets};
2use anyhow::{anyhow, Context, Result};
3use collections::BTreeMap;
4use gpui::{keymap_matcher::Binding, AppContext};
5use schemars::{
6 gen::{SchemaGenerator, SchemaSettings},
7 schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
8 JsonSchema,
9};
10use serde::Deserialize;
11use serde_json::Value;
12use util::{asset_str, ResultExt};
13
14#[derive(Deserialize, Default, Clone, JsonSchema)]
15#[serde(transparent)]
16pub struct KeymapFile(Vec<KeymapBlock>);
17
18#[derive(Deserialize, Default, Clone, JsonSchema)]
19pub struct KeymapBlock {
20 #[serde(default)]
21 context: Option<String>,
22 bindings: BTreeMap<String, KeymapAction>,
23}
24
25#[derive(Deserialize, Default, Clone)]
26#[serde(transparent)]
27pub struct KeymapAction(Value);
28
29impl JsonSchema for KeymapAction {
30 fn schema_name() -> String {
31 "KeymapAction".into()
32 }
33
34 fn json_schema(_: &mut SchemaGenerator) -> Schema {
35 Schema::Bool(true)
36 }
37}
38
39#[derive(Deserialize)]
40struct ActionWithData(Box<str>, Value);
41
42impl KeymapFile {
43 pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
44 let content = asset_str::<SettingsAssets>(asset_path);
45
46 Self::parse(content.as_ref())?.add_to_cx(cx)
47 }
48
49 pub fn parse(content: &str) -> Result<Self> {
50 parse_json_with_comments::<Self>(content)
51 }
52
53 pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {
54 for KeymapBlock { context, bindings } in self.0 {
55 let bindings = bindings
56 .into_iter()
57 .filter_map(|(keystroke, action)| {
58 let action = action.0;
59
60 // This is a workaround for a limitation in serde: serde-rs/json#497
61 // We want to deserialize the action data as a `RawValue` so that we can
62 // deserialize the action itself dynamically directly from the JSON
63 // string. But `RawValue` currently does not work inside of an untagged enum.
64 if let Value::Array(items) = action {
65 let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
66 return Some(Err(anyhow!("Expected array of length 2")));
67 };
68 let serde_json::Value::String(name) = name else {
69 return Some(Err(anyhow!("Expected first item in array to be a string.")))
70 };
71 cx.deserialize_action(
72 &name,
73 Some(data),
74 )
75 } else if let Value::String(name) = action {
76 cx.deserialize_action(&name, None)
77 } else {
78 return Some(Err(anyhow!("Expected two-element array, got {:?}", action)));
79 }
80 .with_context(|| {
81 format!(
82 "invalid binding value for keystroke {keystroke}, context {context:?}"
83 )
84 })
85 .log_err()
86 .map(|action| Binding::load(&keystroke, action, context.as_deref()))
87 })
88 .collect::<Result<Vec<_>>>()?;
89
90 cx.add_bindings(bindings);
91 }
92 Ok(())
93 }
94
95 pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value {
96 let mut root_schema = SchemaSettings::draft07()
97 .with(|settings| settings.option_add_null_type = false)
98 .into_generator()
99 .into_root_schema_for::<KeymapFile>();
100
101 let action_schema = Schema::Object(SchemaObject {
102 subschemas: Some(Box::new(SubschemaValidation {
103 one_of: Some(vec![
104 Schema::Object(SchemaObject {
105 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
106 enum_values: Some(
107 action_names
108 .iter()
109 .map(|name| Value::String(name.to_string()))
110 .collect(),
111 ),
112 ..Default::default()
113 }),
114 Schema::Object(SchemaObject {
115 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
116 ..Default::default()
117 }),
118 ]),
119 ..Default::default()
120 })),
121 ..Default::default()
122 });
123
124 root_schema
125 .definitions
126 .insert("KeymapAction".to_owned(), action_schema);
127
128 serde_json::to_value(root_schema).unwrap()
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use crate::KeymapFile;
135
136 #[test]
137 fn can_deserialize_keymap_with_trailing_comma() {
138 let json = indoc::indoc! {"[
139 // Standard macOS bindings
140 {
141 \"bindings\": {
142 \"up\": \"menu::SelectPrev\",
143 },
144 },
145 ]
146 "
147
148 };
149 KeymapFile::parse(json).unwrap();
150 }
151}