Add command/keybinding to jump between hunks (#4650)
* add command and keybding to jump to next/prev hunk * add textobject for change * Update helix-vcs/src/diff.rs Co-authored-by: Michael Davis <mcarsondavis@gmail.com> * select entire hunk instead of first char * fix selection range Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
This commit is contained in:
parent
453a75a373
commit
af532147c9
5 changed files with 211 additions and 0 deletions
|
@ -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` |
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
@ -2912,6 +2917,100 @@ 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);
|
||||
let hunk_end = if hunk.after.is_empty() {
|
||||
hunk_start + 1
|
||||
} else {
|
||||
doc_text.line_to_char(hunk.after.end as usize)
|
||||
};
|
||||
let new_range = Range::new(hunk_start, hunk_end);
|
||||
if editor.mode == Mode::Select {
|
||||
let head = if new_range.head < range.anchor {
|
||||
new_range.anchor
|
||||
} else {
|
||||
new_range.head
|
||||
};
|
||||
|
||||
Range::new(range.anchor, head)
|
||||
} else {
|
||||
new_range.with_direction(direction)
|
||||
}
|
||||
});
|
||||
|
||||
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<Transaction>;
|
||||
|
@ -4515,6 +4614,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),
|
||||
|
@ -4528,6 +4648,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)
|
||||
|
|
|
@ -100,6 +100,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
|
|||
"[" => { "Left bracket"
|
||||
"d" => goto_prev_diag,
|
||||
"D" => goto_first_diag,
|
||||
"g" => goto_prev_change,
|
||||
"G" => goto_first_change,
|
||||
"f" => goto_prev_function,
|
||||
"t" => goto_prev_class,
|
||||
"a" => goto_prev_parameter,
|
||||
|
@ -111,6 +113,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
|
|||
"]" => { "Right bracket"
|
||||
"d" => goto_next_diag,
|
||||
"D" => goto_last_diag,
|
||||
"g" => goto_next_change,
|
||||
"G" => goto_last_change,
|
||||
"f" => goto_next_function,
|
||||
"t" => goto_next_class,
|
||||
"a" => goto_next_parameter,
|
||||
|
|
|
@ -195,4 +195,85 @@ impl FileHunks<'_> {
|
|||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn next_hunk(&self, line: u32) -> Option<u32> {
|
||||
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<u32> {
|
||||
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, 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 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
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hunk_at(&self, line: u32, include_removal: bool) -> Option<u32> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue