gitkraft_gui/features/diff/update.rs
1//! Update logic for diff-related messages.
2
3use iced::Task;
4
5use crate::message::Message;
6use crate::state::GitKraft;
7
8/// Handle diff-related messages, returning a [`Task`] for any follow-up
9/// async work.
10pub fn update(state: &mut GitKraft, message: Message) -> Task<Message> {
11 match message {
12 Message::SelectDiffByIndex(index) => {
13 let shift_held = state.keyboard_modifiers.shift();
14 let tab = state.active_tab();
15 let repo_path = tab.repo_path.clone();
16 let oid = tab.selected_commit_oid.clone();
17
18 if shift_held {
19 // ── Shift+Click: range selection from anchor to clicked index ──
20 //
21 // The anchor is the file that was last clicked WITHOUT Shift.
22 // Every Shift+Click replaces the selection with everything
23 // between the anchor and the clicked index (inclusive), exactly
24 // like standard file-manager range selection.
25 let anchor = state
26 .active_tab()
27 .anchor_file_index
28 .or(state.active_tab().selected_file_index)
29 .unwrap_or(index);
30
31 let (start, end) = if anchor <= index {
32 (anchor, index)
33 } else {
34 (index, anchor)
35 };
36
37 // Build the range in ascending order so badges are always
38 // numbered top-to-bottom.
39 let range: Vec<usize> = (start..=end).collect();
40 let count = range.len();
41
42 let tab = state.active_tab_mut();
43 tab.selected_commit_file_indices = range;
44 tab.selected_file_index = Some(index);
45
46 if count == 1 {
47 // Range collapsed to a single file — behave like a normal
48 // single-file selection (no multi-diff panel).
49 tab.multi_file_diffs.clear();
50 let file_entry = tab.commit_files.get(start).cloned();
51 if let (Some(entry), Some(path), Some(oid)) = (file_entry, repo_path, oid) {
52 let file_path = entry.display_path().to_string();
53 let tab = state.active_tab_mut();
54 tab.is_loading_file_diff = true;
55 tab.diff_scroll_offset = 0.0;
56 return crate::features::commits::commands::load_single_file_diff(
57 path, oid, file_path,
58 );
59 }
60 } else {
61 // Multiple files in range — load and display them all.
62 tab.selected_diff = None;
63 tab.is_loading_file_diff = true;
64 tab.diff_scroll_offset = 0.0;
65 let file_paths: Vec<String> = tab
66 .selected_commit_file_indices
67 .iter()
68 .filter_map(|&i| {
69 tab.commit_files
70 .get(i)
71 .map(|f| f.display_path().to_string())
72 })
73 .collect();
74 if let (Some(path), Some(oid)) = (repo_path, oid) {
75 return crate::features::commits::commands::load_commit_multi_diffs(
76 path, oid, file_paths,
77 );
78 }
79 }
80 Task::none()
81 } else {
82 // ── Regular click: single-file selection, set range anchor ─────
83 let tab = state.active_tab_mut();
84 tab.anchor_file_index = Some(index); // fix the anchor for future Shift+Clicks
85 tab.selected_commit_file_indices.clear();
86 // NOTE: multi_file_diffs, commit_range_diffs, and
87 // diff_scroll_offset are intentionally NOT cleared here.
88 // Clearing them immediately would change the widget-tree
89 // structure before the replacement diff is ready, causing the
90 // file list panel to visually flash/blink. Instead we defer
91 // those resets to SingleFileDiffLoaded where the new diff
92 // content is set atomically.
93
94 let file_entry = tab.commit_files.get(index).cloned();
95 if let (Some(entry), Some(path), Some(oid)) = (file_entry, repo_path, oid) {
96 let file_path = entry.display_path().to_string();
97 let tab = state.active_tab_mut();
98 tab.selected_file_index = Some(index);
99 tab.is_loading_file_diff = true;
100 crate::features::commits::commands::load_single_file_diff(path, oid, file_path)
101 } else {
102 Task::none()
103 }
104 }
105 }
106
107 Message::SelectDiff(diff_info) => {
108 state.active_tab_mut().context_menu = None;
109 let tab = state.active_tab_mut();
110 tab.selected_diff = Some(diff_info);
111 tab.diff_scroll_offset = 0.0;
112 Task::none()
113 }
114
115 Message::CommitMultiDiffLoaded(result) => {
116 let tab = state.active_tab_mut();
117 tab.is_loading_file_diff = false;
118 match result {
119 Ok(diffs) => {
120 tab.multi_file_diffs = diffs;
121 tab.selected_diff = None;
122 tab.diff_scroll_offset = 0.0;
123 }
124 Err(e) => {
125 tab.multi_file_diffs.clear();
126 tab.error_message = Some(format!("Failed to load multi-file diff: {e}"));
127 }
128 }
129 Task::none()
130 }
131
132 _ => Task::none(),
133 }
134}