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}
99
100pub struct CompositeInputRouter;
102
103impl CompositeInputRouter {
104 pub fn route_key_event(
106 composite: &CompositeBuffer,
107 view_state: &CompositeViewState,
108 event: &KeyEvent,
109 ) -> RoutedEvent {
110 let focused_pane = composite.sources.get(view_state.focused_pane);
111
112 match (event.modifiers, event.code) {
113 (KeyModifiers::NONE, KeyCode::Up) | (KeyModifiers::NONE, KeyCode::Char('k')) => {
115 RoutedEvent::CompositeScroll(ScrollAction::Up(1))
116 }
117 (KeyModifiers::NONE, KeyCode::Down) | (KeyModifiers::NONE, KeyCode::Char('j')) => {
118 RoutedEvent::CompositeScroll(ScrollAction::Down(1))
119 }
120 (KeyModifiers::CONTROL, KeyCode::Char('u')) => {
121 RoutedEvent::CompositeScroll(ScrollAction::PageUp)
122 }
123 (KeyModifiers::CONTROL, KeyCode::Char('d')) => {
124 RoutedEvent::CompositeScroll(ScrollAction::PageDown)
125 }
126 (KeyModifiers::NONE, KeyCode::PageUp) => {
127 RoutedEvent::CompositeScroll(ScrollAction::PageUp)
128 }
129 (KeyModifiers::NONE, KeyCode::PageDown) => {
130 RoutedEvent::CompositeScroll(ScrollAction::PageDown)
131 }
132 (KeyModifiers::NONE, KeyCode::Home) | (KeyModifiers::NONE, KeyCode::Char('g')) => {
133 RoutedEvent::CompositeScroll(ScrollAction::ToTop)
134 }
135 (KeyModifiers::SHIFT, KeyCode::Char('G')) | (KeyModifiers::NONE, KeyCode::End) => {
136 RoutedEvent::CompositeScroll(ScrollAction::ToBottom)
137 }
138
139 (KeyModifiers::NONE, KeyCode::Tab) => RoutedEvent::SwitchPane(Direction::Next),
141 (KeyModifiers::SHIFT, KeyCode::BackTab) => RoutedEvent::SwitchPane(Direction::Prev),
142 (KeyModifiers::NONE, KeyCode::Char('h')) => RoutedEvent::SwitchPane(Direction::Prev),
143 (KeyModifiers::NONE, KeyCode::Char('l')) => RoutedEvent::SwitchPane(Direction::Next),
144
145 (KeyModifiers::NONE, KeyCode::Char('n')) => RoutedEvent::NavigateHunk(Direction::Next),
147 (KeyModifiers::NONE, KeyCode::Char('p')) => RoutedEvent::NavigateHunk(Direction::Prev),
148 (KeyModifiers::NONE, KeyCode::Char(']')) => RoutedEvent::NavigateHunk(Direction::Next),
149 (KeyModifiers::NONE, KeyCode::Char('[')) => RoutedEvent::NavigateHunk(Direction::Prev),
150
151 (KeyModifiers::NONE, KeyCode::Char('q')) | (KeyModifiers::NONE, KeyCode::Esc) => {
153 RoutedEvent::Close
154 }
155
156 (KeyModifiers::NONE, KeyCode::Char('v')) => {
158 RoutedEvent::Selection(SelectionAction::StartVisual)
159 }
160 (KeyModifiers::SHIFT, KeyCode::Char('V')) => {
161 RoutedEvent::Selection(SelectionAction::StartVisualLine)
162 }
163
164 (KeyModifiers::NONE, KeyCode::Char('y')) => RoutedEvent::Yank,
166
167 (KeyModifiers::NONE, KeyCode::Char(c)) => {
169 if let Some(pane) = focused_pane {
170 if pane.editable {
171 RoutedEvent::ToSourceBuffer {
172 buffer_id: pane.buffer_id,
173 action: BufferAction::Insert(c),
174 }
175 } else {
176 RoutedEvent::Blocked("Pane is read-only")
177 }
178 } else {
179 RoutedEvent::Unhandled
180 }
181 }
182 (KeyModifiers::NONE, KeyCode::Backspace) => {
183 if let Some(pane) = focused_pane {
184 if pane.editable {
185 RoutedEvent::ToSourceBuffer {
186 buffer_id: pane.buffer_id,
187 action: BufferAction::Backspace,
188 }
189 } else {
190 RoutedEvent::Blocked("Pane is read-only")
191 }
192 } else {
193 RoutedEvent::Unhandled
194 }
195 }
196 (KeyModifiers::NONE, KeyCode::Delete) => {
197 if let Some(pane) = focused_pane {
198 if pane.editable {
199 RoutedEvent::ToSourceBuffer {
200 buffer_id: pane.buffer_id,
201 action: BufferAction::Delete,
202 }
203 } else {
204 RoutedEvent::Blocked("Pane is read-only")
205 }
206 } else {
207 RoutedEvent::Unhandled
208 }
209 }
210 (KeyModifiers::NONE, KeyCode::Enter) => {
211 if let Some(pane) = focused_pane {
212 if pane.editable {
213 RoutedEvent::ToSourceBuffer {
214 buffer_id: pane.buffer_id,
215 action: BufferAction::NewLine,
216 }
217 } else {
218 RoutedEvent::Blocked("Pane is read-only")
219 }
220 } else {
221 RoutedEvent::Unhandled
222 }
223 }
224
225 (KeyModifiers::NONE, KeyCode::Left) => RoutedEvent::PaneCursor(CursorAction::Left),
227 (KeyModifiers::NONE, KeyCode::Right) => RoutedEvent::PaneCursor(CursorAction::Right),
228 (KeyModifiers::CONTROL, KeyCode::Left) => {
229 RoutedEvent::PaneCursor(CursorAction::WordLeft)
230 }
231 (KeyModifiers::CONTROL, KeyCode::Right) => {
232 RoutedEvent::PaneCursor(CursorAction::WordRight)
233 }
234
235 _ => RoutedEvent::Unhandled,
236 }
237 }
238
239 pub fn display_to_source(
241 composite: &CompositeBuffer,
242 _view_state: &CompositeViewState,
243 display_row: usize,
244 display_col: usize,
245 pane_index: usize,
246 ) -> Option<SourceCoordinate> {
247 let aligned_row = composite.alignment.get_row(display_row)?;
248 let source_ref = aligned_row.get_pane_line(pane_index)?;
249
250 Some(SourceCoordinate {
251 buffer_id: composite.sources.get(pane_index)?.buffer_id,
252 byte_offset: source_ref.byte_range.start + display_col,
253 line: source_ref.line,
254 column: display_col,
255 })
256 }
257
258 pub fn click_to_pane(
260 view_state: &CompositeViewState,
261 click_x: u16,
262 area_x: u16,
263 ) -> Option<usize> {
264 let mut x = area_x;
265 for (i, &width) in view_state.pane_widths.iter().enumerate() {
266 if click_x >= x && click_x < x + width {
267 return Some(i);
268 }
269 x += width + 1; }
271 None
272 }
273
274 pub fn navigate_to_hunk(
276 composite: &CompositeBuffer,
277 view_state: &mut CompositeViewState,
278 direction: Direction,
279 ) -> bool {
280 let current_row = view_state.scroll_row;
281 let new_row = match direction {
282 Direction::Next => composite.alignment.next_hunk_row(current_row),
283 Direction::Prev => composite.alignment.prev_hunk_row(current_row),
284 };
285
286 if let Some(row) = new_row {
287 view_state.scroll_row = row;
288 true
289 } else {
290 false
291 }
292 }
293}
294
295#[derive(Debug, Clone)]
297pub struct SourceCoordinate {
298 pub buffer_id: BufferId,
299 pub byte_offset: usize,
300 pub line: usize,
301 pub column: usize,
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::model::composite_buffer::{CompositeLayout, SourcePane};
308
309 fn create_test_composite() -> (CompositeBuffer, CompositeViewState) {
310 let sources = vec![
311 SourcePane::new(BufferId(1), "OLD", false),
312 SourcePane::new(BufferId(2), "NEW", true),
313 ];
314 let composite = CompositeBuffer::new(
315 BufferId(0),
316 "Test Diff".to_string(),
317 "diff-view".to_string(),
318 CompositeLayout::default(),
319 sources,
320 );
321 let view_state = CompositeViewState::new(BufferId(0), 2);
322 (composite, view_state)
323 }
324
325 #[test]
326 fn test_scroll_routing() {
327 let (composite, view_state) = create_test_composite();
328
329 let event = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
330 let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
331
332 matches!(result, RoutedEvent::CompositeScroll(ScrollAction::Down(1)));
333 }
334
335 #[test]
336 fn test_pane_switch_routing() {
337 let (composite, view_state) = create_test_composite();
338
339 let event = KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE);
340 let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
341
342 matches!(result, RoutedEvent::SwitchPane(Direction::Next));
343 }
344
345 #[test]
346 fn test_readonly_blocking() {
347 let (composite, view_state) = create_test_composite();
348 let event = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
351 let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
352
353 matches!(result, RoutedEvent::Blocked(_));
354 }
355
356 #[test]
357 fn test_editable_routing() {
358 let (composite, mut view_state) = create_test_composite();
359 view_state.focused_pane = 1; let event = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE);
362 let result = CompositeInputRouter::route_key_event(&composite, &view_state, &event);
363
364 matches!(
365 result,
366 RoutedEvent::ToSourceBuffer {
367 buffer_id: BufferId(2),
368 action: BufferAction::Insert('x'),
369 }
370 );
371 }
372}