editor: Fix newline below when selection is at the end of an multibuffer excerpt (#49132)

Switches `newline_below` from adding a newline to the start of the next
line in a multibuffer, to appending a newline at the end of the
selections current lines. This keeps the insertion within the excerpt of
the selection rather than adding a newline to the excerpt below.

Closes #47965

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- Fixed `editor::NewlineBelow` adding a newline to the next multibuffer
excerpt when selection is at the end of an excerpt
This commit is contained in:
Austin Cummings authored and GitHub committed 2026-02-17 14:45:36 +01:00
1 parent 7660a08031
commit b0ffa2f642
2 files changed
+289 -14

No files matched your search

+30 -14
View File
@@ -114,11 +114,11 @@ use git::blame::{GitBlame, GlobalBlameRenderer};
use gpui::{
Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext,
AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context,
DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent,
Focusable, FontId, FontStyle, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers,
MouseButton, MouseDownEvent, MouseMoveEvent, PaintQuad, ParentElement, Pixels, PressureStage,
Render, ScrollHandle, SharedString, SharedUri, Size, Stateful, Styled, Subscription, Task,
TextRun, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle,
DispatchPhase, Edges, Entity, EntityId, EntityInputHandler, EventEmitter, FocusHandle,
FocusOutEvent, Focusable, FontId, FontStyle, FontWeight, Global, HighlightStyle, Hsla,
KeyContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, PaintQuad, ParentElement,
Pixels, PressureStage, Render, ScrollHandle, SharedString, SharedUri, Size, Stateful, Styled,
Subscription, Task, TextRun, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle,
UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, div, point, prelude::*,
pulsating_between, px, relative, size,
};
@@ -5357,10 +5357,7 @@ impl Editor {
pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx);
let mut edits = Vec::new();
let mut buffer_edits: HashMap<EntityId, (Entity<Buffer>, Vec<Point>)> = HashMap::default();
let mut rows = Vec::new();
let mut rows_inserted = 0;
@@ -5368,18 +5365,37 @@ impl Editor {
let cursor = selection.head();
let row = cursor.row;
let point = Point::new(row + 1, 0);
let start_of_line = snapshot.clip_point(point, Bias::Left);
let point = Point::new(row, 0);
let Some((buffer_handle, buffer_point, _)) =
self.buffer.read(cx).point_to_buffer_point(point, cx)
else {
continue;
};
let newline = "\n".to_string();
edits.push((start_of_line..start_of_line, newline));
buffer_edits
.entry(buffer_handle.entity_id())
.or_insert_with(|| (buffer_handle, Vec::new()))
.1
.push(buffer_point);
rows_inserted += 1;
rows.push(row + rows_inserted);
}
self.transact(window, cx, |editor, window, cx| {
editor.edit(edits, cx);
for (_, (buffer_handle, points)) in &buffer_edits {
buffer_handle.update(cx, |buffer, cx| {
let edits: Vec<_> = points
.iter()
.map(|point| {
let target = Point::new(point.row + 1, 0);
let start_of_line = buffer.point_to_offset(target).min(buffer.len());
(start_of_line..start_of_line, "\n")
})
.collect();
buffer.edit(edits, None, cx);
});
}
editor.change_selections(Default::default(), window, cx, |s| {
let mut index = 0;
+259
View File
@@ -3385,6 +3385,265 @@ async fn test_newline_below(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
fn test_newline_below_multibuffer(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(ReadWrite);
multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
cx,
);
multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
cx,
);
multibuffer
});
cx.add_window(|window, cx| {
let mut editor = build_editor(multibuffer, window, cx);
assert_eq!(
editor.text(cx),
indoc! {"
aaa
bbb
ccc
ddd
eee
fff"}
);
// Cursor on the last line of the first excerpt.
// The newline should be inserted within the first excerpt (buffer_1),
// not in the second excerpt (buffer_2).
select_ranges(
&mut editor,
indoc! {"
aaa
bbb
cˇcc
ddd
eee
fff"},
window,
cx,
);
editor.newline_below(&NewlineBelow, window, cx);
assert_text_with_selections(
&mut editor,
indoc! {"
aaa
bbb
ccc
ˇ
ddd
eee
fff"},
cx,
);
buffer_1.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "aaa\nbbb\nccc\n");
});
buffer_2.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "ddd\neee\nfff");
});
editor
});
}
#[gpui::test]
fn test_newline_below_multibuffer_middle_of_excerpt(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(ReadWrite);
multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
cx,
);
multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
cx,
);
multibuffer
});
cx.add_window(|window, cx| {
let mut editor = build_editor(multibuffer, window, cx);
// Cursor in the middle of the first excerpt.
select_ranges(
&mut editor,
indoc! {"
aˇaa
bbb
ccc
ddd
eee
fff"},
window,
cx,
);
editor.newline_below(&NewlineBelow, window, cx);
assert_text_with_selections(
&mut editor,
indoc! {"
aaa
ˇ
bbb
ccc
ddd
eee
fff"},
cx,
);
buffer_1.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "aaa\n\nbbb\nccc");
});
buffer_2.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "ddd\neee\nfff");
});
editor
});
}
#[gpui::test]
fn test_newline_below_multibuffer_last_line_of_last_excerpt(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(ReadWrite);
multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
cx,
);
multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
cx,
);
multibuffer
});
cx.add_window(|window, cx| {
let mut editor = build_editor(multibuffer, window, cx);
// Cursor on the last line of the last excerpt.
select_ranges(
&mut editor,
indoc! {"
aaa
bbb
ccc
ddd
eee
fˇff"},
window,
cx,
);
editor.newline_below(&NewlineBelow, window, cx);
assert_text_with_selections(
&mut editor,
indoc! {"
aaa
bbb
ccc
ddd
eee
fff
ˇ"},
cx,
);
buffer_1.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "aaa\nbbb\nccc");
});
buffer_2.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "ddd\neee\nfff\n");
});
editor
});
}
#[gpui::test]
fn test_newline_below_multibuffer_multiple_cursors(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let buffer_1 = cx.new(|cx| Buffer::local("aaa\nbbb\nccc", cx));
let buffer_2 = cx.new(|cx| Buffer::local("ddd\neee\nfff", cx));
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(ReadWrite);
multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
cx,
);
multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 3))],
cx,
);
multibuffer
});
cx.add_window(|window, cx| {
let mut editor = build_editor(multibuffer, window, cx);
// Cursors on the last line of the first excerpt and the first line
// of the second excerpt. Each newline should go into its respective buffer.
select_ranges(
&mut editor,
indoc! {"
aaa
bbb
cˇcc
dˇdd
eee
fff"},
window,
cx,
);
editor.newline_below(&NewlineBelow, window, cx);
assert_text_with_selections(
&mut editor,
indoc! {"
aaa
bbb
ccc
ˇ
ddd
ˇ
eee
fff"},
cx,
);
buffer_1.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "aaa\nbbb\nccc\n");
});
buffer_2.read_with(cx, |buffer, _| {
assert_eq!(buffer.text(), "ddd\n\neee\nfff");
});
editor
});
}
#[gpui::test]
async fn test_newline_comments(cx: &mut TestAppContext) {
init_test(cx, |settings| {