mirror of
https://github.com/extrawurst/gitui.git
synced 2024-12-27 19:14:26 +03:00
feat: support 'n'/'p' key to move to the next/prev hunk. (#1723)
* feat: support 'n'/'p' key to move to the next/prev hunk. * feat: auto scroll next/prev hunk into visible area. * add unittest for VerticalScroll::move_area_to_visible.
This commit is contained in:
parent
197fc6fdf1
commit
bfcf33fce4
@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
* allow `copy` file path on revision files and status tree [[@yanganto]](https://github.com/yanganto) ([#1516](https://github.com/extrawurst/gitui/pull/1516))
|
||||
* print message of where log will be written if `-l` is set ([#1472](https://github.com/extrawurst/gitui/pull/1472))
|
||||
* show remote branches in log [[@cruessler](https://github.com/cruessler)] ([#1501](https://github.com/extrawurst/gitui/issues/1501))
|
||||
* support 'n'/'p' key to move to the next/prev hunk in diff component [[@hamflx](https://github.com/hamflx)] ([#1523](https://github.com/extrawurst/gitui/issues/1523))
|
||||
|
||||
### Fixes
|
||||
* fixed side effect of crossterm 0.26 on windows that caused double input of all keys [[@pm100]](https://github/pm100) ([#1686](https://github.com/extrawurst/gitui/pull/1686))
|
||||
|
@ -629,6 +629,50 @@ impl DiffComponent {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calc_hunk_move_target(
|
||||
&self,
|
||||
direction: isize,
|
||||
) -> Option<usize> {
|
||||
let diff = self.diff.as_ref()?;
|
||||
if diff.hunks.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let max = diff.hunks.len() - 1;
|
||||
let target_index = self.selected_hunk.map_or(0, |i| {
|
||||
let target = if direction >= 0 {
|
||||
i.saturating_add(direction.unsigned_abs())
|
||||
} else {
|
||||
i.saturating_sub(direction.unsigned_abs())
|
||||
};
|
||||
std::cmp::min(max, target)
|
||||
});
|
||||
Some(target_index)
|
||||
}
|
||||
|
||||
fn diff_hunk_move_up_down(&mut self, direction: isize) {
|
||||
let Some(diff) = &self.diff else { return };
|
||||
let hunk_index = self.calc_hunk_move_target(direction);
|
||||
// return if selected_hunk not change
|
||||
if self.selected_hunk == hunk_index {
|
||||
return;
|
||||
}
|
||||
if let Some(hunk_index) = hunk_index {
|
||||
let line_index = diff
|
||||
.hunks
|
||||
.iter()
|
||||
.take(hunk_index)
|
||||
.fold(0, |sum, hunk| sum + hunk.lines.len());
|
||||
let hunk = &diff.hunks[hunk_index];
|
||||
self.selection = Selection::Single(line_index);
|
||||
self.selected_hunk = Some(hunk_index);
|
||||
self.vertical_scroll.move_area_to_visible(
|
||||
self.current_size.get().1 as usize,
|
||||
line_index,
|
||||
line_index.saturating_add(hunk.lines.len()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_stage(&self) -> bool {
|
||||
self.current.is_stage
|
||||
}
|
||||
@ -710,7 +754,16 @@ impl Component for DiffComponent {
|
||||
self.can_scroll(),
|
||||
self.focused(),
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::diff_hunk_next(&self.key_config),
|
||||
self.calc_hunk_move_target(1) != self.selected_hunk,
|
||||
self.focused(),
|
||||
));
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::diff_hunk_prev(&self.key_config),
|
||||
self.calc_hunk_move_target(-1) != self.selected_hunk,
|
||||
self.focused(),
|
||||
));
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::diff_home_end(&self.key_config),
|
||||
@ -769,7 +822,7 @@ impl Component for DiffComponent {
|
||||
CommandBlocking::PassingOn
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
|
||||
fn event(&mut self, ev: &Event) -> Result<EventState> {
|
||||
if self.focused() {
|
||||
if let Event::Key(e) = ev {
|
||||
@ -815,6 +868,18 @@ impl Component for DiffComponent {
|
||||
self.horizontal_scroll
|
||||
.move_right(HorizontalScrollType::Left);
|
||||
Ok(EventState::Consumed)
|
||||
} else if key_match(
|
||||
e,
|
||||
self.key_config.keys.diff_hunk_next,
|
||||
) {
|
||||
self.diff_hunk_move_up_down(1);
|
||||
Ok(EventState::Consumed)
|
||||
} else if key_match(
|
||||
e,
|
||||
self.key_config.keys.diff_hunk_prev,
|
||||
) {
|
||||
self.diff_hunk_move_up_down(-1);
|
||||
Ok(EventState::Consumed)
|
||||
} else if key_match(
|
||||
e,
|
||||
self.key_config.keys.stage_unstage_item,
|
||||
|
@ -51,6 +51,32 @@ impl VerticalScroll {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn move_area_to_visible(
|
||||
&self,
|
||||
height: usize,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) {
|
||||
let top = self.top.get();
|
||||
let bottom = top + height;
|
||||
let max_top = self.max_top.get();
|
||||
// the top of some content is hidden
|
||||
if start < top {
|
||||
self.top.set(start);
|
||||
return;
|
||||
}
|
||||
// the bottom of some content is hidden and there is visible space available
|
||||
if end > bottom && start > top {
|
||||
let avail_space = start.saturating_sub(top);
|
||||
let diff = std::cmp::min(
|
||||
avail_space,
|
||||
end.saturating_sub(bottom),
|
||||
);
|
||||
let top = top.saturating_add(diff);
|
||||
self.top.set(std::cmp::min(max_top, top));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&self,
|
||||
selection: usize,
|
||||
@ -136,4 +162,41 @@ mod tests {
|
||||
fn test_scroll_zero_height() {
|
||||
assert_eq!(calc_scroll_top(4, 0, 4, 3), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scroll_bottom_into_view() {
|
||||
let visual_height = 10;
|
||||
let line_count = 20;
|
||||
let scroll = VerticalScroll::new();
|
||||
scroll.max_top.set(line_count - visual_height);
|
||||
|
||||
// intersecting with the bottom of the visible area
|
||||
scroll.move_area_to_visible(visual_height, 9, 11);
|
||||
assert_eq!(scroll.get_top(), 1);
|
||||
|
||||
// completely below the visible area
|
||||
scroll.move_area_to_visible(visual_height, 15, 17);
|
||||
assert_eq!(scroll.get_top(), 7);
|
||||
|
||||
// scrolling to the bottom overflow
|
||||
scroll.move_area_to_visible(visual_height, 30, 40);
|
||||
assert_eq!(scroll.get_top(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scroll_top_into_view() {
|
||||
let visual_height = 10;
|
||||
let line_count = 20;
|
||||
let scroll = VerticalScroll::new();
|
||||
scroll.max_top.set(line_count - visual_height);
|
||||
scroll.top.set(4);
|
||||
|
||||
// intersecting with the top of the visible area
|
||||
scroll.move_area_to_visible(visual_height, 2, 8);
|
||||
assert_eq!(scroll.get_top(), 2);
|
||||
|
||||
// completely above the visible area
|
||||
scroll.move_area_to_visible(visual_height, 0, 2);
|
||||
assert_eq!(scroll.get_top(), 0);
|
||||
}
|
||||
}
|
||||
|
@ -109,6 +109,8 @@ pub struct KeysList {
|
||||
pub pull: GituiKeyEvent,
|
||||
pub abort_merge: GituiKeyEvent,
|
||||
pub undo_commit: GituiKeyEvent,
|
||||
pub diff_hunk_next: GituiKeyEvent,
|
||||
pub diff_hunk_prev: GituiKeyEvent,
|
||||
pub stage_unstage_item: GituiKeyEvent,
|
||||
pub tag_annotate: GituiKeyEvent,
|
||||
pub view_submodules: GituiKeyEvent,
|
||||
@ -193,6 +195,8 @@ impl Default for KeysList {
|
||||
open_file_tree: GituiKeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT),
|
||||
file_find: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()),
|
||||
branch_find: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()),
|
||||
diff_hunk_next: GituiKeyEvent::new(KeyCode::Char('n'), KeyModifiers::empty()),
|
||||
diff_hunk_prev: GituiKeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty()),
|
||||
stage_unstage_item: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
|
||||
tag_annotate: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
|
||||
view_submodules: GituiKeyEvent::new(KeyCode::Char('S'), KeyModifiers::SHIFT),
|
||||
|
@ -603,6 +603,30 @@ pub mod commands {
|
||||
CMD_GROUP_LOG,
|
||||
)
|
||||
}
|
||||
pub fn diff_hunk_next(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Next hunk [{}]",
|
||||
key_config.get_hint(key_config.keys.diff_hunk_next),
|
||||
),
|
||||
"move cursor to next hunk",
|
||||
CMD_GROUP_DIFF,
|
||||
)
|
||||
}
|
||||
pub fn diff_hunk_prev(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Prev hunk [{}]",
|
||||
key_config.get_hint(key_config.keys.diff_hunk_prev),
|
||||
),
|
||||
"move cursor to prev hunk",
|
||||
CMD_GROUP_DIFF,
|
||||
)
|
||||
}
|
||||
pub fn diff_home_end(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
|
Loading…
Reference in New Issue
Block a user