Optimize editor rendering when clipped by parent containers (#44995)

Antonio Scandurra created

Fixes #44997

## Summary

Optimizes editor rendering when an editor is partially clipped by a
parent container (e.g., a `List`). The editor now only lays out and
renders lines that are actually visible within the viewport, rather than
all lines in the document.

## Problem

When an `AutoHeight` editor with thousands of lines is placed inside a
scrollable `List` (such as in the Agent Panel thread view), the editor
would lay out **all** lines during prepaint, even though only a small
portion was visible. Profiling showed that ~50% of frame time was spent
in `EditorElement::prepaint` → `LineWithInvisibles::from_chunks`,
processing thousands of invisible lines.

## Solution

Calculate the intersection of the editor's bounds with the current
content mask (which represents the visible viewport after all parent
clipping). Use this to determine:
1. `clipped_top_in_lines` - how many lines are clipped above the
viewport
2. `visible_height_in_lines` - how many lines are actually visible

Then adjust `start_row` and `end_row` to only include visible lines. The
parent container handles positioning, so `scroll_position` remains
unchanged for paint calculations.

## Example

For a 3000-line editor where only 50 lines are visible:
- **Before**: Lay out and render 3000 lines
- **After**: Lay out and render ~50 lines

## Testing

Verified the following scenarios work correctly:
- Editor fully visible (no clipping)
- Editor clipped from top
- Editor clipped from bottom
- Editor completely outside viewport (renders nothing)
- Fractional line clipping at boundaries
- Scrollable editors with internal scroll state inside a clipped
container

Release Notes:

- Improved agent panel performance when rendering large diffs.

Change summary

crates/editor/src/element.rs | 17 +++++++++++++++--
crates/editor/src/test.rs    |  8 +++-----
2 files changed, 18 insertions(+), 7 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -9164,6 +9164,15 @@ impl Element for EditorElement {
                     let height_in_lines = f64::from(bounds.size.height / line_height);
                     let max_row = snapshot.max_point().row().as_f64();
 
+                    // Calculate how much of the editor is clipped by parent containers (e.g., List).
+                    // This allows us to only render lines that are actually visible, which is
+                    // critical for performance when large AutoHeight editors are inside Lists.
+                    let visible_bounds = window.content_mask().bounds;
+                    let clipped_top = (visible_bounds.origin.y - bounds.origin.y).max(px(0.));
+                    let clipped_top_in_lines = f64::from(clipped_top / line_height);
+                    let visible_height_in_lines =
+                        f64::from(visible_bounds.size.height / line_height);
+
                     // The max scroll position for the top of the window
                     let max_scroll_top = if matches!(
                         snapshot.mode,
@@ -9220,10 +9229,14 @@ impl Element for EditorElement {
                     let mut scroll_position = snapshot.scroll_position();
                     // The scroll position is a fractional point, the whole number of which represents
                     // the top of the window in terms of display rows.
-                    let start_row = DisplayRow(scroll_position.y as u32);
+                    // We add clipped_top_in_lines to skip rows that are clipped by parent containers,
+                    // but we don't modify scroll_position itself since the parent handles positioning.
+                    let start_row =
+                        DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32);
                     let max_row = snapshot.max_point().row();
                     let end_row = cmp::min(
-                        (scroll_position.y + height_in_lines).ceil() as u32,
+                        (scroll_position.y + clipped_top_in_lines + visible_height_in_lines).ceil()
+                            as u32,
                         max_row.next_row().0,
                     );
                     let end_row = DisplayRow(end_row);

crates/editor/src/test.rs 🔗

@@ -176,11 +176,9 @@ pub fn block_content_for_tests(
 }
 
 pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> String {
-    cx.draw(
-        gpui::Point::default(),
-        size(px(3000.0), px(3000.0)),
-        |_, _| editor.clone(),
-    );
+    let draw_size = size(px(3000.0), px(3000.0));
+    cx.simulate_resize(draw_size);
+    cx.draw(gpui::Point::default(), draw_size, |_, _| editor.clone());
     let (snapshot, mut lines, blocks) = editor.update_in(cx, |editor, window, cx| {
         let snapshot = editor.snapshot(window, cx);
         let text = editor.display_text(cx);