@@ -29530,3 +29530,38 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) {
trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
assert!(can_trust_after, "worktree should be trusted after trust()");
}
+
+#[gpui::test]
+fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
+ // This test reproduces a bug where drawing an editor at a position above the viewport
+ // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
+ // causes an infinite loop in blocks_in_range.
+ //
+ // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
+ // the content mask intersection produces visible_bounds with origin at the viewport top.
+ // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
+ // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
+ // but the while loop after seek never terminates because cursor.next() is a no-op at end.
+ init_test(cx, |_| {});
+
+ let window = cx.add_window(|_, _| gpui::Empty);
+ let mut cx = VisualTestContext::from_window(*window, cx);
+
+ let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
+ let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
+
+ // Simulate a small viewport (500x500 pixels at origin 0,0)
+ cx.simulate_resize(gpui::size(px(500.), px(500.)));
+
+ // Draw the editor at a very negative Y position, simulating an editor that's been
+ // scrolled way above the visible viewport (like in a List that has scrolled past it).
+ // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
+ // This should NOT hang - it should just render nothing.
+ cx.draw(
+ gpui::point(px(0.), px(-10000.)),
+ gpui::size(px(500.), px(3000.)),
+ |_, _| editor.clone(),
+ );
+
+ // If we get here without hanging, the test passes
+}
@@ -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,16 @@ 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 max_row = snapshot.max_point().row();
+ let start_row = cmp::min(
+ DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32),
+ max_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);
@@ -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);