@@ -3,6 +3,7 @@ pub(crate) mod lsp;
33pub(crate) mod typed;
44
55pub use dap::*;
6+use helix_vcs::Hunk;
67pub use lsp::*;
78use tui::text::Spans;
89pub 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",
@@ -2912,6 +2917,100 @@ fn goto_prev_diag(cx: &mut Context) {
29122917 goto_pos(editor, pos);
29132918}
29142919
2920+fn goto_first_change(cx: &mut Context) {
2921+ goto_first_change_impl(cx, false);
2922+}
2923+
2924+fn goto_last_change(cx: &mut Context) {
2925+ goto_first_change_impl(cx, true);
2926+}
2927+
2928+fn goto_first_change_impl(cx: &mut Context, reverse: bool) {
2929+ let editor = &mut cx.editor;
2930+ let (_, doc) = current!(editor);
2931+ if let Some(handle) = doc.diff_handle() {
2932+ let hunk = {
2933+ let hunks = handle.hunks();
2934+ let idx = if reverse {
2935+ hunks.len().saturating_sub(1)
2936+ } else {
2937+ 0
2938+ };
2939+ hunks.nth_hunk(idx)
2940+ };
2941+ if hunk != Hunk::NONE {
2942+ let pos = doc.text().line_to_char(hunk.after.start as usize);
2943+ goto_pos(editor, pos)
2944+ }
2945+ }
2946+}
2947+
2948+fn goto_next_change(cx: &mut Context) {
2949+ goto_next_change_impl(cx, Direction::Forward)
2950+}
2951+
2952+fn goto_prev_change(cx: &mut Context) {
2953+ goto_next_change_impl(cx, Direction::Backward)
2954+}
2955+
2956+fn goto_next_change_impl(cx: &mut Context, direction: Direction) {
2957+ let count = cx.count() as u32 - 1;
2958+ let motion = move |editor: &mut Editor| {
2959+ let (view, doc) = current!(editor);
2960+ let doc_text = doc.text().slice(..);
2961+ let diff_handle = if let Some(diff_handle) = doc.diff_handle() {
2962+ diff_handle
2963+ } else {
2964+ editor.set_status("Diff is not available in current buffer");
2965+ return;
2966+ };
2967+
2968+ let selection = doc.selection(view.id).clone().transform(|range| {
2969+ let cursor_line = range.cursor_line(doc_text) as u32;
2970+
2971+ let hunks = diff_handle.hunks();
2972+ let hunk_idx = match direction {
2973+ Direction::Forward => hunks
2974+ .next_hunk(cursor_line)
2975+ .map(|idx| (idx + count).min(hunks.len() - 1)),
2976+ Direction::Backward => hunks
2977+ .prev_hunk(cursor_line)
2978+ .map(|idx| idx.saturating_sub(count)),
2979+ };
2980+ // TODO refactor with let..else once MSRV reaches 1.65
2981+ let hunk_idx = if let Some(hunk_idx) = hunk_idx {
2982+ hunk_idx
2983+ } else {
2984+ return range;
2985+ };
2986+ let hunk = hunks.nth_hunk(hunk_idx);
2987+
2988+ let hunk_start = doc_text.line_to_char(hunk.after.start as usize);
2989+ let hunk_end = if hunk.after.is_empty() {
2990+ hunk_start + 1
2991+ } else {
2992+ doc_text.line_to_char(hunk.after.end as usize)
2993+ };
2994+ let new_range = Range::new(hunk_start, hunk_end);
2995+ if editor.mode == Mode::Select {
2996+ let head = if new_range.head < range.anchor {
2997+ new_range.anchor
2998+ } else {
2999+ new_range.head
3000+ };
3001+
3002+ Range::new(range.anchor, head)
3003+ } else {
3004+ new_range.with_direction(direction)
3005+ }
3006+ });
3007+
3008+ doc.set_selection(view.id, selection)
3009+ };
3010+ motion(cx.editor);
3011+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
3012+}
3013+
29153014pub mod insert {
29163015 use super::*;
29173016 pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
@@ -4515,6 +4614,27 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
45154614 )
45164615 };
45174616
4617+ if ch == 'g' && doc.diff_handle().is_none() {
4618+ editor.set_status("Diff is not available in current buffer");
4619+ return;
4620+ }
4621+
4622+ let textobject_change = |range: Range| -> Range {
4623+ let diff_handle = doc.diff_handle().unwrap();
4624+ let hunks = diff_handle.hunks();
4625+ let line = range.cursor_line(text);
4626+ let hunk_idx = if let Some(hunk_idx) = hunks.hunk_at(line as u32, false) {
4627+ hunk_idx
4628+ } else {
4629+ return range;
4630+ };
4631+ let hunk = hunks.nth_hunk(hunk_idx).after;
4632+
4633+ let start = text.line_to_char(hunk.start as usize);
4634+ let end = text.line_to_char(hunk.end as usize);
4635+ Range::new(start, end).with_direction(range.direction())
4636+ };
4637+
45184638 let selection = doc.selection(view.id).clone().transform(|range| {
45194639 match ch {
45204640 'w' => textobject::textobject_word(text, range, objtype, count, false),
@@ -4528,6 +4648,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
45284648 'm' => textobject::textobject_pair_surround_closest(
45294649 text, range, objtype, count,
45304650 ),
4651+ 'g' => textobject_change(range),
45314652 // TODO: cancel new ranges if inconsistent surround matches across lines
45324653 ch if !ch.is_ascii_alphanumeric() => {
45334654 textobject::textobject_pair_surround(text, range, objtype, ch, count)