From 19c8363a8e0d8a2f7a7181bab2c14d87390c0f25 Mon Sep 17 00:00:00 2001 From: Jean Humann <76565654+jean-humann@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:42:28 +0100 Subject: [PATCH] editor: Fix crash from stale state after prepaint depth exhaustion (#49664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #49662 ## Summary Fixes an index-out-of-bounds crash in `EditorElement::prepaint` that occurs when block decorations oscillate in size, exhausting the recursive prepaint budget (`MAX_PREPAINT_DEPTH = 5`). ## Root cause When block decorations resize during `render_blocks`, prepaint recurses to rebuild layout state. Previously, both `resize_blocks()` and `update_renderer_widths()` mutated the display map **before** checking `can_prepaint()`. At max depth: 1. `resize_blocks()` mutated the block map, changing the display row mapping 2. `can_prepaint()` returned false, skipping the recursive rebuild 3. All local state (`snapshot`, `start_row`, `end_row`, `line_layouts`, `row_infos`) remained stale 4. Downstream code — particularly `layout_inline_diagnostics` which takes a **fresh** `editor.snapshot()` — would see the new row mapping but index into the stale `line_layouts` 5. `row.minus(start_row)` produced an index exceeding `line_layouts.len()` → panic The crash is most likely to occur when many block decorations appear or resize simultaneously (e.g., unfolding a folder containing many git repos, causing a burst of diff hunk blocks), or when `place_near` blocks oscillate between inline (height=0) and block (height>=1) mode, burning through all 5 recursion levels without converging. ## Fix Guard both mutation sites with `can_prepaint()` so the display map is NOT mutated when we cannot recurse to rebuild state: - **Block resize**: move `resize_blocks()` inside the `can_prepaint()` guard. At max depth, defer the resize to the next frame via `cx.notify()`. The resize will be re-detected because the stored height still mismatches the rendered height. https://github.com/jean-humann/zed/blob/021978ecf939723a6ba4ab9843572b6bcefe7cb7/crates/editor/src/element.rs#L10207-L10239 - **Renderer widths**: short-circuit `update_renderer_widths()` with `can_prepaint()` so fold widths are not updated at max depth. https://github.com/jean-humann/zed/blob/021978ecf939723a6ba4ab9843572b6bcefe7cb7/crates/editor/src/element.rs#L10097-L10115 In both cases the next frame starts with a fresh recursion budget (`EditorRequestLayoutState::default()` resets `prepaint_depth` to 0) and applies the deferred changes normally. The worst case is one frame of slightly wrong-sized blocks — vastly preferable to a crash. ## Test plan - [x] `cargo check -p editor` passes - [ ] Manual testing with many open files, inline diagnostics, hover popovers, and git diff hunks Release Notes: - Fixed a crash (index out of bounds) during editor rendering when block decorations repeatedly resize, exhausting the recursive prepaint budget. 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Smit Barmase --- crates/editor/src/element.rs | 64 ++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a3d047bde663664ca5d789a168c4eff5a12ad429..b94add33f04a456bd4e5ad8a887f36bc0e04e29b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -9523,7 +9523,7 @@ impl EditorRequestLayoutState { } } - fn can_prepaint(&self) -> bool { + fn has_remaining_prepaint_depth(&self) -> bool { self.prepaint_depth.get() < Self::MAX_PREPAINT_DEPTH } } @@ -10236,29 +10236,21 @@ impl Element for EditorElement { } }) }); - if new_renderer_widths.is_some_and(|new_renderer_widths| { - self.editor.update(cx, |editor, cx| { - editor.update_renderer_widths(new_renderer_widths, cx) - }) - }) { - // If the fold widths have changed, we need to prepaint - // the element again to account for any changes in - // wrapping. - if request_layout.can_prepaint() { - return self.prepaint( - None, - _inspector_id, - bounds, - request_layout, - window, - cx, - ); - } else { - debug_panic!(concat!( - "skipping recursive prepaint at max depth. ", - "renderer widths may be stale." - )); - } + let renderer_widths_changed = request_layout.has_remaining_prepaint_depth() + && new_renderer_widths.is_some_and(|new_renderer_widths| { + self.editor.update(cx, |editor, cx| { + editor.update_renderer_widths(new_renderer_widths, cx) + }) + }); + if renderer_widths_changed { + return self.prepaint( + None, + _inspector_id, + bounds, + request_layout, + window, + cx, + ); } let longest_line_blame_width = self @@ -10374,14 +10366,14 @@ impl Element for EditorElement { resized_blocks, } = blocks; if let Some(resized_blocks) = resized_blocks { - self.editor.update(cx, |editor, cx| { - editor.resize_blocks( - resized_blocks, - autoscroll_request.map(|(autoscroll, _)| autoscroll), - cx, - ) - }); - if request_layout.can_prepaint() { + if request_layout.has_remaining_prepaint_depth() { + self.editor.update(cx, |editor, cx| { + editor.resize_blocks( + resized_blocks, + autoscroll_request.map(|(autoscroll, _)| autoscroll), + cx, + ) + }); return self.prepaint( None, _inspector_id, @@ -10391,10 +10383,10 @@ impl Element for EditorElement { cx, ); } else { - debug_panic!(concat!( - "skipping recursive prepaint at max depth. ", - "block layout may be stale." - )); + debug_panic!( + "dropping block resize because prepaint depth \ + limit was reached" + ); } }