From 85660885ebc7ca34a59fcb80637b73976d5420d9 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sun, 6 Nov 2022 19:36:55 +0100 Subject: [PATCH 1/5] add command and keybding to jump to next/prev hunk --- book/src/keymap.md | 4 ++ helix-term/src/commands.rs | 92 ++++++++++++++++++++++++++++++++ helix-term/src/keymap/default.rs | 4 ++ helix-vcs/src/diff.rs | 51 ++++++++++++++++++ 4 files changed, 151 insertions(+) diff --git a/book/src/keymap.md b/book/src/keymap.md index 6523b09fb88c..91c1922fa0fd 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -320,6 +320,10 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire | `]t` | Go to previous test (**TS**) | `goto_prev_test` | | `]p` | Go to next paragraph | `goto_next_paragraph` | | `[p` | Go to previous paragraph | `goto_prev_paragraph` | +| `]g` | Go to next change | `goto_next_change` | +| `[g` | Go to previous change | `goto_prev_change` | +| `]G` | Go to first change | `goto_first_change` | +| `[G` | Go to last change | `goto_last_change` | | `[Space` | Add newline above | `add_newline_above` | | `]Space` | Add newline below | `add_newline_below` | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a6f88362c8f6..a1bbad380ba3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3,6 +3,7 @@ pub(crate) mod lsp; pub(crate) mod typed; pub use dap::*; +use helix_vcs::Hunk; pub use lsp::*; use tui::text::Spans; pub use typed::*; @@ -308,6 +309,10 @@ impl MappableCommand { goto_last_diag, "Goto last diagnostic", goto_next_diag, "Goto next diagnostic", goto_prev_diag, "Goto previous diagnostic", + goto_next_change, "Goto next change", + goto_prev_change, "Goto previous change", + goto_first_change, "Goto first change", + goto_last_change, "Goto last change", goto_line_start, "Goto line start", goto_line_end, "Goto line end", goto_next_buffer, "Goto next buffer", @@ -2915,6 +2920,93 @@ fn goto_prev_diag(cx: &mut Context) { goto_pos(editor, pos); } +fn goto_first_change(cx: &mut Context) { + goto_first_change_impl(cx, false); +} + +fn goto_last_change(cx: &mut Context) { + goto_first_change_impl(cx, true); +} + +fn goto_first_change_impl(cx: &mut Context, reverse: bool) { + let editor = &mut cx.editor; + let (_, doc) = current!(editor); + if let Some(handle) = doc.diff_handle() { + let hunk = { + let hunks = handle.hunks(); + let idx = if reverse { + hunks.len().saturating_sub(1) + } else { + 0 + }; + hunks.nth_hunk(idx) + }; + if hunk != Hunk::NONE { + let pos = doc.text().line_to_char(hunk.after.start as usize); + goto_pos(editor, pos) + } + } +} + +fn goto_next_change(cx: &mut Context) { + goto_next_change_impl(cx, Direction::Forward) +} + +fn goto_prev_change(cx: &mut Context) { + goto_next_change_impl(cx, Direction::Backward) +} + +fn goto_next_change_impl(cx: &mut Context, direction: Direction) { + let count = cx.count() as u32 - 1; + let motion = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let doc_text = doc.text().slice(..); + let diff_handle = if let Some(diff_handle) = doc.diff_handle() { + diff_handle + } else { + editor.set_status("Diff is not available in current buffer"); + return; + }; + + let selection = doc.selection(view.id).clone().transform(|range| { + let cursor_line = range.cursor_line(doc_text) as u32; + + let hunks = diff_handle.hunks(); + let hunk_idx = match direction { + Direction::Forward => hunks + .next_hunk(cursor_line) + .map(|idx| (idx + count).min(hunks.len() - 1)), + Direction::Backward => hunks + .prev_hunk(cursor_line) + .map(|idx| idx.saturating_sub(count)), + }; + // TODO refactor with let..else once MSRV reaches 1.65 + let hunk_idx = if let Some(hunk_idx) = hunk_idx { + hunk_idx + } else { + return range; + }; + let hunk = hunks.nth_hunk(hunk_idx); + + let hunk_start = doc_text.line_to_char(hunk.after.start as usize); + if editor.mode == Mode::Select { + let head = if hunk_start + 1 < range.anchor { + hunk_start + } else { + hunk_start + 1 + }; + Range::new(range.anchor, head) + } else { + Range::new(hunk_start, hunk_start) + } + }); + + doc.set_selection(view.id, selection) + }; + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); +} + 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 118764d97585..d740c6fd9acb 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -100,6 +100,8 @@ pub fn default() -> HashMap { "[" => { "Left bracket" "d" => goto_prev_diag, "D" => goto_first_diag, + "g" => goto_prev_change, + "G" => goto_first_change, "f" => goto_prev_function, "c" => goto_prev_class, "a" => goto_prev_parameter, @@ -111,6 +113,8 @@ pub fn default() -> HashMap { "]" => { "Right bracket" "d" => goto_next_diag, "D" => goto_last_diag, + "g" => goto_next_change, + "G" => goto_last_change, "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 b1acd1f29937..e70293fe7c28 100644 --- a/helix-vcs/src/diff.rs +++ b/helix-vcs/src/diff.rs @@ -195,4 +195,55 @@ impl FileHunks<'_> { pub fn is_empty(&self) -> bool { self.len() == 0 } + + pub fn next_hunk(&self, line: u32) -> Option { + let hunk_range = if self.inverted { + |hunk: &Hunk| hunk.before.clone() + } else { + |hunk: &Hunk| hunk.after.clone() + }; + + let res = self + .hunks + .binary_search_by_key(&line, |hunk| hunk_range(hunk).start); + + 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 prev_hunk(&self, line: u32) -> Option { + let hunk_range = if self.inverted { + |hunk: &Hunk| hunk.before.clone() + } else { + |hunk: &Hunk| hunk.after.clone() + }; + let res = self + .hunks + .binary_search_by_key(&line, |hunk| hunk_range(hunk).end); + + 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), + } + } } From e93c0fa155a47f66f88d86374baf980decf47b8a Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Wed, 30 Nov 2022 19:17:00 +0100 Subject: [PATCH 2/5] add textobject for change --- book/src/usage.md | 1 + helix-term/src/commands.rs | 22 ++++++++++++++++++++++ helix-vcs/src/diff.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/book/src/usage.md b/book/src/usage.md index 646bf926d536..a6eb9ec1d4f1 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -143,6 +143,7 @@ though, we climb the syntax tree and then take the previous selection. So | `a` | Argument/parameter | | `o` | Comment | | `t` | Test | +| `g` | Change | > NOTE: `f`, `c`, etc need a tree-sitter grammar active for the current document and a special tree-sitter query file to work properly. [Only diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a1bbad380ba3..fe043ae401a1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4608,6 +4608,27 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { ) }; + if ch == 'g' && doc.diff_handle().is_none() { + editor.set_status("Diff is not available in current buffer"); + return; + } + + let textobject_change = |range: Range| -> Range { + let diff_handle = doc.diff_handle().unwrap(); + let hunks = diff_handle.hunks(); + let line = range.cursor_line(text); + let hunk_idx = if let Some(hunk_idx) = hunks.hunk_at(line as u32, false) { + hunk_idx + } else { + return range; + }; + let hunk = hunks.nth_hunk(hunk_idx).after; + + let start = text.line_to_char(hunk.start as usize); + let end = text.line_to_char(hunk.end as usize); + Range::new(start, end).with_direction(range.direction()) + }; + let selection = doc.selection(view.id).clone().transform(|range| { match ch { 'w' => textobject::textobject_word(text, range, objtype, count, false), @@ -4621,6 +4642,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { 'm' => textobject::textobject_pair_surround_closest( text, range, objtype, count, ), + 'g' => textobject_change(range), // TODO: cancel new ranges if inconsistent surround matches across lines ch if !ch.is_ascii_alphanumeric() => { textobject::textobject_pair_surround(text, range, objtype, ch, count) diff --git a/helix-vcs/src/diff.rs b/helix-vcs/src/diff.rs index e70293fe7c28..ef42d9cff35d 100644 --- a/helix-vcs/src/diff.rs +++ b/helix-vcs/src/diff.rs @@ -246,4 +246,34 @@ impl FileHunks<'_> { Err(pos) | Ok(pos) => Some(pos as u32 - 1), } } + + pub fn hunk_at(&self, line: u32, include_removal: bool) -> Option { + let hunk_range = if self.inverted { + |hunk: &Hunk| hunk.before.clone() + } else { + |hunk: &Hunk| hunk.after.clone() + }; + + let res = self + .hunks + .binary_search_by_key(&line, |hunk| hunk_range(hunk).start); + + match res { + // Search found a hunk that starts exactly at this line, return it + Ok(pos) => Some(pos as u32), + + // No hunk starts exactly at this line, so the search returns + // the position where a hunk starting at this line should be inserted. + // The previous hunk contains this hunk if it exists and doesn't end before this line + Err(0) => None, + Err(pos) => { + let hunk = hunk_range(&self.hunks[pos - 1]); + if hunk.end > line || include_removal && hunk.start == line && hunk.is_empty() { + Some(pos as u32 - 1) + } else { + None + } + } + } + } } From 47ac823830db75c7925a8d7066cadf5800e90166 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 2 Dec 2022 05:24:17 +0100 Subject: [PATCH 3/5] Update helix-vcs/src/diff.rs Co-authored-by: Michael Davis --- helix-vcs/src/diff.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-vcs/src/diff.rs b/helix-vcs/src/diff.rs index ef42d9cff35d..9c6a362f7db8 100644 --- a/helix-vcs/src/diff.rs +++ b/helix-vcs/src/diff.rs @@ -233,10 +233,10 @@ impl FileHunks<'_> { 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 + // We can usually just return that hunk, however a 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 + // and as such we must jump to the previous 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 From 29c6b5590abb20c8ae062763756c6229a2860280 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 2 Dec 2022 16:09:21 +0100 Subject: [PATCH 4/5] select entire hunk instead of first char --- helix-term/src/commands.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fe043ae401a1..58ecbe096fdb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2989,15 +2989,22 @@ fn goto_next_change_impl(cx: &mut Context, direction: Direction) { let hunk = hunks.nth_hunk(hunk_idx); let hunk_start = doc_text.line_to_char(hunk.after.start as usize); + let hunk_end = if hunk.after.is_empty() { + hunk_start + 1 + } else { + doc_text.line_to_char(hunk.after.end as usize) - 1 + }; + let new_range = Range::new(hunk_start, hunk_end); if editor.mode == Mode::Select { - let head = if hunk_start + 1 < range.anchor { - hunk_start + let head = if new_range.head < range.anchor { + new_range.anchor } else { - hunk_start + 1 + new_range.head }; + Range::new(range.anchor, head) } else { - Range::new(hunk_start, hunk_start) + new_range.with_direction(direction) } }); From a6df6e1b147b507fb8bb82c2dbdde852ccbb5a33 Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Fri, 2 Dec 2022 16:49:43 +0100 Subject: [PATCH 5/5] fix selection range --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 58ecbe096fdb..543874dc5261 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2992,7 +2992,7 @@ fn goto_next_change_impl(cx: &mut Context, direction: Direction) { let hunk_end = if hunk.after.is_empty() { hunk_start + 1 } else { - doc_text.line_to_char(hunk.after.end as usize) - 1 + doc_text.line_to_char(hunk.after.end as usize) }; let new_range = Range::new(hunk_start, hunk_end); if editor.mode == Mode::Select {