keymap_file.rs

  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,
  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    bindings: BTreeMap<String, KeymapAction>,
 23}
 24
 25#[derive(Debug, 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        if content.is_empty() {
 51            return Ok(Self::default());
 52        }
 53        parse_json_with_comments::<Self>(content)
 54    }
 55
 56    pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {
 57        for KeymapBlock { context, bindings } in self.0 {
 58            let bindings = bindings
 59                .into_iter()
 60                .filter_map(|(keystroke, action)| {
 61                    let action = action.0;
 62
 63                    // This is a workaround for a limitation in serde: serde-rs/json#497
 64                    // We want to deserialize the action data as a `RawValue` so that we can
 65                    // deserialize the action itself dynamically directly from the JSON
 66                    // string. But `RawValue` currently does not work inside of an untagged enum.
 67                    match action {
 68                        Value::Array(items) => {
 69                            let Ok([name, data]): Result<[serde_json::Value; 2], _> =
 70                                items.try_into()
 71                            else {
 72                                return Some(Err(anyhow!("Expected array of length 2")));
 73                            };
 74                            let serde_json::Value::String(name) = name else {
 75                                return Some(Err(anyhow!(
 76                                    "Expected first item in array to be a string."
 77                                )));
 78                            };
 79                            cx.build_action(&name, Some(data))
 80                        }
 81                        Value::String(name) => cx.build_action(&name, None),
 82                        Value::Null => Ok(no_action()),
 83                        _ => {
 84                            return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
 85                        }
 86                    }
 87                    .with_context(|| {
 88                        format!(
 89                            "invalid binding value for keystroke {keystroke}, context {context:?}"
 90                        )
 91                    })
 92                    .log_err()
 93                    .map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
 94                })
 95                .collect::<Result<Vec<_>>>()?;
 96
 97            cx.bind_keys(bindings);
 98        }
 99        Ok(())
100    }
101
102    pub fn generate_json_schema(action_names: &[SharedString]) -> serde_json::Value {
103        let mut root_schema = SchemaSettings::draft07()
104            .with(|settings| settings.option_add_null_type = false)
105            .into_generator()
106            .into_root_schema_for::<KeymapFile>();
107
108        let action_schema = Schema::Object(SchemaObject {
109            subschemas: Some(Box::new(SubschemaValidation {
110                one_of: Some(vec![
111                    Schema::Object(SchemaObject {
112                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
113                        enum_values: Some(
114                            action_names
115                                .iter()
116                                .map(|name| Value::String(name.to_string()))
117                                .collect(),
118                        ),
119                        ..Default::default()
120                    }),
121                    Schema::Object(SchemaObject {
122                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
123                        ..Default::default()
124                    }),
125                    Schema::Object(SchemaObject {
126                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
127                        ..Default::default()
128                    }),
129                ]),
130                ..Default::default()
131            })),
132            ..Default::default()
133        });
134
135        root_schema
136            .definitions
137            .insert("KeymapAction".to_owned(), action_schema);
138
139        serde_json::to_value(root_schema).unwrap()
140    }
141}
142
143fn no_action() -> Box<dyn gpui::Action> {
144    gpui::NoAction.boxed_clone()
145}
146
147#[cfg(test)]
148mod tests {
149    use crate::KeymapFile;
150
151    #[test]
152    fn can_deserialize_keymap_with_trailing_comma() {
153        let json = indoc::indoc! {"[
154              // Standard macOS bindings
155              {
156                \"bindings\": {
157                  \"up\": \"menu::SelectPrev\",
158                },
159              },
160            ]
161                  "
162
163        };
164        KeymapFile::parse(json).unwrap();
165    }
166}