From d22f5ae32eba67e7ab4af9af1902bdbe0cdac819 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 6 Nov 2022 19:36:55 +0100 Subject: [PATCH] add command and keybding to jump to next/prev hunk --- helix-term/src/commands.rs | 34 +++++++++++++++++++ helix-term/src/keymap/default.rs | 2 ++ helix-vcs/src/diff.rs | 56 ++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4e3c321cb8fd0..46265368898cb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -306,6 +306,8 @@ impl MappableCommand { goto_last_diag, "Goto last diagnostic", goto_next_diag, "Goto next diagnostic", goto_prev_diag, "Goto previous diagnostic", + goto_next_hunk, "Goto next hunk of changes", + goto_prev_hunk, "Goto previous hunk of changes", goto_line_start, "Goto line start", goto_line_end, "Goto line end", goto_next_buffer, "Goto next buffer", @@ -2895,6 +2897,38 @@ fn goto_prev_diag(cx: &mut Context) { goto_pos(editor, pos); } +fn goto_next_hunk(cx: &mut Context) { + goto_next_hunk_impl::(cx) +} + +fn goto_prev_hunk(cx: &mut Context) { + goto_next_hunk_impl::(cx) +} + +fn goto_next_hunk_impl(cx: &mut Context) { + let editor = &mut cx.editor; + let (view, doc) = current!(editor); + + let cursor_pos = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); + + let cursor_line = doc.text().char_to_line(cursor_pos); + + let next_changed_line = doc.diff_handle().and_then(|diff_handle| { + let hunks = diff_handle.hunks(); + let hunk_idx = hunks.next_hunk::(cursor_line as u32)?; + let line = hunks.nth_hunk(hunk_idx).after.start; + Some(line) + }); + + if let Some(next_change_line) = next_changed_line { + let pos = doc.text().line_to_char(next_change_line as usize); + goto_pos(cx.editor, pos) + } +} + pub mod insert { use super::*; pub type Hook = fn(&Rope, &Selection, char) -> Option; diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 118764d975858..118e7591a4ef1 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -100,6 +100,7 @@ pub fn default() -> HashMap { "[" => { "Left bracket" "d" => goto_prev_diag, "D" => goto_first_diag, + "h" => goto_prev_hunk, "f" => goto_prev_function, "c" => goto_prev_class, "a" => goto_prev_parameter, @@ -111,6 +112,7 @@ pub fn default() -> HashMap { "]" => { "Right bracket" "d" => goto_next_diag, "D" => goto_last_diag, + "h" => goto_next_hunk, "f" => goto_next_function, "c" => goto_next_class, "a" => goto_next_parameter, diff --git a/helix-vcs/src/diff.rs b/helix-vcs/src/diff.rs index 6a397ac886177..32ed7d62dd4ce 100644 --- a/helix-vcs/src/diff.rs +++ b/helix-vcs/src/diff.rs @@ -159,4 +159,60 @@ impl FileHunks<'_> { None => Hunk::NONE, } } + + fn next_hunk_impl(&self, line: u32) -> Option { + let hunk_range = |hunk: &Hunk| { + if INVERT { + hunk.before.clone() + } else { + hunk.after.clone() + } + }; + let res = self.hunks.binary_search_by_key(&line, |hunk| { + let range = hunk_range(hunk); + if REVERSE { + range.end + } else { + range.start + } + }); + + if REVERSE { + match res { + // Search found a hunk that ends exactly at this line (so it does not include the current line). + // We can usually just return that hunk, howver a specical special case for empty hunk is necessary + // which represents a pure removal. + // Removals are technically empty but are still shown as single line hunks + // and as such we must jump to the previus hunk (if it exists) if we are already inside the removal + Ok(pos) if !hunk_range(&self.hunks[pos]).is_empty() => Some(pos as u32), + + // No hunk ends exactly at this line, so the search returns + // the position where a hunk ending at this line should be inserted. + // That position before this one is exactly the position of the previous hunk + Err(0) | Ok(0) => None, + Err(pos) | Ok(pos) => Some(pos as u32 - 1), + } + } else { + match res { + // Search found a hunk that starts exactly at this line, return the next hunk if it exists. + Ok(pos) if pos + 1 == self.hunks.len() => None, + Ok(pos) => Some(pos as u32 + 1), + + // No hunk starts exactly at this line, so the search returns + // the position where a hunk starting at this line should be inserted. + // That position is exactly the position of the next hunk or the end + // of the list if no such hunk exists + Err(pos) if pos == self.hunks.len() => None, + Err(pos) => Some(pos as u32), + } + } + } + + pub fn next_hunk(&self, line: u32) -> Option { + if self.inverted { + self.next_hunk_impl::(line) + } else { + self.next_hunk_impl::(line) + } + } }