1use crate::{settings_store::parse_json_with_comments, SettingsAssets};
2use anyhow::{anyhow, Context, Result};
3use collections::BTreeMap;
4use gpui::{Action, AppContext, KeyBinding, SharedString};
5use schemars::{
6 gen::{SchemaGenerator, SchemaSettings},
7 schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
8 JsonSchema, Map,
9};
10use serde::Deserialize;
11use serde_json::Value;
12use util::{asset_str, ResultExt};
13
14#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
15#[serde(transparent)]
16pub struct KeymapFile(Vec<KeymapBlock>);
17
18#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
19pub struct KeymapBlock {
20 #[serde(default)]
21 context: Option<String>,
22 #[serde(default)]
23 use_key_equivalents: Option<bool>,
24 bindings: BTreeMap<String, KeymapAction>,
25}
26
27impl KeymapBlock {
28 pub fn context(&self) -> Option<&str> {
29 self.context.as_deref()
30 }
31
32 pub fn bindings(&self) -> &BTreeMap<String, KeymapAction> {
33 &self.bindings
34 }
35}
36
37#[derive(Debug, Deserialize, Default, Clone)]
38#[serde(transparent)]
39pub struct KeymapAction(Value);
40
41impl std::fmt::Display for KeymapAction {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match &self.0 {
44 Value::String(s) => write!(f, "{}", s),
45 Value::Array(arr) => {
46 let strings: Vec<String> = arr.iter().map(|v| v.to_string()).collect();
47 write!(f, "{}", strings.join(", "))
48 }
49 _ => write!(f, "{}", self.0),
50 }
51 }
52}
53
54impl JsonSchema for KeymapAction {
55 fn schema_name() -> String {
56 "KeymapAction".into()
57 }
58
59 fn json_schema(_: &mut SchemaGenerator) -> Schema {
60 Schema::Bool(true)
61 }
62}
63
64impl KeymapFile {
65 pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
66 let content = asset_str::<SettingsAssets>(asset_path);
67
68 Self::parse(content.as_ref())?.add_to_cx(cx)
69 }
70
71 pub fn parse(content: &str) -> Result<Self> {
72 if content.is_empty() {
73 return Ok(Self::default());
74 }
75 parse_json_with_comments::<Self>(content)
76 }
77
78 pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {
79 let key_equivalents = crate::key_equivalents::get_key_equivalents(&cx.keyboard_layout());
80
81 for KeymapBlock {
82 context,
83 use_key_equivalents,
84 bindings,
85 } in self.0
86 {
87 let bindings = bindings
88 .into_iter()
89 .filter_map(|(keystroke, action)| {
90 let action = action.0;
91
92 // This is a workaround for a limitation in serde: serde-rs/json#497
93 // We want to deserialize the action data as a `RawValue` so that we can
94 // deserialize the action itself dynamically directly from the JSON
95 // string. But `RawValue` currently does not work inside of an untagged enum.
96 match action {
97 Value::Array(items) => {
98 let Ok([name, data]): Result<[serde_json::Value; 2], _> =
99 items.try_into()
100 else {
101 return Some(Err(anyhow!("Expected array of length 2")));
102 };
103 let serde_json::Value::String(name) = name else {
104 return Some(Err(anyhow!(
105 "Expected first item in array to be a string."
106 )));
107 };
108 cx.build_action(&name, Some(data))
109 }
110 Value::String(name) => cx.build_action(&name, None),
111 Value::Null => Ok(no_action()),
112 _ => {
113 return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
114 }
115 }
116 .with_context(|| {
117 format!(
118 "invalid binding value for keystroke {keystroke}, context {context:?}"
119 )
120 })
121 .log_err()
122 .map(|action| {
123 KeyBinding::load(
124 &keystroke,
125 action,
126 context.as_deref(),
127 if use_key_equivalents.unwrap_or_default() {
128 key_equivalents.as_ref()
129 } else {
130 None
131 },
132 )
133 })
134 })
135 .collect::<Result<Vec<_>>>()?;
136
137 cx.bind_keys(bindings);
138 }
139 Ok(())
140 }
141
142 pub fn generate_json_schema(
143 action_names: &[SharedString],
144 deprecations: &[(SharedString, SharedString)],
145 ) -> serde_json::Value {
146 let mut root_schema = SchemaSettings::draft07()
147 .with(|settings| settings.option_add_null_type = false)
148 .into_generator()
149 .into_root_schema_for::<KeymapFile>();
150
151 let mut alternatives = vec![
152 Schema::Object(SchemaObject {
153 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
154 enum_values: Some(
155 action_names
156 .iter()
157 .map(|name| Value::String(name.to_string()))
158 .collect(),
159 ),
160 ..Default::default()
161 }),
162 Schema::Object(SchemaObject {
163 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
164 ..Default::default()
165 }),
166 Schema::Object(SchemaObject {
167 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
168 ..Default::default()
169 }),
170 ];
171 for (old, new) in deprecations {
172 alternatives.push(Schema::Object(SchemaObject {
173 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
174 const_value: Some(Value::String(old.to_string())),
175 extensions: Map::from_iter([(
176 // deprecationMessage is not part of the JSON Schema spec,
177 // but json-language-server recognizes it.
178 "deprecationMessage".to_owned(),
179 format!("Deprecated, use {new}").into(),
180 )]),
181 ..Default::default()
182 }));
183 }
184 let action_schema = Schema::Object(SchemaObject {
185 subschemas: Some(Box::new(SubschemaValidation {
186 one_of: Some(alternatives),
187 ..Default::default()
188 })),
189 ..Default::default()
190 });
191
192 root_schema
193 .definitions
194 .insert("KeymapAction".to_owned(), action_schema);
195
196 serde_json::to_value(root_schema).unwrap()
197 }
198
199 pub fn blocks(&self) -> &[KeymapBlock] {
200 &self.0
201 }
202}
203
204fn no_action() -> Box<dyn gpui::Action> {
205 gpui::NoAction.boxed_clone()
206}
207
208#[cfg(test)]
209mod tests {
210 use crate::KeymapFile;
211
212 #[test]
213 fn can_deserialize_keymap_with_trailing_comma() {
214 let json = indoc::indoc! {"[
215 // Standard macOS bindings
216 {
217 \"bindings\": {
218 \"up\": \"menu::SelectPrev\",
219 },
220 },
221 ]
222 "
223
224 };
225 KeymapFile::parse(json).unwrap();
226 }
227}