From 3196c4957f5d74699f6e63aafe8b8a9c3354fe32 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 7 May 2026 00:03:19 +0200 Subject: [PATCH] =?UTF-8?q?test=20sweep:=20cover=20core/(root)=20=E2=80=94?= =?UTF-8?q?=20cell,=20cursor,=20charset,=20tabs,=20reflow=20(T-91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds test/terminal/core/core_test.dart — 36 unit tests across the small standalone files that sit directly under `lib/src/terminal/src/core/*.dart`: - CellData (constructor + empty + getHash + toString), - CursorStyle (default ctor, every set/unset attr getter pair, all three colour-mode setters per channel, reset, the .empty singleton) + CursorPosition, - Charset (translate, designate/use, save/restore, asciiTranslator, decSpecGraphicsTranslator with in-table, out-of-table, and high- codepoint paths), - TabStops (default 8-column grid, find with empty-range / out-of- bounds / no-stop-in-range cases, setAt/clearAt/clearAll/reset), - reflow (empty input, single-line passthrough, grow, shrink-with- split, wrapped-run continuation, wide-char boundary on the new width, inner wide-char clamp during _addPart, anchor reparent on the main path, anchor reparent past trimmedLength). Two source-side cleanups folded in: - `CursorStyle.isItalis` was a defined-but-never-called getter with a typo. No external callers reference it; renamed to `isItalic` in the same change as the test that exercises it. - `_LineBuilder.isEmpty` in reflow.dart was dead — the only callers use `isNotEmpty` or check `_lines.isNotEmpty` separately. Removed. Coverage delta: - cell.dart: 3/7 → 7/7. - charset.dart: 12/25 → 25/25. - cursor.dart: 2/62 → 62/62. - tabs.dart: 0/23 → 23/23. - reflow.dart: 24/72 → 71/71 (file shrank by one line after the isEmpty getter removal). - Total project: 52.72% → 54.62%; coverage_floor bumped 52 → 54. Note for follow-up (not blocking): the post-loop "anchor.x >= to" branch in reflow's `_addPart` reparents anchors past trimmedLength onto whatever builder line is active at that moment. If no subsequent content is added (no wrapped continuations after the last shrink iteration), that builder line is never emitted by `finish()` and the anchor lands on a dangling reference. The path is exercised by the new test, but the contract it implements is arguably broken — anchors that should follow the source content end up off the visible buffer. Worth a separate ticket if real terminals trip it. Co-Authored-By: Claude --- .pql/pql-plan.json | 214 +++++++------- lib/src/terminal/src/core/cursor.dart | 2 +- lib/src/terminal/src/core/reflow.dart | 2 - pubspec.yaml | 2 +- test/terminal/core/core_test.dart | 408 ++++++++++++++++++++++++++ 5 files changed, 517 insertions(+), 111 deletions(-) create mode 100644 test/terminal/core/core_test.dart diff --git a/.pql/pql-plan.json b/.pql/pql-plan.json index 8b1f8394..ae81e1af 100644 --- a/.pql/pql-plan.json +++ b/.pql/pql-plan.json @@ -1,5 +1,5 @@ { - "exported_at": "2026-05-06T21:51:46Z", + "exported_at": "2026-05-06T22:03:19Z", "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-3", @@ -19,7 +19,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-4", @@ -29,7 +29,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-5", @@ -39,7 +39,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-6", @@ -49,7 +49,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-7", @@ -59,7 +59,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-8", @@ -69,7 +69,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-9", @@ -79,7 +79,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-10", @@ -89,7 +89,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-11", @@ -99,7 +99,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-12", @@ -109,7 +109,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-13", @@ -119,7 +119,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-14", @@ -129,7 +129,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-15", @@ -139,7 +139,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-16", @@ -149,7 +149,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-17", @@ -159,7 +159,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-18", @@ -169,7 +169,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-19", @@ -179,7 +179,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-20", @@ -189,7 +189,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-21", @@ -199,7 +199,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-22", @@ -209,7 +209,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/accessibility.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-23", @@ -219,7 +219,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-24", @@ -229,7 +229,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-25", @@ -239,7 +239,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-26", @@ -249,7 +249,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-27", @@ -259,7 +259,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-28", @@ -269,7 +269,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-29", @@ -279,7 +279,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-30", @@ -289,7 +289,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-31", @@ -299,7 +299,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-32", @@ -309,7 +309,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-33", @@ -319,7 +319,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-34", @@ -329,7 +329,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-35", @@ -339,7 +339,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-36", @@ -349,7 +349,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-37", @@ -359,7 +359,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-38", @@ -369,7 +369,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-39", @@ -379,7 +379,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-40", @@ -389,7 +389,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-41", @@ -399,7 +399,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-42", @@ -409,7 +409,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-43", @@ -419,7 +419,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-44", @@ -429,7 +429,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-45", @@ -439,7 +439,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-46", @@ -449,7 +449,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/extensions.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-47", @@ -459,7 +459,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-48", @@ -469,7 +469,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-49", @@ -479,7 +479,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-50", @@ -489,7 +489,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-51", @@ -499,7 +499,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-52", @@ -509,7 +509,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-53", @@ -519,7 +519,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-54", @@ -529,7 +529,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-55", @@ -539,7 +539,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-56", @@ -549,7 +549,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-57", @@ -559,7 +559,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-58", @@ -569,7 +569,7 @@ "status": "active", "date": "2026-04-23", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-59", @@ -579,7 +579,7 @@ "status": "active", "date": "2026-04-25", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-60", @@ -589,7 +589,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-61", @@ -599,7 +599,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-62", @@ -609,7 +609,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-63", @@ -619,7 +619,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-64", @@ -629,7 +629,7 @@ "status": "active", "date": "2026-05-03", "file_path": "decisions/architecture.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-65", @@ -639,7 +639,7 @@ "status": "active", "date": "2026-04-26", "file_path": "decisions/tooling.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "D-66", @@ -649,7 +649,7 @@ "status": "active", "date": "2026-05-06", "file_path": "decisions/testing.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "Q-19", @@ -820,7 +820,7 @@ "title": "(withdrawn)", "status": "resolved", "file_path": "decisions/questions-process.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "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:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-2", @@ -929,7 +929,7 @@ "status": "active", "date": "2026-04-20", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-3", @@ -939,7 +939,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-4", @@ -949,7 +949,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-5", @@ -959,7 +959,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-6", @@ -969,7 +969,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-7", @@ -979,7 +979,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-8", @@ -989,7 +989,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-9", @@ -999,7 +999,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-10", @@ -1009,7 +1009,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-11", @@ -1019,7 +1019,7 @@ "status": "active", "date": "2026-04-21", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" }, { "id": "R-12", @@ -1029,7 +1029,7 @@ "status": "active", "date": "2026-04-22", "file_path": "decisions/rejected.md", - "synced_at": "2026-05-06 21:51:08" + "synced_at": "2026-05-06 22:01:32" } ], "decision_refs": [ diff --git a/lib/src/terminal/src/core/cursor.dart b/lib/src/terminal/src/core/cursor.dart index e06c980b..741f3d67 100644 --- a/lib/src/terminal/src/core/cursor.dart +++ b/lib/src/terminal/src/core/cursor.dart @@ -81,7 +81,7 @@ class CursorStyle { bool get isFaint => (attrs & CellAttr.faint) != 0; - bool get isItalis => (attrs & CellAttr.italic) != 0; + bool get isItalic => (attrs & CellAttr.italic) != 0; bool get isUnderline => (attrs & CellAttr.underline) != 0; diff --git a/lib/src/terminal/src/core/reflow.dart b/lib/src/terminal/src/core/reflow.dart index 05451458..4812e8bb 100644 --- a/lib/src/terminal/src/core/reflow.dart +++ b/lib/src/terminal/src/core/reflow.dart @@ -16,8 +16,6 @@ class _LineBuilder { int get length => _length; - bool get isEmpty => _length == 0; - bool get isNotEmpty => _length != 0; /// Adds a range of cells from [src] to the builder. Anchors within the range diff --git a/pubspec.yaml b/pubspec.yaml index 2bd5f6da..1750723c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: 52 +coverage_floor: 54 # Project metadata (was project.yaml, folded in per D-056). # version: above is the single source of truth. The Makefile reads diff --git a/test/terminal/core/core_test.dart b/test/terminal/core/core_test.dart new file mode 100644 index 00000000..f67c9b92 --- /dev/null +++ b/test/terminal/core/core_test.dart @@ -0,0 +1,408 @@ +/// Pure-Dart tests for the small `lib/src/terminal/src/core/*.dart` +/// files — cell, cursor, charset, tabs, reflow. +library; + +import 'package:clide/src/terminal/src/core/buffer/line.dart'; +import 'package:clide/src/terminal/src/core/cell.dart'; +import 'package:clide/src/terminal/src/core/charset.dart'; +import 'package:clide/src/terminal/src/core/cursor.dart'; +import 'package:clide/src/terminal/src/core/reflow.dart'; +import 'package:clide/src/terminal/src/core/tabs.dart'; +import 'package:clide/src/terminal/src/utils/circular_buffer.dart'; +import 'package:test/test.dart'; + +void main() { + group('CellData', () { + test('explicit constructor stores all four channels', () { + final c = CellData(foreground: 1, background: 2, flags: 4, content: 8); + expect(c.foreground, 1); + expect(c.background, 2); + expect(c.flags, 4); + expect(c.content, 8); + }); + + test('CellData.empty factory zeroes everything', () { + final c = CellData.empty(); + expect(c.foreground, 0); + expect(c.background, 0); + expect(c.flags, 0); + expect(c.content, 0); + }); + + test('getHash combines all four channels (different inputs → different hash)', () { + final a = CellData(foreground: 1, background: 2, flags: 3, content: 4); + final b = CellData(foreground: 1, background: 2, flags: 3, content: 4); + expect(a.getHash(), b.getHash()); + final c = CellData(foreground: 1, background: 2, flags: 3, content: 5); + expect(a.getHash(), isNot(c.getHash())); + }); + + test('toString shape is the documented debug format', () { + final c = CellData(foreground: 1, background: 2, flags: 4, content: 8); + expect( + c.toString(), + 'CellData{foreground: 1, background: 2, flags: 4, content: 8}', + ); + }); + }); + + group('CursorStyle', () { + test('default constructor zeroes all three fields', () { + final c = CursorStyle(); + expect(c.foreground, 0); + expect(c.background, 0); + expect(c.attrs, 0); + }); + + test('every set / unset attr toggles the right CellAttr bit', () { + final c = CursorStyle(); + // Pair each setter with its corresponding getter and unsetter. + final cases = <(void Function(), void Function(), bool Function())>[ + (c.setBold, c.unsetBold, () => c.isBold), + (c.setFaint, c.unsetFaint, () => c.isFaint), + (c.setItalic, c.unsetItalic, () => c.isItalic), + (c.setUnderline, c.unsetUnderline, () => c.isUnderline), + (c.setBlink, c.unsetBlink, () => c.isBlink), + (c.setInverse, c.unsetInverse, () => c.isInverse), + (c.setInvisible, c.unsetInvisible, () => c.isInvisible), + ]; + for (final (set, unset, getter) in cases) { + c.attrs = 0; + expect(getter(), isFalse); + set(); + expect(getter(), isTrue); + unset(); + expect(getter(), isFalse); + } + // Strikethrough has no getter — exercise its set / unset directly. + c.attrs = 0; + c.setStrikethrough(); + expect(c.attrs & CellAttr.strikethrough, isNot(0)); + c.unsetStrikethrough(); + expect(c.attrs & CellAttr.strikethrough, 0); + }); + + test('foreground 16 / 256 / RGB encode the right CellColor type bits', () { + final c = CursorStyle(); + c.setForegroundColor16(3); + expect(c.foreground, 3 | CellColor.named); + c.setForegroundColor256(200); + expect(c.foreground, 200 | CellColor.palette); + c.setForegroundColorRgb(0x10, 0x20, 0x30); + expect(c.foreground, 0x10 << 16 | 0x20 << 8 | 0x30 | CellColor.rgb); + c.resetForegroundColor(); + expect(c.foreground, 0); + }); + + test('background 16 / 256 / RGB encode the right CellColor type bits', () { + final c = CursorStyle(); + c.setBackgroundColor16(5); + expect(c.background, 5 | CellColor.named); + c.setBackgroundColor256(123); + expect(c.background, 123 | CellColor.palette); + c.setBackgroundColorRgb(1, 2, 3); + expect(c.background, 1 << 16 | 2 << 8 | 3 | CellColor.rgb); + c.resetBackgroundColor(); + expect(c.background, 0); + }); + + test('reset wipes all three fields', () { + final c = CursorStyle() + ..setBold() + ..setForegroundColor16(2) + ..setBackgroundColor256(99); + c.reset(); + expect(c.foreground, 0); + expect(c.background, 0); + expect(c.attrs, 0); + }); + + test('CursorStyle.empty exposes a default-zero singleton', () { + // Don't mutate this — it's a shared singleton. + expect(CursorStyle.empty.foreground, 0); + expect(CursorStyle.empty.background, 0); + expect(CursorStyle.empty.attrs, 0); + }); + }); + + group('CursorPosition', () { + test('exposes mutable x / y fields', () { + final p = CursorPosition(3, 5); + expect(p.x, 3); + expect(p.y, 5); + p.x = 7; + p.y = 9; + expect(p.x, 7); + expect(p.y, 9); + }); + }); + + group('Charset', () { + test('asciiTranslator is identity', () { + expect(asciiTranslator(0x41), 0x41); + expect(asciiTranslator(0x7e), 0x7e); + }); + + test('decSpecGraphicsTranslator maps the documented codepoints', () { + // 0x6a → BOX DRAWINGS LIGHT UP AND LEFT (0x2518) + expect(decSpecGraphicsTranslator(0x6a), 0x2518); + // 0x71 → BOX DRAWINGS LIGHT HORIZONTAL (0x2500) + expect(decSpecGraphicsTranslator(0x71), 0x2500); + }); + + test('decSpecGraphicsTranslator passes unmapped low-codepoints through', () { + expect(decSpecGraphicsTranslator(0x41), 0x41); // 'A' is unmapped + }); + + test('decSpecGraphicsTranslator passes high codepoints through', () { + // The spec only maps 0x5f..0x7e; anything ≥ 127 is identity. + expect(decSpecGraphicsTranslator(0x100), 0x100); + expect(decSpecGraphicsTranslator(0x4E2D), 0x4E2D); + }); + + test('translate defaults to ascii when no charset is designated', () { + final c = Charset(); + expect(c.translate(0x6a), 0x6a); + }); + + test('designate + use switch the active translator', () { + final c = Charset(); + // Designate the DEC special-graphics charset at slot 0… + c.designate(0, '0'.codeUnitAt(0)); + // …and select slot 0 as the active charset. + c.use(0); + expect(c.translate(0x6a), 0x2518); // now maps via DEC graphics + // Switch back to a slot with no designated charset → ascii fallback. + c.use(1); + expect(c.translate(0x6a), 0x6a); + }); + + test('designate ignores unknown charset names without changing state', () { + final c = Charset(); + c.designate(0, 0xFFFF); // not in the _charsets table + c.use(0); + expect(c.translate(0x6a), 0x6a); // still ascii + }); + + test('save / restore round-trips both the map and the active index', () { + final c = Charset(); + c.designate(0, '0'.codeUnitAt(0)); + c.use(0); + c.save(); + // Mutate after save… + c.designate(0, 'B'.codeUnitAt(0)); // ascii in slot 0 — translate is identity + c.use(1); + expect(c.translate(0x6a), 0x6a); + // …restore should reinstate the saved DEC-graphics binding. + c.restore(); + expect(c.translate(0x6a), 0x2518); + }); + }); + + group('TabStops', () { + test('default state has tab stops every 8 columns', () { + final t = TabStops(); + for (var i = 0; i < 100; i++) { + expect(t.isSetAt(i), i % 8 == 0, reason: 'col $i'); + } + }); + + test('find — returns first set stop in [start, end)', () { + final t = TabStops(); + // Default: 0, 8, 16, 24, ... + expect(t.find(1, 20), 8); + expect(t.find(8, 20), 8); + expect(t.find(9, 16), isNull); // no stops in [9, 16) + expect(t.find(0, 1), 0); + }); + + test('find returns null when start >= end', () { + final t = TabStops(); + expect(t.find(10, 10), isNull); + expect(t.find(20, 5), isNull); + }); + + test('find clamps end to the underlying array length', () { + final t = TabStops(); + // 99999 > _kMaxColumns; should still return a valid stop without + // walking out of bounds. + expect(t.find(0, 99999), 0); + }); + + test('setAt + clearAt toggle individual columns', () { + final t = TabStops(); + t.clearAt(8); + expect(t.isSetAt(8), isFalse); + t.setAt(3); + expect(t.isSetAt(3), isTrue); + t.clearAt(3); + expect(t.isSetAt(3), isFalse); + }); + + test('clearAll wipes every stop without restoring the default grid', () { + final t = TabStops(); + t.clearAll(); + for (var i = 0; i < 100; i++) { + expect(t.isSetAt(i), isFalse, reason: 'col $i'); + } + }); + + test('reset clears then re-installs the default 8-column grid', () { + final t = TabStops(); + t.setAt(3); + t.clearAt(8); + t.reset(); + // 3 should no longer be set; 8 should be set again. + expect(t.isSetAt(3), isFalse); + expect(t.isSetAt(8), isTrue); + }); + }); + + group('reflow', () { + BufferLine line(int width, [String? text]) { + final l = BufferLine(width); + if (text != null) { + for (var i = 0; i < text.length; i++) { + l.setCodePoint(i, text.codeUnitAt(i)); + } + } + return l; + } + + IndexAwareCircularBuffer ring(List ls) { + final r = IndexAwareCircularBuffer(ls.length + 4); + for (final l in ls) { + r.push(l); + } + return r; + } + + test('empty input produces empty output', () { + final out = reflow(ring([]), 80, 80); + expect(out, isEmpty); + }); + + test('single empty line is reused as-is', () { + final l = line(8); + final out = reflow(ring([l]), 8, 8); + expect(out.length, 1); + expect(identical(out.first, l), isTrue); + }); + + test('grow width keeps lines and resizes them to the new width', () { + final out = reflow(ring([line(4, 'abcd')]), 4, 8); + expect(out, hasLength(1)); + expect(out.first.length, 8); + expect(out.first.getCodePoint(0), 'a'.codeUnitAt(0)); + expect(out.first.getCodePoint(3), 'd'.codeUnitAt(0)); + }); + + test('shrink width splits a single full line into wrapped pieces', () { + // 8 chars on a width-8 line, reflowed to width 4 → two width-4 lines. + final out = reflow(ring([line(8, 'abcdefgh')]), 8, 4); + expect(out, hasLength(2)); + expect(out[0].getCodePoint(0), 'a'.codeUnitAt(0)); + expect(out[0].isWrapped, isFalse); // first line of a logical run + expect(out[1].getCodePoint(0), 'e'.codeUnitAt(0)); + expect(out[1].isWrapped, isTrue); + }); + + test('continues a wrapped run across input lines and re-emits wrapped output', () { + // Two width-4 lines forming a single logical line "abcdefgh", reflowed + // back to width 8 → a single line of 8 chars. + final a = line(4, 'abcd'); + final b = line(4, 'efgh')..isWrapped = true; + final out = reflow(ring([a, b]), 4, 8); + expect(out, hasLength(1)); + expect(out.first.getCodePoint(0), 'a'.codeUnitAt(0)); + expect(out.first.getCodePoint(7), 'h'.codeUnitAt(0)); + }); + + test('shrink across a wide char does not split it across two output lines', () { + // Layout on a width-6 input: 'ab中cd' — the wide char straddles + // columns 2..3. Reflow to width 4 → first line is 'ab' + space (the + // wide char wouldn't fit), second line carries the wide char + 'cd'. + final l = BufferLine(6); + l.setCodePoint(0, 'a'.codeUnitAt(0)); + l.setCodePoint(1, 'b'.codeUnitAt(0)); + l.setCodePoint(2, '中'.runes.first); // width 2 → occupies 2..3 + l.setCodePoint(4, 'c'.codeUnitAt(0)); + l.setCodePoint(5, 'd'.codeUnitAt(0)); + + final out = reflow(ring([l]), 6, 4); + expect(out.length, greaterThanOrEqualTo(2)); + // First output line should not have started its last cell on the wide + // char's first half — i.e. cell index 3 should be empty (width-clamp + // branch fired during the initial fill). + expect(out[0].getCodePoint(3), 0); + }); + + test('shrink with a wide char at the new boundary takes the from=newWidth-1 path', () { + // The branch at reflow.dart:88 only fires when the cell at the new + // boundary (newWidth-1) is itself the first half of a wide char. + final l = BufferLine(6); + l.setCodePoint(0, 'a'.codeUnitAt(0)); + l.setCodePoint(1, 'b'.codeUnitAt(0)); + l.setCodePoint(2, 'c'.codeUnitAt(0)); + l.setCodePoint(3, '中'.runes.first); // wide → straddles 3..4 + l.setCodePoint(5, 'd'.codeUnitAt(0)); + + // newWidth=4 → boundary cell is 3, which is the wide char's first half. + final out = reflow(ring([l]), 6, 4); + expect(out.length, greaterThanOrEqualTo(2)); + // The wide char should not survive as a half-cell on the first line. + 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. + 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))); + }); + + test('inner wide-char clamp during _addPart leaves the wide cell to the next iteration', () { + // The clamp at reflow.dart:124 fires when the iteration is about to + // fill the builder AND the last cell would be the first half of a + // wide char — _addPart pushes that copy to the next iteration. + final src = BufferLine(9); + for (var i = 0; i < 7; i++) { + src.setCodePoint(i, 'a'.codeUnitAt(0) + i); + } + // Set wide at index 7 (straddles 7..8). + src.setCodePoint(7, '中'.runes.first); + final out = reflow(ring([src]), 9, 4); + expect(out.length, greaterThanOrEqualTo(2)); + }); + + test('anchors on the source line get reparented to the reflowed output', () { + // CellAnchor.attached delegates to its owning BufferLine.attached + // (which itself reflects whether the line sits in an + // IndexAwareCircularBuffer). Reflow returns a plain List, so the + // output lines aren't "attached" — but anchors should still point + // to one of the lines in the result. + final src = line(8, 'abcdefgh'); + final anchorAtStart = src.createAnchor(0); + final anchorMid = src.createAnchor(5); + final out = reflow(ring([src]), 8, 4); + expect(out, hasLength(2)); + + // anchorAtStart was at x=0 on the original line; reflow's first + // step is `_lines.add(line)`, so the original line is the first + // output. The anchor should still point to that same instance. + expect(out.contains(anchorAtStart.line), isTrue); + + // anchorMid was at x=5; the [4..8) tail is split out via _addPart + // and lands on the second output line, with the anchor reparented + // there. + expect(out.contains(anchorMid.line), isTrue); + expect(anchorMid.line, isNot(equals(anchorAtStart.line))); + }); + }); +}