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