editor: Fix the merging of adjacent selection edits (#45363)

Closes #45046

The root of the issue is anchor resolution. When we apply adjacent
edits, they get merged into a single edit. In the scenario described in
the issue, this is what happens:

1. We create an anchor at the end of each selection (bias::right) on the
snapshot before the edits.
2. We collect the edits and apply them to the buffer.
3. Since the edits are adjacent (>=), the buffer merges them into a
single edit.
4. As a result, we apply one edit to the text buffer, creating a single
visible fragment with length = 3.
5. The buffer ends up with fragments like: [F(len = 3, visible = true),
F(len = 1, visible = false), ...]
6. After the edits, we resolve the previously created anchors to produce
zero-width selections (cursors).
7. All anchors resolve into deleted fragments, so their resolved offset
equals the cumulative visible offset, which is 3.
8. We now have 3 cursors with identical coordinates (0;3).
9. These cursors get merged into a single cursor.

I tried several approaches, but they either felt wrong or didn’t work.
In particular, I tried adjusting anchor resolution using the delta
stored in handle_input, but this doesn’t help because selections are
merged immediately after anchor resolution.

The only workable solution I found is to avoid anchors entirely for the
adjacent-edit case. Instead, we can compute the final cursor positions
directly from the edits and create the selections based on that
information.

Release Notes:

- Fixed an issue where adjacent selection insert would merge cursors

---------

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
This commit is contained in:
Marco Mihai CondracheandLukas Wirth authored and GitHub committed 2026-01-04 19:31:27 +00:00
1 parent 0821cddcaa
commit ce99e7ef26
3 files changed
+124 -21

No files matched your search

+43 -4
View File
@@ -1257,6 +1257,33 @@ impl MultiBuffer {
I: IntoIterator<Item = (Range<S>, T)>,
S: ToOffset,
T: Into<Arc<str>>,
{
self.edit_internal(edits, autoindent_mode, true, cx);
}
pub fn edit_non_coalesce<I, S, T>(
&mut self,
edits: I,
autoindent_mode: Option<AutoindentMode>,
cx: &mut Context<Self>,
) where
I: IntoIterator<Item = (Range<S>, T)>,
S: ToOffset,
T: Into<Arc<str>>,
{
self.edit_internal(edits, autoindent_mode, false, cx);
}
fn edit_internal<I, S, T>(
&mut self,
edits: I,
autoindent_mode: Option<AutoindentMode>,
coalesce_adjacent: bool,
cx: &mut Context<Self>,
) where
I: IntoIterator<Item = (Range<S>, T)>,
S: ToOffset,
T: Into<Arc<str>>,
{
if self.read_only() || self.buffers.is_empty() {
return;
@@ -1274,13 +1301,14 @@ impl MultiBuffer {
})
.collect::<Vec<_>>();
return edit_internal(self, edits, autoindent_mode, cx);
return edit_internal(self, edits, autoindent_mode, coalesce_adjacent, cx);
// Non-generic part of edit, hoisted out to avoid blowing up LLVM IR.
fn edit_internal(
this: &mut MultiBuffer,
edits: Vec<(Range<MultiBufferOffset>, Arc<str>)>,
mut autoindent_mode: Option<AutoindentMode>,
coalesce_adjacent: bool,
cx: &mut Context<MultiBuffer>,
) {
let original_indent_columns = match &mut autoindent_mode {
@@ -1322,7 +1350,13 @@ impl MultiBuffer {
..
}) = edits.peek()
{
if range.end >= next_range.start {
let should_coalesce = if coalesce_adjacent {
range.end >= next_range.start
} else {
range.end > next_range.start
};
if should_coalesce {
range.end = cmp::max(next_range.end, range.end);
is_insertion |= *next_is_insertion;
if excerpt_id == *next_excerpt_id {
@@ -1365,8 +1399,13 @@ impl MultiBuffer {
autoindent_mode.clone()
};
buffer.edit(deletions, deletion_autoindent_mode, cx);
buffer.edit(insertions, insertion_autoindent_mode, cx);
if coalesce_adjacent {
buffer.edit(deletions, deletion_autoindent_mode, cx);
buffer.edit(insertions, insertion_autoindent_mode, cx);
} else {
buffer.edit_non_coalesce(deletions, deletion_autoindent_mode, cx);
buffer.edit_non_coalesce(insertions, insertion_autoindent_mode, cx);
}
})
}