Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command/keybinding to jump between hunks #4650

Merged
merged 5 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions book/src/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
pascalkuthe marked this conversation as resolved.
Show resolved Hide resolved
| `[Space` | Add newline above | `add_newline_above` |
| `]Space` | Add newline below | `add_newline_below` |

Expand Down
1 change: 1 addition & 0 deletions book/src/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
121 changes: 121 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -2915,6 +2920,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)
pascalkuthe marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

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>;
Expand Down Expand Up @@ -4516,6 +4615,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),
Expand All @@ -4529,6 +4649,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)
Expand Down
4 changes: 4 additions & 0 deletions helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
"c" => goto_prev_class,
"a" => goto_prev_parameter,
Expand All @@ -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,
"c" => goto_next_class,
"a" => goto_next_parameter,
Expand Down
81 changes: 81 additions & 0 deletions helix-vcs/src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}
}