keymap_file.rs

  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}