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:
@@ -354,17 +354,30 @@ void main() {
|
||||
expect(out[0].getCodePoint(3), 0);
|
||||
});
|
||||
|
||||
test('anchors on the source line tail (past trimmedLength) get reparented', () {
|
||||
// The post-loop branch in `_addPart` reparents anchors whose x sits
|
||||
// beyond the trimmed-content range — the empty tail of an over-wide
|
||||
// source line. The anchor moves off the source onto whichever
|
||||
// builder line was active at the time, regardless of whether that
|
||||
// builder line ends up emitted — so the only contract we can rely
|
||||
// on is that the anchor no longer points to the source.
|
||||
test('anchors on the source line tail (past trimmedLength) land on a line in the result (T-92)', () {
|
||||
// The post-loop tail-anchor branch in `_addPart` reparents the
|
||||
// anchor onto the active builder line. The fix in T-92 makes
|
||||
// `finish()` emit that builder line when it has anchors, even if
|
||||
// it's otherwise empty — so the anchor's `line` is guaranteed to
|
||||
// be in the reflow result and stays attached after replaceWith.
|
||||
final src = line(12, 'abcdefgh'); // width 12, content 8
|
||||
final tail = src.createAnchor(10); // past trimmedLength (8)
|
||||
reflow(ring([src]), 12, 4);
|
||||
expect(tail.line, isNot(equals(src)));
|
||||
final out = reflow(ring([src]), 12, 4);
|
||||
expect(tail.line, isNotNull);
|
||||
expect(out.contains(tail.line), isTrue);
|
||||
});
|
||||
|
||||
test('SelectAllTextIntent-shaped end anchor survives shrink (T-92 regression)', () {
|
||||
// Models the actual bug path: SelectAllTextIntent (actions.dart)
|
||||
// creates an end anchor at x = viewWidth on the last buffer line.
|
||||
// Resizing narrower must not silently drop the selection.
|
||||
final src = line(12, 'short');
|
||||
final endAnchor = src.createAnchor(12); // viewWidth, past trimmedLength
|
||||
final out = reflow(ring([src]), 12, 4);
|
||||
expect(out.contains(endAnchor.line), isTrue,
|
||||
reason: 'end anchor must land on a line that survives '
|
||||
'lines.replaceWith — otherwise selection.attached is false '
|
||||
'and the selection silently vanishes after resize.');
|
||||
});
|
||||
|
||||
test('inner wide-char clamp during _addPart leaves the wide cell to the next iteration', () {
|
||||
|
||||
Reference in New Issue
Block a user