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}