1use crate::model::composite_buffer::CompositeBuffer;
7use crate::model::event::BufferId;
8use crate::view::composite_view::CompositeViewState;
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10
11#[derive(Debug, Clone)]
13pub enum RoutedEvent {
14 CompositeScroll(ScrollAction),
16 SwitchPane(Direction),
18 NavigateHunk(Direction),
20 ToSourceBuffer {
22 buffer_id: BufferId,
23 action: BufferAction,
24 },
25 PaneCursor(CursorAction),
27 Selection(SelectionAction),
29 Yank,
31 Blocked(&'static str),
33 Close,
35 Unhandled,
37}
38
39#[derive(Debug, Clone, Copy)]
41pub enum SelectionAction {
42 StartVisual,
44 StartVisualLine,
46 ClearSelection,
48 ExtendUp,
50 ExtendDown,
52 ExtendLeft,
54 ExtendRight,
56}
57
58#[derive(Debug, Clone, Copy)]
60pub enum ScrollAction {
61 Up(usize),
62 Down(usize),
63 PageUp,
64 PageDown,
65 ToTop,
66 ToBottom,
67 ToRow(usize),
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Direction {
73 Next,
74 Prev,
75}
76
77#[derive(Debug, Clone)]
79pub enum BufferAction {
80 Insert(char),
81 InsertString(String),
82 Delete,
83 Backspace,
84 NewLine,
85}
86
87#[derive(Debug, Clone, Copy)]
89pub enum CursorAction {
90 Up,
91 Down,
92 Left,
93 Right,
94 LineStart,
95 LineEnd,
96 WordLeft,
97 WordRight,
98 Top,
99 Bottom,
100}
101
102pub struct CompositeInputRouter;
104
105impl CompositeInputRouter {
106 pub fn route_key_event(
113 _composite: &CompositeBuffer,
114 _view_state: &CompositeViewState,
115 event: &KeyEvent,
116 ) -> RoutedEvent {
117 match (event.modifiers, event.code) {
118 (KeyModifiers::NONE, KeyCode::Char('j')) => {
120 RoutedEvent::CompositeScroll(ScrollAction::Down(1))
121 }
122 (KeyModifiers::NONE, KeyCode::Char('k')) => {
123 RoutedEvent::CompositeScroll(ScrollAction::Up(1))
124 }
125
126 (KeyModifiers::NONE, KeyCode::Tab) => RoutedEvent::SwitchPane(Direction::Next),
128 (KeyModifiers::SHIFT, KeyCode::BackTab) => RoutedEvent::SwitchPane(Direction::Prev),
129
130 _ => RoutedEvent::Unhandled,
134 }
135 }
136
137 pub fn display_to_source(
139 composite: &CompositeBuffer,
140 _view_state: &CompositeViewState,
141 display_row: usize,
142 display_col: usize,
143 pane_index: usize,
144 ) -> Option<SourceCoordinate> {
145 let aligned_row = composite.alignment.get_row(display_row)?;
146 let source_ref = aligned_row.get_pane_line(pane_index)?;
147
148 Some(SourceCoordinate {
149 buffer_id: composite.sources.get(pane_index)?.buffer_id,
150 byte_offset: source_ref.byte_range.start + display_col,
151 line: source_ref.line,
152 column: display_col,
153 })
154 }
155
156 pub fn click_to_pane(
158 view_state: &CompositeViewState,
159 click_x: u16,
160 area_x: u16,
161 ) -> Option<usize> {
162 let mut x = area_x;
163 for (i, &width) in view_state.pane_widths.iter().enumerate() {
164 if click_x >= x && click_x < x + width {
165 return Some(i);
166 }
167 x += width + 1; }
169 None
170 }
171
172 pub fn navigate_to_hunk(
174 composite: &CompositeBuffer,
175 view_state: &mut CompositeViewState,
176 direction: Direction,
177 ) -> bool {
178 let current_row = view_state.scroll_row;
179 let new_row = match direction {
180 Direction::Next => composite.alignment.next_hunk_row(current_row),
181 Direction::Prev => composite.alignment.prev_hunk_row(current_row),
182 };
183
184 if let Some(row) = new_row {
185 view_state.scroll_row = row;
186 true
187 } else {
188 false
189 }
190 }
191}
192
193#[derive(Debug, Clone)]
195pub struct SourceCoordinate {
196 pub buffer_id: BufferId,
197 pub byte_offset: usize,
198 pub line: usize,
199 pub column: usize,
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::model::composite_buffer::{CompositeLayout, SourcePane};
206
207 fn create_test_composite() -> (CompositeBuffer, CompositeViewState) {
208 let sources = vec![
209 SourcePane::new(BufferId(1), "OLD", false),
210 SourcePane::new(BufferId(2), "NEW", true),
211 ];
212 let composite = CompositeBuffer::new(
213 BufferId(0),
214 "Test Diff".to_string(),
215 "diff-view".to_string(),
216 CompositeLayout::default(),
217 sources,
218 );
219 let view_state = CompositeViewState::new(BufferId(0), 2);
220 (composite, view_state)
221 }
222
223 #[test]
224 fn test_scroll_routing() {
225 let (composite, view_state) = create_test_composite();
226
227 let event = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
228 let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
229
230 matches!(result, RoutedEvent::CompositeScroll(ScrollAction::Down(1)));
231 }
232
233 #[test]
234 fn test_pane_switch_routing() {
235 let (composite, view_state) = create_test_composite();
236
237 let event = KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE);
238 let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
239
240 matches!(result, RoutedEvent::SwitchPane(Direction::Next));
241 }
242
243 #[test]
244 fn test_readonly_blocking() {
245 let (composite, view_state) = create_test_composite();
246 let event = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
249 let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
250
251 matches!(result, RoutedEvent::Blocked(_));
252 }
253
254 #[test]
255 fn test_editable_routing() {
256 let (composite, mut view_state) = create_test_composite();
257 view_state.focused_pane = 1; let event = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
260 let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
261
262 matches!(
263 result,
264 RoutedEvent::ToSourceBuffer {
265 buffer_id: BufferId(2),
266 action: BufferAction::Insert('x'),
267 }
268 );
269 }
270}