fix dangling tail anchors in reflow on partially-filled lines (T-92)
test / unit + widget + golden + a11y (push) Failing after 29s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m0s
test / unit + widget + golden + a11y (push) Failing after 29s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m0s
`_LineReflow._addPart`'s post-loop block reparents anchors past the
source line's trimmed content onto whatever `_builder._result` was
active at that moment. When no further content lands in the builder
(non-wrapped lines, or the last logical line of a wrapped run),
`finish()` was emitting only when `_builder.isNotEmpty` — leaving
the empty result line with the reparented anchor unappended. The
anchor then pointed to a `BufferLine` that the reflow output never
included, `lines.replaceWith(reflowResult)` discarded it, and
`CellAnchor.attached` returned false. The selection controller's
`extent.attached` null-check then dropped the selection silently
on resize.
The fix adds a `_LineBuilder.hasAnchors` getter and uses it in
`finish()` so the builder line is also emitted when it's carrying
an anchor — even when otherwise empty. Trade-off: an extra trailing
line in the reflow output when (and only when) a tail anchor would
have dangled. `Buffer.resize` already pads the result to `newHeight`
afterward, so for the common case (resize fits inside view height)
the total ring length is unchanged; only when the result already
meets / exceeds `newHeight` does the buffer grow by one. Acceptable
in exchange for selections surviving a width change.
User-visible trigger paths:
- `SelectAllTextIntent` (Ctrl+A) creates an end anchor at
`x = viewWidth` on the last buffer line — exactly the past-
trimmed-length position. Resizing narrower while the selection
was active dropped it.
- Mouse drag selections past the end of a partially-filled line
hit the same shape.
Tests:
- The pre-existing `reflow anchors on the source line tail (past
trimmedLength) get reparented` test was originally written to
document the buggy behaviour ("anchor moves off the source onto
a dangling builder line"). Updated to assert the post-fix
contract: `out.contains(tail.line)` is true.
- New `SelectAllTextIntent-shaped end anchor survives shrink`
regression test that mirrors the actual production trigger
(anchor at `x = viewWidth` on a partially-filled line, narrower
reflow).
reflow.dart 71/71 → 72/72 (the new getter is a one-liner). Project
coverage 54.62% unchanged within rounding.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,15 @@ class _LineBuilder {
|
||||
|
||||
bool get isNotEmpty => _length != 0;
|
||||
|
||||
/// True when the result line carries at least one CellAnchor. Used by
|
||||
/// [_LineReflow.finish] to flush an otherwise-empty trailing line when
|
||||
/// the tail-anchor branch in `_addPart` reparented an anchor onto it
|
||||
/// — without this guard the anchor would point to a `BufferLine` that
|
||||
/// reflow never emits, the selection controller's `extent.attached`
|
||||
/// check would fail, and the selection would silently vanish on resize
|
||||
/// (T-92).
|
||||
bool get hasAnchors => _result.anchors.isNotEmpty;
|
||||
|
||||
/// Adds a range of cells from [src] to the builder. Anchors within the range
|
||||
/// will be reparented to the new line returned by [take].
|
||||
void add(BufferLine src, int start, int length) {
|
||||
@@ -152,7 +161,11 @@ class _LineReflow {
|
||||
|
||||
/// Finalizes the reflow operation and returns the result.
|
||||
List<BufferLine> finish() {
|
||||
if (_builder.isNotEmpty) {
|
||||
// Emit the trailing builder line if it has content OR if it's
|
||||
// carrying an anchor. The latter keeps tail-anchored selections
|
||||
// (e.g. SelectAllTextIntent's end anchor at x=viewWidth) attached
|
||||
// to a line that's actually in the reflow output (T-92).
|
||||
if (_builder.isNotEmpty || _builder.hasAnchors) {
|
||||
_lines.add(_builder.take(wrapped: _lines.isNotEmpty));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user