Fix editor hang when positioned above viewport#45077
Merged
Conversation
When an editor is positioned above the visible viewport (e.g., an AutoHeight editor inside a List that has been scrolled past), the clipping calculation produces a very large start_row that exceeds max_row. This caused two issues: 1. In debug mode: subtraction overflow panic in Range::len() 2. In release mode: integer wraparound leading to infinite loop in blocks_in_range The fix clamps start_row to max_row before using it, ensuring the row range is always valid. Added a regression test that draws an editor at a very negative Y position to simulate the scrolled-past scenario.
Contributor
The editor optimization from #45077 uses window.content_mask().bounds to determine visible rows. In tests, the content mask defaults to the window's viewport size (1920x1043 from TestDisplay), not the draw size passed to cx.draw(). Fix by calling cx.simulate_resize() before drawing to ensure the window's viewport matches the intended draw size (3000x3000). This fix was originally part of #44995 but was lost when that PR was reverted in #45011.
HactarCE
pushed a commit
that referenced
this pull request
Dec 17, 2025
Fixes the hang introduced in #44995 (which was reverted in #45011) and re-enables the optimization. ## Background PR #44995 introduced an optimization to skip rendering lines that are clipped by parent containers (e.g., when a large AutoHeight editor is inside a scrollable List). This significantly improved performance for large diffs in the Agent Panel. However, #45011 reverted this change because it caused the main thread to hang for 100+ seconds in certain scenarios, requiring a force quit to recover. ## Root Cause The original analysis in #45011 suggested that visible_bounds wasn’t being intersected properly, but that was incorrect—the intersection via with_content_mask works correctly. The actual bug: when an editor is positioned above the visible viewport (e.g., scrolled past in a List), the clipping calculation produces a start_row that exceeds max_row: 1. Editor’s bounds.origin.y becomes very negative (e.g., -10000px) 2. After intersection, visible_bounds.origin.y is at the viewport top (e.g., 0) 3. clipped_top_in_lines = (0 - (-10000)) / line_height = huge number 4. start_row = huge number, but end_row is clamped to max_row 5. This creates an invalid range where start_row > end_row This caused two different failures depending on build mode: - Debug mode: Panic from subtraction overflow in Range<DisplayRow>::len() - Release mode: Integer wraparound causing blocks_in_range to enter an infinite loop (the 100+ second hang) ## Fix Simply clamp start_row to max_row, ensuring the row range is always valid: ```rs let start_row = cmp::min( DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32), max_row, ); ``` ## Testing Added a regression test that draws an editor at y=-10000 to simulate an editor that’s been scrolled past in a List. This would panic in debug mode (and hang in release mode) before the fix. Release Notes: - Improved agent panel performance when rendering large diffs.
nathansobo
added a commit
that referenced
this pull request
Dec 22, 2025
This brings the terminal element's viewport culling in line with the editor optimization in PR #44995 and the fix in PR #45077. ## Problem When a terminal is inside a scrollable container (e.g., the Agent Panel thread view), it would render ALL cells during prepaint, even when the terminal was entirely outside the viewport. This caused unnecessary CPU usage when multiple terminal tool outputs existed in the Agent Panel. ## Solution Calculate the intersection of the terminal's bounds with the current content_mask (the visible viewport after all parent clipping). If the intersection has zero area, skip all cell processing entirely. ### Important distinction between content modes: - **ContentMode::Scrollable**: Cells use the terminal's internal coordinate system with negative line numbers for scrollback history. We cannot filter cells by screen-space row numbers, so we render all cells when visible. The early-exit for zero intersection handles the offscreen case. - **ContentMode::Inline**: Cells use 0-based line numbers (no scrollback). We can filter cells to only those in the visible row range, using the same logic that existed before but now extracted into a helper function with proper bounds clamping to prevent the hang bug from PR #45077. ## Testing Added comprehensive unit tests for: - compute_visible_row_range edge cases - Cell filtering logic for Inline mode - Line/i32 comparison behavior Manually verified: - Terminal fully visible (no clipping) - Terminal clipped from top/bottom - Terminal completely outside viewport (renders nothing) - Scrollable terminals with scrollback history work correctly Release Notes: - Improved Agent Panel performance when terminals are scrolled offscreen.
nathansobo
added a commit
that referenced
this pull request
Dec 23, 2025
This brings the terminal element's viewport culling in line with the editor optimization in PR #44995 and the fix in PR #45077. ## Problem When a terminal is inside a scrollable container (e.g., the Agent Panel thread view), it would render ALL cells during prepaint, even when the terminal was entirely outside the viewport. This caused unnecessary CPU usage when multiple terminal tool outputs existed in the Agent Panel. ## Solution Calculate the intersection of the terminal's bounds with the current content_mask (the visible viewport after all parent clipping). If the intersection has zero area, skip all cell processing entirely. ### Important distinction between content modes: - **ContentMode::Scrollable**: Cells use the terminal's internal coordinate system with negative line numbers for scrollback history. We cannot filter cells by screen-space row numbers, so we render all cells when visible. The early-exit for zero intersection handles the offscreen case. - **ContentMode::Inline**: Cells use 0-based line numbers (no scrollback). We can filter cells to only those in the visible row range, using the same logic that existed before but now extracted into a helper function with proper bounds clamping to prevent the hang bug from PR #45077. ## Testing Added comprehensive unit tests for: - compute_visible_row_range edge cases - Cell filtering logic for Inline mode - Line/i32 comparison behavior Manually verified: - Terminal fully visible (no clipping) - Terminal clipped from top/bottom - Terminal completely outside viewport (renders nothing) - Scrollable terminals with scrollback history work correctly Release Notes: - Improved Agent Panel performance when terminals are scrolled offscreen.
nathansobo
added a commit
that referenced
this pull request
Dec 26, 2025
This brings the terminal element's viewport culling in line with the editor optimization in PR #44995 and the fix in PR #45077. ## Problem When a terminal is inside a scrollable container (e.g., the Agent Panel thread view), it would render ALL cells during prepaint, even when the terminal was entirely outside the viewport. This caused unnecessary CPU usage when multiple terminal tool outputs existed in the Agent Panel. ## Solution Calculate the intersection of the terminal's bounds with the current content_mask (the visible viewport after all parent clipping). If the intersection has zero area, skip all cell processing entirely. ### Three code paths 1. **Offscreen** (`intersection.size <= 0`): Early exit, process 0 cells 2. **Fully visible** (`intersection == bounds`): Fast path, stream cells directly (no allocation) 3. **Partially clipped**: Group cells by line, skip/take visible rows only ### Key insight: filter by screen position, not buffer coordinates The previous approach tried to filter cells by `cell.point.line` (terminal buffer coordinates), which breaks in Scrollable mode where cells can have negative line numbers for scrollback history. The new approach filters by **screen position** using `chunk_by(line).skip(N).take(M)`, which works regardless of the actual line numbers because we're filtering on enumerated line group index. ## Testing Added comprehensive unit tests for: - Screen-position filtering with positive lines (Inline mode) - Screen-position filtering with negative lines (Scrollable mode with scrollback) - Edge cases (skip all, positioning math) - Unified filtering works for both modes Manually verified: - Terminal fully visible (no clipping) ✓ - Terminal clipped from top/bottom ✓ - Terminal completely outside viewport ✓ - Scrollable terminals with scrollback history ✓ - Selection/interaction still works ✓ Release Notes: - Improved Agent Panel performance when terminals are scrolled offscreen. /cc @as-cii
CherryWorm
pushed a commit
to CherryWorm/zed
that referenced
this pull request
Dec 30, 2025
…dustries#45537) This brings the terminal element's viewport culling in line with the editor optimization in PR zed-industries#44995 and the fix in PR zed-industries#45077. ## Problem When a terminal is inside a scrollable container (e.g., the Agent Panel thread view), it would render ALL cells during prepaint, even when the terminal was entirely outside the viewport. This caused unnecessary CPU usage when multiple terminal tool outputs existed in the Agent Panel. ## Solution Calculate the intersection of the terminal's bounds with the current content_mask (the visible viewport after all parent clipping). If the intersection has zero area, skip all cell processing entirely. ### Three code paths 1. **Offscreen** (`intersection.size <= 0`): Early exit, process 0 cells 2. **Fully visible** (`intersection == bounds`): Fast path, stream cells directly (no allocation) 3. **Partially clipped**: Group cells by line, skip/take visible rows only ### Key insight: filter by screen position, not buffer coordinates The previous approach tried to filter cells by `cell.point.line` (terminal buffer coordinates), which breaks in Scrollable mode where cells can have negative line numbers for scrollback history. The new approach filters by **screen position** using `chunk_by(line).skip(N).take(M)`, which works regardless of the actual line numbers because we're filtering on enumerated line group index. ## Testing Added comprehensive unit tests for: - Screen-position filtering with positive lines (Inline mode) - Screen-position filtering with negative lines (Scrollable mode with scrollback) - Edge cases (skip all, positioning math) - Unified filtering works for both modes Manually verified: - Terminal fully visible (no clipping) ✓ - Terminal clipped from top/bottom ✓ - Terminal completely outside viewport ✓ - Scrollable terminals with scrollback history ✓ - Selection/interaction still works ✓ Release Notes: - Improved Agent Panel performance when terminals are scrolled offscreen. /cc @as-cii
rtfeldman
pushed a commit
that referenced
this pull request
Jan 5, 2026
This brings the terminal element's viewport culling in line with the editor optimization in PR #44995 and the fix in PR #45077. ## Problem When a terminal is inside a scrollable container (e.g., the Agent Panel thread view), it would render ALL cells during prepaint, even when the terminal was entirely outside the viewport. This caused unnecessary CPU usage when multiple terminal tool outputs existed in the Agent Panel. ## Solution Calculate the intersection of the terminal's bounds with the current content_mask (the visible viewport after all parent clipping). If the intersection has zero area, skip all cell processing entirely. ### Three code paths 1. **Offscreen** (`intersection.size <= 0`): Early exit, process 0 cells 2. **Fully visible** (`intersection == bounds`): Fast path, stream cells directly (no allocation) 3. **Partially clipped**: Group cells by line, skip/take visible rows only ### Key insight: filter by screen position, not buffer coordinates The previous approach tried to filter cells by `cell.point.line` (terminal buffer coordinates), which breaks in Scrollable mode where cells can have negative line numbers for scrollback history. The new approach filters by **screen position** using `chunk_by(line).skip(N).take(M)`, which works regardless of the actual line numbers because we're filtering on enumerated line group index. ## Testing Added comprehensive unit tests for: - Screen-position filtering with positive lines (Inline mode) - Screen-position filtering with negative lines (Scrollable mode with scrollback) - Edge cases (skip all, positioning math) - Unified filtering works for both modes Manually verified: - Terminal fully visible (no clipping) ✓ - Terminal clipped from top/bottom ✓ - Terminal completely outside viewport ✓ - Scrollable terminals with scrollback history ✓ - Selection/interaction still works ✓ Release Notes: - Improved Agent Panel performance when terminals are scrolled offscreen. /cc @as-cii
LivioGama
pushed a commit
to LivioGama/zed
that referenced
this pull request
Jan 20, 2026
Fixes the hang introduced in zed-industries#44995 (which was reverted in zed-industries#45011) and re-enables the optimization. ## Background PR zed-industries#44995 introduced an optimization to skip rendering lines that are clipped by parent containers (e.g., when a large AutoHeight editor is inside a scrollable List). This significantly improved performance for large diffs in the Agent Panel. However, zed-industries#45011 reverted this change because it caused the main thread to hang for 100+ seconds in certain scenarios, requiring a force quit to recover. ## Root Cause The original analysis in zed-industries#45011 suggested that visible_bounds wasn’t being intersected properly, but that was incorrect—the intersection via with_content_mask works correctly. The actual bug: when an editor is positioned above the visible viewport (e.g., scrolled past in a List), the clipping calculation produces a start_row that exceeds max_row: 1. Editor’s bounds.origin.y becomes very negative (e.g., -10000px) 2. After intersection, visible_bounds.origin.y is at the viewport top (e.g., 0) 3. clipped_top_in_lines = (0 - (-10000)) / line_height = huge number 4. start_row = huge number, but end_row is clamped to max_row 5. This creates an invalid range where start_row > end_row This caused two different failures depending on build mode: - Debug mode: Panic from subtraction overflow in Range<DisplayRow>::len() - Release mode: Integer wraparound causing blocks_in_range to enter an infinite loop (the 100+ second hang) ## Fix Simply clamp start_row to max_row, ensuring the row range is always valid: ```rs let start_row = cmp::min( DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32), max_row, ); ``` ## Testing Added a regression test that draws an editor at y=-10000 to simulate an editor that’s been scrolled past in a List. This would panic in debug mode (and hang in release mode) before the fix. Release Notes: - Improved agent panel performance when rendering large diffs.
LivioGama
pushed a commit
to LivioGama/zed
that referenced
this pull request
Jan 20, 2026
…dustries#45537) This brings the terminal element's viewport culling in line with the editor optimization in PR zed-industries#44995 and the fix in PR zed-industries#45077. ## Problem When a terminal is inside a scrollable container (e.g., the Agent Panel thread view), it would render ALL cells during prepaint, even when the terminal was entirely outside the viewport. This caused unnecessary CPU usage when multiple terminal tool outputs existed in the Agent Panel. ## Solution Calculate the intersection of the terminal's bounds with the current content_mask (the visible viewport after all parent clipping). If the intersection has zero area, skip all cell processing entirely. ### Three code paths 1. **Offscreen** (`intersection.size <= 0`): Early exit, process 0 cells 2. **Fully visible** (`intersection == bounds`): Fast path, stream cells directly (no allocation) 3. **Partially clipped**: Group cells by line, skip/take visible rows only ### Key insight: filter by screen position, not buffer coordinates The previous approach tried to filter cells by `cell.point.line` (terminal buffer coordinates), which breaks in Scrollable mode where cells can have negative line numbers for scrollback history. The new approach filters by **screen position** using `chunk_by(line).skip(N).take(M)`, which works regardless of the actual line numbers because we're filtering on enumerated line group index. ## Testing Added comprehensive unit tests for: - Screen-position filtering with positive lines (Inline mode) - Screen-position filtering with negative lines (Scrollable mode with scrollback) - Edge cases (skip all, positioning math) - Unified filtering works for both modes Manually verified: - Terminal fully visible (no clipping) ✓ - Terminal clipped from top/bottom ✓ - Terminal completely outside viewport ✓ - Scrollable terminals with scrollback history ✓ - Selection/interaction still works ✓ Release Notes: - Improved Agent Panel performance when terminals are scrolled offscreen. /cc @as-cii
LivioGama
pushed a commit
to LivioGama/zed
that referenced
this pull request
Jan 20, 2026
Fixes the hang introduced in zed-industries#44995 (which was reverted in zed-industries#45011) and re-enables the optimization. ## Background PR zed-industries#44995 introduced an optimization to skip rendering lines that are clipped by parent containers (e.g., when a large AutoHeight editor is inside a scrollable List). This significantly improved performance for large diffs in the Agent Panel. However, zed-industries#45011 reverted this change because it caused the main thread to hang for 100+ seconds in certain scenarios, requiring a force quit to recover. ## Root Cause The original analysis in zed-industries#45011 suggested that visible_bounds wasn’t being intersected properly, but that was incorrect—the intersection via with_content_mask works correctly. The actual bug: when an editor is positioned above the visible viewport (e.g., scrolled past in a List), the clipping calculation produces a start_row that exceeds max_row: 1. Editor’s bounds.origin.y becomes very negative (e.g., -10000px) 2. After intersection, visible_bounds.origin.y is at the viewport top (e.g., 0) 3. clipped_top_in_lines = (0 - (-10000)) / line_height = huge number 4. start_row = huge number, but end_row is clamped to max_row 5. This creates an invalid range where start_row > end_row This caused two different failures depending on build mode: - Debug mode: Panic from subtraction overflow in Range<DisplayRow>::len() - Release mode: Integer wraparound causing blocks_in_range to enter an infinite loop (the 100+ second hang) ## Fix Simply clamp start_row to max_row, ensuring the row range is always valid: ```rs let start_row = cmp::min( DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32), max_row, ); ``` ## Testing Added a regression test that draws an editor at y=-10000 to simulate an editor that’s been scrolled past in a List. This would panic in debug mode (and hang in release mode) before the fix. Release Notes: - Improved agent panel performance when rendering large diffs.
LivioGama
pushed a commit
to LivioGama/zed
that referenced
this pull request
Jan 20, 2026
…dustries#45537) This brings the terminal element's viewport culling in line with the editor optimization in PR zed-industries#44995 and the fix in PR zed-industries#45077. ## Problem When a terminal is inside a scrollable container (e.g., the Agent Panel thread view), it would render ALL cells during prepaint, even when the terminal was entirely outside the viewport. This caused unnecessary CPU usage when multiple terminal tool outputs existed in the Agent Panel. ## Solution Calculate the intersection of the terminal's bounds with the current content_mask (the visible viewport after all parent clipping). If the intersection has zero area, skip all cell processing entirely. ### Three code paths 1. **Offscreen** (`intersection.size <= 0`): Early exit, process 0 cells 2. **Fully visible** (`intersection == bounds`): Fast path, stream cells directly (no allocation) 3. **Partially clipped**: Group cells by line, skip/take visible rows only ### Key insight: filter by screen position, not buffer coordinates The previous approach tried to filter cells by `cell.point.line` (terminal buffer coordinates), which breaks in Scrollable mode where cells can have negative line numbers for scrollback history. The new approach filters by **screen position** using `chunk_by(line).skip(N).take(M)`, which works regardless of the actual line numbers because we're filtering on enumerated line group index. ## Testing Added comprehensive unit tests for: - Screen-position filtering with positive lines (Inline mode) - Screen-position filtering with negative lines (Scrollable mode with scrollback) - Edge cases (skip all, positioning math) - Unified filtering works for both modes Manually verified: - Terminal fully visible (no clipping) ✓ - Terminal clipped from top/bottom ✓ - Terminal completely outside viewport ✓ - Scrollable terminals with scrollback history ✓ - Selection/interaction still works ✓ Release Notes: - Improved Agent Panel performance when terminals are scrolled offscreen. /cc @as-cii
LivioGama
pushed a commit
to LivioGama/zed
that referenced
this pull request
Feb 15, 2026
Fixes the hang introduced in zed-industries#44995 (which was reverted in zed-industries#45011) and re-enables the optimization. ## Background PR zed-industries#44995 introduced an optimization to skip rendering lines that are clipped by parent containers (e.g., when a large AutoHeight editor is inside a scrollable List). This significantly improved performance for large diffs in the Agent Panel. However, zed-industries#45011 reverted this change because it caused the main thread to hang for 100+ seconds in certain scenarios, requiring a force quit to recover. ## Root Cause The original analysis in zed-industries#45011 suggested that visible_bounds wasn’t being intersected properly, but that was incorrect—the intersection via with_content_mask works correctly. The actual bug: when an editor is positioned above the visible viewport (e.g., scrolled past in a List), the clipping calculation produces a start_row that exceeds max_row: 1. Editor’s bounds.origin.y becomes very negative (e.g., -10000px) 2. After intersection, visible_bounds.origin.y is at the viewport top (e.g., 0) 3. clipped_top_in_lines = (0 - (-10000)) / line_height = huge number 4. start_row = huge number, but end_row is clamped to max_row 5. This creates an invalid range where start_row > end_row This caused two different failures depending on build mode: - Debug mode: Panic from subtraction overflow in Range<DisplayRow>::len() - Release mode: Integer wraparound causing blocks_in_range to enter an infinite loop (the 100+ second hang) ## Fix Simply clamp start_row to max_row, ensuring the row range is always valid: ```rs let start_row = cmp::min( DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32), max_row, ); ``` ## Testing Added a regression test that draws an editor at y=-10000 to simulate an editor that’s been scrolled past in a List. This would panic in debug mode (and hang in release mode) before the fix. Release Notes: - Improved agent panel performance when rendering large diffs.
LivioGama
pushed a commit
to LivioGama/zed
that referenced
this pull request
Feb 15, 2026
…dustries#45537) This brings the terminal element's viewport culling in line with the editor optimization in PR zed-industries#44995 and the fix in PR zed-industries#45077. ## Problem When a terminal is inside a scrollable container (e.g., the Agent Panel thread view), it would render ALL cells during prepaint, even when the terminal was entirely outside the viewport. This caused unnecessary CPU usage when multiple terminal tool outputs existed in the Agent Panel. ## Solution Calculate the intersection of the terminal's bounds with the current content_mask (the visible viewport after all parent clipping). If the intersection has zero area, skip all cell processing entirely. ### Three code paths 1. **Offscreen** (`intersection.size <= 0`): Early exit, process 0 cells 2. **Fully visible** (`intersection == bounds`): Fast path, stream cells directly (no allocation) 3. **Partially clipped**: Group cells by line, skip/take visible rows only ### Key insight: filter by screen position, not buffer coordinates The previous approach tried to filter cells by `cell.point.line` (terminal buffer coordinates), which breaks in Scrollable mode where cells can have negative line numbers for scrollback history. The new approach filters by **screen position** using `chunk_by(line).skip(N).take(M)`, which works regardless of the actual line numbers because we're filtering on enumerated line group index. ## Testing Added comprehensive unit tests for: - Screen-position filtering with positive lines (Inline mode) - Screen-position filtering with negative lines (Scrollable mode with scrollback) - Edge cases (skip all, positioning math) - Unified filtering works for both modes Manually verified: - Terminal fully visible (no clipping) ✓ - Terminal clipped from top/bottom ✓ - Terminal completely outside viewport ✓ - Scrollable terminals with scrollback history ✓ - Selection/interaction still works ✓ Release Notes: - Improved Agent Panel performance when terminals are scrolled offscreen. /cc @as-cii
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.
Fixes the hang introduced in #44995 (which was reverted in #45011) and re-enables the optimization.
Background
PR #44995 introduced an optimization to skip rendering lines that are clipped by parent containers (e.g., when a large AutoHeight editor is inside a scrollable List). This significantly improved performance for large diffs in the Agent Panel.
However, #45011 reverted this change because it caused the main thread to hang for 100+ seconds in certain scenarios, requiring a force quit to recover.
Root Cause
The original analysis in #45011 suggested that visible_bounds wasn’t being intersected properly, but that was incorrect—the intersection via with_content_mask works correctly. The actual bug: when an editor is positioned above the visible viewport (e.g., scrolled past in a List), the clipping calculation produces a start_row that exceeds max_row:
This caused two different failures depending on build mode:
Fix
Simply clamp start_row to max_row, ensuring the row range is always valid:
Testing
Added a regression test that draws an editor at y=-10000 to simulate an editor that’s been scrolled past in a List. This would panic in debug mode (and hang in release mode) before the fix.
Release Notes: