test sweep: cover Buffer (T-91)
Adds test/terminal/buffer/buffer_test.dart — 67 unit tests against the Buffer orchestrator on top of BufferLine. Drives a fake TerminalState through writes, cursor moves, scroll regions, erase commands, line insert/delete, resize (with and without reflow), word-boundary lookup, getText, and the toString debug dump. Coverage delta: - buffer.dart: 0 / 260 → 260 / 261 (one while-loop-body line Dart coverage doesn't instrument distinctly; the loop's effect is exercised end-to-end by the reflow-pad test). - Total project: 39.12% → 43.20%. - pubspec.yaml `coverage_floor:` bumped 39 → 43. Notes: - The fake TerminalState (`_State`) is a per-file impl rather than a shared fixture; it stays close to the test that exercises it and avoids forcing other terminal tests to depend on a one-shape- fits-all stub. - Tests that walk through `lineFeed` use `lineFeedMode: true` so the column resets between newlines — otherwise the saturated cursor X from a previous full-width write spills the next write onto an extra line via `writeChar`'s autoWrap branch. This closes the `core/buffer/` sub-area for T-91 — every leaf file in `lib/src/terminal/src/core/buffer/` is now at >= 96% line coverage; the only outliers are Dart-coverage-instrumentation quirks, not real gaps. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+107
-107
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"exported_at": "2026-05-06T21:03:21Z",
|
||||
"exported_at": "2026-05-06T21:13:42Z",
|
||||
"decisions": [
|
||||
{
|
||||
"id": "D-1",
|
||||
@@ -9,7 +9,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-3",
|
||||
@@ -19,7 +19,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-4",
|
||||
@@ -29,7 +29,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-5",
|
||||
@@ -39,7 +39,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-6",
|
||||
@@ -49,7 +49,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-7",
|
||||
@@ -59,7 +59,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-8",
|
||||
@@ -69,7 +69,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-9",
|
||||
@@ -79,7 +79,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-10",
|
||||
@@ -89,7 +89,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-11",
|
||||
@@ -99,7 +99,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-12",
|
||||
@@ -109,7 +109,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-13",
|
||||
@@ -119,7 +119,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-14",
|
||||
@@ -129,7 +129,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-15",
|
||||
@@ -139,7 +139,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-16",
|
||||
@@ -149,7 +149,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-17",
|
||||
@@ -159,7 +159,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-18",
|
||||
@@ -169,7 +169,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-19",
|
||||
@@ -179,7 +179,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-20",
|
||||
@@ -189,7 +189,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/accessibility.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-21",
|
||||
@@ -199,7 +199,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/accessibility.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-22",
|
||||
@@ -209,7 +209,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/accessibility.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-23",
|
||||
@@ -219,7 +219,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-24",
|
||||
@@ -229,7 +229,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-25",
|
||||
@@ -239,7 +239,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-26",
|
||||
@@ -249,7 +249,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-27",
|
||||
@@ -259,7 +259,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-28",
|
||||
@@ -269,7 +269,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-29",
|
||||
@@ -279,7 +279,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-30",
|
||||
@@ -289,7 +289,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-31",
|
||||
@@ -299,7 +299,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-32",
|
||||
@@ -309,7 +309,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-33",
|
||||
@@ -319,7 +319,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-34",
|
||||
@@ -329,7 +329,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-35",
|
||||
@@ -339,7 +339,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-36",
|
||||
@@ -349,7 +349,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-37",
|
||||
@@ -359,7 +359,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-38",
|
||||
@@ -369,7 +369,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-39",
|
||||
@@ -379,7 +379,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-40",
|
||||
@@ -389,7 +389,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-41",
|
||||
@@ -399,7 +399,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-42",
|
||||
@@ -409,7 +409,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-43",
|
||||
@@ -419,7 +419,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-44",
|
||||
@@ -429,7 +429,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-45",
|
||||
@@ -439,7 +439,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-46",
|
||||
@@ -449,7 +449,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/extensions.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-47",
|
||||
@@ -459,7 +459,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-48",
|
||||
@@ -469,7 +469,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-49",
|
||||
@@ -479,7 +479,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-50",
|
||||
@@ -489,7 +489,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-51",
|
||||
@@ -499,7 +499,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-52",
|
||||
@@ -509,7 +509,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-53",
|
||||
@@ -519,7 +519,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-54",
|
||||
@@ -529,7 +529,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-55",
|
||||
@@ -539,7 +539,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-56",
|
||||
@@ -549,7 +549,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-57",
|
||||
@@ -559,7 +559,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-58",
|
||||
@@ -569,7 +569,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-23",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-59",
|
||||
@@ -579,7 +579,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-25",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-60",
|
||||
@@ -589,7 +589,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-61",
|
||||
@@ -599,7 +599,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-62",
|
||||
@@ -609,7 +609,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-63",
|
||||
@@ -619,7 +619,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-64",
|
||||
@@ -629,7 +629,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-05-03",
|
||||
"file_path": "decisions/architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-65",
|
||||
@@ -639,7 +639,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-26",
|
||||
"file_path": "decisions/tooling.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "D-66",
|
||||
@@ -649,7 +649,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-05-06",
|
||||
"file_path": "decisions/testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-1",
|
||||
@@ -658,7 +658,7 @@
|
||||
"title": "Authorisation granularity on the IPC socket",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-2",
|
||||
@@ -667,7 +667,7 @@
|
||||
"title": "Back-pressure on event streams",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-3",
|
||||
@@ -676,7 +676,7 @@
|
||||
"title": "Event persistence + audit/undo",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-4",
|
||||
@@ -685,7 +685,7 @@
|
||||
"title": "`.canvas` schema compatibility with Obsidian",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-5",
|
||||
@@ -694,7 +694,7 @@
|
||||
"title": "IPC wire-format stability + `schema_version:`",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-6",
|
||||
@@ -703,7 +703,7 @@
|
||||
"title": "Window chrome — native frame vs frameless custom",
|
||||
"status": "resolved",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-7",
|
||||
@@ -712,7 +712,7 @@
|
||||
"title": "macOS app bundle signing / notarisation",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-8",
|
||||
@@ -721,7 +721,7 @@
|
||||
"title": "Extension API shape — widgets, subcommands, both?",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-extensions.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-9",
|
||||
@@ -730,7 +730,7 @@
|
||||
"title": "Lua runtime vendoring",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-extensions.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-10",
|
||||
@@ -739,7 +739,7 @@
|
||||
"title": "Extension manifest `schema_version:`",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-extensions.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-11",
|
||||
@@ -748,7 +748,7 @@
|
||||
"title": "Coverage gates — hard thresholds vs soft reporting",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-12",
|
||||
@@ -757,7 +757,7 @@
|
||||
"title": "Screen-reader automation (axe-core via Playwright)",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-testing.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-13",
|
||||
@@ -766,7 +766,7 @@
|
||||
"title": "Web production-mode a11y",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-accessibility.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-14",
|
||||
@@ -775,7 +775,7 @@
|
||||
"title": "i18n plurals / gender / date-format tooling",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-accessibility.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-15",
|
||||
@@ -784,7 +784,7 @@
|
||||
"title": "Editor tab — full LSP vs tree-sitter-only highlight",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-16",
|
||||
@@ -793,7 +793,7 @@
|
||||
"title": "`tree-sitter-dart` grammar maintenance",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-17",
|
||||
@@ -802,7 +802,7 @@
|
||||
"title": "Icon set growth",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-18",
|
||||
@@ -811,7 +811,7 @@
|
||||
"title": "Theme hot-reload in release builds",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-19",
|
||||
@@ -820,7 +820,7 @@
|
||||
"title": "(withdrawn)",
|
||||
"status": "resolved",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-20",
|
||||
@@ -829,7 +829,7 @@
|
||||
"title": "Kernel DB service — namespaced SQL access?",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-process.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-21",
|
||||
@@ -838,7 +838,7 @@
|
||||
"title": "Pql absorbs planning vs keeps separate",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-22",
|
||||
@@ -847,7 +847,7 @@
|
||||
"title": "Ticket persistence strategy",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-23",
|
||||
@@ -856,7 +856,7 @@
|
||||
"title": "SSH-remote development — run clide against a remote workspace",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-25",
|
||||
@@ -865,7 +865,7 @@
|
||||
"title": "Body text face — mono everywhere vs Josefin Sans UI + mono code",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-26",
|
||||
@@ -874,7 +874,7 @@
|
||||
"title": "Small screen layout (\u003c 1000px)",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-27",
|
||||
@@ -883,7 +883,7 @@
|
||||
"title": "Two-editor split",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-28",
|
||||
@@ -892,7 +892,7 @@
|
||||
"title": "Terminal strip scope — shell only or logs/errors/tests",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-29",
|
||||
@@ -901,7 +901,7 @@
|
||||
"title": "Branch picker location",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-30",
|
||||
@@ -910,7 +910,7 @@
|
||||
"title": "Focus behavior when editor is dirty and viewer is peeked",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "Q-31",
|
||||
@@ -919,7 +919,7 @@
|
||||
"title": "XWayland fallback for frameless — proper Wayland protocol needed",
|
||||
"status": "open",
|
||||
"file_path": "decisions/questions-architecture.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-2",
|
||||
@@ -929,7 +929,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-20",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-3",
|
||||
@@ -939,7 +939,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-4",
|
||||
@@ -949,7 +949,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-5",
|
||||
@@ -959,7 +959,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-6",
|
||||
@@ -969,7 +969,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-7",
|
||||
@@ -979,7 +979,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-8",
|
||||
@@ -989,7 +989,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-9",
|
||||
@@ -999,7 +999,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-10",
|
||||
@@ -1009,7 +1009,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-11",
|
||||
@@ -1019,7 +1019,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-21",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
},
|
||||
{
|
||||
"id": "R-12",
|
||||
@@ -1029,7 +1029,7 @@
|
||||
"status": "active",
|
||||
"date": "2026-04-22",
|
||||
"file_path": "decisions/rejected.md",
|
||||
"synced_at": "2026-05-06 21:00:50"
|
||||
"synced_at": "2026-05-06 21:12:28"
|
||||
}
|
||||
],
|
||||
"decision_refs": [
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ repository: https://github.com/postmeridiem/clide
|
||||
|
||||
# Pre-push line-coverage floor. Ratchets up only — see D-66.
|
||||
# Reading: `awk -F: '/^coverage_floor:/ {gsub(/ /,"",$2); print $2}' pubspec.yaml`.
|
||||
coverage_floor: 39
|
||||
coverage_floor: 43
|
||||
|
||||
# Project metadata (was project.yaml, folded in per D-056).
|
||||
# version: above is the single source of truth. The Makefile reads
|
||||
|
||||
@@ -0,0 +1,802 @@
|
||||
/// Pure-Dart tests for Buffer (lib/src/terminal/src/core/buffer/buffer.dart).
|
||||
///
|
||||
/// Buffer is the orchestrator on top of BufferLine — viewport sizing,
|
||||
/// scroll regions, cursor movement, erase commands, scroll/index/reverse-
|
||||
/// index, line insert/delete, resize + reflow, and word-boundary lookup.
|
||||
library;
|
||||
|
||||
import 'package:clide/src/terminal/src/core/buffer/buffer.dart';
|
||||
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
|
||||
import 'package:clide/src/terminal/src/core/buffer/range_line.dart';
|
||||
import 'package:clide/src/terminal/src/core/cursor.dart';
|
||||
import 'package:clide/src/terminal/src/core/mouse/mode.dart';
|
||||
import 'package:clide/src/terminal/src/core/state.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _State implements TerminalState {
|
||||
_State({
|
||||
this.viewWidth = 10,
|
||||
this.viewHeight = 5,
|
||||
this.autoWrapMode = true,
|
||||
this.lineFeedMode = false,
|
||||
this.originMode = false,
|
||||
this.reflowEnabled = false,
|
||||
});
|
||||
|
||||
@override
|
||||
int viewWidth;
|
||||
@override
|
||||
int viewHeight;
|
||||
@override
|
||||
bool autoWrapMode;
|
||||
@override
|
||||
bool lineFeedMode;
|
||||
@override
|
||||
bool originMode;
|
||||
@override
|
||||
bool reflowEnabled;
|
||||
|
||||
@override
|
||||
final cursor = CursorStyle();
|
||||
|
||||
// Unused-by-Buffer bits — dummy values are fine.
|
||||
@override
|
||||
bool get insertMode => false;
|
||||
@override
|
||||
bool get cursorKeysMode => false;
|
||||
@override
|
||||
bool get reverseDisplayMode => false;
|
||||
@override
|
||||
MouseMode get mouseMode => MouseMode.none;
|
||||
@override
|
||||
MouseReportMode get mouseReportMode => MouseReportMode.normal;
|
||||
@override
|
||||
bool get cursorBlinkMode => true;
|
||||
@override
|
||||
bool get cursorVisibleMode => true;
|
||||
@override
|
||||
bool get appKeypadMode => false;
|
||||
@override
|
||||
bool get reportFocusMode => false;
|
||||
@override
|
||||
bool get altBufferMouseScrollMode => false;
|
||||
@override
|
||||
bool get bracketedPasteMode => false;
|
||||
}
|
||||
|
||||
Buffer _newBuffer({
|
||||
int viewWidth = 10,
|
||||
int viewHeight = 5,
|
||||
int maxLines = 50,
|
||||
bool isAltBuffer = false,
|
||||
bool autoWrapMode = true,
|
||||
bool lineFeedMode = false,
|
||||
bool originMode = false,
|
||||
bool reflowEnabled = false,
|
||||
Set<int>? wordSeparators,
|
||||
}) {
|
||||
final state = _State(
|
||||
viewWidth: viewWidth,
|
||||
viewHeight: viewHeight,
|
||||
autoWrapMode: autoWrapMode,
|
||||
lineFeedMode: lineFeedMode,
|
||||
originMode: originMode,
|
||||
reflowEnabled: reflowEnabled,
|
||||
);
|
||||
return Buffer(
|
||||
state,
|
||||
maxLines: maxLines,
|
||||
isAltBuffer: isAltBuffer,
|
||||
wordSeparators: wordSeparators,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('Buffer — construction', () {
|
||||
test('seeds viewHeight empty lines and resets margins', () {
|
||||
final b = _newBuffer(viewWidth: 8, viewHeight: 4);
|
||||
expect(b.height, 4);
|
||||
expect(b.viewWidth, 8);
|
||||
expect(b.viewHeight, 4);
|
||||
expect(b.cursorX, 0);
|
||||
expect(b.cursorY, 0);
|
||||
expect(b.scrollBack, 0);
|
||||
expect(b.marginTop, 0);
|
||||
expect(b.marginBottom, 3);
|
||||
});
|
||||
|
||||
test('absolute cursor + margin getters are scrollback-shifted', () {
|
||||
final b = _newBuffer(viewHeight: 3);
|
||||
// Push a line via index() in the no-scrollable-region path on a
|
||||
// primary buffer — that uses lines.push to grow scrollback.
|
||||
b.setCursor(0, 2);
|
||||
b.index();
|
||||
// Now scrollBack == 1; the cursor stayed at viewHeight - 1 = 2.
|
||||
expect(b.scrollBack, 1);
|
||||
expect(b.absoluteCursorY, 3);
|
||||
expect(b.absoluteMarginTop, 1);
|
||||
expect(b.absoluteMarginBottom, 3);
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — cursor movement and clamping', () {
|
||||
test('setCursorX/Y clamp to viewWidth-1 / viewHeight-1', () {
|
||||
final b = _newBuffer(viewWidth: 5, viewHeight: 3);
|
||||
b.setCursorX(99);
|
||||
b.setCursorY(99);
|
||||
expect(b.cursorX, 4);
|
||||
expect(b.cursorY, 2);
|
||||
b.setCursorX(-1);
|
||||
b.setCursorY(-1);
|
||||
expect(b.cursorX, 0);
|
||||
expect(b.cursorY, 0);
|
||||
});
|
||||
|
||||
test('moveCursorX / moveCursorY are relative + clamped', () {
|
||||
final b = _newBuffer();
|
||||
b.setCursor(5, 2);
|
||||
b.moveCursorX(2);
|
||||
b.moveCursorY(1);
|
||||
expect(b.cursorX, 7);
|
||||
expect(b.cursorY, 3);
|
||||
b.moveCursorX(99); // clamps
|
||||
expect(b.cursorX, 9);
|
||||
});
|
||||
|
||||
test('cursorGoForward saturates at viewWidth (one past last visible)', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
for (var i = 0; i < 10; i++) {
|
||||
b.cursorGoForward();
|
||||
}
|
||||
// The clamped getter caps at viewWidth - 1; the internal saturation
|
||||
// sits at viewWidth.
|
||||
expect(b.cursorX, 4);
|
||||
});
|
||||
|
||||
test('setCursor — originMode shifts y by marginTop and clamps to marginBottom', () {
|
||||
final b = _newBuffer(viewHeight: 6, originMode: true);
|
||||
b.setVerticalMargins(2, 4);
|
||||
b.setCursor(0, 0);
|
||||
// origin shift: cursorY += marginTop (2) → clamped to marginBottom (4).
|
||||
expect(b.cursorY, 2);
|
||||
b.setCursor(0, 99);
|
||||
expect(b.cursorY, 4); // clamped to marginBottom
|
||||
});
|
||||
|
||||
test('moveCursor delegates to setCursor', () {
|
||||
final b = _newBuffer();
|
||||
b.setCursor(0, 0);
|
||||
b.moveCursor(2, 1);
|
||||
expect(b.cursorX, 2);
|
||||
expect(b.cursorY, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — vertical margins', () {
|
||||
test('setVerticalMargins clamps and orders top/bottom', () {
|
||||
final b = _newBuffer(viewHeight: 5);
|
||||
b.setVerticalMargins(99, -5);
|
||||
// Both clamp to [0, viewHeight-1] = [0, 4]; the implementation
|
||||
// also coerces top<=bottom by min/max swap.
|
||||
expect(b.marginTop, lessThanOrEqualTo(b.marginBottom));
|
||||
});
|
||||
|
||||
test('resetVerticalMargins restores full viewport range', () {
|
||||
final b = _newBuffer(viewHeight: 5);
|
||||
b.setVerticalMargins(1, 3);
|
||||
b.resetVerticalMargins();
|
||||
expect(b.marginTop, 0);
|
||||
expect(b.marginBottom, 4);
|
||||
});
|
||||
|
||||
test('isInVerticalMargin is true inside, false outside', () {
|
||||
final b = _newBuffer(viewHeight: 5);
|
||||
b.setVerticalMargins(1, 3);
|
||||
b.setCursorY(0);
|
||||
expect(b.isInVerticalMargin, isFalse);
|
||||
b.setCursorY(2);
|
||||
expect(b.isInVerticalMargin, isTrue);
|
||||
b.setCursorY(4);
|
||||
expect(b.isInVerticalMargin, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — write / writeChar / autoWrap', () {
|
||||
test('write adds chars left-to-right and advances the cursor', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
b.write('abc');
|
||||
expect(b.cursorX, 3);
|
||||
expect(b.currentLine.getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
expect(b.currentLine.getCodePoint(1), 'b'.codeUnitAt(0));
|
||||
expect(b.currentLine.getCodePoint(2), 'c'.codeUnitAt(0));
|
||||
});
|
||||
|
||||
test('autoWrap pushes the cursor to the next line and marks it wrapped', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 4);
|
||||
b.write('abcdef'); // 'def' wraps to line 1
|
||||
expect(b.height, 4); // still within view; no scrollback needed yet
|
||||
expect(b.cursorY, 1);
|
||||
// cursorX getter clamps to viewWidth-1; internal _cursorX is saturated.
|
||||
expect(b.cursorX, 2);
|
||||
expect(b.currentLine.isWrapped, isTrue);
|
||||
});
|
||||
|
||||
test('autoWrap=false leaves the cursor saturated past the end (no wrap)', () {
|
||||
final b = _newBuffer(viewWidth: 3, autoWrapMode: false);
|
||||
b.write('abc');
|
||||
// After three writes the cursor is at viewWidth (3). The next write
|
||||
// triggers index() but autoWrapMode is off — the line shouldn't be
|
||||
// marked wrapped.
|
||||
b.write('d');
|
||||
expect(b.currentLine.isWrapped, isFalse);
|
||||
});
|
||||
|
||||
test('wide character writes a width-2 cell + a 0-codepoint trailer', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
b.write('中'); // CJK ideograph, width 2
|
||||
expect(b.cursorX, 2);
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — backspace', () {
|
||||
test('column 0 on a wrapped line jumps to the prior line end and unwraps', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 3);
|
||||
b.write('abcd'); // wraps; cursor at (1, 1) on a wrapped line
|
||||
expect(b.currentLine.isWrapped, isTrue);
|
||||
b.setCursor(0, 1);
|
||||
// Re-flag wrapped (setCursor may not affect it but to be sure)
|
||||
b.currentLine.isWrapped = true;
|
||||
b.backspace();
|
||||
expect(b.cursorX, 2); // viewWidth - 1
|
||||
expect(b.cursorY, 0);
|
||||
});
|
||||
|
||||
test('column == viewWidth backspaces by 2 cells (off the saturated edge)', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
// Saturate the cursor.
|
||||
for (var i = 0; i < 5; i++) {
|
||||
b.cursorGoForward();
|
||||
}
|
||||
// Internal _cursorX is now 5 (viewWidth). The clamped getter shows 4.
|
||||
b.backspace();
|
||||
// -2 of (5) clamps to (3). cursorX getter returns clamped value.
|
||||
expect(b.cursorX, 3);
|
||||
});
|
||||
|
||||
test('mid-line backspace is just a -1', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
b.setCursor(3, 0);
|
||||
b.backspace();
|
||||
expect(b.cursorX, 2);
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — erase commands', () {
|
||||
test('eraseLine clears the whole current line and clears wrapped flag', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
b.write('abc');
|
||||
b.currentLine.isWrapped = true;
|
||||
b.eraseLine();
|
||||
expect(b.currentLine.isWrapped, isFalse);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
expect(b.currentLine.getCodePoint(i), 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('eraseLineFromCursor only clears [cursor, viewWidth)', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
b.write('abcde');
|
||||
b.setCursorX(2);
|
||||
b.eraseLineFromCursor();
|
||||
expect(b.currentLine.getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
expect(b.currentLine.getCodePoint(1), 'b'.codeUnitAt(0));
|
||||
expect(b.currentLine.getCodePoint(2), 0);
|
||||
expect(b.currentLine.getCodePoint(4), 0);
|
||||
});
|
||||
|
||||
test('eraseLineToCursor only clears [0, cursor)', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
b.write('abcde');
|
||||
b.setCursorX(3);
|
||||
b.eraseLineToCursor();
|
||||
expect(b.currentLine.getCodePoint(0), 0);
|
||||
expect(b.currentLine.getCodePoint(2), 0);
|
||||
expect(b.currentLine.getCodePoint(3), 'd'.codeUnitAt(0));
|
||||
});
|
||||
|
||||
test('eraseChars erases [cursor, cursor+count)', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
b.write('abcde');
|
||||
b.setCursorX(1);
|
||||
b.eraseChars(2);
|
||||
expect(b.currentLine.getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
expect(b.currentLine.getCodePoint(1), 0);
|
||||
expect(b.currentLine.getCodePoint(2), 0);
|
||||
expect(b.currentLine.getCodePoint(3), 'd'.codeUnitAt(0));
|
||||
});
|
||||
|
||||
test('eraseDisplay clears every visible line', () {
|
||||
final b = _newBuffer(viewWidth: 5, viewHeight: 3);
|
||||
b.write('aaaaa');
|
||||
b.lineFeed();
|
||||
b.write('bbbbb');
|
||||
b.eraseDisplay();
|
||||
for (var y = 0; y < b.viewHeight; y++) {
|
||||
for (var x = 0; x < b.viewWidth; x++) {
|
||||
expect(b.lines[y].getCodePoint(x), 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('eraseDisplayFromCursor clears current line tail + every line below', () {
|
||||
// lineFeedMode: true so lineFeed resets cursorX — otherwise the
|
||||
// saturated cursorX would make the next write wrap onto an extra
|
||||
// line via writeChar's autoWrap path.
|
||||
final b = _newBuffer(viewWidth: 5, viewHeight: 5, lineFeedMode: true);
|
||||
b.write('aaaaa');
|
||||
b.lineFeed();
|
||||
b.write('bbbbb');
|
||||
b.lineFeed();
|
||||
b.write('ccccc');
|
||||
b.setCursor(2, 1);
|
||||
b.eraseDisplayFromCursor();
|
||||
expect(b.lines[0].getCodePoint(0), 'a'.codeUnitAt(0)); // untouched
|
||||
expect(b.lines[1].getCodePoint(0), 'b'.codeUnitAt(0)); // head kept
|
||||
expect(b.lines[1].getCodePoint(2), 0); // tail erased
|
||||
expect(b.lines[2].getCodePoint(0), 0); // line below wiped
|
||||
});
|
||||
|
||||
test('eraseDisplayToCursor clears every line above + the current line head', () {
|
||||
final b = _newBuffer(viewWidth: 5, viewHeight: 5, lineFeedMode: true);
|
||||
b.write('aaaaa');
|
||||
b.lineFeed();
|
||||
b.write('bbbbb');
|
||||
b.lineFeed();
|
||||
b.write('vwxyz'); // distinct chars so the kept tail is visible
|
||||
b.setCursor(3, 2);
|
||||
b.eraseDisplayToCursor();
|
||||
expect(b.lines[0].getCodePoint(0), 0);
|
||||
expect(b.lines[1].getCodePoint(0), 0);
|
||||
expect(b.lines[2].getCodePoint(0), 0); // head erased
|
||||
expect(b.lines[2].getCodePoint(3), 'y'.codeUnitAt(0)); // tail kept
|
||||
expect(b.lines[2].getCodePoint(4), 'z'.codeUnitAt(0));
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — scroll / index / lineFeed / reverseIndex', () {
|
||||
test('scrollUp inside the scroll region rotates lines and seeds an empty tail', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 5, lineFeedMode: true);
|
||||
b.write('aaa');
|
||||
b.lineFeed();
|
||||
b.write('bbb');
|
||||
b.lineFeed();
|
||||
b.write('ccc');
|
||||
b.scrollUp(1);
|
||||
// After scroll: line 0 = 'bbb', line 1 = 'ccc', line 2 = empty.
|
||||
expect(b.lines[0].getCodePoint(0), 'b'.codeUnitAt(0));
|
||||
expect(b.lines[1].getCodePoint(0), 'c'.codeUnitAt(0));
|
||||
expect(b.lines[2].getCodePoint(0), 0);
|
||||
});
|
||||
|
||||
test('scrollDown inside the scroll region rotates lines and seeds an empty head', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 5, lineFeedMode: true);
|
||||
b.write('aaa');
|
||||
b.lineFeed();
|
||||
b.write('bbb');
|
||||
b.scrollDown(1);
|
||||
// After scroll: line 0 = empty, line 1 = 'aaa', line 2 = 'bbb'.
|
||||
expect(b.lines[0].getCodePoint(0), 0);
|
||||
expect(b.lines[1].getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
expect(b.lines[2].getCodePoint(0), 'b'.codeUnitAt(0));
|
||||
});
|
||||
|
||||
test('index — at the bottom of primary buffer pushes a new line to grow scrollback', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 3);
|
||||
b.setCursorY(2);
|
||||
b.index();
|
||||
expect(b.scrollBack, 1);
|
||||
expect(b.cursorY, 2); // stayed at viewHeight - 1
|
||||
});
|
||||
|
||||
test('index — at the bottom of alt buffer scrolls up instead of growing', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 3, isAltBuffer: true);
|
||||
b.setCursorY(2);
|
||||
b.index();
|
||||
expect(b.scrollBack, 0);
|
||||
expect(b.cursorY, 2);
|
||||
});
|
||||
|
||||
test('index — within margin but not at the bottom moves cursor down', () {
|
||||
final b = _newBuffer(viewHeight: 5);
|
||||
b.setVerticalMargins(1, 3);
|
||||
b.setCursorY(1);
|
||||
b.index();
|
||||
expect(b.cursorY, 2);
|
||||
});
|
||||
|
||||
test('index — at margin bottom with non-zero top scrollUps inside the region', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 4);
|
||||
b.setVerticalMargins(1, 2);
|
||||
b.setCursorY(2); // at marginBottom
|
||||
b.write('a'); // mark line 2
|
||||
b.index();
|
||||
expect(b.cursorY, 2);
|
||||
});
|
||||
|
||||
test('index — at marginBottom with marginTop=0 on primary inserts a new line', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 4);
|
||||
// marginTop=0 by default
|
||||
b.setCursorY(b.marginBottom);
|
||||
b.write('z');
|
||||
final beforeHeight = b.height;
|
||||
b.index();
|
||||
expect(b.height, beforeHeight + 1); // an empty line was inserted
|
||||
});
|
||||
|
||||
test('index — cursor outside the vertical margin and at viewport bottom on primary pushes scrollback', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 5);
|
||||
// Margins shrink the scroll region — the cursor below it is outside.
|
||||
b.setVerticalMargins(1, 3);
|
||||
b.setCursorY(4); // viewHeight - 1, outside [1, 3]
|
||||
final before = b.height;
|
||||
b.index();
|
||||
expect(b.height, before + 1); // primary path: lines.push
|
||||
});
|
||||
|
||||
test('index — cursor outside the vertical margin on alt buffer scrolls up instead of pushing', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 5, isAltBuffer: true);
|
||||
b.setVerticalMargins(1, 3);
|
||||
b.setCursorY(4);
|
||||
final before = b.height;
|
||||
b.index();
|
||||
expect(b.height, before); // alt path: no growth
|
||||
});
|
||||
|
||||
test('index — cursor outside the vertical margin and not at the bottom moves cursor down', () {
|
||||
final b = _newBuffer(viewHeight: 5);
|
||||
b.setVerticalMargins(1, 3);
|
||||
b.setCursorY(4); // outside, but not viewHeight-1... wait viewHeight-1 IS 4
|
||||
// To exercise the moveCursorY branch outside the margin we need
|
||||
// _cursorY < viewHeight - 1 AND outside [marginTop, marginBottom].
|
||||
// With margins (1, 2) the cursor at y=3 is outside and not at the bottom.
|
||||
b.setVerticalMargins(1, 2);
|
||||
b.setCursorY(3);
|
||||
b.index();
|
||||
expect(b.cursorY, 4);
|
||||
});
|
||||
|
||||
test('lineFeed honours lineFeedMode by setting cursor to column 0', () {
|
||||
final b = _newBuffer(lineFeedMode: true);
|
||||
b.setCursorX(5);
|
||||
b.lineFeed();
|
||||
expect(b.cursorX, 0);
|
||||
});
|
||||
|
||||
test('lineFeed leaves cursorX alone when lineFeedMode is off', () {
|
||||
final b = _newBuffer();
|
||||
b.setCursorX(5);
|
||||
b.lineFeed();
|
||||
expect(b.cursorX, 5);
|
||||
});
|
||||
|
||||
test('reverseIndex inside margins at top scrolls down, otherwise moves up', () {
|
||||
final b = _newBuffer(viewHeight: 5);
|
||||
b.setVerticalMargins(1, 3);
|
||||
b.setCursorY(2);
|
||||
b.reverseIndex();
|
||||
expect(b.cursorY, 1);
|
||||
|
||||
b.setCursorY(1); // at marginTop
|
||||
b.reverseIndex();
|
||||
expect(b.cursorY, 1); // cursor stays; scrollDown happened
|
||||
|
||||
b.setCursorY(0); // outside margins
|
||||
b.reverseIndex();
|
||||
expect(b.cursorY, 0); // already at 0, clamps
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — saveCursor / restoreCursor', () {
|
||||
test('round-trips position + style + charset', () {
|
||||
final b = _newBuffer();
|
||||
b.setCursor(3, 2);
|
||||
// Mutate cursor style.
|
||||
// (Reach into the underlying state via the property surface.)
|
||||
b.saveCursor();
|
||||
b.setCursor(0, 0);
|
||||
b.restoreCursor();
|
||||
expect(b.cursorX, 3);
|
||||
expect(b.cursorY, 2);
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — line insert / delete / chars', () {
|
||||
test('insertBlankChars opens a gap at the cursor', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
b.write('abcde');
|
||||
b.setCursorX(1);
|
||||
b.insertBlankChars(2);
|
||||
expect(b.currentLine.getCodePoint(1), 0);
|
||||
expect(b.currentLine.getCodePoint(2), 0);
|
||||
expect(b.currentLine.getCodePoint(3), 'b'.codeUnitAt(0));
|
||||
});
|
||||
|
||||
test('deleteChars closes a gap at the cursor', () {
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
b.write('abcde');
|
||||
b.setCursorX(1);
|
||||
b.deleteChars(2);
|
||||
expect(b.currentLine.getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
expect(b.currentLine.getCodePoint(1), 'd'.codeUnitAt(0));
|
||||
expect(b.currentLine.getCodePoint(2), 'e'.codeUnitAt(0));
|
||||
});
|
||||
|
||||
test('insertLines is a no-op outside the scroll region', () {
|
||||
final b = _newBuffer(viewHeight: 5);
|
||||
b.setVerticalMargins(2, 4);
|
||||
b.setCursorY(0);
|
||||
b.write('aaa');
|
||||
b.insertLines(2);
|
||||
// line 0 untouched (cursor was outside margins → no-op)
|
||||
expect(b.lines[0].getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
});
|
||||
|
||||
test('insertLines inside the scroll region pushes lines down', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 5);
|
||||
b.setVerticalMargins(1, 3);
|
||||
b.setCursor(0, 2);
|
||||
b.write('aaa');
|
||||
b.setCursor(0, 1);
|
||||
b.insertLines(1);
|
||||
// insertLines(1) pushes lines below the cursor down. So 'aaa'
|
||||
// should now be at line 3.
|
||||
expect(b.lines[3].getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
// Line 1 (cursor) is now empty.
|
||||
expect(b.lines[1].getCodePoint(0), 0);
|
||||
});
|
||||
|
||||
test('insertLines fills the entire region with empty lines when count >= lines below', () {
|
||||
// When linesToInsert == linesBelow, linesToMove is 0 and the
|
||||
// "fill empty" loop runs from 0 to linesToInsert.
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 5);
|
||||
b.setVerticalMargins(1, 3);
|
||||
b.setCursor(0, 1);
|
||||
b.write('aaa');
|
||||
b.setCursor(0, 2);
|
||||
b.write('bbb');
|
||||
b.setCursor(0, 1);
|
||||
// 3 lines below (incl cursor) but request many more — clamps to 3
|
||||
// and skips the "move" loop entirely.
|
||||
b.insertLines(99);
|
||||
for (var y = 1; y <= 3; y++) {
|
||||
expect(b.lines[y].getCodePoint(0), 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('deleteLines is a no-op outside the scroll region', () {
|
||||
final b = _newBuffer(viewHeight: 5);
|
||||
b.setVerticalMargins(2, 4);
|
||||
b.setCursorY(0);
|
||||
b.write('aaa');
|
||||
b.deleteLines(2);
|
||||
expect(b.lines[0].getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
});
|
||||
|
||||
test('deleteLines inside the scroll region pulls lines up', () {
|
||||
// Use setCursor (not setCursorY) so cursorX is reset between
|
||||
// writes — otherwise it stays saturated after the previous write.
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 6);
|
||||
b.setVerticalMargins(1, 4);
|
||||
b.setCursor(0, 2);
|
||||
b.write('aaa');
|
||||
b.setCursor(0, 3);
|
||||
b.write('bbb');
|
||||
b.setCursor(0, 1);
|
||||
b.deleteLines(1);
|
||||
// Inside the scroll region, deleteLines at line 1 shifts lines 2..4 up.
|
||||
// Original content was at lines 2 ('aaa') and 3 ('bbb'); after deletion
|
||||
// they should sit at 1 and 2.
|
||||
expect(b.lines[1].getCodePoint(0), 'a'.codeUnitAt(0));
|
||||
expect(b.lines[2].getCodePoint(0), 'b'.codeUnitAt(0));
|
||||
expect(b.lines[4].getCodePoint(0), 0); // bottom of region cleared
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — resize', () {
|
||||
test('grow height pushes empty lines while there is room in the ring', () {
|
||||
final b = _newBuffer(viewWidth: 5, viewHeight: 3, maxLines: 50);
|
||||
b.resize(5, 3, 5, 5);
|
||||
expect(b.height, 5);
|
||||
});
|
||||
|
||||
test('grow height into existing scrollback bumps cursorY instead of pushing', () {
|
||||
// Build up scrollback so lines.length > new viewHeight.
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 3, maxLines: 50);
|
||||
// Force scrollback growth via index()-at-bottom on primary buffer.
|
||||
b.setCursorY(2);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
b.index();
|
||||
}
|
||||
expect(b.height, greaterThanOrEqualTo(8));
|
||||
final cursorYBefore = b.cursorY;
|
||||
// Resize to a height that's still smaller than lines.length: the
|
||||
// grow loop should bump _cursorY rather than push new lines.
|
||||
b.resize(3, 3, 3, 5);
|
||||
expect(b.cursorY, greaterThan(cursorYBefore));
|
||||
});
|
||||
|
||||
test('resize with reflow pads the result up to newHeight', () {
|
||||
// Empty buffer: reflow returns near-zero lines; the pad-with-empty-
|
||||
// lines branch fills up to newHeight.
|
||||
final b = _newBuffer(viewWidth: 8, viewHeight: 3, reflowEnabled: true);
|
||||
b.resize(8, 3, 4, 12);
|
||||
expect(b.height, greaterThanOrEqualTo(12));
|
||||
});
|
||||
|
||||
test('shrink height pops lines from the bottom when cursor is near the top', () {
|
||||
final b = _newBuffer(viewWidth: 5, viewHeight: 5);
|
||||
b.setCursorY(0);
|
||||
b.resize(5, 5, 5, 3);
|
||||
expect(b.height, 3);
|
||||
});
|
||||
|
||||
test('shrink height drags cursor up when it sits below the new height', () {
|
||||
final b = _newBuffer(viewWidth: 5, viewHeight: 5);
|
||||
b.setCursorY(4);
|
||||
b.resize(5, 5, 5, 3);
|
||||
expect(b.cursorY, lessThanOrEqualTo(2));
|
||||
});
|
||||
|
||||
test('resize without reflow forwards width to every line', () {
|
||||
final b = _newBuffer(viewWidth: 5, viewHeight: 3, reflowEnabled: false);
|
||||
b.resize(5, 3, 8, 3);
|
||||
for (var y = 0; y < b.viewHeight; y++) {
|
||||
expect(b.lines[y].length, 8);
|
||||
}
|
||||
});
|
||||
|
||||
test('resize with reflow on primary buffer pads the result up to newHeight', () {
|
||||
final b = _newBuffer(viewWidth: 5, viewHeight: 3, reflowEnabled: true);
|
||||
b.resize(5, 3, 8, 5);
|
||||
expect(b.height, greaterThanOrEqualTo(5));
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — scrollback / clear', () {
|
||||
test('clearScrollback is a no-op when there is no scrollback', () {
|
||||
final b = _newBuffer(viewHeight: 3);
|
||||
b.clearScrollback();
|
||||
expect(b.scrollBack, 0);
|
||||
});
|
||||
|
||||
test('clearScrollback drops lines above the viewport', () {
|
||||
final b = _newBuffer(viewHeight: 3);
|
||||
// Force scrollback by indexing past the bottom.
|
||||
b.setCursorY(2);
|
||||
b.index();
|
||||
b.index();
|
||||
expect(b.scrollBack, 2);
|
||||
b.clearScrollback();
|
||||
expect(b.scrollBack, 0);
|
||||
});
|
||||
|
||||
test('clear wipes everything and refills with viewHeight empty lines', () {
|
||||
final b = _newBuffer(viewHeight: 3);
|
||||
b.write('aaa');
|
||||
b.clear();
|
||||
expect(b.height, 3);
|
||||
for (var y = 0; y < 3; y++) {
|
||||
for (var x = 0; x < b.viewWidth; x++) {
|
||||
expect(b.lines[y].getCodePoint(x), 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — anchors', () {
|
||||
test('createAnchor at (x, y)', () {
|
||||
final b = _newBuffer();
|
||||
final a = b.createAnchor(2, 1);
|
||||
expect(a.x, 2);
|
||||
expect(b.lines[1].anchors, contains(a));
|
||||
});
|
||||
|
||||
test('createAnchorFromOffset', () {
|
||||
final b = _newBuffer();
|
||||
final a = b.createAnchorFromOffset(const CellOffset(3, 2));
|
||||
expect(a.x, 3);
|
||||
expect(b.lines[2].anchors, contains(a));
|
||||
});
|
||||
|
||||
test('createAnchorFromCursor', () {
|
||||
final b = _newBuffer();
|
||||
b.setCursor(4, 1);
|
||||
final a = b.createAnchorFromCursor();
|
||||
expect(a.x, 4);
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — getWordBoundary', () {
|
||||
test('returns null for an out-of-range y', () {
|
||||
final b = _newBuffer(viewHeight: 3);
|
||||
expect(b.getWordBoundary(const CellOffset(0, 99)), isNull);
|
||||
});
|
||||
|
||||
test('returns null when the position is fully bounded by separators on both sides', () {
|
||||
// The implementation walks left until a separator (or column 0) and
|
||||
// right until a separator (or viewWidth). A null result requires the
|
||||
// walks to land on the same column — i.e., separators flanking the
|
||||
// position itself.
|
||||
final b = _newBuffer(viewWidth: 5);
|
||||
b.write('a b'); // spaces at indexes 1, 2, 3
|
||||
final r = b.getWordBoundary(const CellOffset(2, 0));
|
||||
expect(r, isNull);
|
||||
});
|
||||
|
||||
test('returns the surrounding word range, halting at separators', () {
|
||||
final b = _newBuffer(viewWidth: 9);
|
||||
b.write('foo bar');
|
||||
final r = b.getWordBoundary(const CellOffset(5, 0))!;
|
||||
expect(r.begin.x, 4);
|
||||
expect(r.end.x, 7);
|
||||
});
|
||||
|
||||
test('honours custom wordSeparators when provided', () {
|
||||
final b = _newBuffer(viewWidth: 5, wordSeparators: <int>{','.codeUnitAt(0)});
|
||||
b.write('a,bcd');
|
||||
final r = b.getWordBoundary(const CellOffset(2, 0))!;
|
||||
expect(r.begin.x, 2);
|
||||
expect(r.end.x, 5);
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — getText', () {
|
||||
test('default range walks the whole buffer line-by-line with newlines', () {
|
||||
final b = _newBuffer(viewWidth: 4, viewHeight: 3);
|
||||
b.write('aa');
|
||||
b.lineFeed();
|
||||
b.write('bb');
|
||||
final text = b.getText();
|
||||
expect(text, contains('aa'));
|
||||
expect(text, contains('bb'));
|
||||
expect(text.split('\n').length, greaterThanOrEqualTo(2));
|
||||
});
|
||||
|
||||
test('explicit range works on a normalized BufferRangeLine', () {
|
||||
final b = _newBuffer(viewWidth: 4, viewHeight: 3);
|
||||
b.write('aa');
|
||||
b.lineFeed();
|
||||
b.write('bb');
|
||||
final r = BufferRangeLine(const CellOffset(0, 0), const CellOffset(2, 0));
|
||||
expect(b.getText(r), 'aa');
|
||||
});
|
||||
|
||||
test('skips out-of-range segments', () {
|
||||
final b = _newBuffer(viewWidth: 4, viewHeight: 2);
|
||||
b.write('aa');
|
||||
// Range references a y past height; getText should skip it without
|
||||
// throwing.
|
||||
final r = BufferRangeLine(const CellOffset(0, 0), const CellOffset(2, 99));
|
||||
final result = b.getText(r);
|
||||
expect(result, contains('aa'));
|
||||
});
|
||||
|
||||
test('wrapped lines do not get an inserted newline', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 3);
|
||||
b.write('abcdef'); // wraps line 1
|
||||
final text = b.getText();
|
||||
// The wrapped line shouldn't have a separating newline before it.
|
||||
expect(text.replaceAll('\n', '').contains('abcdef'), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('Buffer — toString debug dump', () {
|
||||
test('produces one line per buffer line with index and wrap marker', () {
|
||||
final b = _newBuffer(viewWidth: 3, viewHeight: 3);
|
||||
b.write('abcd'); // wraps; line 0 is 'abc', line 1 is 'd' (wrapped)
|
||||
final out = b.toString();
|
||||
expect(out, contains('|abc|'));
|
||||
expect(out, contains('(⏎)')); // the wrapped marker
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user