wip: linewise cut/copy improvements

Peter Tripp created

Change summary

crates/editor/src/editor.rs       | 14 +++++
crates/editor/src/editor_tests.rs | 90 +++++++++++++++++++++++++++++++++
2 files changed, 104 insertions(+)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -11918,6 +11918,13 @@ impl Editor {
                     text.push_str(chunk);
                     len += chunk.len();
                 }
+
+                // If cutting an entire line, ensure it ends with exactly one newline
+                if is_entire_line && !text.ends_with('\n') {
+                    text.push('\n');
+                    len += 1;
+                }
+
                 clipboard_selections.push(ClipboardSelection {
                     len,
                     is_entire_line,
@@ -12049,6 +12056,13 @@ impl Editor {
                         text.push_str(chunk);
                         len += chunk.len();
                     }
+
+                    // If copying an entire line, ensure it ends with exactly one newline
+                    if is_entire_line && !text.ends_with('\n') {
+                        text.push('\n');
+                        len += 1;
+                    }
+
                     clipboard_selections.push(ClipboardSelection {
                         len,
                         is_entire_line,

crates/editor/src/editor_tests.rs 🔗

@@ -6076,6 +6076,96 @@ if is_entire_line {
     );
 }
 
+#[gpui::test]
+async fn test_copy_entire_line(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    cx.set_state("line1\nline2\nlastˇ line");
+    cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
+    assert_eq!(
+        cx.read_from_clipboard()
+            .and_then(|item| item.text().as_deref().map(str::to_string)),
+        Some("last line\n".to_string()),
+        "Copying last line of file without newline should include trailing newline"
+    );
+
+    cx.set_state("line1\nˇline2\nlast line");
+    cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
+    assert_eq!(
+        cx.read_from_clipboard()
+            .and_then(|item| item.text().as_deref().map(str::to_string)),
+        Some("line2\n".to_string()),
+        "Copying a line without a selection should copy that line with a trailing newline"
+    );
+
+    cx.set_state("ˇ");
+    cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
+    assert_eq!(
+        cx.read_from_clipboard()
+            .and_then(|item| item.text().as_deref().map(str::to_string)),
+        Some("\n".to_string()),
+        "Copying empty line should be a newline"
+    );
+
+    cx.set_state("line1\nline2\nlast line\nˇ");
+    cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
+    assert_eq!(
+        cx.read_from_clipboard()
+            .and_then(|item| item.text().as_deref().map(str::to_string)),
+        Some("\n".to_string()),
+        "Copying empty line at end of file should be a newline"
+    );
+}
+
+#[gpui::test]
+async fn test_cut_entire_line(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+
+    cx.set_state("line1\nline2\nlastˇ line");
+    cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
+    assert_eq!(
+        cx.read_from_clipboard()
+            .and_then(|item| item.text().as_deref().map(str::to_string)),
+        Some("last line\n".to_string()),
+        "Cutting last line of file without newline should include trailing newline"
+    );
+    cx.assert_editor_state("line1\nline2\nˇ");
+
+    cx.set_state("line1\nˇline2\nlast line");
+    cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
+    assert_eq!(
+        cx.read_from_clipboard()
+            .and_then(|item| item.text().as_deref().map(str::to_string)),
+        Some("line2\n".to_string()),
+        "Cutting a line without a selection should cut that line with a trailing newline"
+    );
+    cx.assert_editor_state("line1\nˇlast line");
+
+    cx.set_state("ˇ");
+    cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
+    assert_eq!(
+        cx.read_from_clipboard()
+            .and_then(|item| item.text().as_deref().map(str::to_string)),
+        Some("\n".to_string()),
+        "Cutting empty line should be a newline"
+    );
+    cx.assert_editor_state("ˇ");
+
+    cx.set_state("line1\nline2\nlast line\nˇ");
+    cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
+    assert_eq!(
+        cx.read_from_clipboard()
+            .and_then(|item| item.text().as_deref().map(str::to_string)),
+        Some("\n".to_string()),
+        "Cutting empty line at end of file should be a newline"
+    );
+    cx.assert_editor_state("line1\nline2\nlast lineˇ");
+}
+
 #[gpui::test]
 async fn test_paste_multiline(cx: &mut TestAppContext) {
     init_test(cx, |_| {});