gitkraft_tui/features/diff/
events.rs1use crossterm::event::{KeyCode, KeyEvent};
2
3use crate::app::{App, DiffSubPane};
4
5pub fn handle_key(app: &mut App, key: KeyEvent) {
7 match app.tab().diff_sub_pane {
8 DiffSubPane::FileList => handle_file_list_key(app, key),
9 DiffSubPane::Content => handle_content_key(app, key),
10 }
11}
12
13fn handle_file_list_key(app: &mut App, key: KeyEvent) {
14 match key.code {
15 KeyCode::Char('j') => navigate_file_down(app),
17 KeyCode::Char('k') => navigate_file_up(app),
18 KeyCode::Char('J') => select_file_down(app),
23 KeyCode::Char('K') => select_file_up(app),
25 KeyCode::Enter | KeyCode::Char('l') if !app.tab().commit_files.is_empty() => {
27 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
28 }
29 KeyCode::Char('H') => {
31 let path = app
32 .tab()
33 .commit_files
34 .get(app.tab().commit_diff_file_index)
35 .map(|f| f.display_path().to_string());
36 if let Some(p) = path {
37 app.open_file_history(p);
38 }
39 }
40 KeyCode::Char('B') => {
42 let path = app
43 .tab()
44 .commit_files
45 .get(app.tab().commit_diff_file_index)
46 .map(|f| f.display_path().to_string());
47 if let Some(p) = path {
48 app.open_file_blame(p);
49 }
50 }
51 KeyCode::Char('e') => {
53 app.open_commit_files_in_editor();
54 }
55 _ => {}
56 }
57}
58
59fn handle_content_key(app: &mut App, key: KeyEvent) {
60 match key.code {
61 KeyCode::Char('j') => {
63 app.tab_mut().diff_scroll = app.tab().diff_scroll.saturating_add(1);
64 }
65 KeyCode::Char('k') => {
67 app.tab_mut().diff_scroll = app.tab().diff_scroll.saturating_sub(1);
68 }
69 KeyCode::Char('g') => {
71 app.tab_mut().diff_scroll = 0;
72 }
73 KeyCode::Char('G') => {
75 let total_lines = app
76 .tab()
77 .selected_diff
78 .as_ref()
79 .map(|d| d.hunks.iter().map(|h| h.lines.len() as u16).sum::<u16>())
80 .unwrap_or(0);
81 app.tab_mut().diff_scroll = total_lines.saturating_sub(1);
82 }
83 KeyCode::PageDown | KeyCode::Char('d') => {
85 app.tab_mut().diff_scroll = app.tab().diff_scroll.saturating_add(20);
86 }
87 KeyCode::PageUp | KeyCode::Char('u') => {
89 app.tab_mut().diff_scroll = app.tab().diff_scroll.saturating_sub(20);
90 }
91 KeyCode::Char('h') => {
93 navigate_file_up(app);
94 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
95 }
96 KeyCode::Char('l') => {
98 navigate_file_down(app);
99 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
100 }
101 KeyCode::Esc => {
103 app.tab_mut().diff_sub_pane = DiffSubPane::FileList;
104 }
105 KeyCode::Char('e') => {
108 app.open_commit_files_in_editor();
109 }
110 _ => {}
111 }
112}
113
114pub fn navigate_file_down(app: &mut App) {
116 if app.tab().commit_files.is_empty() {
117 return;
118 }
119 let len = app.tab().commit_files.len();
120 let current = app.tab().commit_diff_file_index;
121 let new_idx = (current + 1) % len;
122 apply_single_file_navigation(app, new_idx);
123}
124
125pub fn navigate_file_up(app: &mut App) {
127 if app.tab().commit_files.is_empty() {
128 return;
129 }
130 let len = app.tab().commit_files.len();
131 let current = app.tab().commit_diff_file_index;
132 let new_idx = if current == 0 { len - 1 } else { current - 1 };
133 apply_single_file_navigation(app, new_idx);
134}
135
136fn extend_file_selection(app: &mut App, next_idx_fn: impl Fn(usize, usize) -> Option<usize>) {
147 if app.tab().commit_files.is_empty() {
148 return;
149 }
150 let len = app.tab().commit_files.len();
151 let current = app.tab().commit_diff_file_index;
152 let anchor = app.tab().anchor_file_index.unwrap_or(current);
153 let new_idx = next_idx_fn(current, len).unwrap_or(current);
156
157 let range: std::collections::HashSet<usize> = gitkraft_core::ascending_range(anchor, new_idx)
159 .into_iter()
160 .collect();
161 app.tab_mut().selected_file_indices = range;
162 app.tab_mut().commit_diff_file_index = new_idx;
163
164 let count = app.tab().selected_file_indices.len();
165 app.tab_mut().status_message = Some(format!("{count} file(s) selected"));
166
167 let all_selected: Vec<usize> = app.tab().selected_file_indices.iter().copied().collect();
173 for idx in all_selected {
174 app.load_diff_for_file_index(idx);
175 }
176
177 app.tab_mut().diff_scroll = 0;
179}
180
181pub fn select_file_down(app: &mut App) {
183 extend_file_selection(
184 app,
185 |cur, len| {
186 if cur + 1 >= len {
187 None
188 } else {
189 Some(cur + 1)
190 }
191 },
192 );
193}
194
195pub fn select_file_up(app: &mut App) {
197 extend_file_selection(app, |cur, _| if cur == 0 { None } else { Some(cur - 1) });
198}
199
200pub fn handle_file_history_key(app: &mut App, key: KeyEvent) {
202 let len = app.tab().file_history_commits.len();
203 match key.code {
204 KeyCode::Char('j') | KeyCode::Down if len > 0 => {
205 let cur = app.tab().file_history_cursor;
206 app.tab_mut().file_history_cursor = (cur + 1).min(len - 1);
207 }
208 KeyCode::Char('k') | KeyCode::Up => {
209 let cur = app.tab().file_history_cursor;
210 app.tab_mut().file_history_cursor = cur.saturating_sub(1);
211 }
212 KeyCode::Char('g') => {
213 app.tab_mut().file_history_cursor = 0;
214 }
215 KeyCode::Char('G') if len > 0 => {
216 app.tab_mut().file_history_cursor = len - 1;
217 }
218 KeyCode::Enter => {
219 let cursor = app.tab().file_history_cursor;
221 if let Some(commit) = app.tab().file_history_commits.get(cursor).cloned() {
222 let oid = commit.oid.clone();
223 let tab = app.tab_mut();
224 tab.file_history_path = None;
225 tab.file_history_commits.clear();
226 tab.selected_commit_oid = Some(oid);
227 }
228 app.load_commit_diff_by_oid();
229 }
230 KeyCode::Esc | KeyCode::Char('q') => {
231 let tab = app.tab_mut();
232 tab.file_history_path = None;
233 tab.file_history_commits.clear();
234 tab.file_history_cursor = 0;
235 tab.status_message = Some("File history closed".into());
236 }
237 _ => {}
238 }
239}
240
241pub fn handle_blame_key(app: &mut App, key: KeyEvent) {
243 match key.code {
244 KeyCode::Char('j') | KeyCode::Down => {
245 app.tab_mut().blame_scroll = app.tab().blame_scroll.saturating_add(1);
246 }
247 KeyCode::Char('k') | KeyCode::Up => {
248 app.tab_mut().blame_scroll = app.tab().blame_scroll.saturating_sub(1);
249 }
250 KeyCode::Char('d') => {
251 app.tab_mut().blame_scroll = app.tab().blame_scroll.saturating_add(10);
252 }
253 KeyCode::Char('u') => {
254 app.tab_mut().blame_scroll = app.tab().blame_scroll.saturating_sub(10);
255 }
256 KeyCode::Char('g') => {
257 app.tab_mut().blame_scroll = 0;
258 }
259 KeyCode::Char('G') => {
260 let len = app.tab().blame_lines.len() as u16;
261 app.tab_mut().blame_scroll = len.saturating_sub(1);
262 }
263 KeyCode::Esc | KeyCode::Char('q') => {
264 let tab = app.tab_mut();
265 tab.blame_path = None;
266 tab.blame_lines.clear();
267 tab.blame_scroll = 0;
268 tab.status_message = Some("Blame closed".into());
269 }
270 _ => {}
271 }
272}
273
274fn apply_single_file_navigation(app: &mut App, new_idx: usize) {
278 app.tab_mut().anchor_file_index = Some(new_idx);
280 app.tab_mut().commit_diff_file_index = new_idx;
281 app.tab_mut().selected_file_indices.clear();
282 app.tab_mut().selected_file_indices.insert(new_idx);
283 app.tab_mut().diff_scroll = 0;
284 if let Some(cached) = app.tab().commit_diffs.get(&new_idx).cloned() {
285 app.tab_mut().selected_diff = Some(cached);
286 } else {
287 let file_path = app.tab().commit_files[new_idx].display_path().to_string();
288 app.load_single_file_diff(new_idx, file_path);
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::app::{App, DiffSubPane};
296 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
297
298 fn make_commit_info(summary: &str) -> gitkraft_core::CommitInfo {
301 gitkraft_core::CommitInfo {
302 oid: "abc1234567890".to_string(),
303 short_oid: "abc1234".to_string(),
304 summary: summary.to_string(),
305 message: summary.to_string(),
306 author_name: "author".to_string(),
307 author_email: "a@b.com".to_string(),
308 time: Default::default(),
309 parent_ids: vec![],
310 }
311 }
312
313 fn app_with_history() -> App {
314 let mut app = App::new();
315 app.tab_mut().file_history_path = Some("src/main.rs".to_string());
316 app.tab_mut().file_history_commits = vec![
317 make_commit_info("commit 0"),
318 make_commit_info("commit 1"),
319 make_commit_info("commit 2"),
320 ];
321 app.tab_mut().file_history_cursor = 0;
322 app
323 }
324
325 #[test]
326 fn file_history_j_moves_cursor_down() {
327 let mut app = app_with_history();
328 handle_file_history_key(&mut app, key(KeyCode::Char('j')));
329 assert_eq!(app.tab().file_history_cursor, 1);
330 }
331
332 #[test]
333 fn file_history_k_moves_cursor_up() {
334 let mut app = app_with_history();
335 app.tab_mut().file_history_cursor = 2;
336 handle_file_history_key(&mut app, key(KeyCode::Char('k')));
337 assert_eq!(app.tab().file_history_cursor, 1);
338 }
339
340 #[test]
341 fn file_history_cursor_clamps_at_bottom() {
342 let mut app = app_with_history();
343 app.tab_mut().file_history_cursor = 2;
344 handle_file_history_key(&mut app, key(KeyCode::Char('j')));
345 assert_eq!(app.tab().file_history_cursor, 2);
346 }
347
348 #[test]
349 fn file_history_cursor_clamps_at_top() {
350 let mut app = app_with_history();
351 handle_file_history_key(&mut app, key(KeyCode::Char('k')));
352 assert_eq!(app.tab().file_history_cursor, 0);
353 }
354
355 #[test]
356 fn file_history_esc_closes_overlay() {
357 let mut app = app_with_history();
358 handle_file_history_key(&mut app, key(KeyCode::Esc));
359 assert!(app.tab().file_history_path.is_none());
360 assert!(app.tab().file_history_commits.is_empty());
361 }
362
363 fn app_with_blame() -> App {
366 let mut app = App::new();
367 app.tab_mut().blame_path = Some("src/main.rs".to_string());
368 app.tab_mut().blame_scroll = 5;
369 app
370 }
371
372 #[test]
373 fn blame_j_scrolls_down() {
374 let mut app = app_with_blame();
375 handle_blame_key(&mut app, key(KeyCode::Char('j')));
376 assert_eq!(app.tab().blame_scroll, 6);
377 }
378
379 #[test]
380 fn blame_k_scrolls_up() {
381 let mut app = app_with_blame();
382 handle_blame_key(&mut app, key(KeyCode::Char('k')));
383 assert_eq!(app.tab().blame_scroll, 4);
384 }
385
386 #[test]
387 fn blame_esc_closes_overlay() {
388 let mut app = app_with_blame();
389 handle_blame_key(&mut app, key(KeyCode::Esc));
390 assert!(app.tab().blame_path.is_none());
391 assert!(app.tab().blame_lines.is_empty());
392 assert_eq!(app.tab().blame_scroll, 0);
393 }
394
395 fn key(code: KeyCode) -> KeyEvent {
396 KeyEvent::new(code, KeyModifiers::NONE)
397 }
398
399 fn make_commit_files(count: usize) -> Vec<gitkraft_core::DiffFileEntry> {
400 (0..count)
401 .map(|i| gitkraft_core::DiffFileEntry {
402 old_file: String::new(),
403 new_file: format!("file{i}.rs"),
404 status: gitkraft_core::FileStatus::Modified,
405 })
406 .collect()
407 }
408
409 #[test]
412 fn navigate_file_down_noop_on_empty_files() {
413 let mut app = App::new();
414 navigate_file_down(&mut app);
415 assert_eq!(app.tab().commit_diff_file_index, 0);
416 assert!(app.tab().selected_file_indices.is_empty());
417 }
418
419 #[test]
420 fn navigate_file_down_advances_index() {
421 let mut app = App::new();
422 app.tab_mut().commit_files = make_commit_files(3);
423 app.tab_mut().commit_diff_file_index = 0;
424 navigate_file_down(&mut app);
425 assert_eq!(app.tab().commit_diff_file_index, 1);
426 }
427
428 #[test]
429 fn navigate_file_down_wraps_to_first() {
430 let mut app = App::new();
431 app.tab_mut().commit_files = make_commit_files(3);
432 app.tab_mut().commit_diff_file_index = 2;
433 navigate_file_down(&mut app);
434 assert_eq!(app.tab().commit_diff_file_index, 0);
435 }
436
437 #[test]
438 fn navigate_file_down_clears_multi_selection() {
439 let mut app = App::new();
440 app.tab_mut().commit_files = make_commit_files(3);
441 app.tab_mut().selected_file_indices.insert(0);
442 app.tab_mut().selected_file_indices.insert(1);
443 navigate_file_down(&mut app);
444 assert_eq!(app.tab().selected_file_indices.len(), 1);
445 assert!(app.tab().selected_file_indices.contains(&1));
446 }
447
448 #[test]
449 fn navigate_file_down_resets_scroll() {
450 let mut app = App::new();
451 app.tab_mut().commit_files = make_commit_files(2);
452 app.tab_mut().diff_scroll = 99;
453 navigate_file_down(&mut app);
454 assert_eq!(app.tab().diff_scroll, 0);
455 }
456
457 #[test]
460 fn navigate_file_up_noop_on_empty_files() {
461 let mut app = App::new();
462 navigate_file_up(&mut app);
463 assert_eq!(app.tab().commit_diff_file_index, 0);
464 assert!(app.tab().selected_file_indices.is_empty());
465 }
466
467 #[test]
468 fn navigate_file_up_decreases_index() {
469 let mut app = App::new();
470 app.tab_mut().commit_files = make_commit_files(3);
471 app.tab_mut().commit_diff_file_index = 2;
472 navigate_file_up(&mut app);
473 assert_eq!(app.tab().commit_diff_file_index, 1);
474 }
475
476 #[test]
477 fn navigate_file_up_wraps_to_last() {
478 let mut app = App::new();
479 app.tab_mut().commit_files = make_commit_files(3);
480 app.tab_mut().commit_diff_file_index = 0;
481 navigate_file_up(&mut app);
482 assert_eq!(app.tab().commit_diff_file_index, 2);
483 }
484
485 #[test]
486 fn navigate_file_up_clears_multi_selection() {
487 let mut app = App::new();
488 app.tab_mut().commit_files = make_commit_files(3);
489 app.tab_mut().commit_diff_file_index = 2;
490 app.tab_mut().selected_file_indices.insert(1);
491 app.tab_mut().selected_file_indices.insert(2);
492 navigate_file_up(&mut app);
493 assert_eq!(app.tab().selected_file_indices.len(), 1);
494 assert!(app.tab().selected_file_indices.contains(&1));
495 }
496
497 #[test]
500 fn select_file_down_noop_on_empty_files() {
501 let mut app = App::new();
502 select_file_down(&mut app);
503 assert!(app.tab().selected_file_indices.is_empty());
504 assert_eq!(app.tab().commit_diff_file_index, 0);
505 }
506
507 #[test]
508 fn select_file_down_adds_both_indices_to_selection() {
509 let mut app = App::new();
510 app.tab_mut().commit_files = make_commit_files(3);
511 app.tab_mut().commit_diff_file_index = 0;
512 select_file_down(&mut app);
513 assert!(app.tab().selected_file_indices.contains(&0));
514 assert!(app.tab().selected_file_indices.contains(&1));
515 assert_eq!(app.tab().commit_diff_file_index, 1);
516 }
517
518 #[test]
519 fn select_file_down_stops_at_last_file() {
520 let mut app = App::new();
521 app.tab_mut().commit_files = make_commit_files(3);
522 app.tab_mut().commit_diff_file_index = 2;
523 app.tab_mut().anchor_file_index = Some(2);
524 select_file_down(&mut app);
525 assert_eq!(app.tab().commit_diff_file_index, 2);
527 assert!(app.tab().selected_file_indices.contains(&2));
529 }
530
531 #[test]
532 fn select_file_down_extends_existing_selection() {
533 let mut app = App::new();
534 app.tab_mut().commit_files = make_commit_files(4);
535 app.tab_mut().anchor_file_index = Some(0);
537 app.tab_mut().commit_diff_file_index = 1;
538 select_file_down(&mut app);
539 assert!(app.tab().selected_file_indices.contains(&0));
541 assert!(app.tab().selected_file_indices.contains(&1));
542 assert!(app.tab().selected_file_indices.contains(&2));
543 assert_eq!(app.tab().commit_diff_file_index, 2);
544 }
545
546 #[test]
549 fn select_file_up_noop_on_empty_files() {
550 let mut app = App::new();
551 select_file_up(&mut app);
552 assert!(app.tab().selected_file_indices.is_empty());
553 assert_eq!(app.tab().commit_diff_file_index, 0);
554 }
555
556 #[test]
557 fn select_file_up_adds_both_indices_to_selection() {
558 let mut app = App::new();
559 app.tab_mut().commit_files = make_commit_files(3);
560 app.tab_mut().commit_diff_file_index = 2;
561 select_file_up(&mut app);
562 assert!(app.tab().selected_file_indices.contains(&2));
563 assert!(app.tab().selected_file_indices.contains(&1));
564 assert_eq!(app.tab().commit_diff_file_index, 1);
565 }
566
567 #[test]
568 fn select_file_up_stops_at_first_file() {
569 let mut app = App::new();
570 app.tab_mut().commit_files = make_commit_files(3);
571 app.tab_mut().commit_diff_file_index = 0;
572 app.tab_mut().anchor_file_index = Some(0);
573 select_file_up(&mut app);
574 assert_eq!(app.tab().commit_diff_file_index, 0);
576 assert!(app.tab().selected_file_indices.contains(&0));
578 }
579
580 #[test]
581 fn select_file_up_extends_existing_selection() {
582 let mut app = App::new();
583 app.tab_mut().commit_files = make_commit_files(4);
584 app.tab_mut().anchor_file_index = Some(3);
586 app.tab_mut().commit_diff_file_index = 2;
587 select_file_up(&mut app);
588 assert!(app.tab().selected_file_indices.contains(&1));
590 assert!(app.tab().selected_file_indices.contains(&2));
591 assert!(app.tab().selected_file_indices.contains(&3));
592 assert_eq!(app.tab().commit_diff_file_index, 1);
593 }
594
595 #[test]
598 fn j_in_file_list_navigates_down() {
599 let mut app = App::new();
600 app.tab_mut().commit_files = make_commit_files(3);
601 app.tab_mut().diff_sub_pane = DiffSubPane::FileList;
602 handle_key(&mut app, key(KeyCode::Char('j')));
603 assert_eq!(app.tab().commit_diff_file_index, 1);
604 }
605
606 #[test]
607 fn k_in_file_list_navigates_up() {
608 let mut app = App::new();
609 app.tab_mut().commit_files = make_commit_files(3);
610 app.tab_mut().commit_diff_file_index = 2;
611 app.tab_mut().diff_sub_pane = DiffSubPane::FileList;
612 handle_key(&mut app, key(KeyCode::Char('k')));
613 assert_eq!(app.tab().commit_diff_file_index, 1);
614 }
615
616 #[test]
617 fn enter_in_file_list_enters_content_sub_pane() {
618 let mut app = App::new();
619 app.tab_mut().commit_files = make_commit_files(2);
620 app.tab_mut().diff_sub_pane = DiffSubPane::FileList;
621 handle_key(&mut app, key(KeyCode::Enter));
622 assert_eq!(app.tab().diff_sub_pane, DiffSubPane::Content);
623 }
624
625 #[test]
626 fn l_in_file_list_enters_content_sub_pane() {
627 let mut app = App::new();
628 app.tab_mut().commit_files = make_commit_files(2);
629 app.tab_mut().diff_sub_pane = DiffSubPane::FileList;
630 handle_key(&mut app, key(KeyCode::Char('l')));
631 assert_eq!(app.tab().diff_sub_pane, DiffSubPane::Content);
632 }
633
634 #[test]
635 fn enter_in_file_list_without_files_stays_in_file_list() {
636 let mut app = App::new();
637 app.tab_mut().diff_sub_pane = DiffSubPane::FileList;
638 handle_key(&mut app, key(KeyCode::Enter));
639 assert_eq!(app.tab().diff_sub_pane, DiffSubPane::FileList);
640 }
641
642 #[test]
645 fn j_in_content_scrolls_down() {
646 let mut app = App::new();
647 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
648 app.tab_mut().diff_scroll = 3;
649 handle_key(&mut app, key(KeyCode::Char('j')));
650 assert_eq!(app.tab().diff_scroll, 4);
651 }
652
653 #[test]
654 fn k_in_content_scrolls_up() {
655 let mut app = App::new();
656 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
657 app.tab_mut().diff_scroll = 5;
658 handle_key(&mut app, key(KeyCode::Char('k')));
659 assert_eq!(app.tab().diff_scroll, 4);
660 }
661
662 #[test]
663 fn k_in_content_does_not_underflow() {
664 let mut app = App::new();
665 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
666 app.tab_mut().diff_scroll = 0;
667 handle_key(&mut app, key(KeyCode::Char('k')));
668 assert_eq!(app.tab().diff_scroll, 0);
669 }
670
671 #[test]
672 fn g_in_content_scrolls_to_top() {
673 let mut app = App::new();
674 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
675 app.tab_mut().diff_scroll = 42;
676 handle_key(&mut app, key(KeyCode::Char('g')));
677 assert_eq!(app.tab().diff_scroll, 0);
678 }
679
680 #[test]
681 fn d_in_content_pages_down() {
682 let mut app = App::new();
683 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
684 app.tab_mut().diff_scroll = 0;
685 handle_key(&mut app, key(KeyCode::Char('d')));
686 assert_eq!(app.tab().diff_scroll, 20);
687 }
688
689 #[test]
690 fn u_in_content_pages_up() {
691 let mut app = App::new();
692 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
693 app.tab_mut().diff_scroll = 25;
694 handle_key(&mut app, key(KeyCode::Char('u')));
695 assert_eq!(app.tab().diff_scroll, 5);
696 }
697
698 #[test]
699 fn esc_in_content_returns_to_file_list() {
700 let mut app = App::new();
701 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
702 handle_key(&mut app, key(KeyCode::Esc));
703 assert_eq!(app.tab().diff_sub_pane, DiffSubPane::FileList);
704 }
705
706 #[test]
707 fn h_in_content_navigates_file_up_and_stays_in_content() {
708 let mut app = App::new();
709 app.tab_mut().commit_files = make_commit_files(3);
710 app.tab_mut().commit_diff_file_index = 2;
711 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
712 handle_key(&mut app, key(KeyCode::Char('h')));
713 assert_eq!(app.tab().commit_diff_file_index, 1);
714 assert_eq!(app.tab().diff_sub_pane, DiffSubPane::Content);
715 }
716
717 #[test]
718 fn l_in_content_navigates_file_down_and_stays_in_content() {
719 let mut app = App::new();
720 app.tab_mut().commit_files = make_commit_files(3);
721 app.tab_mut().commit_diff_file_index = 0;
722 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
723 handle_key(&mut app, key(KeyCode::Char('l')));
724 assert_eq!(app.tab().commit_diff_file_index, 1);
725 assert_eq!(app.tab().diff_sub_pane, DiffSubPane::Content);
726 }
727
728 #[test]
729 fn e_in_content_sub_pane_also_opens_file() {
730 let mut app = App::new();
731 app.tab_mut().repo_path = Some(std::path::PathBuf::from("/tmp/repo"));
732 app.tab_mut().commit_files = vec![gitkraft_core::DiffFileEntry {
733 old_file: String::new(),
734 new_file: "src/lib.rs".to_string(),
735 status: gitkraft_core::FileStatus::Modified,
736 }];
737 app.tab_mut().diff_sub_pane = DiffSubPane::Content;
739 app.editor = gitkraft_core::Editor::Helix;
740
741 handle_key(&mut app, key(KeyCode::Char('e')));
742
743 assert!(
744 app.pending_editor_open.is_some(),
745 "e in Content sub-pane must also queue a terminal editor open"
746 );
747 }
748
749 #[test]
750 fn e_in_file_list_queues_editor_open() {
751 let mut app = App::new();
752 app.tab_mut().repo_path = Some(std::path::PathBuf::from("/tmp/repo"));
753 app.tab_mut().commit_files = vec![gitkraft_core::DiffFileEntry {
754 old_file: String::new(),
755 new_file: "src/lib.rs".to_string(),
756 status: gitkraft_core::FileStatus::Modified,
757 }];
758 app.tab_mut().diff_sub_pane = DiffSubPane::FileList;
759 app.editor = gitkraft_core::Editor::Helix; handle_key(&mut app, key(KeyCode::Char('e')));
762
763 assert!(
764 app.pending_editor_open.is_some(),
765 "e in file list must queue a terminal editor open"
766 );
767 }
768}