mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 17:08:21 +00: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
5 changed files with 159 additions and 2 deletions
|
|
@ -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))
|
* 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))
|
* 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))
|
* 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
|
### 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))
|
* 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(())
|
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 {
|
const fn is_stage(&self) -> bool {
|
||||||
self.current.is_stage
|
self.current.is_stage
|
||||||
}
|
}
|
||||||
|
|
@ -710,7 +754,16 @@ impl Component for DiffComponent {
|
||||||
self.can_scroll(),
|
self.can_scroll(),
|
||||||
self.focused(),
|
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(
|
out.push(
|
||||||
CommandInfo::new(
|
CommandInfo::new(
|
||||||
strings::commands::diff_home_end(&self.key_config),
|
strings::commands::diff_home_end(&self.key_config),
|
||||||
|
|
@ -769,7 +822,7 @@ impl Component for DiffComponent {
|
||||||
CommandBlocking::PassingOn
|
CommandBlocking::PassingOn
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cognitive_complexity)]
|
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
|
||||||
fn event(&mut self, ev: &Event) -> Result<EventState> {
|
fn event(&mut self, ev: &Event) -> Result<EventState> {
|
||||||
if self.focused() {
|
if self.focused() {
|
||||||
if let Event::Key(e) = ev {
|
if let Event::Key(e) = ev {
|
||||||
|
|
@ -815,6 +868,18 @@ impl Component for DiffComponent {
|
||||||
self.horizontal_scroll
|
self.horizontal_scroll
|
||||||
.move_right(HorizontalScrollType::Left);
|
.move_right(HorizontalScrollType::Left);
|
||||||
Ok(EventState::Consumed)
|
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(
|
} else if key_match(
|
||||||
e,
|
e,
|
||||||
self.key_config.keys.stage_unstage_item,
|
self.key_config.keys.stage_unstage_item,
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,32 @@ impl VerticalScroll {
|
||||||
true
|
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(
|
pub fn update(
|
||||||
&self,
|
&self,
|
||||||
selection: usize,
|
selection: usize,
|
||||||
|
|
@ -136,4 +162,41 @@ mod tests {
|
||||||
fn test_scroll_zero_height() {
|
fn test_scroll_zero_height() {
|
||||||
assert_eq!(calc_scroll_top(4, 0, 4, 3), 0);
|
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 pull: GituiKeyEvent,
|
||||||
pub abort_merge: GituiKeyEvent,
|
pub abort_merge: GituiKeyEvent,
|
||||||
pub undo_commit: GituiKeyEvent,
|
pub undo_commit: GituiKeyEvent,
|
||||||
|
pub diff_hunk_next: GituiKeyEvent,
|
||||||
|
pub diff_hunk_prev: GituiKeyEvent,
|
||||||
pub stage_unstage_item: GituiKeyEvent,
|
pub stage_unstage_item: GituiKeyEvent,
|
||||||
pub tag_annotate: GituiKeyEvent,
|
pub tag_annotate: GituiKeyEvent,
|
||||||
pub view_submodules: GituiKeyEvent,
|
pub view_submodules: GituiKeyEvent,
|
||||||
|
|
@ -193,6 +195,8 @@ impl Default for KeysList {
|
||||||
open_file_tree: GituiKeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT),
|
open_file_tree: GituiKeyEvent::new(KeyCode::Char('F'), KeyModifiers::SHIFT),
|
||||||
file_find: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()),
|
file_find: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::empty()),
|
||||||
branch_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()),
|
stage_unstage_item: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
|
||||||
tag_annotate: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
|
tag_annotate: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
|
||||||
view_submodules: GituiKeyEvent::new(KeyCode::Char('S'), KeyModifiers::SHIFT),
|
view_submodules: GituiKeyEvent::new(KeyCode::Char('S'), KeyModifiers::SHIFT),
|
||||||
|
|
|
||||||
|
|
@ -603,6 +603,30 @@ pub mod commands {
|
||||||
CMD_GROUP_LOG,
|
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(
|
pub fn diff_home_end(
|
||||||
key_config: &SharedKeyConfig,
|
key_config: &SharedKeyConfig,
|
||||||
) -> CommandText {
|
) -> CommandText {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue