git: Introduce restore and next action (#50324)

Add a `git::RestoreAndNext` action that restores the diff hunk at the
cursor and advances to the next hunk. In the git diff view, the default
restore keybinding (`cmd-alt-z` on macOS, `ctrl-k ctrl-r` on
Linux/Windows) is remapped to this action so users can quickly restore
hunks in sequence. Also refactor `go_to_hunk_before_or_after_position`
to accept a `wrap_around` parameter, eliminating duplicated
hunk-navigation logic in `do_stage_or_unstage_and_next` and
`restore_and_next`.

Release Notes:

- Added a `git: restore and next` action that restores the diff hunk at
  the cursor and moves to the next one. In the git diff view, the
  default restore keybinding (`cmd-alt-z` on macOS, `ctrl-k ctrl-r` on
  Linux/Windows) now triggers this action instead of `git: restore`.

---------

Co-authored-by: Afonso <4775087+afonsograca@users.noreply.github.com>
This commit is contained in:
Dino 2026-03-09 10:50:43 +00:00 committed by GitHub
parent 8475280eb1
commit 0a436bec17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 145 additions and 35 deletions

View file

@ -982,6 +982,7 @@
"ctrl-shift-enter": "git::Amend",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll",
"ctrl-k ctrl-r": "git::RestoreAndNext",
},
},
{

View file

@ -1033,6 +1033,7 @@
"cmd-shift-enter": "git::Amend",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll",
"cmd-alt-z": "git::RestoreAndNext",
},
},
{

View file

@ -983,6 +983,7 @@
"ctrl-shift-enter": "git::Amend",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll",
"ctrl-k ctrl-r": "git::RestoreAndNext",
},
},
{

View file

@ -831,6 +831,7 @@ fn render_diff_hunk_controls(
&snapshot,
position,
Direction::Next,
true,
window,
cx,
);
@ -866,6 +867,7 @@ fn render_diff_hunk_controls(
&snapshot,
point,
Direction::Prev,
true,
window,
cx,
);

View file

@ -11683,6 +11683,43 @@ impl Editor {
self.restore_hunks_in_ranges(selections, window, cx);
}
/// Restores the diff hunks in the editor's selections and moves the cursor
/// to the next diff hunk. Wraps around to the beginning of the buffer if
/// not all diff hunks are expanded.
pub fn restore_and_next(
&mut self,
_: &::git::RestoreAndNext,
window: &mut Window,
cx: &mut Context<Self>,
) {
let selections = self
.selections
.all(&self.display_snapshot(cx))
.into_iter()
.map(|selection| selection.range())
.collect();
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
self.restore_hunks_in_ranges(selections, window, cx);
let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
let wrap_around = !all_diff_hunks_expanded;
let snapshot = self.snapshot(window, cx);
let position = self
.selections
.newest::<Point>(&snapshot.display_snapshot)
.head();
self.go_to_hunk_before_or_after_position(
&snapshot,
position,
Direction::Next,
wrap_around,
window,
cx,
);
}
pub fn restore_hunks_in_ranges(
&mut self,
ranges: Vec<Range<Point>>,
@ -17735,6 +17772,7 @@ impl Editor {
&snapshot,
selection.head(),
Direction::Next,
true,
window,
cx,
);
@ -17745,14 +17783,15 @@ impl Editor {
snapshot: &EditorSnapshot,
position: Point,
direction: Direction,
wrap_around: bool,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let row = if direction == Direction::Next {
self.hunk_after_position(snapshot, position)
self.hunk_after_position(snapshot, position, wrap_around)
.map(|hunk| hunk.row_range.start)
} else {
self.hunk_before_position(snapshot, position)
self.hunk_before_position(snapshot, position, wrap_around)
};
if let Some(row) = row {
@ -17770,17 +17809,23 @@ impl Editor {
&mut self,
snapshot: &EditorSnapshot,
position: Point,
wrap_around: bool,
) -> Option<MultiBufferDiffHunk> {
snapshot
let result = snapshot
.buffer_snapshot()
.diff_hunks_in_range(position..snapshot.buffer_snapshot().max_point())
.find(|hunk| hunk.row_range.start.0 > position.row)
.or_else(|| {
.find(|hunk| hunk.row_range.start.0 > position.row);
if wrap_around {
result.or_else(|| {
snapshot
.buffer_snapshot()
.diff_hunks_in_range(Point::zero()..position)
.find(|hunk| hunk.row_range.end.0 < position.row)
})
} else {
result
}
}
fn go_to_prev_hunk(
@ -17796,6 +17841,7 @@ impl Editor {
&snapshot,
selection.head(),
Direction::Prev,
true,
window,
cx,
);
@ -17805,11 +17851,15 @@ impl Editor {
&mut self,
snapshot: &EditorSnapshot,
position: Point,
wrap_around: bool,
) -> Option<MultiBufferRow> {
snapshot
.buffer_snapshot()
.diff_hunk_before(position)
.or_else(|| snapshot.buffer_snapshot().diff_hunk_before(Point::MAX))
let result = snapshot.buffer_snapshot().diff_hunk_before(position);
if wrap_around {
result.or_else(|| snapshot.buffer_snapshot().diff_hunk_before(Point::MAX))
} else {
result
}
}
fn go_to_next_change(
@ -20793,38 +20843,23 @@ impl Editor {
}
self.stage_or_unstage_diff_hunks(stage, ranges, cx);
let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
let wrap_around = !all_diff_hunks_expanded;
let snapshot = self.snapshot(window, cx);
let position = self
.selections
.newest::<Point>(&snapshot.display_snapshot)
.head();
let mut row = snapshot
.buffer_snapshot()
.diff_hunks_in_range(position..snapshot.buffer_snapshot().max_point())
.find(|hunk| hunk.row_range.start.0 > position.row)
.map(|hunk| hunk.row_range.start);
let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded();
// Outside of the project diff editor, wrap around to the beginning.
if !all_diff_hunks_expanded {
row = row.or_else(|| {
snapshot
.buffer_snapshot()
.diff_hunks_in_range(Point::zero()..position)
.find(|hunk| hunk.row_range.end.0 < position.row)
.map(|hunk| hunk.row_range.start)
});
}
if let Some(row) = row {
let destination = Point::new(row.0, 0);
let autoscroll = Autoscroll::center();
self.unfold_ranges(&[destination..destination], false, false, cx);
self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| {
s.select_ranges([destination..destination]);
});
}
self.go_to_hunk_before_or_after_position(
&snapshot,
position,
Direction::Next,
wrap_around,
window,
cx,
);
}
pub(crate) fn do_stage_or_unstage(
@ -29249,6 +29284,7 @@ fn render_diff_hunk_controls(
&snapshot,
position,
Direction::Next,
true,
window,
cx,
);
@ -29284,6 +29320,7 @@ fn render_diff_hunk_controls(
&snapshot,
point,
Direction::Prev,
true,
window,
cx,
);

View file

@ -33557,3 +33557,66 @@ comment */ˇ»;"#},
assert_text_with_selections(editor, indoc! {r#"let arr = [«1, 2, 3]ˇ»;"#}, cx);
});
}
#[gpui::test]
async fn test_restore_and_next(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let diff_base = r#"
one
two
three
four
five
"#
.unindent();
cx.set_state(
&r#"
ONE
two
ˇTHREE
four
FIVE
"#
.unindent(),
);
cx.set_head_text(&diff_base);
cx.update_editor(|editor, window, cx| {
editor.set_expand_all_diff_hunks(cx);
editor.restore_and_next(&Default::default(), window, cx);
});
cx.run_until_parked();
cx.assert_state_with_diff(
r#"
- one
+ ONE
two
three
four
- ˇfive
+ FIVE
"#
.unindent(),
);
cx.update_editor(|editor, window, cx| {
editor.restore_and_next(&Default::default(), window, cx);
});
cx.run_until_parked();
cx.assert_state_with_diff(
r#"
- one
+ ONE
two
three
four
ˇfive
"#
.unindent(),
);
}

View file

@ -637,6 +637,7 @@ impl EditorElement {
register_action(editor, window, Editor::accept_edit_prediction);
register_action(editor, window, Editor::restore_file);
register_action(editor, window, Editor::git_restore);
register_action(editor, window, Editor::restore_and_next);
register_action(editor, window, Editor::apply_all_diff_hunks);
register_action(editor, window, Editor::apply_selected_diff_hunks);
register_action(editor, window, Editor::open_active_item_in_terminal);

View file

@ -40,6 +40,9 @@ actions!(
/// Restores the selected hunks to their original state.
#[action(deprecated_aliases = ["editor::RevertSelectedHunks"])]
Restore,
/// Restores the selected hunks to their original state and moves to the
/// next one.
RestoreAndNext,
// per-file
/// Shows git blame information for the current file.
#[action(deprecated_aliases = ["editor::ToggleGitBlame"])]

View file

@ -1343,6 +1343,7 @@ impl GitPanel {
&snapshot,
language::Point::new(0, 0),
Direction::Next,
true,
window,
cx,
);