@@ -11687,13 +11687,26 @@ impl Editor {
rows.end.previous_row().0,
buffer.line_len(rows.end.previous_row()),
);
- let text = buffer
- .text_for_range(start..end)
- .chain(Some("\n"))
- .collect::<String>();
+
+ let mut text = buffer.text_for_range(start..end).collect::<String>();
+
let insert_location = if upwards {
- Point::new(rows.end.0, 0)
+ // When duplicating upward, we need to insert before the current line.
+ // If we're on the last line and it doesn't end with a newline,
+ // we need to add a newline before the duplicated content.
+ let needs_leading_newline = rows.end.0 >= buffer.max_point().row
+ && buffer.max_point().column > 0
+ && !text.ends_with('\n');
+
+ if needs_leading_newline {
+ text.insert(0, '\n');
+ end
+ } else {
+ text.push('\n');
+ Point::new(rows.end.0, 0)
+ }
} else {
+ text.push('\n');
start
};
edits.push((insert_location..insert_location, text));
@@ -12503,9 +12516,18 @@ impl Editor {
let mut start = selection.start;
let mut end = selection.end;
let is_entire_line = selection.is_empty() || self.selections.line_mode();
+ let mut add_trailing_newline = false;
if is_entire_line {
start = Point::new(start.row, 0);
- end = cmp::min(max_point, Point::new(end.row + 1, 0));
+ let next_line_start = Point::new(end.row + 1, 0);
+ if next_line_start <= max_point {
+ end = next_line_start;
+ } else {
+ // We're on the last line without a trailing newline.
+ // Copy to the end of the line and add a newline afterwards.
+ end = Point::new(end.row, buffer.line_len(MultiBufferRow(end.row)));
+ add_trailing_newline = true;
+ }
}
let mut trimmed_selections = Vec::new();
@@ -12556,6 +12578,10 @@ impl Editor {
text.push_str(chunk);
len += chunk.len();
}
+ if add_trailing_newline {
+ text.push('\n');
+ len += 1;
+ }
clipboard_selections.push(ClipboardSelection {
len,
is_entire_line,
@@ -26478,3 +26478,64 @@ fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
.map(Rgba::from)
.collect()
}
+
+#[gpui::test]
+fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx.add_window(|window, cx| {
+ let buffer = MultiBuffer::build_simple("line1\nline2", cx);
+ build_editor(buffer, window, cx)
+ });
+
+ editor
+ .update(cx, |editor, window, cx| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
+ ])
+ });
+
+ editor.duplicate_line_up(&DuplicateLineUp, window, cx);
+
+ assert_eq!(
+ editor.display_text(cx),
+ "line1\nline2\nline2",
+ "Duplicating last line upward should create duplicate above, not on same line"
+ );
+
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
+ "Selection should remain on the original line"
+ );
+ })
+ .unwrap();
+}
+
+#[gpui::test]
+async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ cx.set_state("line1\nline2Ė");
+
+ cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
+
+ let clipboard_text = cx
+ .read_from_clipboard()
+ .and_then(|item| item.text().as_deref().map(str::to_string));
+
+ assert_eq!(
+ clipboard_text,
+ Some("line2\n".to_string()),
+ "Copying a line without trailing newline should include a newline"
+ );
+
+ cx.set_state("line1\nĖ");
+
+ cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
+
+ cx.assert_editor_state("line1\nline2\nĖ");
+}