diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 37d356aa98..f32e2f921a 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -370,8 +370,8 @@ "ctrl-shift-v": "markdown::OpenPreview", "ctrl-alt-shift-c": "editor::DisplayCursorNames", "ctrl-alt-y": "git::ToggleStaged", - "alt-y": "git::StageAndNext", - "alt-shift-y": "git::UnstageAndNext", + "alt-y": ["git::StageAndNext", { "whole_excerpt": false }], + "alt-shift-y": ["git::UnstageAndNext", { "whole_excerpt": false }], "alt-.": "editor::GoToHunk", "alt-,": "editor::GoToPrevHunk" } diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index a7ecf5fdaf..46e66269cb 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -131,8 +131,8 @@ "cmd-;": "editor::ToggleLineNumbers", "cmd-alt-z": "git::Restore", "cmd-alt-y": "git::ToggleStaged", - "cmd-y": "git::StageAndNext", - "cmd-shift-y": "git::UnstageAndNext", + "cmd-y": ["git::StageAndNext", { "whole_excerpt": false }], + "cmd-shift-y": ["git::UnstageAndNext", { "whole_excerpt": false }], "cmd-'": "editor::ToggleSelectedDiffHunks", "cmd-\"": "editor::ExpandAllDiffHunks", "cmd-alt-g b": "editor::ToggleGitBlame", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dc0f5fc88b..4c25549f94 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7722,7 +7722,7 @@ impl Editor { let mut revert_changes = HashMap::default(); let chunk_by = self .snapshot(window, cx) - .hunks_for_ranges(ranges.into_iter()) + .hunks_for_ranges(ranges) .into_iter() .chunk_by(|hunk| hunk.buffer_id); for (buffer_id, hunks) in &chunk_by { @@ -11424,27 +11424,37 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option { - let mut hunk = snapshot - .buffer_snapshot - .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) - .find(|hunk| hunk.row_range.start.0 > position.row); - if hunk.is_none() { - hunk = snapshot - .buffer_snapshot - .diff_hunks_in_range(Point::zero()..position) - .find(|hunk| hunk.row_range.end.0 < position.row) - } + let hunk = self.hunk_after_position(snapshot, position); + if let Some(hunk) = &hunk { - let destination = Point::new(hunk.row_range.start.0, 0); - self.unfold_ranges(&[destination..destination], false, false, cx); + let point = Point::new(hunk.row_range.start.0, 0); + + self.unfold_ranges(&[point..point], false, false, cx); self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(vec![destination..destination]); + s.select_ranges([point..point]); }); } hunk } + fn hunk_after_position( + &mut self, + snapshot: &EditorSnapshot, + position: Point, + ) -> Option { + snapshot + .buffer_snapshot + .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) + .find(|hunk| hunk.row_range.start.0 > position.row) + .or_else(|| { + snapshot + .buffer_snapshot + .diff_hunks_in_range(Point::zero()..position) + .find(|hunk| hunk.row_range.end.0 < position.row) + }) + } + fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, window: &mut Window, cx: &mut Context) { let snapshot = self.snapshot(window, cx); let selection = self.selections.newest::(cx); @@ -13528,20 +13538,20 @@ impl Editor { pub fn stage_and_next( &mut self, - _: &::git::StageAndNext, + action: &::git::StageAndNext, window: &mut Window, cx: &mut Context, ) { - self.do_stage_or_unstage_and_next(true, window, cx); + self.do_stage_or_unstage_and_next(true, action.whole_excerpt, window, cx); } pub fn unstage_and_next( &mut self, - _: &::git::UnstageAndNext, + action: &::git::UnstageAndNext, window: &mut Window, cx: &mut Context, ) { - self.do_stage_or_unstage_and_next(false, window, cx); + self.do_stage_or_unstage_and_next(false, action.whole_excerpt, window, cx); } pub fn stage_or_unstage_diff_hunks( @@ -13563,16 +13573,36 @@ impl Editor { fn do_stage_or_unstage_and_next( &mut self, stage: bool, + whole_excerpt: bool, window: &mut Window, cx: &mut Context, ) { let mut ranges = self.selections.disjoint_anchor_ranges().collect::>(); + if ranges.iter().any(|range| range.start != range.end) { self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx); return; } - if !self.buffer().read(cx).is_singleton() { + if !whole_excerpt { + let snapshot = self.snapshot(window, cx); + let newest_range = self.selections.newest::(cx).range(); + + let run_twice = snapshot + .hunks_for_ranges([newest_range]) + .first() + .is_some_and(|hunk| { + let next_line = Point::new(hunk.row_range.end.0 + 1, 0); + self.hunk_after_position(&snapshot, next_line) + .is_some_and(|other| other.row_range == hunk.row_range) + }); + + if run_twice { + self.go_to_next_hunk(&Default::default(), window, cx); + } + } else if !self.buffer().read(cx).is_singleton() { + self.stage_or_unstage_diff_hunks(stage, &ranges[..], window, cx); + if let Some((excerpt_id, buffer, range)) = self.active_excerpt(cx) { if buffer.read(cx).is_empty() { let buffer = buffer.read(cx); @@ -13586,9 +13616,9 @@ impl Editor { let Some(project) = self.project.as_ref() else { return; }; - let project = project.read(cx); - let Some(repo) = project.git_store().read(cx).active_repository() else { + let Some(repo) = project.read(cx).git_store().read(cx).active_repository() + else { return; }; @@ -13620,7 +13650,7 @@ impl Editor { point = snapshot.clip_point(point, Bias::Right); self.change_selections(Some(Autoscroll::top_relative(6)), window, cx, |s| { s.select_ranges([point..point]); - }) + }); } return; } @@ -13756,7 +13786,7 @@ impl Editor { cx: &mut Context, ) { let snapshot = self.snapshot(window, cx); - let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx).into_iter()); + let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx)); let mut ranges_by_buffer = HashMap::default(); self.transact(window, cx, |editor, _window, cx| { for hunk in hunks { @@ -17083,7 +17113,7 @@ impl EditorSnapshot { pub fn hunks_for_ranges( &self, - ranges: impl Iterator>, + ranges: impl IntoIterator>, ) -> Vec { let mut hunks = Vec::new(); let mut processed_buffer_rows: HashMap>> = diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index 5111382493..74e666dd4b 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -35,15 +35,23 @@ pub struct Push { pub options: Option, } -impl_actions!(git, [Push]); +#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)] +pub struct StageAndNext { + pub whole_excerpt: bool, +} + +#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)] +pub struct UnstageAndNext { + pub whole_excerpt: bool, +} + +impl_actions!(git, [Push, StageAndNext, UnstageAndNext]); actions!( git, [ // per-hunk ToggleStaged, - StageAndNext, - UnstageAndNext, // per-file StageFile, UnstageFile, diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index f892ff0dfa..c4e49f58d8 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -813,7 +813,9 @@ impl Render for ProjectDiffToolbar { Button::new("stage", "Stage") .tooltip(Tooltip::for_action_title_in( "Stage", - &StageAndNext, + &StageAndNext { + whole_excerpt: false, + }, &focus_handle, )) // don't actually disable the button so it's mashable @@ -823,14 +825,22 @@ impl Render for ProjectDiffToolbar { Color::Disabled }) .on_click(cx.listener(|this, _, window, cx| { - this.dispatch_action(&StageAndNext, window, cx) + this.dispatch_action( + &StageAndNext { + whole_excerpt: false, + }, + window, + cx, + ) })), ) .child( Button::new("unstage", "Unstage") .tooltip(Tooltip::for_action_title_in( "Unstage", - &UnstageAndNext, + &UnstageAndNext { + whole_excerpt: false, + }, &focus_handle, )) .color(if button_states.unstage { @@ -839,7 +849,13 @@ impl Render for ProjectDiffToolbar { Color::Disabled }) .on_click(cx.listener(|this, _, window, cx| { - this.dispatch_action(&UnstageAndNext, window, cx) + this.dispatch_action( + &UnstageAndNext { + whole_excerpt: false, + }, + window, + cx, + ) })), ) }), diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index db40eab55f..1cec009e85 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -399,6 +399,8 @@ macro_rules! action_with_deprecated_aliases { /// Registers the action and implements the Action trait for any struct that implements Clone, /// Default, PartialEq, serde_deserialize::Deserialize, and schemars::JsonSchema. /// +/// Similar to `actions!`, but accepts structs with fields. +/// /// Fields and variants that don't make sense for user configuration should be annotated with /// #[serde(skip)]. #[macro_export] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 81faf89f57..f593cdf46f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4112,6 +4112,8 @@ mod tests { | "vim::PushLiteral" | "vim::Number" | "vim::SelectRegister" + | "git::StageAndNext" + | "git::UnstageAndNext" | "terminal::SendText" | "terminal::SendKeystroke" | "app_menu::OpenApplicationMenu"