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