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