use crate::{settings_store::parse_json_with_comments, SettingsAssets}; use anyhow::{anyhow, Context, Result}; use collections::BTreeMap; use gpui::{actions, Action, AppContext, KeyBinding, SharedString}; use schemars::{ gen::{SchemaGenerator, SchemaSettings}, schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, JsonSchema, }; use serde::Deserialize; use serde_json::Value; use util::{asset_str, ResultExt}; #[derive(Debug, Deserialize, Default, Clone, JsonSchema)] #[serde(transparent)] pub struct KeymapFile(Vec); #[derive(Debug, Deserialize, Default, Clone, JsonSchema)] pub struct KeymapBlock { #[serde(default)] context: Option, bindings: BTreeMap, } #[derive(Debug, Deserialize, Default, Clone)] #[serde(transparent)] pub struct KeymapAction(Value); impl JsonSchema for KeymapAction { fn schema_name() -> String { "KeymapAction".into() } fn json_schema(_: &mut SchemaGenerator) -> Schema { Schema::Bool(true) } } #[derive(Deserialize)] struct ActionWithData(Box, Value); impl KeymapFile { pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> { let content = asset_str::(asset_path); Self::parse(content.as_ref())?.add_to_cx(cx) } pub fn parse(content: &str) -> Result { parse_json_with_comments::(content) } pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> { for KeymapBlock { context, bindings } in self.0 { let bindings = bindings .into_iter() .filter_map(|(keystroke, action)| { let action = action.0; // This is a workaround for a limitation in serde: serde-rs/json#497 // We want to deserialize the action data as a `RawValue` so that we can // deserialize the action itself dynamically directly from the JSON // string. But `RawValue` currently does not work inside of an untagged enum. match action { Value::Array(items) => { let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else { return Some(Err(anyhow!("Expected array of length 2"))); }; let serde_json::Value::String(name) = name else { return Some(Err(anyhow!( "Expected first item in array to be a string." ))); }; gpui::build_action(&name, Some(data)) } Value::String(name) => gpui::build_action(&name, None), Value::Null => Ok(no_action()), _ => { return Some(Err(anyhow!("Expected two-element array, got {action:?}"))) } } .with_context(|| { format!( "invalid binding value for keystroke {keystroke}, context {context:?}" ) }) .log_err() .map(|action| KeyBinding::load(&keystroke, action, context.as_deref())) }) .collect::>>()?; cx.bind_keys(bindings); } Ok(()) } pub fn generate_json_schema(action_names: &[SharedString]) -> serde_json::Value { let mut root_schema = SchemaSettings::draft07() .with(|settings| settings.option_add_null_type = false) .into_generator() .into_root_schema_for::(); let action_schema = Schema::Object(SchemaObject { subschemas: Some(Box::new(SubschemaValidation { one_of: Some(vec![ Schema::Object(SchemaObject { instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), enum_values: Some( action_names .iter() .map(|name| Value::String(name.to_string())) .collect(), ), ..Default::default() }), Schema::Object(SchemaObject { instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), ..Default::default() }), Schema::Object(SchemaObject { instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))), ..Default::default() }), ]), ..Default::default() })), ..Default::default() }); root_schema .definitions .insert("KeymapAction".to_owned(), action_schema); serde_json::to_value(root_schema).unwrap() } } actions!(NoAction); fn no_action() -> Box { NoAction.boxed_clone() } #[cfg(test)] mod tests { use crate::KeymapFile; #[test] fn can_deserialize_keymap_with_trailing_comma() { let json = indoc::indoc! {"[ // Standard macOS bindings { \"bindings\": { \"up\": \"menu::SelectPrev\", }, }, ] " }; KeymapFile::parse(json).unwrap(); } }