ricecoder_keybinds/
conflict.rs

1//! Conflict detection and resolution for keybinds
2
3use std::collections::HashMap;
4
5use crate::models::{Keybind, KeyCombo};
6
7/// Represents a conflict between multiple keybinds
8#[derive(Debug, Clone)]
9pub struct Conflict {
10    pub key_combo: KeyCombo,
11    pub actions: Vec<String>,
12}
13
14/// Represents a suggested resolution for a conflict
15#[derive(Debug, Clone)]
16pub struct Resolution {
17    pub action_id: String,
18    pub suggested_key: String,
19    pub reason: String,
20}
21
22/// Detects and suggests resolutions for keybind conflicts
23pub struct ConflictDetector;
24
25impl ConflictDetector {
26    /// Detect all conflicts in a set of keybinds
27    pub fn detect(keybinds: &[Keybind]) -> Vec<Conflict> {
28        let mut key_to_actions: HashMap<String, Vec<String>> = HashMap::new();
29
30        // Build reverse index
31        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        // Find conflicts (keys with multiple actions)
42        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    /// Suggest resolutions for a conflict
58    pub fn suggest_resolution(conflict: &Conflict, keybinds: &[Keybind]) -> Vec<Resolution> {
59        let mut suggestions = Vec::new();
60
61        // Get category information for conflicting actions
62        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        // Suggest alternatives based on category
69        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    /// Suggest an alternative key based on category
90    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        // Both should suggest editing category alternatives
203        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        // Both should suggest navigation category alternatives
223        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        // Each should have a different category in the reason
242        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        // Add a keybind with invalid key syntax
255        keybinds.push(Keybind::new("invalid", "InvalidKey", "editing", "Invalid"));
256
257        // Should only detect valid keybinds
258        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        // Add conflicting keybinds
273        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        // Verify each conflict has exactly 2 actions
280        for conflict in &conflicts {
281            assert_eq!(conflict.actions.len(), 2);
282        }
283    }
284}