1//! Keymap module for managing keyboard shortcuts and their associated actions
2//!
3//! This module provides JSON-based keymap configuration support, allowing
4//! keybindings to be loaded from external files rather than hardcoded.
5
6use anyhow::{anyhow, Context as _, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::Path;
11
12pub mod extensions;
13
14/// Represents a complete keymap configuration
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Keymap {
17 /// Optional context where these bindings apply (e.g., "Editor", "Menu")
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub context: Option<String>,
20
21 /// The key bindings in this keymap
22 pub bindings: HashMap<String, String>,
23}
24
25impl Keymap {
26 /// Create a new keymap with the given bindings
27 pub fn new(bindings: HashMap<String, String>) -> Self {
28 Self {
29 context: None,
30 bindings,
31 }
32 }
33
34 /// Create a new keymap with context
35 pub fn with_context(context: impl Into<String>, bindings: HashMap<String, String>) -> Self {
36 Self {
37 context: Some(context.into()),
38 bindings,
39 }
40 }
41}
42
43/// A collection of keymaps, typically loaded from multiple files
44#[derive(Debug, Default)]
45pub struct KeymapCollection {
46 keymaps: Vec<Keymap>,
47}
48
49impl KeymapCollection {
50 /// Create a new empty keymap collection
51 pub fn new() -> Self {
52 Self::default()
53 }
54
55 /// Load a keymap from a JSON file
56 pub fn load_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
57 let path = path.as_ref();
58 let contents = fs::read_to_string(path)
59 .with_context(|| format!("Failed to read keymap file: {}", path.display()))?;
60
61 self.load_json(&contents)
62 .with_context(|| format!("Failed to parse keymap file: {}", path.display()))?;
63
64 Ok(())
65 }
66
67 /// Load keymaps from a JSON string
68 pub fn load_json(&mut self, json: &str) -> Result<()> {
69 // Try parsing as an array first (multiple keymaps)
70 if let Ok(keymaps) = serde_json::from_str::<Vec<Keymap>>(json) {
71 self.keymaps.extend(keymaps);
72 return Ok(());
73 }
74
75 // Try parsing as a single keymap
76 if let Ok(keymap) = serde_json::from_str::<Keymap>(json) {
77 self.keymaps.push(keymap);
78 return Ok(());
79 }
80
81 Err(anyhow!("Invalid keymap JSON format"))
82 }
83
84 /// Load default keymaps
85 pub fn load_defaults(&mut self) -> Result<()> {
86 let default_keymap = include_str!("../default-keymap.json");
87 self.load_json(default_keymap)?;
88 Ok(())
89 }
90
91 /// Get all key binding specifications from this collection
92 ///
93 /// Returns a list of binding specifications that can be used to create
94 /// actual GPUI key bindings with concrete action types.
95 pub fn get_binding_specs(&self) -> Vec<BindingSpec> {
96 let mut specs = Vec::new();
97
98 for keymap in &self.keymaps {
99 let context = keymap.context.as_deref();
100
101 for (keystrokes, action_name) in &keymap.bindings {
102 specs.push(BindingSpec {
103 keystrokes: keystrokes.clone(),
104 action_name: action_name.clone(),
105 context: context.map(String::from),
106 });
107 }
108 }
109
110 specs
111 }
112
113 /// Get all keymaps in this collection
114 pub fn keymaps(&self) -> &[Keymap] {
115 &self.keymaps
116 }
117
118 /// Add a keymap to this collection
119 pub fn add(&mut self, keymap: Keymap) {
120 self.keymaps.push(keymap);
121 }
122
123 /// Clear all keymaps from this collection
124 pub fn clear(&mut self) {
125 self.keymaps.clear();
126 }
127
128 /// Find all bindings for a given action
129 pub fn find_bindings_for_action(&self, action_name: &str) -> Vec<BindingSpec> {
130 self.get_binding_specs()
131 .into_iter()
132 .filter(|spec| spec.action_name == action_name)
133 .collect()
134 }
135
136 /// Find the action for a given keystroke in a context
137 pub fn find_action(&self, keystrokes: &str, context: Option<&str>) -> Option<&str> {
138 // First try to find a binding with matching context
139 if let Some(context) = context {
140 for keymap in &self.keymaps {
141 if keymap.context.as_deref() == Some(context) {
142 if let Some(action) = keymap.bindings.get(keystrokes) {
143 return Some(action);
144 }
145 }
146 }
147 }
148
149 // Then try bindings without context (global)
150 for keymap in &self.keymaps {
151 if keymap.context.is_none() {
152 if let Some(action) = keymap.bindings.get(keystrokes) {
153 return Some(action);
154 }
155 }
156 }
157
158 None
159 }
160}
161
162/// Specification for a key binding
163#[derive(Debug, Clone)]
164pub struct BindingSpec {
165 /// The keystroke sequence (e.g., "cmd-s", "ctrl-shift-p")
166 pub keystrokes: String,
167 /// The action name to trigger
168 pub action_name: String,
169 /// Optional context where this binding applies
170 pub context: Option<String>,
171}
172
173/// Helper function to create a simple binding
174pub fn binding(key: impl Into<String>, action: impl Into<String>) -> (String, String) {
175 (key.into(), action.into())
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_parse_simple_keymap() {
184 let json = r#"{
185 "context": "Editor",
186 "bindings": {
187 "cmd-s": "Save",
188 "cmd-z": "Undo"
189 }
190 }"#;
191
192 let keymap: Keymap = serde_json::from_str(json).unwrap();
193 assert_eq!(keymap.context, Some("Editor".to_string()));
194 assert_eq!(keymap.bindings.len(), 2);
195 assert_eq!(keymap.bindings.get("cmd-s"), Some(&"Save".to_string()));
196 assert_eq!(keymap.bindings.get("cmd-z"), Some(&"Undo".to_string()));
197 }
198
199 #[test]
200 fn test_parse_multiple_keymaps() {
201 let json = r#"[
202 {
203 "bindings": {
204 "cmd-s": "Save",
205 "cmd-z": "Undo"
206 }
207 },
208 {
209 "context": "Menu",
210 "bindings": {
211 "enter": "Select",
212 "escape": "Cancel"
213 }
214 }
215 ]"#;
216
217 let keymaps: Vec<Keymap> = serde_json::from_str(json).unwrap();
218 assert_eq!(keymaps.len(), 2);
219 assert_eq!(keymaps[0].context, None);
220 assert_eq!(keymaps[1].context, Some("Menu".to_string()));
221 }
222
223 #[test]
224 fn test_keymap_collection() {
225 let mut collection = KeymapCollection::new();
226
227 let json1 = r#"{ "bindings": { "cmd-s": "Save" } }"#;
228 let json2 = r#"{ "context": "Menu", "bindings": { "enter": "Select" } }"#;
229
230 collection.load_json(json1).unwrap();
231 collection.load_json(json2).unwrap();
232
233 assert_eq!(collection.keymaps().len(), 2);
234 assert_eq!(collection.keymaps()[0].context, None);
235 assert_eq!(collection.keymaps()[1].context, Some("Menu".to_string()));
236
237 let specs = collection.get_binding_specs();
238 assert_eq!(specs.len(), 2);
239 assert_eq!(specs[0].keystrokes, "cmd-s");
240 assert_eq!(specs[0].action_name, "Save");
241 assert_eq!(specs[0].context, None);
242 }
243
244 #[test]
245 fn test_find_action() {
246 let mut collection = KeymapCollection::new();
247
248 collection.add(Keymap::new(
249 [("cmd-s", "Save"), ("cmd-z", "Undo")]
250 .iter()
251 .map(|(k, v)| (k.to_string(), v.to_string()))
252 .collect(),
253 ));
254
255 collection.add(Keymap::with_context(
256 "Editor",
257 [("cmd-x", "Cut")]
258 .iter()
259 .map(|(k, v)| (k.to_string(), v.to_string()))
260 .collect(),
261 ));
262
263 // Global binding
264 assert_eq!(collection.find_action("cmd-s", None), Some("Save"));
265 assert_eq!(
266 collection.find_action("cmd-s", Some("Editor")),
267 Some("Save")
268 );
269
270 // Context-specific binding
271 assert_eq!(collection.find_action("cmd-x", Some("Editor")), Some("Cut"));
272 assert_eq!(collection.find_action("cmd-x", None), None);
273 assert_eq!(collection.find_action("cmd-x", Some("Menu")), None);
274 }
275
276 #[test]
277 fn test_serialize_keymap() {
278 let mut bindings = HashMap::new();
279 bindings.insert("cmd-s".to_string(), "Save".to_string());
280 bindings.insert("cmd-z".to_string(), "Undo".to_string());
281
282 let keymap = Keymap::with_context("Editor", bindings);
283
284 let json = serde_json::to_string_pretty(&keymap).unwrap();
285 let parsed: Keymap = serde_json::from_str(&json).unwrap();
286
287 assert_eq!(parsed.context, keymap.context);
288 assert_eq!(parsed.bindings, keymap.bindings);
289 }
290}