Skip to main content

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                tab.multi_file_diffs.clear();
87                tab.commit_range_diffs.clear();
88
89                let file_entry = tab.commit_files.get(index).cloned();
90                if let (Some(entry), Some(path), Some(oid)) = (file_entry, repo_path, oid) {
91                    let file_path = entry.display_path().to_string();
92                    let tab = state.active_tab_mut();
93                    tab.selected_file_index = Some(index);
94                    tab.is_loading_file_diff = true;
95                    tab.diff_scroll_offset = 0.0;
96                    crate::features::commits::commands::load_single_file_diff(path, oid, file_path)
97                } else {
98                    Task::none()
99                }
100            }
101        }
102
103        Message::SelectDiff(diff_info) => {
104            state.active_tab_mut().context_menu = None;
105            let tab = state.active_tab_mut();
106            tab.selected_diff = Some(diff_info);
107            tab.diff_scroll_offset = 0.0;
108            Task::none()
109        }
110
111        Message::CommitMultiDiffLoaded(result) => {
112            let tab = state.active_tab_mut();
113            tab.is_loading_file_diff = false;
114            match result {
115                Ok(diffs) => {
116                    tab.multi_file_diffs = diffs;
117                    tab.selected_diff = None;
118                    tab.diff_scroll_offset = 0.0;
119                }
120                Err(e) => {
121                    tab.multi_file_diffs.clear();
122                    tab.error_message = Some(format!("Failed to load multi-file diff: {e}"));
123                }
124            }
125            Task::none()
126        }
127
128        _ => Task::none(),
129    }
130}