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 #[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(action_names: &[SharedString]) -> serde_json::Value {
143 let mut root_schema = SchemaSettings::draft07()
144 .with(|settings| settings.option_add_null_type = false)
145 .into_generator()
146 .into_root_schema_for::<KeymapFile>();
147
148 let action_schema = Schema::Object(SchemaObject {
149 subschemas: Some(Box::new(SubschemaValidation {
150 one_of: Some(vec![
151 Schema::Object(SchemaObject {
152 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
153 enum_values: Some(
154 action_names
155 .iter()
156 .map(|name| Value::String(name.to_string()))
157 .collect(),
158 ),
159 ..Default::default()
160 }),
161 Schema::Object(SchemaObject {
162 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
163 ..Default::default()
164 }),
165 Schema::Object(SchemaObject {
166 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
167 ..Default::default()
168 }),
169 ]),
170 ..Default::default()
171 })),
172 ..Default::default()
173 });
174
175 root_schema
176 .definitions
177 .insert("KeymapAction".to_owned(), action_schema);
178
179 serde_json::to_value(root_schema).unwrap()
180 }
181
182 pub fn blocks(&self) -> &[KeymapBlock] {
183 &self.0
184 }
185}
186
187fn no_action() -> Box<dyn gpui::Action> {
188 gpui::NoAction.boxed_clone()
189}
190
191#[cfg(test)]
192mod tests {
193 use crate::KeymapFile;
194
195 #[test]
196 fn can_deserialize_keymap_with_trailing_comma() {
197 let json = indoc::indoc! {"[
198 // Standard macOS bindings
199 {
200 \"bindings\": {
201 \"up\": \"menu::SelectPrev\",
202 },
203 },
204 ]
205 "
206
207 };
208 KeymapFile::parse(json).unwrap();
209 }
210}