Skip to content

editor: Fix the merging of adjacent selection edits#45363

Merged
Veykril merged 16 commits intozed-industries:mainfrom
marcocondrache:fix/adjacenct-edits
Jan 4, 2026
Merged

editor: Fix the merging of adjacent selection edits#45363
Veykril merged 16 commits intozed-industries:mainfrom
marcocondrache:fix/adjacenct-edits

Conversation

@marcocondrache
Copy link
Contributor

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

@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Dec 19, 2025
@github-actions github-actions bot added the community champion Issues filed by our amazing community champions! 🫶 label Dec 19, 2025
@marcocondrache marcocondrache changed the title editor: Fix merge of adjacent selection edits editor: Fix the merging of adjacent selection edits Dec 19, 2025
@Veykril Veykril self-assigned this Dec 19, 2025
@Veykril
Copy link
Member

Veykril commented Dec 19, 2025

Hmm, could we just have a buffer edit variant here that disallows merging the passed in edits for this case?

@marcocondrache
Copy link
Contributor Author

@Veykril I also tried disabling the merge in the edit method to see if that would solve the issue, but it didn’t. The fragments still ended up being merged into a single one. I’d actually prefer this solution as well, so I’ll take another look and see if I can get it to work

@marcocondrache marcocondrache force-pushed the fix/adjacenct-edits branch 3 times, most recently from 6f6e363 to da64585 Compare December 19, 2025 17:30
self.buffers.is_empty()
}

pub fn edit<I, S, T>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay thats a lot of call sites, lets change plans slightly here. Let's make this function (with the arg) private and rename it something else. Then have two methods edit and edit_non_coalesce that just calls out to the private one but the bool parameter being different. That way we will only need to touch that one (or maybe couple) callsite(s). That way we also dont obscure this behavior too much behind a random non-descriptive bool

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this makes a lot of sense. I didn't realize how many there were until I checked the changes on the PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be done

Copy link
Member

@Veykril Veykril left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@Veykril
Copy link
Member

Veykril commented Jan 4, 2026

needs a rebase

@marcocondrache
Copy link
Contributor Author

@Veykril thank you. I will rebase later.

marcocondrache and others added 13 commits January 4, 2026 19:43
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
This reverts commit 48770ac.
This reverts commit 70b439d.
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
@marcocondrache
Copy link
Contributor Author

@Veykril I've rebased 🙂

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
@Veykril Veykril enabled auto-merge (squash) January 4, 2026 19:28
@Veykril Veykril merged commit ce99e7e into zed-industries:main Jan 4, 2026
23 checks passed
@github-project-automation github-project-automation bot moved this from Community PRs to Done in Quality Week – December 2025 Jan 4, 2026
@marcocondrache marcocondrache deleted the fix/adjacenct-edits branch January 4, 2026 19:31
rtfeldman pushed a commit that referenced this pull request Jan 5, 2026
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>
LivioGama pushed a commit to LivioGama/zed that referenced this pull request Jan 20, 2026
…5363)

Closes zed-industries#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>
LivioGama pushed a commit to LivioGama/zed that referenced this pull request Jan 20, 2026
…5363)

Closes zed-industries#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>
LivioGama pushed a commit to LivioGama/zed that referenced this pull request Feb 15, 2026
…5363)

Closes zed-industries#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement community champion Issues filed by our amazing community champions! 🫶

Projects

Development

Successfully merging this pull request may close these issues.

Adjacent selection merge upon insertion of text

2 participants