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:
hamflx 2023-06-19 23:52:05 +08:00 committed by GitHub
parent 197fc6fdf1
commit bfcf33fce4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 159 additions and 2 deletions

View File

@ -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))

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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),

View File

@ -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 {