1use std::collections::HashMap;
4
5use crate::models::{Keybind, KeyCombo};
6
7#[derive(Debug, Clone)]
9pub struct Conflict {
10 pub key_combo: KeyCombo,
11 pub actions: Vec<String>,
12}
13
14#[derive(Debug, Clone)]
16pub struct Resolution {
17 pub action_id: String,
18 pub suggested_key: String,
19 pub reason: String,
20}
21
22pub struct ConflictDetector;
24
25impl ConflictDetector {
26 pub fn detect(keybinds: &[Keybind]) -> Vec<Conflict> {
28 let mut key_to_actions: HashMap<String, Vec<String>> = HashMap::new();
29
30 for keybind in keybinds {
32 if let Ok(key_combo) = keybind.parse_key() {
33 let key_str = key_combo.to_string();
34 key_to_actions
35 .entry(key_str)
36 .or_default()
37 .push(keybind.action_id.clone());
38 }
39 }
40
41 let mut conflicts = Vec::new();
43 for (key_str, actions) in key_to_actions {
44 if actions.len() > 1 {
45 if let Ok(key_combo) = key_str.parse() {
46 conflicts.push(Conflict {
47 key_combo,
48 actions,
49 });
50 }
51 }
52 }
53
54 conflicts
55 }
56
57 pub fn suggest_resolution(conflict: &Conflict, keybinds: &[Keybind]) -> Vec<Resolution> {
59 let mut suggestions = Vec::new();
60
61 let action_categories: HashMap<String, String> = keybinds
63 .iter()
64 .filter(|kb| conflict.actions.contains(&kb.action_id))
65 .map(|kb| (kb.action_id.clone(), kb.category.clone()))
66 .collect();
67
68 for action_id in &conflict.actions {
70 let category = action_categories
71 .get(action_id)
72 .map(|s| s.as_str())
73 .unwrap_or("general");
74
75 let suggested_key = Self::suggest_alternative_key(category);
76 suggestions.push(Resolution {
77 action_id: action_id.clone(),
78 suggested_key,
79 reason: format!(
80 "Suggested alternative for {} action",
81 category
82 ),
83 });
84 }
85
86 suggestions
87 }
88
89 fn suggest_alternative_key(category: &str) -> String {
91 match category {
92 "editing" => "Ctrl+Alt+S".to_string(),
93 "navigation" => "Ctrl+Alt+N".to_string(),
94 "search" => "Ctrl+Alt+F".to_string(),
95 "view" => "Ctrl+Alt+V".to_string(),
96 _ => "Ctrl+Alt+X".to_string(),
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn test_detect_no_conflicts() {
107 let keybinds = vec![
108 Keybind::new("editor.save", "Ctrl+S", "editing", "Save"),
109 Keybind::new("editor.undo", "Ctrl+Z", "editing", "Undo"),
110 ];
111
112 let conflicts = ConflictDetector::detect(&keybinds);
113 assert_eq!(conflicts.len(), 0);
114 }
115
116 #[test]
117 fn test_detect_conflicts() {
118 let keybinds = vec![
119 Keybind::new("editor.save", "Ctrl+S", "editing", "Save"),
120 Keybind::new("editor.save_all", "Ctrl+S", "editing", "Save all"),
121 ];
122
123 let conflicts = ConflictDetector::detect(&keybinds);
124 assert_eq!(conflicts.len(), 1);
125 assert_eq!(conflicts[0].actions.len(), 2);
126 assert!(conflicts[0].actions.contains(&"editor.save".to_string()));
127 assert!(conflicts[0].actions.contains(&"editor.save_all".to_string()));
128 }
129
130 #[test]
131 fn test_suggest_resolution() {
132 let keybinds = vec![
133 Keybind::new("editor.save", "Ctrl+S", "editing", "Save"),
134 Keybind::new("editor.save_all", "Ctrl+S", "editing", "Save all"),
135 ];
136
137 let conflicts = ConflictDetector::detect(&keybinds);
138 assert_eq!(conflicts.len(), 1);
139
140 let resolutions = ConflictDetector::suggest_resolution(&conflicts[0], &keybinds);
141 assert_eq!(resolutions.len(), 2);
142 assert!(resolutions.iter().any(|r| r.action_id == "editor.save"));
143 assert!(resolutions.iter().any(|r| r.action_id == "editor.save_all"));
144 }
145
146 #[test]
147 fn test_multiple_conflicts() {
148 let keybinds = vec![
149 Keybind::new("editor.save", "Ctrl+S", "editing", "Save"),
150 Keybind::new("editor.save_all", "Ctrl+S", "editing", "Save all"),
151 Keybind::new("nav.next", "Tab", "navigation", "Next"),
152 Keybind::new("nav.prev", "Tab", "navigation", "Previous"),
153 ];
154
155 let conflicts = ConflictDetector::detect(&keybinds);
156 assert_eq!(conflicts.len(), 2);
157 }
158
159 #[test]
160 fn test_empty_keybind_set() {
161 let keybinds: Vec<Keybind> = vec![];
162 let conflicts = ConflictDetector::detect(&keybinds);
163 assert_eq!(conflicts.len(), 0);
164 }
165
166 #[test]
167 fn test_single_keybind() {
168 let keybinds = vec![Keybind::new("editor.save", "Ctrl+S", "editing", "Save")];
169 let conflicts = ConflictDetector::detect(&keybinds);
170 assert_eq!(conflicts.len(), 0);
171 }
172
173 #[test]
174 fn test_three_way_conflict() {
175 let keybinds = vec![
176 Keybind::new("action1", "Ctrl+S", "editing", "Action 1"),
177 Keybind::new("action2", "Ctrl+S", "editing", "Action 2"),
178 Keybind::new("action3", "Ctrl+S", "editing", "Action 3"),
179 ];
180
181 let conflicts = ConflictDetector::detect(&keybinds);
182 assert_eq!(conflicts.len(), 1);
183 assert_eq!(conflicts[0].actions.len(), 3);
184 assert!(conflicts[0].actions.contains(&"action1".to_string()));
185 assert!(conflicts[0].actions.contains(&"action2".to_string()));
186 assert!(conflicts[0].actions.contains(&"action3".to_string()));
187 }
188
189 #[test]
190 fn test_resolution_suggestions_editing() {
191 let keybinds = vec![
192 Keybind::new("editor.save", "Ctrl+S", "editing", "Save"),
193 Keybind::new("editor.save_all", "Ctrl+S", "editing", "Save all"),
194 ];
195
196 let conflicts = ConflictDetector::detect(&keybinds);
197 assert_eq!(conflicts.len(), 1);
198
199 let resolutions = ConflictDetector::suggest_resolution(&conflicts[0], &keybinds);
200 assert_eq!(resolutions.len(), 2);
201
202 for resolution in &resolutions {
204 assert!(resolution.reason.contains("editing"));
205 assert!(resolution.suggested_key.contains("Ctrl+Alt"));
206 }
207 }
208
209 #[test]
210 fn test_resolution_suggestions_navigation() {
211 let keybinds = vec![
212 Keybind::new("nav.next", "Tab", "navigation", "Next"),
213 Keybind::new("nav.prev", "Tab", "navigation", "Previous"),
214 ];
215
216 let conflicts = ConflictDetector::detect(&keybinds);
217 assert_eq!(conflicts.len(), 1);
218
219 let resolutions = ConflictDetector::suggest_resolution(&conflicts[0], &keybinds);
220 assert_eq!(resolutions.len(), 2);
221
222 for resolution in &resolutions {
224 assert!(resolution.reason.contains("navigation"));
225 }
226 }
227
228 #[test]
229 fn test_resolution_suggestions_mixed_categories() {
230 let keybinds = vec![
231 Keybind::new("editor.save", "Ctrl+S", "editing", "Save"),
232 Keybind::new("search.find", "Ctrl+S", "search", "Find"),
233 ];
234
235 let conflicts = ConflictDetector::detect(&keybinds);
236 assert_eq!(conflicts.len(), 1);
237
238 let resolutions = ConflictDetector::suggest_resolution(&conflicts[0], &keybinds);
239 assert_eq!(resolutions.len(), 2);
240
241 let reasons: Vec<String> = resolutions.iter().map(|r| r.reason.clone()).collect();
243 assert!(reasons.iter().any(|r| r.contains("editing")));
244 assert!(reasons.iter().any(|r| r.contains("search")));
245 }
246
247 #[test]
248 fn test_invalid_key_syntax_ignored() {
249 let mut keybinds = vec![
250 Keybind::new("editor.save", "Ctrl+S", "editing", "Save"),
251 Keybind::new("editor.undo", "Ctrl+Z", "editing", "Undo"),
252 ];
253
254 keybinds.push(Keybind::new("invalid", "InvalidKey", "editing", "Invalid"));
256
257 let conflicts = ConflictDetector::detect(&keybinds);
259 assert_eq!(conflicts.len(), 0);
260 }
261
262 #[test]
263 fn test_conflict_with_many_keybinds() {
264 let mut keybinds = vec![
265 Keybind::new("action1", "Ctrl+A", "editing", "Action 1"),
266 Keybind::new("action2", "Ctrl+B", "editing", "Action 2"),
267 Keybind::new("action3", "Ctrl+C", "editing", "Action 3"),
268 Keybind::new("action4", "Ctrl+D", "editing", "Action 4"),
269 Keybind::new("action5", "Ctrl+E", "editing", "Action 5"),
270 ];
271
272 keybinds.push(Keybind::new("conflict1", "Ctrl+A", "editing", "Conflict 1"));
274 keybinds.push(Keybind::new("conflict2", "Ctrl+B", "editing", "Conflict 2"));
275
276 let conflicts = ConflictDetector::detect(&keybinds);
277 assert_eq!(conflicts.len(), 2);
278
279 for conflict in &conflicts {
281 assert_eq!(conflict.actions.len(), 2);
282 }
283 }
284}