editor: Fix crash from stale state after prepaint depth exhaustion (#49664)

Jean Humann , Claude Opus 4.6 (1M context) , and Smit Barmase created

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) <noreply@anthropic.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

crates/editor/src/element.rs | 64 ++++++++++++++++---------------------
1 file changed, 28 insertions(+), 36 deletions(-)

Detailed changes

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"
+                            );
                         }
                     }