From 8a60933dd7de15dcddd7790f0c16818dc53e1177 Mon Sep 17 00:00:00 2001 From: Aswin Date: Fri, 8 May 2026 10:32:15 +0530 Subject: [PATCH] fix(circular_buffer): narrow trimStart patch to _absoluteStartIndex advancement only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original patch's _dropChild loop nulled BufferLine._owner and BufferLine._absoluteIndex on trimmed entries. CellAnchors holding direct references to those entries — whose .y/.offset getters guard _owner!.index access with assert(attached), stripped in release mode — then null-deref'd on the next access from xterm's escape parser (eraseLineFromCursor → currentLine → ... → CellAnchor.y → _absoluteIndex! on null → _TypeError). Symptom: app shows "Unexpected Error: _TypeError" dialog on every shell prompt redraw following any clearScrollback in release builds. Pre-patch behaviour was silent index corruption (rendered off-screen, no crash); the broader patch traded silent for loud. Narrowing to advance _absoluteStartIndex only preserves the original selection-render and copy fixes (those rely on surviving entries' index, which the narrowed patch handles correctly) without exposing the pre-existing CellAnchor release-mode invariant violation. Orphan-anchor cleanup is now a non-goal of trimStart and should be handled by callers (Buffer.clearScrollback, or the host application) at their own layer. --- lib/src/utils/circular_buffer.dart | 36 ++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/src/utils/circular_buffer.dart b/lib/src/utils/circular_buffer.dart index f74fb436..492c0852 100644 --- a/lib/src/utils/circular_buffer.dart +++ b/lib/src/utils/circular_buffer.dart @@ -217,15 +217,43 @@ class IndexAwareCircularBuffer { } } - /// Removes [count] elements starting at [index], shifting all elements after - /// [index] to the left. + /// Removes [count] elements from the start of the list, shifting all + /// remaining elements to the left. + /// + /// `_absoluteStartIndex` is advanced by `count` so surviving entries' + /// [IndexedItem.index] (computed as + /// `_absoluteIndex - _absoluteStartIndex`) stays consistent with their + /// new logical position. This matches the invariant that + /// capacity-overflow eviction in [push] / [insert] already maintains. + /// Without it, after a trim every surviving entry reports `index` + /// values offset by `count`, breaking consumers that read back + /// `selection.begin.offset.y` for rendering and copy after + /// [Buffer.clearScrollback]. + /// + /// This method **does not detach the trimmed entries**. Doing so would + /// null-deref any external [CellAnchor] still holding a direct + /// reference to a trimmed line: [CellAnchor.y] and [CellAnchor.offset] + /// guard their `_owner!.index` access only with `assert(attached)`, + /// which is stripped in release mode, so a `_detach()`'d trimmed line + /// observed through a stale anchor reaches `_absoluteIndex! - …` and + /// throws `_TypeError`. An earlier revision of this patch added that + /// `_dropChild` loop and immediately produced `_TypeError` dialogs on + /// every shell prompt redraw following any `\e[3J` in release builds. /// - /// This method is cheap since it does not actually modify the list, but - /// instead just adjusts the start index and length. + /// Orphan anchors on trimmed entries therefore retain + /// `attached == true` and their [CellAnchor.y] returns + /// increasingly-negative values as the buffer continues to advance. + /// Renderers that compute `index * cellHeight` will place these + /// anchors off-screen — visually equivalent to a cleared selection. + /// Callers that need to explicitly clear outstanding selections before + /// trimming should do so at their own layer (e.g. + /// [TerminalController.clearSelection] from + /// [Buffer.clearScrollback]'s caller surface in the host application). void trimStart(int count) { if (count > _length) count = _length; _startIndex += count; _startIndex %= _array.length; + _absoluteStartIndex += count; _length -= count; }