Implement readline/emacs/macos style ctrl-k cut and ctrl-y yank (#21003)

Peter Tripp and Conrad Irwin created

- Added support for ctrl-k / ctrl-y alternate cut/yank buffer on macos.

Co-authored-by: Conrad Irwin <conrad@zed.dev>

Change summary

assets/keymaps/default-macos.json |  3 +
crates/editor/src/actions.rs      |  2 +
crates/editor/src/editor.rs       | 42 ++++++++++++++++++++++++++++----
crates/editor/src/element.rs      |  2 +
4 files changed, 42 insertions(+), 7 deletions(-)

Detailed changes

assets/keymaps/default-macos.json 🔗

@@ -49,8 +49,9 @@
       "ctrl-d": "editor::Delete",
       "tab": "editor::Tab",
       "shift-tab": "editor::TabPrev",
-      "ctrl-k": "editor::CutToEndOfLine",
       "ctrl-t": "editor::Transpose",
+      "ctrl-k": "editor::KillRingCut",
+      "ctrl-y": "editor::KillRingYank",
       "cmd-k q": "editor::Rewrap",
       "cmd-k cmd-q": "editor::Rewrap",
       "cmd-backspace": "editor::DeleteToBeginningOfLine",

crates/editor/src/editor.rs 🔗

@@ -74,7 +74,7 @@ use gpui::{
     div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
     AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry,
     ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent,
-    FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
+    FocusableView, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext,
     ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, ParentElement, Pixels, Render,
     ScrollStrategy, SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task,
     TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View,
@@ -7364,7 +7364,7 @@ impl Editor {
             .update(cx, |buffer, cx| buffer.edit(edits, None, cx));
     }
 
-    pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
+    pub fn cut_common(&mut self, cx: &mut ViewContext<Self>) -> ClipboardItem {
         let mut text = String::new();
         let buffer = self.buffer.read(cx).snapshot(cx);
         let mut selections = self.selections.all::<Point>(cx);
@@ -7408,11 +7408,38 @@ impl Editor {
                 s.select(selections);
             });
             this.insert("", cx);
-            cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
-                text,
-                clipboard_selections,
-            ));
         });
+        ClipboardItem::new_string_with_json_metadata(text, clipboard_selections)
+    }
+
+    pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
+        let item = self.cut_common(cx);
+        cx.write_to_clipboard(item);
+    }
+
+    pub fn kill_ring_cut(&mut self, _: &KillRingCut, cx: &mut ViewContext<Self>) {
+        self.change_selections(None, cx, |s| {
+            s.move_with(|snapshot, sel| {
+                if sel.is_empty() {
+                    sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row()))
+                }
+            });
+        });
+        let item = self.cut_common(cx);
+        cx.set_global(KillRing(item))
+    }
+
+    pub fn kill_ring_yank(&mut self, _: &KillRingYank, cx: &mut ViewContext<Self>) {
+        let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() {
+            if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() {
+                (kill_ring.text().to_string(), kill_ring.metadata_json())
+            } else {
+                return;
+            }
+        } else {
+            return;
+        };
+        self.do_paste(&text, metadata, false, cx);
     }
 
     pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
@@ -15145,4 +15172,7 @@ fn check_multiline_range(buffer: &Buffer, range: Range<usize>) -> Range<usize> {
     }
 }
 
+pub struct KillRing(ClipboardItem);
+impl Global for KillRing {}
+
 const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);

crates/editor/src/element.rs 🔗

@@ -217,6 +217,8 @@ impl EditorElement {
         register_action(view, cx, Editor::transpose);
         register_action(view, cx, Editor::rewrap);
         register_action(view, cx, Editor::cut);
+        register_action(view, cx, Editor::kill_ring_cut);
+        register_action(view, cx, Editor::kill_ring_yank);
         register_action(view, cx, Editor::copy);
         register_action(view, cx, Editor::paste);
         register_action(view, cx, Editor::undo);