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}