Skip to content

editor: Speed up multi cursor editing - #58510

Merged
Anthony-Eid merged 11 commits into
zed-industries:mainfrom
Rani367:fix-32051-multicursor-perf
Jun 17, 2026
Merged

editor: Speed up multi cursor editing#58510
Anthony-Eid merged 11 commits into
zed-industries:mainfrom
Rani367:fix-32051-multicursor-perf

Conversation

@Rani367

@Rani367 Rani367 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Early draft for #32051 (multi-cursor editing is very slow, and basically hangs at high cursor counts). Opening it early like @Anthony-Eid suggested so we can agree on direction before I go further.

Root cause

Typing with a cursor on every line is mostly O(N)-per-keystroke work spread across a few places. I profiled it with sample and per-phase timers around handle_input. At 1000 cursors (~24ms before this PR): the post-edit display-map sync is ~11.8ms, the CRDT edit (apply_local_edit) ~3.7ms, resolving all N selections plus the per-cursor input loop ~3.5ms, the post-edit selection round-trip ~2.6ms, and change_selections plus transact machinery ~2.4ms. The display sync re-runs per-edit SumTree work through every layer even when nothing transforms (the plain-text case), and selections get re-resolved through the full display round-trip several times per keystroke.

What this does

  • Display fast paths: InlayMap::sync (no inlays) and WrapMap::interpolate (no soft-wrap) skip the O(edits) transform-tree rebuild and return the passthrough snapshot, gated so a pending inlay splice or existing wraps still take the slow path.
  • Selection fast path: when nothing collapses buffer content (!has_folds() && !has_replacement_blocks()), resolve Anchor to Point to Offset batched and skip the per-selection display round-trip (the todo(lw)).
  • Render and autoscroll resolve only the first/last/newest selections instead of all N per frame.
  • A parameterized Multi-cursor input/cursors/{1000,10000,100000} benchmark.

Results

  • Typing (handle_input): ~2.1x faster (24ms to 11.7ms at 1k).
  • Type plus two word-deletes (criterion): 29% faster (89.9 to 63.8ms) at 1k, 37% (959 to 606ms) at 10k.
  • Post-edit display sync alone: 11.8 to 3.8ms at 1k, 133 to 45ms at 10k.

All editor (759), display_map (67), multi_buffer (58), and text (36) tests pass, and ./script/clippy is clean.

Direction

This is ~2x, and I can get the current architecture to roughly 3.5-4.5x with a few more safe changes (hoisting the per-cursor language/editability checks, cheaper snapshot clones). Genuine VS Code numbers (~1-2us/cursor) aren't reachable while the buffer is a CRDT rope of fragments with anchor selections and a 5-layer transform stack. That would need a plain-offset cursor model and/or decoupling display layout from the edit path, which is a bigger effort I'd want to design with you. One thing I'd need your call on: set_active_selections sends an UpdateSelections collab op every keystroke (building N anchors); can that be debounced to transaction end, or do presence/follow-mode features rely on per-keystroke cursor broadcast?

Release Notes:

  • editor: Improved multi cursor editing performance

@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Jun 4, 2026
Comment thread crates/editor/benches/editor_render.rs Outdated
Comment on lines +65 to +70
fn editor_multi_cursor_input(bencher: &mut Bencher<'_>, args: &(usize, TestAppContext)) {
let (line_count, cx) = args;
let mut cx = cx.clone();

let text = "line:\n".repeat(*line_count);
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we want a general approach to this bench maybe we should remove the multi cursor 100s lines bench above?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed it and the init_context helper it used. The new Multi-cursor input group covers 1000, 10k, and an opt-in 100k.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

awesome! Thanks

@Anthony-Eid

Copy link
Copy Markdown
Contributor

Thank you for opening this up, I'll need some time to review this in depth because this isn't an area I have too much experience in yet.

cc: @Veykril As someone with more context on the display map than me. Let me know if you want to pair

Multi-cursor insertions/deletions were O(N) per keystroke with a large
per-cursor constant (zed-industries#32051). Profiling showed the cost was dominated by
the post-edit display-map sync re-running per-edit SumTree work through
every layer even when all transforms are trivial, plus a per-selection
Anchor -> DisplayPoint -> Point round-trip resolved several times per
keystroke.

- display_map: isomorphic fast paths in InlayMap::sync (no inlays) and
  WrapMap::interpolate (no soft-wraps) that skip the O(edits) transform-
  tree rebuild and emit the passthrough snapshot directly.
- selections_collection: when the display collapses no buffer content
  (no folds, no replacement blocks), resolve selections
  Anchor -> Point -> Offset fully batched, skipping the per-selection
  display round-trip (resolves the existing todo(lw)).
- autoscroll / element: resolve only the first/last/newest selections
  instead of all N every frame.
- Add a parameterized "Multi-cursor input" benchmark.

Typing with 1000 cursors is ~2x faster; the type+delete benchmark
improves ~29% at 1k and ~37% at 10k cursors.
@Rani367
Rani367 force-pushed the fix-32051-multicursor-perf branch from 192f5cb to 76a9ad5 Compare June 5, 2026 19:26
@Rani367
Rani367 marked this pull request as ready for review June 9, 2026 18:52
@Anthony-Eid Anthony-Eid self-assigned this Jun 17, 2026
@Anthony-Eid

Copy link
Copy Markdown
Contributor

Thanks for this PR! I pushed up some clean ups and two slight performance improvements to this PR. I'm going to enable auto merge

@Anthony-Eid
Anthony-Eid enabled auto-merge June 17, 2026 07:48
@Anthony-Eid Anthony-Eid changed the title editor: Speed up multi-cursor editing editor: Speed up multi cursor editing Jun 17, 2026
@Anthony-Eid
Anthony-Eid added this pull request to the merge queue Jun 17, 2026
Merged via the queue into zed-industries:main with commit 1722fe6 Jun 17, 2026
36 checks passed
This was referenced Jun 18, 2026
@yara-blue yara-blue added the area:editor Feedback for code editing, formatting, editor iterations, etc label Jun 25, 2026
@Rani367
Rani367 deleted the fix-32051-multicursor-perf branch July 3, 2026 14:20
jolutz pushed a commit to jolutz/zed that referenced this pull request Aug 8, 2026
Early draft for zed-industries#32051 (multi-cursor editing is very slow, and basically
hangs at high cursor counts). Opening it early like @Anthony-Eid
suggested so we can agree on direction before I go further.

## Root cause

Typing with a cursor on every line is mostly O(N)-per-keystroke work
spread across a few places. I profiled it with `sample` and per-phase
timers around `handle_input`. At 1000 cursors (~24ms before this PR):
the post-edit display-map sync is ~11.8ms, the CRDT edit
(`apply_local_edit`) ~3.7ms, resolving all N selections plus the
per-cursor input loop ~3.5ms, the post-edit selection round-trip ~2.6ms,
and `change_selections` plus transact machinery ~2.4ms. The display sync
re-runs per-edit `SumTree` work through every layer even when nothing
transforms (the plain-text case), and selections get re-resolved through
the full display round-trip several times per keystroke.

## What this does

- Display fast paths: `InlayMap::sync` (no inlays) and
`WrapMap::interpolate` (no soft-wrap) skip the O(edits) transform-tree
rebuild and return the passthrough snapshot, gated so a pending inlay
splice or existing wraps still take the slow path.
- Selection fast path: when nothing collapses buffer content
(`!has_folds() && !has_replacement_blocks()`), resolve `Anchor` to
`Point` to `Offset` batched and skip the per-selection display
round-trip (the `todo(lw)`).
- Render and autoscroll resolve only the first/last/newest selections
instead of all N per frame.
- A parameterized `Multi-cursor input/cursors/{1000,10000,100000}`
benchmark.

## Results

- Typing (`handle_input`): ~2.1x faster (24ms to 11.7ms at 1k).
- Type plus two word-deletes (criterion): 29% faster (89.9 to 63.8ms) at
1k, 37% (959 to 606ms) at 10k.
- Post-edit display sync alone: 11.8 to 3.8ms at 1k, 133 to 45ms at 10k.

All `editor` (759), `display_map` (67), `multi_buffer` (58), and `text`
(36) tests pass, and `./script/clippy` is clean.

## Direction

This is ~2x, and I can get the current architecture to roughly 3.5-4.5x
with a few more safe changes (hoisting the per-cursor
language/editability checks, cheaper snapshot clones). Genuine VS Code
numbers (~1-2us/cursor) aren't reachable while the buffer is a CRDT rope
of fragments with anchor selections and a 5-layer transform stack. That
would need a plain-offset cursor model and/or decoupling display layout
from the edit path, which is a bigger effort I'd want to design with
you. One thing I'd need your call on: `set_active_selections` sends an
`UpdateSelections` collab op every keystroke (building N anchors); can
that be debounced to transaction end, or do presence/follow-mode
features rely on per-keystroke cursor broadcast?

Release Notes:

- editor: Improved multi cursor editing performance

---------

Co-authored-by: Anthony Eid <anthony@zed.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:editor Feedback for code editing, formatting, editor iterations, etc cla-signed The user has signed the Contributor License Agreement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants