Skip to content

Commit edd00c6

Browse files
committed
add command and keybding to jump to next/prev hunk
1 parent d14a031 commit edd00c6

File tree

4 files changed

+129
-0
lines changed

4 files changed

+129
-0
lines changed

book/src/keymap.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,10 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
320320
| `]t` | Go to previous test (**TS**) | `goto_prev_test` |
321321
| `]p` | Go to next paragraph | `goto_next_paragraph` |
322322
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
323+
| `]g` | Go to next change | `goto_next_change` |
324+
| `[g` | Go to previous change | `goto_prev_change` |
325+
| `]G` | Go to first change | `goto_first_change` |
326+
| `[G` | Go to last change | `goto_last_change` |
323327
| `[Space` | Add newline above | `add_newline_above` |
324328
| `]Space` | Add newline below | `add_newline_below` |
325329

helix-term/src/commands.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub(crate) mod lsp;
33
pub(crate) mod typed;
44

55
pub use dap::*;
6+
use helix_vcs::Hunk;
67
pub use lsp::*;
78
use tui::text::Spans;
89
pub use typed::*;
@@ -308,6 +309,10 @@ impl MappableCommand {
308309
goto_last_diag, "Goto last diagnostic",
309310
goto_next_diag, "Goto next diagnostic",
310311
goto_prev_diag, "Goto previous diagnostic",
312+
goto_next_change, "Goto next change",
313+
goto_prev_change, "Goto previous change",
314+
goto_first_change, "Goto first change",
315+
goto_last_change, "Goto last change",
311316
goto_line_start, "Goto line start",
312317
goto_line_end, "Goto line end",
313318
goto_next_buffer, "Goto next buffer",
@@ -2915,6 +2920,66 @@ fn goto_prev_diag(cx: &mut Context) {
29152920
goto_pos(editor, pos);
29162921
}
29172922

2923+
fn goto_first_change(cx: &mut Context) {
2924+
goto_first_hunk_impl(cx, false);
2925+
}
2926+
2927+
fn goto_last_change(cx: &mut Context) {
2928+
goto_first_hunk_impl(cx, true);
2929+
}
2930+
2931+
fn goto_first_hunk_impl(cx: &mut Context, reverse: bool) {
2932+
let editor = &mut cx.editor;
2933+
let (_, doc) = current!(editor);
2934+
if let Some(handle) = doc.diff_handle() {
2935+
let hunk = {
2936+
let hunks = handle.hunks();
2937+
let idx = if reverse {
2938+
hunks.len().saturating_sub(1)
2939+
} else {
2940+
0
2941+
};
2942+
hunks.nth_hunk(idx)
2943+
};
2944+
if hunk != Hunk::NONE {
2945+
let pos = doc.text().line_to_char(hunk.after.start as usize);
2946+
goto_pos(editor, pos)
2947+
}
2948+
}
2949+
}
2950+
2951+
fn goto_next_change(cx: &mut Context) {
2952+
goto_next_change_impl::<false>(cx)
2953+
}
2954+
2955+
fn goto_prev_change(cx: &mut Context) {
2956+
goto_next_change_impl::<true>(cx)
2957+
}
2958+
2959+
fn goto_next_change_impl<const REVERSE: bool>(cx: &mut Context) {
2960+
let editor = &mut cx.editor;
2961+
let (view, doc) = current!(editor);
2962+
2963+
let cursor_pos = doc
2964+
.selection(view.id)
2965+
.primary()
2966+
.cursor(doc.text().slice(..));
2967+
2968+
let cursor_line = doc.text().char_to_line(cursor_pos);
2969+
2970+
let next_changed_line = doc.diff_handle().and_then(|diff_handle| {
2971+
let hunks = diff_handle.hunks();
2972+
let hunk_idx = hunks.next_hunk::<REVERSE>(cursor_line as u32)?;
2973+
let line = hunks.nth_hunk(hunk_idx).after.start;
2974+
Some(line)
2975+
});
2976+
2977+
if let Some(next_change_line) = next_changed_line {
2978+
let pos = doc.text().line_to_char(next_change_line as usize);
2979+
goto_pos(cx.editor, pos)
2980+
}
2981+
}
2982+
29182983
pub mod insert {
29192984
use super::*;
29202985
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;

helix-term/src/keymap/default.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
100100
"[" => { "Left bracket"
101101
"d" => goto_prev_diag,
102102
"D" => goto_first_diag,
103+
"g" => goto_prev_change,
104+
"G" => goto_first_change,
103105
"f" => goto_prev_function,
104106
"c" => goto_prev_class,
105107
"a" => goto_prev_parameter,
@@ -111,6 +113,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
111113
"]" => { "Right bracket"
112114
"d" => goto_next_diag,
113115
"D" => goto_last_diag,
116+
"g" => goto_next_change,
117+
"G" => goto_last_change,
114118
"f" => goto_next_function,
115119
"c" => goto_next_class,
116120
"a" => goto_next_parameter,

helix-vcs/src/diff.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,60 @@ impl FileHunks<'_> {
183183
pub fn is_empty(&self) -> bool {
184184
self.len() == 0
185185
}
186+
187+
fn next_hunk_impl<const INVERT: bool, const REVERSE: bool>(&self, line: u32) -> Option<u32> {
188+
let hunk_range = |hunk: &Hunk| {
189+
if INVERT {
190+
hunk.before.clone()
191+
} else {
192+
hunk.after.clone()
193+
}
194+
};
195+
let res = self.hunks.binary_search_by_key(&line, |hunk| {
196+
let range = hunk_range(hunk);
197+
if REVERSE {
198+
range.end
199+
} else {
200+
range.start
201+
}
202+
});
203+
204+
if REVERSE {
205+
match res {
206+
// Search found a hunk that ends exactly at this line (so it does not include the current line).
207+
// We can usually just return that hunk, howver a specical special case for empty hunk is necessary
208+
// which represents a pure removal.
209+
// Removals are technically empty but are still shown as single line hunks
210+
// and as such we must jump to the previus hunk (if it exists) if we are already inside the removal
211+
Ok(pos) if !hunk_range(&self.hunks[pos]).is_empty() => Some(pos as u32),
212+
213+
// No hunk ends exactly at this line, so the search returns
214+
// the position where a hunk ending at this line should be inserted.
215+
// That position before this one is exactly the position of the previous hunk
216+
Err(0) | Ok(0) => None,
217+
Err(pos) | Ok(pos) => Some(pos as u32 - 1),
218+
}
219+
} else {
220+
match res {
221+
// Search found a hunk that starts exactly at this line, return the next hunk if it exists.
222+
Ok(pos) if pos + 1 == self.hunks.len() => None,
223+
Ok(pos) => Some(pos as u32 + 1),
224+
225+
// No hunk starts exactly at this line, so the search returns
226+
// the position where a hunk starting at this line should be inserted.
227+
// That position is exactly the position of the next hunk or the end
228+
// of the list if no such hunk exists
229+
Err(pos) if pos == self.hunks.len() => None,
230+
Err(pos) => Some(pos as u32),
231+
}
232+
}
233+
}
234+
235+
pub fn next_hunk<const REVERSE: bool>(&self, line: u32) -> Option<u32> {
236+
if self.inverted {
237+
self.next_hunk_impl::<true, REVERSE>(line)
238+
} else {
239+
self.next_hunk_impl::<false, REVERSE>(line)
240+
}
241+
}
186242
}

0 commit comments

Comments
 (0)