1use crate::{settings_store::parse_json_with_comments, SettingsAssets};
2use anyhow::{anyhow, Context, Result};
3use collections::{BTreeMap, HashMap};
4use gpui::{Action, AppContext, KeyBinding, NoAction, SharedString};
5use schemars::{
6 gen::{SchemaGenerator, SchemaSettings},
7 schema::{ArrayValidation, InstanceType, 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_for_registered_actions(cx: &mut AppContext) -> Value {
143 let mut generator = SchemaSettings::draft07()
144 .with(|settings| settings.option_add_null_type = false)
145 .into_generator();
146
147 let action_schemas = cx.action_schemas(&mut generator);
148 let deprecations = cx.action_deprecations();
149 KeymapFile::generate_json_schema(generator, action_schemas, deprecations)
150 }
151
152 fn generate_json_schema(
153 generator: SchemaGenerator,
154 action_schemas: Vec<(SharedString, Option<Schema>)>,
155 deprecations: &HashMap<SharedString, SharedString>,
156 ) -> serde_json::Value {
157 fn set<I, O>(input: I) -> Option<O>
158 where
159 I: Into<O>,
160 {
161 Some(input.into())
162 }
163
164 fn add_deprecation(schema_object: &mut SchemaObject, message: String) {
165 schema_object.extensions.insert(
166 // deprecationMessage is not part of the JSON Schema spec,
167 // but json-language-server recognizes it.
168 "deprecationMessage".to_owned(),
169 Value::String(message),
170 );
171 }
172
173 fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) {
174 add_deprecation(schema_object, format!("Deprecated, use {new_name}"));
175 }
176
177 fn add_description(schema_object: &mut SchemaObject, description: String) {
178 schema_object
179 .metadata
180 .get_or_insert(Default::default())
181 .description = Some(description);
182 }
183
184 let empty_object: SchemaObject = SchemaObject {
185 instance_type: set(InstanceType::Object),
186 ..Default::default()
187 };
188
189 // This is a workaround for a json-language-server issue where it matches the first
190 // alternative that matches the value's shape and uses that for documentation.
191 //
192 // In the case of the array validations, it would even provide an error saying that the name
193 // must match the name of the first alternative.
194 let mut plain_action = SchemaObject {
195 instance_type: set(InstanceType::String),
196 const_value: Some(Value::String("".to_owned())),
197 ..Default::default()
198 };
199 let no_action_message = "No action named this.";
200 add_description(&mut plain_action, no_action_message.to_owned());
201 add_deprecation(&mut plain_action, no_action_message.to_owned());
202 let mut matches_action_name = SchemaObject {
203 const_value: Some(Value::String("".to_owned())),
204 ..Default::default()
205 };
206 let no_action_message = "No action named this that takes input.";
207 add_description(&mut matches_action_name, no_action_message.to_owned());
208 add_deprecation(&mut matches_action_name, no_action_message.to_owned());
209 let action_with_input = SchemaObject {
210 instance_type: set(InstanceType::Array),
211 array: set(ArrayValidation {
212 items: set(vec![
213 matches_action_name.into(),
214 // Accept any value, as we want this to be the preferred match when there is a
215 // typo in the name.
216 Schema::Bool(true),
217 ]),
218 min_items: Some(2),
219 max_items: Some(2),
220 ..Default::default()
221 }),
222 ..Default::default()
223 };
224 let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()];
225
226 for (name, action_schema) in action_schemas.iter() {
227 let schema = if let Some(Schema::Object(schema)) = action_schema {
228 Some(schema.clone())
229 } else {
230 None
231 };
232
233 let description = schema.as_ref().and_then(|schema| {
234 schema
235 .metadata
236 .as_ref()
237 .and_then(|metadata| metadata.description.clone())
238 });
239
240 let deprecation = if name == NoAction.name() {
241 Some("null")
242 } else {
243 deprecations.get(name).map(|new_name| new_name.as_ref())
244 };
245
246 // Add an alternative for plain action names.
247 let mut plain_action = SchemaObject {
248 instance_type: set(InstanceType::String),
249 const_value: Some(Value::String(name.to_string())),
250 ..Default::default()
251 };
252 if let Some(new_name) = deprecation {
253 add_deprecation_preferred_name(&mut plain_action, new_name);
254 }
255 if let Some(description) = description.clone() {
256 add_description(&mut plain_action, description);
257 }
258 keymap_action_alternatives.push(plain_action.into());
259
260 // Add an alternative for actions with data specified as a [name, data] array.
261 //
262 // When a struct with no deserializable fields is added with impl_actions! /
263 // impl_actions_as! an empty object schema is produced. The action should be invoked
264 // without data in this case.
265 if let Some(schema) = schema {
266 if schema != empty_object {
267 let mut matches_action_name = SchemaObject {
268 const_value: Some(Value::String(name.to_string())),
269 ..Default::default()
270 };
271 if let Some(description) = description.clone() {
272 add_description(&mut matches_action_name, description.to_string());
273 }
274 if let Some(new_name) = deprecation {
275 add_deprecation_preferred_name(&mut matches_action_name, new_name);
276 }
277 let action_with_input = SchemaObject {
278 instance_type: set(InstanceType::Array),
279 array: set(ArrayValidation {
280 items: set(vec![matches_action_name.into(), schema.into()]),
281 min_items: Some(2),
282 max_items: Some(2),
283 ..Default::default()
284 }),
285 ..Default::default()
286 };
287 keymap_action_alternatives.push(action_with_input.into());
288 }
289 }
290 }
291
292 // Placing null first causes json-language-server to default assuming actions should be
293 // null, so place it last.
294 keymap_action_alternatives.push(
295 SchemaObject {
296 instance_type: set(InstanceType::Null),
297 ..Default::default()
298 }
299 .into(),
300 );
301
302 let action_schema = SchemaObject {
303 subschemas: set(SubschemaValidation {
304 one_of: Some(keymap_action_alternatives),
305 ..Default::default()
306 }),
307 ..Default::default()
308 }
309 .into();
310
311 let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
312 root_schema
313 .definitions
314 .insert("KeymapAction".to_owned(), action_schema);
315
316 // This and other json schemas can be viewed via `debug: open language server logs` ->
317 // `json-language-server` -> `Server Info`.
318 serde_json::to_value(root_schema).unwrap()
319 }
320
321 pub fn blocks(&self) -> &[KeymapBlock] {
322 &self.0
323 }
324}
325
326fn no_action() -> Box<dyn gpui::Action> {
327 gpui::NoAction.boxed_clone()
328}
329
330#[cfg(test)]
331mod tests {
332 use crate::KeymapFile;
333
334 #[test]
335 fn can_deserialize_keymap_with_trailing_comma() {
336 let json = indoc::indoc! {"[
337 // Standard macOS bindings
338 {
339 \"bindings\": {
340 \"up\": \"menu::SelectPrev\",
341 },
342 },
343 ]
344 "
345
346 };
347 KeymapFile::parse(json).unwrap();
348 }
349}