From 0a436bec175806d9d1785f79c4ab793e2a5e772e Mon Sep 17 00:00:00 2001 From: Dino Date: Mon, 9 Mar 2026 10:50:43 +0000 Subject: [PATCH] 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> --- assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + assets/keymaps/default-windows.json | 1 + crates/agent_ui/src/agent_diff.rs | 2 + crates/editor/src/editor.rs | 107 +++++++++++++++++++--------- crates/editor/src/editor_tests.rs | 63 ++++++++++++++++ crates/editor/src/element.rs | 1 + crates/git/src/git.rs | 3 + crates/git_ui/src/git_panel.rs | 1 + 9 files changed, 145 insertions(+), 35 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 0b354ef1c039c2fe7dde2f20bb30ef71f067e84d..21ab61065896953fdc950943ee89e778ee3ef726 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -982,6 +982,7 @@ "ctrl-shift-enter": "git::Amend", "ctrl-space": "git::StageAll", "ctrl-shift-space": "git::UnstageAll", + "ctrl-k ctrl-r": "git::RestoreAndNext", }, }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 052475ddb981c4db5495914096ffd72dee54d80f..ae2e80bcccc6c86a17d6640cde07ff9211d4cbbf 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -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", }, }, { diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index ef2b339951382a44433372b34e7e62b082428362..a81e34cc16bb1a8e55c7106b22c55c9aa5796136 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -983,6 +983,7 @@ "ctrl-shift-enter": "git::Amend", "ctrl-space": "git::StageAll", "ctrl-shift-space": "git::UnstageAll", + "ctrl-k ctrl-r": "git::RestoreAndNext", }, }, { diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 8fa68b0c510c086d7c6e224b24675e6f19344b82..13e62eb502de1d4bf454b47b216374a0abf2bc79 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -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, ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ead4f97ee351246f4d00f4275c4a736c7ffa4926..cb63e5f85d766637f5775bb864d79998ada9c254 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, + ) { + 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::(&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>, @@ -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, ) { 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 { - 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 { - 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::(&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, ); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 3cb2ac6ceec6e54b93266e2052403722651f89e3..d3da58733dd0a24622a6dcde87f638069e206cf4 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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(), + ); +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 159aee456a6894824ff8e3e212281074498df3c6..b7207fce71bc71c5bdd5962ca3328030935238ca 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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); diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index 805d8d181ab7a434b565d38bdb2f802a8a3cda1a..13745c1fdfc0523d850b95e45a81cae286a77a00 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -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"])] diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 61d94b68a118525bd9b67217a929ce7462696dc7..8205f5ee7b6a9966a37a8406331d171d8ca57f1d 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1343,6 +1343,7 @@ impl GitPanel { &snapshot, language::Point::new(0, 0), Direction::Next, + true, window, cx, );