1use crate::{settings_store::parse_json_with_comments, SettingsAssets};
2use anyhow::{anyhow, Context, Result};
3use collections::{BTreeMap, HashMap};
4use gpui::{Action, AppContext, KeyBinding, SharedString};
5use schemars::{
6 gen::SchemaGenerator,
7 schema::{ArrayValidation, InstanceType, Metadata, Schema, SchemaObject, SubschemaValidation},
8 JsonSchema,
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 generator: SchemaGenerator,
144 action_schemas: Vec<(SharedString, Option<Schema>)>,
145 deprecations: &HashMap<SharedString, SharedString>,
146 ) -> serde_json::Value {
147 fn set<I, O>(input: I) -> Option<O>
148 where
149 I: Into<O>,
150 {
151 Some(input.into())
152 }
153
154 fn add_deprecation_notice(schema_object: &mut SchemaObject, new_name: &SharedString) {
155 schema_object.extensions.insert(
156 // deprecationMessage is not part of the JSON Schema spec,
157 // but json-language-server recognizes it.
158 "deprecationMessage".to_owned(),
159 format!("Deprecated, use {new_name}").into(),
160 );
161 }
162
163 let empty_object: SchemaObject = SchemaObject {
164 instance_type: set(InstanceType::Object),
165 ..Default::default()
166 };
167
168 let mut keymap_action_alternatives = Vec::new();
169 for (name, action_schema) in action_schemas.iter() {
170 let schema = if let Some(Schema::Object(schema)) = action_schema {
171 Some(schema.clone())
172 } else {
173 None
174 };
175
176 // If the type has a description, also apply it to the value. Ideally it would be
177 // removed and applied to the overall array, but `json-language-server` does not show
178 // these descriptions.
179 let description = schema.as_ref().and_then(|schema| {
180 schema
181 .metadata
182 .as_ref()
183 .and_then(|metadata| metadata.description.as_ref())
184 });
185 let mut matches_action_name = SchemaObject {
186 const_value: Some(Value::String(name.to_string())),
187 ..Default::default()
188 };
189 if let Some(description) = description {
190 matches_action_name.metadata = set(Metadata {
191 description: Some(description.clone()),
192 ..Default::default()
193 });
194 }
195
196 // Add an alternative for plain action names.
197 let deprecation = deprecations.get(name);
198 let mut plain_action = SchemaObject {
199 instance_type: set(InstanceType::String),
200 const_value: Some(Value::String(name.to_string())),
201 ..Default::default()
202 };
203 if let Some(new_name) = deprecation {
204 add_deprecation_notice(&mut plain_action, new_name);
205 }
206 keymap_action_alternatives.push(plain_action.into());
207
208 // When all fields are skipped or an empty struct is added with impl_actions! /
209 // impl_actions_as! an empty struct is produced. The action should be invoked without
210 // data in this case.
211 if let Some(schema) = schema {
212 if schema != empty_object {
213 let mut action_with_data = SchemaObject {
214 instance_type: set(InstanceType::Array),
215 array: Some(
216 ArrayValidation {
217 items: set(vec![matches_action_name.into(), schema.into()]),
218 min_items: Some(2),
219 max_items: Some(2),
220 ..Default::default()
221 }
222 .into(),
223 ),
224 ..Default::default()
225 };
226 if let Some(new_name) = deprecation {
227 add_deprecation_notice(&mut action_with_data, new_name);
228 }
229 keymap_action_alternatives.push(action_with_data.into());
230 }
231 }
232 }
233
234 // Placing null first causes json-language-server to default assuming actions should be
235 // null, so place it last.
236 keymap_action_alternatives.push(
237 SchemaObject {
238 instance_type: set(InstanceType::Null),
239 ..Default::default()
240 }
241 .into(),
242 );
243
244 let action_schema = SchemaObject {
245 subschemas: set(SubschemaValidation {
246 one_of: Some(keymap_action_alternatives),
247 ..Default::default()
248 }),
249 ..Default::default()
250 }
251 .into();
252
253 let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
254 root_schema
255 .definitions
256 .insert("KeymapAction".to_owned(), action_schema);
257
258 serde_json::to_value(root_schema).unwrap()
259 }
260
261 pub fn blocks(&self) -> &[KeymapBlock] {
262 &self.0
263 }
264}
265
266fn no_action() -> Box<dyn gpui::Action> {
267 gpui::NoAction.boxed_clone()
268}
269
270#[cfg(test)]
271mod tests {
272 use crate::KeymapFile;
273
274 #[test]
275 fn can_deserialize_keymap_with_trailing_comma() {
276 let json = indoc::indoc! {"[
277 // Standard macOS bindings
278 {
279 \"bindings\": {
280 \"up\": \"menu::SelectPrev\",
281 },
282 },
283 ]
284 "
285
286 };
287 KeymapFile::parse(json).unwrap();
288 }
289}