Skip to content

Commit d22f5ae

Browse files
committed
add command and keybding to jump to next/prev hunk
1 parent 8e02b6b commit d22f5ae

File tree

3 files changed

+92
-0
lines changed

3 files changed

+92
-0
lines changed

helix-term/src/commands.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ impl MappableCommand {
306306
goto_last_diag, "Goto last diagnostic",
307307
goto_next_diag, "Goto next diagnostic",
308308
goto_prev_diag, "Goto previous diagnostic",
309+
goto_next_hunk, "Goto next hunk of changes",
310+
goto_prev_hunk, "Goto previous hunk of changes",
309311
goto_line_start, "Goto line start",
310312
goto_line_end, "Goto line end",
311313
goto_next_buffer, "Goto next buffer",
@@ -2895,6 +2897,38 @@ fn goto_prev_diag(cx: &mut Context) {
28952897
goto_pos(editor, pos);
28962898
}
28972899

2900+
fn goto_next_hunk(cx: &mut Context) {
2901+
goto_next_hunk_impl::<false>(cx)
2902+
}
2903+
2904+
fn goto_prev_hunk(cx: &mut Context) {
2905+
goto_next_hunk_impl::<true>(cx)
2906+
}
2907+
2908+
fn goto_next_hunk_impl<const REVERSE: bool>(cx: &mut Context) {
2909+
let editor = &mut cx.editor;
2910+
let (view, doc) = current!(editor);
2911+
2912+
let cursor_pos = doc
2913+
.selection(view.id)
2914+
.primary()
2915+
.cursor(doc.text().slice(..));
2916+
2917+
let cursor_line = doc.text().char_to_line(cursor_pos);
2918+
2919+
let next_changed_line = doc.diff_handle().and_then(|diff_handle| {
2920+
let hunks = diff_handle.hunks();
2921+
let hunk_idx = hunks.next_hunk::<REVERSE>(cursor_line as u32)?;
2922+
let line = hunks.nth_hunk(hunk_idx).after.start;
2923+
Some(line)
2924+
});
2925+
2926+
if let Some(next_change_line) = next_changed_line {
2927+
let pos = doc.text().line_to_char(next_change_line as usize);
2928+
goto_pos(cx.editor, pos)
2929+
}
2930+
}
2931+
28982932
pub mod insert {
28992933
use super::*;
29002934
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;

helix-term/src/keymap/default.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
100100
"[" => { "Left bracket"
101101
"d" => goto_prev_diag,
102102
"D" => goto_first_diag,
103+
"h" => goto_prev_hunk,
103104
"f" => goto_prev_function,
104105
"c" => goto_prev_class,
105106
"a" => goto_prev_parameter,
@@ -111,6 +112,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
111112
"]" => { "Right bracket"
112113
"d" => goto_next_diag,
113114
"D" => goto_last_diag,
115+
"h" => goto_next_hunk,
114116
"f" => goto_next_function,
115117
"c" => goto_next_class,
116118
"a" => goto_next_parameter,

helix-vcs/src/diff.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,60 @@ impl FileHunks<'_> {
159159
None => Hunk::NONE,
160160
}
161161
}
162+
163+
fn next_hunk_impl<const INVERT: bool, const REVERSE: bool>(&self, line: u32) -> Option<u32> {
164+
let hunk_range = |hunk: &Hunk| {
165+
if INVERT {
166+
hunk.before.clone()
167+
} else {
168+
hunk.after.clone()
169+
}
170+
};
171+
let res = self.hunks.binary_search_by_key(&line, |hunk| {
172+
let range = hunk_range(hunk);
173+
if REVERSE {
174+
range.end
175+
} else {
176+
range.start
177+
}
178+
});
179+
180+
if REVERSE {
181+
match res {
182+
// Search found a hunk that ends exactly at this line (so it does not include the current line).
183+
// We can usually just return that hunk, howver a specical special case for empty hunk is necessary
184+
// which represents a pure removal.
185+
// Removals are technically empty but are still shown as single line hunks
186+
// and as such we must jump to the previus hunk (if it exists) if we are already inside the removal
187+
Ok(pos) if !hunk_range(&self.hunks[pos]).is_empty() => Some(pos as u32),
188+
189+
// No hunk ends exactly at this line, so the search returns
190+
// the position where a hunk ending at this line should be inserted.
191+
// That position before this one is exactly the position of the previous hunk
192+
Err(0) | Ok(0) => None,
193+
Err(pos) | Ok(pos) => Some(pos as u32 - 1),
194+
}
195+
} else {
196+
match res {
197+
// Search found a hunk that starts exactly at this line, return the next hunk if it exists.
198+
Ok(pos) if pos + 1 == self.hunks.len() => None,
199+
Ok(pos) => Some(pos as u32 + 1),
200+
201+
// No hunk starts exactly at this line, so the search returns
202+
// the position where a hunk starting at this line should be inserted.
203+
// That position is exactly the position of the next hunk or the end
204+
// of the list if no such hunk exists
205+
Err(pos) if pos == self.hunks.len() => None,
206+
Err(pos) => Some(pos as u32),
207+
}
208+
}
209+
}
210+
211+
pub fn next_hunk<const REVERSE: bool>(&self, line: u32) -> Option<u32> {
212+
if self.inverted {
213+
self.next_hunk_impl::<true, REVERSE>(line)
214+
} else {
215+
self.next_hunk_impl::<false, REVERSE>(line)
216+
}
217+
}
162218
}

0 commit comments

Comments
 (0)