editor: Speed up multi cursor editing - #58510
Merged
Anthony-Eid merged 11 commits intoJun 17, 2026
Merged
Conversation
Anthony-Eid
reviewed
Jun 5, 2026
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)); |
Contributor
There was a problem hiding this comment.
If we want a general approach to this bench maybe we should remove the multi cursor 100s lines bench above?
Contributor
Author
There was a problem hiding this comment.
Removed it and the init_context helper it used. The new Multi-cursor input group covers 1000, 10k, and an opt-in 100k.
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
force-pushed
the
fix-32051-multicursor-perf
branch
from
June 5, 2026 19:26
192f5cb to
76a9ad5
Compare
Rani367
marked this pull request as ready for review
June 9, 2026 18:52
…-perf # Conflicts: # crates/benchmarks/benches/editor_render.rs
We now use the transforms summary instead, this should make the function always O(1) instead of O(n) and average (1)
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
enabled auto-merge
June 17, 2026 07:48
This was referenced Jun 18, 2026
Closed
This was referenced Jun 22, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
sampleand per-phase timers aroundhandle_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, andchange_selectionsplus transact machinery ~2.4ms. The display sync re-runs per-editSumTreework 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
InlayMap::sync(no inlays) andWrapMap::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.!has_folds() && !has_replacement_blocks()), resolveAnchortoPointtoOffsetbatched and skip the per-selection display round-trip (thetodo(lw)).Multi-cursor input/cursors/{1000,10000,100000}benchmark.Results
handle_input): ~2.1x faster (24ms to 11.7ms at 1k).All
editor(759),display_map(67),multi_buffer(58), andtext(36) tests pass, and./script/clippyis 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_selectionssends anUpdateSelectionscollab 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: