Skip to content

Commit 39dab15

Browse files
committed
add command and keybding to jump to next/prev hunk
1 parent b982b9c commit 39dab15

File tree

4 files changed

+133
-0
lines changed

4 files changed

+133
-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",
@@ -2916,6 +2921,66 @@ fn goto_prev_diag(cx: &mut Context) {
29162921
goto_pos(editor, pos);
29172922
}
29182923

2924+
fn goto_first_change(cx: &mut Context) {
2925+
goto_first_hunk_impl(cx, false);
2926+
}
2927+
2928+
fn goto_last_change(cx: &mut Context) {
2929+
goto_first_hunk_impl(cx, true);
2930+
}
2931+
2932+
fn goto_first_hunk_impl(cx: &mut Context, reverse: bool) {
2933+
let editor = &mut cx.editor;
2934+
let (_, doc) = current!(editor);
2935+
if let Some(handle) = doc.diff_handle() {
2936+
let hunk = {
2937+
let hunks = handle.hunks();
2938+
let idx = if reverse {
2939+
hunks.len().saturating_sub(1)
2940+
} else {
2941+
0
2942+
};
2943+
hunks.nth_hunk(idx)
2944+
};
2945+
if hunk != Hunk::NONE {
2946+
let pos = doc.text().line_to_char(hunk.after.start as usize);
2947+
goto_pos(editor, pos)
2948+
}
2949+
}
2950+
}
2951+
2952+
fn goto_next_change(cx: &mut Context) {
2953+
goto_next_change_impl::<false>(cx)
2954+
}
2955+
2956+
fn goto_prev_change(cx: &mut Context) {
2957+
goto_next_change_impl::<true>(cx)
2958+
}
2959+
2960+
fn goto_next_change_impl<const REVERSE: bool>(cx: &mut Context) {
2961+
let editor = &mut cx.editor;
2962+
let (view, doc) = current!(editor);
2963+
2964+
let cursor_pos = doc
2965+
.selection(view.id)
2966+
.primary()
2967+
.cursor(doc.text().slice(..));
2968+
2969+
let cursor_line = doc.text().char_to_line(cursor_pos);
2970+
2971+
let next_changed_line = doc.diff_handle().and_then(|diff_handle| {
2972+
let hunks = diff_handle.hunks();
2973+
let hunk_idx = hunks.next_hunk::<REVERSE>(cursor_line as u32)?;
2974+
let line = hunks.nth_hunk(hunk_idx).after.start;
2975+
Some(line)
2976+
});
2977+
2978+
if let Some(next_change_line) = next_changed_line {
2979+
let pos = doc.text().line_to_char(next_change_line as usize);
2980+
goto_pos(cx.editor, pos)
2981+
}
2982+
}
2983+
29192984
pub mod insert {
29202985
use super::*;
29212986
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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,64 @@ impl FileHunks<'_> {
175175
None => Hunk::NONE,
176176
}
177177
}
178+
179+
pub fn len(&self) -> u32 {
180+
self.hunks.len() as u32
181+
}
182+
183+
fn next_hunk_impl<const INVERT: bool, const REVERSE: bool>(&self, line: u32) -> Option<u32> {
184+
let hunk_range = |hunk: &Hunk| {
185+
if INVERT {
186+
hunk.before.clone()
187+
} else {
188+
hunk.after.clone()
189+
}
190+
};
191+
let res = self.hunks.binary_search_by_key(&line, |hunk| {
192+
let range = hunk_range(hunk);
193+
if REVERSE {
194+
range.end
195+
} else {
196+
range.start
197+
}
198+
});
199+
200+
if REVERSE {
201+
match res {
202+
// Search found a hunk that ends exactly at this line (so it does not include the current line).
203+
// We can usually just return that hunk, howver a specical special case for empty hunk is necessary
204+
// which represents a pure removal.
205+
// Removals are technically empty but are still shown as single line hunks
206+
// and as such we must jump to the previus hunk (if it exists) if we are already inside the removal
207+
Ok(pos) if !hunk_range(&self.hunks[pos]).is_empty() => Some(pos as u32),
208+
209+
// No hunk ends exactly at this line, so the search returns
210+
// the position where a hunk ending at this line should be inserted.
211+
// That position before this one is exactly the position of the previous hunk
212+
Err(0) | Ok(0) => None,
213+
Err(pos) | Ok(pos) => Some(pos as u32 - 1),
214+
}
215+
} else {
216+
match res {
217+
// Search found a hunk that starts exactly at this line, return the next hunk if it exists.
218+
Ok(pos) if pos + 1 == self.hunks.len() => None,
219+
Ok(pos) => Some(pos as u32 + 1),
220+
221+
// No hunk starts exactly at this line, so the search returns
222+
// the position where a hunk starting at this line should be inserted.
223+
// That position is exactly the position of the next hunk or the end
224+
// of the list if no such hunk exists
225+
Err(pos) if pos == self.hunks.len() => None,
226+
Err(pos) => Some(pos as u32),
227+
}
228+
}
229+
}
230+
231+
pub fn next_hunk<const REVERSE: bool>(&self, line: u32) -> Option<u32> {
232+
if self.inverted {
233+
self.next_hunk_impl::<true, REVERSE>(line)
234+
} else {
235+
self.next_hunk_impl::<false, REVERSE>(line)
236+
}
237+
}
178238
}

0 commit comments

Comments
 (0)