From a94b0931c7394cee09438176fee20986fd929aee Mon Sep 17 00:00:00 2001 From: AidanV <84053180+AidanV@users.noreply.github.com> Date: Wed, 10 Sep 2025 11:40:00 -0700 Subject: [PATCH] editor: Fix cuts on end of line cutting whole line (#34553) Closes #19816 Release Notes: - Improved `ctrl-k` (`editor::CutToEndOfLine`) behavior when used at the end of lines - Add option to make `editor::CutToEndOfLine` not gobble newlines. ```json { "context": "Editor", "bindings": { "ctrl-k": ["editor::CutToEndOfLine", { "stop_at_newlines": true }] } }, ``` --------- Co-authored-by: Peter Tripp --- crates/editor/src/actions.rs | 11 +++++-- crates/editor/src/editor.rs | 34 +++++++++++++++----- crates/editor/src/editor_tests.rs | 52 +++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 35510b36e9b80e3e8c4d9cd82a3b6a8f7da67429..9dac77970a4560d4e9684c925b5ac4878157941f 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -253,6 +253,15 @@ pub struct DeleteToPreviousWordStart { pub ignore_brackets: bool, } +/// Cuts from cursor to end of line. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = editor)] +#[serde(deny_unknown_fields)] +pub struct CutToEndOfLine { + #[serde(default)] + pub stop_at_newlines: bool, +} + /// Folds all code blocks at the specified indentation level. #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] #[action(namespace = editor)] @@ -412,8 +421,6 @@ actions!( CopyPermalinkToLine, /// Cuts selected text to the clipboard. Cut, - /// Cuts from cursor to end of line. - CutToEndOfLine, /// Deletes the character after the cursor. Delete, /// Deletes the current line. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 46f28f078978a60c99f53ff384efcf30214a00a2..aacf6d4f282ca12b273949a08704782839864a66 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12249,7 +12249,12 @@ impl Editor { .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); } - pub fn cut_common(&mut self, window: &mut Window, cx: &mut Context) -> ClipboardItem { + pub fn cut_common( + &mut self, + cut_no_selection_line: bool, + window: &mut Window, + cx: &mut Context, + ) -> ClipboardItem { let mut text = String::new(); let buffer = self.buffer.read(cx).snapshot(cx); let mut selections = self.selections.all::(cx); @@ -12258,7 +12263,8 @@ impl Editor { let max_point = buffer.max_point(); let mut is_first = true; for selection in &mut selections { - let is_entire_line = selection.is_empty() || self.selections.line_mode; + let is_entire_line = + (selection.is_empty() && cut_no_selection_line) || self.selections.line_mode; if is_entire_line { selection.start = Point::new(selection.start.row, 0); if !selection.is_empty() && selection.end.column == 0 { @@ -12299,7 +12305,7 @@ impl Editor { pub fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - let item = self.cut_common(window, cx); + let item = self.cut_common(true, window, cx); cx.write_to_clipboard(item); } @@ -12308,11 +12314,14 @@ impl Editor { self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(|snapshot, sel| { if sel.is_empty() { - sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row())) + sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row())); + } + if sel.is_empty() { + sel.end = DisplayPoint::new(sel.end.row() + 1_u32, 0); } }); }); - let item = self.cut_common(window, cx); + let item = self.cut_common(true, window, cx); cx.set_global(KillRing(item)) } @@ -13474,7 +13483,7 @@ impl Editor { pub fn cut_to_end_of_line( &mut self, - _: &CutToEndOfLine, + action: &CutToEndOfLine, window: &mut Window, cx: &mut Context, ) { @@ -13487,7 +13496,18 @@ impl Editor { window, cx, ); - this.cut(&Cut, window, cx); + if !action.stop_at_newlines { + this.change_selections(Default::default(), window, cx, |s| { + s.move_with(|_, sel| { + if sel.is_empty() { + sel.end = DisplayPoint::new(sel.end.row() + 1_u32, 0); + } + }); + }); + } + this.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); + let item = this.cut_common(false, window, cx); + cx.write_to_clipboard(item); }); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6cf911fb4c2bca8cd86a03729dc250377d1d12d5..4e3c5012e14f7919a8994bb2951c71cd75d0e404 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6730,6 +6730,58 @@ async fn test_hard_wrap(cx: &mut TestAppContext) { )); } +#[gpui::test] +async fn test_cut_line_ends(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + The quick« brownˇ» + fox jumps overˇ + the lazy dog"}); + cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx)); + cx.assert_editor_state(indoc! {" + The quickˇ + ˇthe lazy dog"}); + + cx.set_state(indoc! {" + The quick« brownˇ» + fox jumps overˇ + the lazy dog"}); + cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx)); + cx.assert_editor_state(indoc! {" + The quickˇ + fox jumps overˇthe lazy dog"}); + + cx.set_state(indoc! {" + The quick« brownˇ» + fox jumps overˇ + the lazy dog"}); + cx.update_editor(|e, window, cx| { + e.cut_to_end_of_line( + &CutToEndOfLine { + stop_at_newlines: true, + }, + window, + cx, + ) + }); + cx.assert_editor_state(indoc! {" + The quickˇ + fox jumps overˇ + the lazy dog"}); + + cx.set_state(indoc! {" + The quick« brownˇ» + fox jumps overˇ + the lazy dog"}); + cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx)); + cx.assert_editor_state(indoc! {" + The quickˇ + fox jumps overˇthe lazy dog"}); +} + #[gpui::test] async fn test_clipboard(cx: &mut TestAppContext) { init_test(cx, |_| {});