Skip to content

fix: clamp inverted visible range in ListView to prevent RangeError (fixes #333230) #333234

Description

@vs-code-engineering

Summary

RangeError: Invalid array length is thrown in ListView.probeDynamicHeights (src/vs/base/browser/ui/list/listView.ts) when it evaluates new Array<number>(range.end - range.start) with an inverted range (end < start). The negative length throws immediately.

The inverted range originates in getVisibleRange(renderTop, renderHeight). When the viewport is collapsed or hidden during a layout pass (renderHeight <= 0), the expression renderTop + renderHeight - 1 becomes smaller than renderTop. Because rangeMap.indexAfter(position) = min(indexAt(position) + 1, count) is monotonic in position, the resulting end can resolve to an index before start, yielding { start, end } with end < start. This malformed IRange violates the start <= end invariant that consumers assume.

This is a recent regression (new bucket in 1.135.0, 411 users): the previous _rerender iterated with for (let i = range.start; i < range.end; i++), which silently tolerated an inverted range by not iterating. The batched-measurement rewrite replaced that loop with an eagerly-sized array, turning the previously-harmless inverted range into a hard crash.

Fixes #333230
Recommended reviewer: @connor4312

Culprit Commit

c2b336daae7101676f179b30f92641cdfcc6b38c — "list: batch dynamic height measurements (#330967)" by Connor Peet (connor4312), 2026-08-18. This commit falls within the reported regression range (0d0c8a6...68161d9, 1.134.0-insider → 1.135.0-insider) and introduced probeDynamicHeights, which allocates new Array<number>(range.end - range.start) without guaranteeing end >= start.

Code Flow

flowchart TD
    A[layout / setScrollDimensions with renderHeight <= 0] --> B[onScroll]
    B --> C[_rerender]
    C --> D["getVisibleRange(renderTop, renderHeight)"]
    D --> E["end = indexAfter(renderTop + renderHeight - 1)<br/>can be < start when renderHeight &le; 0"]
    E --> F["inverted IRange: end < start"]
    F --> G["probeDynamicHeights(range)"]
    G --> H["new Array(range.end - range.start)<br/>negative length"]
    H --> I["RangeError: Invalid array length"]
Loading

Affected Files

  • src/vs/base/browser/ui/list/listView.tsgetVisibleRange (producer of the inverted range) and probeDynamicHeights (crash site).

Repro Steps

Not reliably reproducible via manual steps; occurs under a layout race. Conceptually:

  1. Host a ListView inside a widget whose container can be laid out with zero/negative height (e.g., a collapsed chat/inline-chat widget).
  2. Trigger a layout/scroll pass while renderHeight <= 0.
  3. getVisibleRange returns an inverted range; probeDynamicHeights allocates a negative-length array and throws RangeError: Invalid array length.

How the Fix Works

Chosen approachsrc/vs/base/browser/ui/list/listView.ts, getVisibleRange: compute start first, then clamp end with Math.max(start, indexAfter(renderTop + renderHeight - 1)). This fixes the bug at the data producer rather than at the crash site: getVisibleRange constructs the IRange, so enforcing the start <= end invariant there guarantees every consumer (including probeDynamicHeights) receives a well-formed range. An empty range (start === end) is valid and yields a zero-length array, restoring the harmless behavior the previous for-loop had.

After this change, getVisibleRange cannot produce a range with end < start because end is explicitly clamped to be at least start, so range.end - range.start is always >= 0 and new Array(...) can no longer receive a negative length.

Alternatives considered:

  • Guard at the crash site in probeDynamicHeights (e.g., early-return or clamp the array length there) — rejected because it patches the symptom at the bottom of the stack while leaving the malformed IRange flowing to every other consumer; the fix must live where the invalid data is produced.

Recommended Owner

connor4312 (Connor Peet) — authored the culprit commit c2b336daae71 and is an active repository collaborator with recent commits.

Generated by errors-fix · opus48 · 452.1 AIC · ⌖ 11.3 AIC · ⊞ 18.6K ·


Note

This was originally intended as a pull request, but PR creation failed. The changes have been pushed to the branch fix/listview-invalid-array-length-333230-c6490afebd6733ec.

Original error: ERR_API: [2026-08-28T19:30:03.784Z] create pull request in microsoft/vscode failed (attempt 1)

Original error: Validation Failed: {"resource":"PullRequest","code":"custom","field":"fork_collab","message":"fork_collab Fork collab can't be granted by someone without permission"} - https://docs.github.com/rest/pulls/pulls#create-a-pull-request
Retryable: false
Suggestion: This error cannot be resolved by retrying. Please check the error details and fix the underlying issue.

To create the pull request manually:

gh pr create --title "fix: clamp inverted visible range in ListView to prevent RangeError (fixes #333230)" --base main --head vscodebot-pr:fix/listview-invalid-array-length-333230-c6490afebd6733ec --repo microsoft/vscode
Show patch (36 lines)
From 831642c2af7cf873ca037b26bebd3cdaed4e65f1 Mon Sep 17 00:00:00 2001
X-GH-AW-Base-Commit: 8853be931dfb182c2497ecd99768a9442e4b7019
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Fri, 28 Aug 2026 19:22:48 +0000
Subject: [PATCH] fix: clamp inverted visible range in ListView to prevent
 RangeError (fixes #333230)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 src/vs/base/browser/ui/list/listView.ts | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts
index aee77ac1234..bd4b69eb17f 100644
--- a/src/vs/base/browser/ui/list/listView.ts
+++ b/src/vs/base/browser/ui/list/listView.ts
@@ -1560,9 +1560,13 @@ export class ListView<T> implements IListView<T> {
 	}
 
 	private getVisibleRange(renderTop: number, renderHeight: number): IRange {
+		const start = this.rangeMap.indexAt(renderTop);
 		return {
-			start: this.rangeMap.indexAt(renderTop),
-			end: this.rangeMap.indexAfter(renderTop + renderHeight - 1)
+			start,
+			// When the viewport is collapsed or hidden (renderHeight <= 0), the end
+			// position can resolve to an index before `start`, producing an inverted
+			// range. Clamp it so consumers always receive `end >= start`.
+			end: Math.max(start, this.rangeMap.indexAfter(renderTop + renderHeight - 1))
 		};
 	}
 
-- 
2.54.0

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions