Add keybindings for accepting hunks (#19749)

Richard Feldman created

I went with Cmd-Shift-Y on macOS (Ctrl-Shift-Y on Linux) for "yes accept
this individual hunk" - both are currently unused.

I went with Cmd-Shift-A on macOS (Ctrl-Alt-A on Linux) for "accept all
hunks" - both are unused. (Ctrl-Shift-A on Linux was taken, as is
Ctrl-Alt-Y, so although the pairing of Ctrl-Shift-Y and Ctrl-Alt-A isn't
necessarily obvious, the letters seem intuitive - "yes" and "all" - and
those key combinations don't conflict with anything.)

Release Notes:

- Added keybindings for applying hunks in Proposed Changes
<img width="247" alt="Screenshot 2024-10-25 at 12 47 00 PM"
src="https://github.com/user-attachments/assets/d6355621-ba80-4ee2-8918-b7239a4d29be">

Change summary

assets/keymaps/default-linux.json            |  7 ++++
assets/keymaps/default-macos.json            |  7 ++++
crates/editor/src/actions.rs                 |  1 
crates/editor/src/element.rs                 |  1 
crates/editor/src/hunk_diff.rs               | 14 ++++++---
crates/editor/src/proposed_changes_editor.rs | 33 +++++++++++++--------
6 files changed, 45 insertions(+), 18 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -505,6 +505,13 @@
       "ctrl-enter": "assistant::InlineAssist"
     }
   },
+  {
+    "context": "ProposedChangesEditor",
+    "bindings": {
+      "ctrl-shift-y": "editor::ApplyDiffHunk",
+      "ctrl-alt-a": "editor::ApplyAllDiffHunks"
+    }
+  },
   {
     "context": "Editor && jupyter && !ContextEditor",
     "bindings": {

assets/keymaps/default-macos.json 🔗

@@ -538,6 +538,13 @@
       "ctrl-enter": "assistant::InlineAssist"
     }
   },
+  {
+    "context": "ProposedChangesEditor",
+    "bindings": {
+      "cmd-shift-y": "editor::ApplyDiffHunk",
+      "cmd-shift-a": "editor::ApplyAllDiffHunks"
+    }
+  },
   {
     "context": "PromptEditor",
     "bindings": {

crates/editor/src/actions.rs 🔗

@@ -193,6 +193,7 @@ gpui::actions!(
         AcceptPartialInlineCompletion,
         AddSelectionAbove,
         AddSelectionBelow,
+        ApplyAllDiffHunks,
         ApplyDiffHunk,
         Backspace,
         Cancel,

crates/editor/src/element.rs 🔗

@@ -444,6 +444,7 @@ impl EditorElement {
         register_action(view, cx, Editor::accept_inline_completion);
         register_action(view, cx, Editor::revert_file);
         register_action(view, cx, Editor::revert_selected_hunks);
+        register_action(view, cx, Editor::apply_all_diff_hunks);
         register_action(view, cx, Editor::apply_selected_diff_hunks);
         register_action(view, cx, Editor::open_active_item_in_terminal);
         register_action(view, cx, Editor::reload_file)

crates/editor/src/hunk_diff.rs 🔗

@@ -16,10 +16,10 @@ use util::RangeExt;
 use workspace::Item;
 
 use crate::{
-    editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyDiffHunk,
-    BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
-    DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
-    RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
+    editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks,
+    ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight,
+    DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk,
+    RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
 };
 
 #[derive(Debug, Clone)]
@@ -352,7 +352,11 @@ impl Editor {
         None
     }
 
-    pub(crate) fn apply_all_diff_hunks(&mut self, cx: &mut ViewContext<Self>) {
+    pub(crate) fn apply_all_diff_hunks(
+        &mut self,
+        _: &ApplyAllDiffHunks,
+        cx: &mut ViewContext<Self>,
+    ) {
         let buffers = self.buffer.read(cx).all_buffers();
         for branch_buffer in buffers {
             branch_buffer.update(cx, |branch_buffer, cx| {

crates/editor/src/proposed_changes_editor.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{Editor, EditorEvent, SemanticsProvider};
+use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider};
 use collections::HashSet;
 use futures::{channel::mpsc, future::join_all};
 use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, Task, View};
@@ -8,7 +8,7 @@ use project::Project;
 use smol::stream::StreamExt;
 use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
 use text::ToOffset;
-use ui::prelude::*;
+use ui::{prelude::*, ButtonLike, KeyBinding};
 use workspace::{
     searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
     ToolbarItemView, Workspace,
@@ -232,7 +232,10 @@ impl ProposedChangesEditor {
 
 impl Render for ProposedChangesEditor {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        self.editor.clone()
+        div()
+            .size_full()
+            .key_context("ProposedChangesEditor")
+            .child(self.editor.clone())
     }
 }
 
@@ -331,17 +334,21 @@ impl ProposedChangesEditorToolbar {
 }
 
 impl Render for ProposedChangesEditorToolbar {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let editor = self.current_editor.clone();
-        Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
-            if let Some(editor) = &editor {
-                editor.update(cx, |editor, cx| {
-                    editor.editor.update(cx, |editor, cx| {
-                        editor.apply_all_diff_hunks(cx);
-                    })
-                });
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All"));
+
+        match &self.current_editor {
+            Some(editor) => {
+                let focus_handle = editor.focus_handle(cx);
+                let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx)
+                    .map(|binding| binding.into_any_element());
+
+                button_like.children(keybinding).on_click({
+                    move |_event, cx| focus_handle.dispatch_action(&ApplyAllDiffHunks, cx)
+                })
             }
-        })
+            None => button_like.disabled(true),
+        }
     }
 }