From 8924478b39774a11ae76e24124f0f11735bd9b39 Mon Sep 17 00:00:00 2001 From: JavaZeroo <71128095+JavaZeroo@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:50:58 +0800 Subject: [PATCH] feat(core): seal retained row storage behind an owner publication path refs #169 Screen::lines/history become private with read-only accessors; mark_order_unknown is removed because no bypass write exists any more. 23 files migrate 363 read sites, test fixtures and the r164 probe publish through publish(), and the Find overlay crops by document rank instead of numeric identity. --- docs/EVIDENCE.md | 1 + experiments/core/alternate_mode_test.cpp | 46 +++--- experiments/core/buffer_state_test.cpp | 88 ++++++------ experiments/core/candidate_terminal.cpp | 12 +- experiments/core/candidate_terminal_test.cpp | 132 +++++++++--------- experiments/core/decawm_mode_test.cpp | 30 ++-- experiments/core/history_test.cpp | 108 +++++++------- experiments/core/history_view.hpp | 12 +- experiments/core/order_query_test.cpp | 2 +- experiments/core/resize_repaint_witness.cpp | 4 +- experiments/core/resize_test.cpp | 32 ++--- .../results/2026-10-29-order-query/README.md | 2 + .../2026-10-29-sealed-row-storage/README.md | 34 +++++ .../validation.json | 44 ++++++ experiments/core/screen.hpp | 14 +- experiments/core/search_test.cpp | 16 +-- experiments/core/selection_test.cpp | 22 +-- experiments/core/trace.cpp | 6 +- experiments/measure/measure.cpp | 16 +-- experiments/presentation/render.hpp | 2 +- experiments/shell/search_interactions.hpp | 19 ++- experiments/shell/shell.cpp | 4 +- src/qt/resize_smoke.cpp | 6 +- src/qt/shell_smoke.cpp | 8 +- src/qt/terminal_widget.hpp | 2 +- src/qt/unicode_input_test.cpp | 2 +- 26 files changed, 381 insertions(+), 283 deletions(-) create mode 100644 experiments/core/results/2026-10-29-sealed-row-storage/README.md create mode 100644 experiments/core/results/2026-10-29-sealed-row-storage/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index e3e15839..a3497484 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -67,6 +67,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | 范围 | 结果与复现入口 | 可以支持的结论与限制 | |---|---|---| +| 行存储封存与结构单一发布(#169 第二片) | [固定结果](../experiments/core/results/2026-10-29-sealed-row-storage/README.md)、[验证身份](../experiments/core/results/2026-10-29-sealed-row-storage/validation.json) | `Screen::lines/history` 私有,只读口 + 23 文件/363 处迁移;Find overlay 改文档 rank;默认 Release 构建 exit 0、核心 8/8、扩展快测 20/20、candidate 源 /W4 编译 0。无新 VT/重排行为;运行中跨重排 Search、Qt 与候选运行由宿主作业复核。 | | 默认核心缩小与重绘交错(#156) | [固定四-case机制诊断](../experiments/shell/results/2026-10-29-resize-repaint-witness/README.md)、[脱敏验证](../experiments/shell/results/2026-10-29-resize-repaint-witness/validation.json) | A/C保留2499B,B/D在第五段重绘首次失效且行分配路径不同;只见证机制,原CI切点/根因未知,未修生产。witness结束后Job仍active1,发起一次终止后归零;成员身份/逐句柄退出未知,不称全树自然退出。 | | 持续 cmd 输出与原始 Ctrl-C 回归(#36 未关闭) | [受监督入口](../experiments/conpty/README.md#持续-cmd-输出与-ctrlc-独立回归)、[三次 fresh 公开记录及本地字节复核](../experiments/conpty/results/2026-10-26-ctrl-c-retest/validation.json) | 原百万次输出、单次 ETX、五秒期限和退出 37 保留;三次 fresh 原始字节/Job0 本地重算。后续成功未解释历史失败,未改产品 Ctrl-C 路径。 | | 持续输出中断的原失败记录 | [#36](https://github.com/JavaZeroo/termforge/issues/36)、[ConPTY 输入机制研究](research/conpty-ctrl-c-input.md)、[原 R1 历史证据](../experiments/shell/results/2026-10-23-r1-foundations/validation.json) | 原 Windows 11 回归中的 normal/resize 超时与 ETW/插桩观察;调试扰动及管道读完成缺口保留。不是当前全套结果,后续未复现不能抹去此失败。 | diff --git a/experiments/core/alternate_mode_test.cpp b/experiments/core/alternate_mode_test.cpp index 2094c859..3cf2d045 100644 --- a/experiments/core/alternate_mode_test.cpp +++ b/experiments/core/alternate_mode_test.cpp @@ -25,10 +25,10 @@ static void enter_and_restore(ParseMode mode) { check(parser.screen.row == 0 && parser.screen.column == 0 && parser.screen.style.fg == -1 && parser.screen.style.bg == -1, "1049 entry did not home the cursor with default writing colours"); - check(text(parser.screen.lines[0]) == " ", "alternate entry retained primary text"); + check(text(parser.screen.visible_lines()[0]) == " ", "alternate entry retained primary text"); parser.feed("ALT\x1b[?1049l"); check(parser.screen.active_buffer() == ScreenBuffer::primary && - text(parser.screen.lines[0]) == "MAIN " && + text(parser.screen.visible_lines()[0]) == "MAIN " && parser.screen.row == 0 && parser.screen.column == 4, "1049 exit did not restore primary text and cursor"); check(parser.screen.style.fg == 1 && parser.screen.style.bg == 4, @@ -41,18 +41,18 @@ static void repeated_entry_and_exit(ParseMode mode) { check(parser.screen.row == 0 && parser.screen.column == 0 && parser.screen.style.fg == -1 && parser.screen.style.bg == -1, "repeated 1049 entry did not home with default writing colours"); - check(text(parser.screen.lines[0]) == " " && text(parser.screen.lines[2]) == " ", + check(text(parser.screen.visible_lines()[0]) == " " && text(parser.screen.visible_lines()[2]) == " ", "repeated 1049 entry retained old alternate text"); parser.feed("NEW\x1b[?1049l!\x1b[?1049l?"); - check(text(parser.screen.lines[0]) == "MAIN!? " && parser.screen.column == 6, + check(text(parser.screen.visible_lines()[0]) == "MAIN!? " && parser.screen.column == 6, "repeated entry overwrote primary state or repeated exit rewound it"); check(parser.screen.style.fg == 1 && parser.screen.style.bg == 4, "alternate colour writes changed saved primary colours"); parser.feed("\x1b[?1049h"); - check(text(parser.screen.lines[0]) == " " && parser.screen.column == 0, + check(text(parser.screen.visible_lines()[0]) == " " && parser.screen.column == 0, "1049 re-entry retained text from the earlier alternate visit"); parser.feed("\x1b[?1049l"); - check(text(parser.screen.lines[0]) == "MAIN!? " && parser.screen.column == 6, + check(text(parser.screen.visible_lines()[0]) == "MAIN!? " && parser.screen.column == 6, "later primary writes were not preserved across re-entry"); parser.finish(); } @@ -60,11 +60,11 @@ static void pending_wrap_roundtrip(ParseMode mode) { Parser parser(25, 91, 2, mode); parser.feed("\x1b[3;91HW\x1b[?1049hALT\x1b[?1049lX"); check(parser.screen.row == 3 && parser.screen.column == 1 && - text(parser.screen.lines[2]) == std::string(90, ' ') + "W" && - text(parser.screen.lines[3]) == "X" + std::string(90, ' '), + text(parser.screen.visible_lines()[2]) == std::string(90, ' ') + "W" && + text(parser.screen.visible_lines()[3]) == "X" + std::string(90, ' '), "return lost the primary pending wrap before the next printable byte"); - check(parser.screen.lines[2].ending == LineEnding::soft_wrap && - parser.screen.lines[2].next_id == parser.screen.lines[3].id, + check(parser.screen.visible_lines()[2].ending == LineEnding::soft_wrap && + parser.screen.visible_lines()[2].next_id == parser.screen.visible_lines()[3].id, "restored pending wrap did not preserve the real continuation"); parser.finish(); } @@ -85,24 +85,24 @@ static void observed_colour_trace(ParseMode mode, size_t split, bool bytewise) { const auto& screen = parser.screen; check(screen.active_buffer() == ScreenBuffer::primary && screen.row == 3 && screen.column == 34, "five-stage trace ended in the wrong primary cursor state"); - check(text(screen.lines[1]) == " D0 H H" + std::string(83, ' ') && - text(screen.lines[3]) == " M0 H H M1 H M1S1 H M1S2 H M1D0 H" + std::string(57, ' '), + check(text(screen.visible_lines()[1]) == " D0 H H" + std::string(83, ' ') && + text(screen.visible_lines()[3]) == " M0 H H M1 H M1S1 H M1S2 H M1D0 H" + std::string(57, ' '), "five-stage trace did not preserve the literal primary text"); - check(screen.lines[3][12].text == "H" && screen.lines[3][12].style.fg == 1 && - screen.lines[3][12].style.bg == 4 && screen.lines[3][33].style.fg == -1 && - screen.lines[3][33].style.bg == -1, + check(screen.visible_lines()[3][12].text == "H" && screen.visible_lines()[3][12].style.fg == 1 && + screen.visible_lines()[3][12].style.bg == 4 && screen.visible_lines()[3][33].style.fg == -1 && + screen.visible_lines()[3][33].style.bg == -1, "return output did not use restored main colours and the explicit default control"); } static void history_and_resize(ParseMode mode) { Parser parser(2, 8, 3, mode); parser.feed("A\r\nB\r\nC\r\nD\x1b[?1049hX\r\nY\r\nZ\r\nT"); - check(parser.screen.history.empty(), "alternate output entered retained history"); + check(parser.screen.history_lines().empty(), "alternate output entered retained history"); parser.screen.resize(3, 6); parser.feed("\x1b[?1049l"); const auto& screen = parser.screen; - check(screen.height() == 3 && screen.width() == 6 && screen.history.size() == 2 && - text(screen.history[0]) == "A " && text(screen.history[1]) == "B " && - text(screen.lines[0]) == "C " && text(screen.lines[1]) == "D " && + check(screen.height() == 3 && screen.width() == 6 && screen.history_lines().size() == 2 && + text(screen.history_lines()[0]) == "A " && text(screen.history_lines()[1]) == "B " && + text(screen.visible_lines()[0]) == "C " && text(screen.visible_lines()[1]) == "D " && screen.row == 1 && screen.column == 1, "1049 output or resize changed the saved primary history and writing position"); parser.finish(); @@ -151,7 +151,7 @@ static void recovery_keeps_private_sequences_separate() { parser.feed(sequence + "\x1b[31mR"); parser.finish(); check(parser.screen.active_buffer() == ScreenBuffer::primary && - text(parser.screen.lines[0]) == "R " && parser.screen.lines[0][0].style.fg == 1 && + text(parser.screen.visible_lines()[0]) == "R " && parser.screen.visible_lines()[0][0].style.fg == 1 && parser.diagnostics().ignored_sequences == 1 && parser.diagnostics().max_sequence_bytes <= 33, "an unsupported private sequence mutated a buffer or contaminated the next CSI"); } @@ -160,12 +160,12 @@ static void recovery_keeps_private_sequences_separate() { parser.feed(std::string("\x1b[?1049") + cancellation + "\x1b[31mR"); parser.finish(); check(parser.screen.active_buffer() == ScreenBuffer::primary && - text(parser.screen.lines[0]) == "R " && parser.screen.lines[0][0].style.fg == 1, + text(parser.screen.visible_lines()[0]) == "R " && parser.screen.visible_lines()[0][0].style.fg == 1, "cancellation or ESC resynchronization retained the private prefix"); } Parser truncated(2, 8, 1, ParseMode::recovery); truncated.feed("\x1b[?1049"); truncated.finish(); truncated.feed("\x1b[31mR"); truncated.finish(); - check(truncated.screen.active_buffer() == ScreenBuffer::primary && truncated.screen.lines[0][0].style.fg == 1, + check(truncated.screen.active_buffer() == ScreenBuffer::primary && truncated.screen.visible_lines()[0][0].style.fg == 1, "truncated private input contaminated the next normal CSI"); } static void identical_repeated_text_is_a_new_document(ParseMode mode) { @@ -188,7 +188,7 @@ static void strict_rejects_unsupported_private_modes_atomically() { try { parser.feed(input); } catch (const InputError&) { rejected = true; } check(rejected && parser.screen.active_buffer() == ScreenBuffer::primary && - text(parser.screen.lines[0]) == "MAIN " && + text(parser.screen.visible_lines()[0]) == "MAIN " && parser.screen.style.fg == 1 && parser.screen.style.bg == 4, "strict private dispatch accepted an unsupported command or partially applied its modes"); } diff --git a/experiments/core/buffer_state_test.cpp b/experiments/core/buffer_state_test.cpp index e5ae7fbe..f17fbccc 100644 --- a/experiments/core/buffer_state_test.cpp +++ b/experiments/core/buffer_state_test.cpp @@ -28,27 +28,27 @@ static void independent_text_and_cursor() { Parser parser(2, 8, 2); parser.feed("MAIN\r\nBASE\r\nEND"); auto& screen = parser.screen; - const auto main_top = screen.lines.front().id; - const auto main_bottom = screen.lines.back().id; + const auto main_top = screen.visible_lines().front().id; + const auto main_bottom = screen.visible_lines().back().id; screen.activate_buffer(ScreenBuffer::alternate); parser.feed("ALT"); screen.move(1, 1); - const auto alternate_top = screen.lines.front().id; + const auto alternate_top = screen.visible_lines().front().id; screen.activate_buffer(ScreenBuffer::primary); check(screen.active_buffer() == ScreenBuffer::primary && screen.row == 1 && screen.column == 3, "primary cursor was not restored"); - check(screen.history.size() == 1 && row_text(screen.history.front()) == "MAIN " && - row_text(screen.lines[0]) == "BASE " && row_text(screen.lines[1]) == "END ", + check(screen.history_lines().size() == 1 && row_text(screen.history_lines().front()) == "MAIN " && + row_text(screen.visible_lines()[0]) == "BASE " && row_text(screen.visible_lines()[1]) == "END ", "primary retained text was changed by alternate output"); - check(screen.lines.front().id == main_top && screen.lines.back().id == main_bottom, + check(screen.visible_lines().front().id == main_top && screen.visible_lines().back().id == main_bottom, "activation replaced saved primary row identities"); check(alternate_top > main_bottom, "alternate rows reused primary row identities"); parser.feed("!"); screen.activate_buffer(ScreenBuffer::alternate); - check(screen.row == 1 && screen.column == 1 && row_text(screen.lines.front()) == "ALT ", + check(screen.row == 1 && screen.column == 1 && row_text(screen.visible_lines().front()) == "ALT ", "alternate text or cursor was not preserved"); screen.activate_buffer(ScreenBuffer::primary); - check(row_text(screen.lines.back()) == "END! ", "alternate activation overwrote later primary output"); + check(row_text(screen.visible_lines().back()) == "END! ", "alternate activation overwrote later primary output"); } static void writing_context_roundtrip() { Parser parser(2, 4, 2); @@ -63,12 +63,12 @@ static void writing_context_roundtrip() { screen.style.fg == 1 && screen.style.bg == 4, "activation lost the saved writing context"); parser.feed("\xcc\x81"); - check(screen.lines[0][3].text == "D\xcc\x81" && screen.lines[0][3].marks == 1, + check(screen.visible_lines()[0][3].text == "D\xcc\x81" && screen.visible_lines()[0][3].marks == 1, "restored combining attachment did not target its saved base"); parser.feed("E"); - check(row_text(screen.lines[0]) == "ABCD\xcc\x81" && row_text(screen.lines[1]) == "E " && - screen.lines[0].ending == LineEnding::soft_wrap && screen.lines[1][0].style.fg == 1 && - screen.lines[1][0].style.bg == 4, + check(row_text(screen.visible_lines()[0]) == "ABCD\xcc\x81" && row_text(screen.visible_lines()[1]) == "E " && + screen.visible_lines()[0].ending == LineEnding::soft_wrap && screen.visible_lines()[1][0].style.fg == 1 && + screen.visible_lines()[1][0].style.bg == 4, "restored pending wrap did not resume with saved text and style"); screen.activate_buffer(ScreenBuffer::alternate); check(screen.row == 1 && screen.column == 1 && screen.style.fg == 2 && screen.style.bg == 5, @@ -78,42 +78,42 @@ static void bounded_independent_history() { Parser parser(2, 8, 2); parser.feed("ONE\r\nTWO\r\nTHREE\r\nFOUR\r\nFIVE"); auto& screen = parser.screen; - const auto main_last = screen.lines.back().id; + const auto main_last = screen.visible_lines().back().id; screen.activate_buffer(ScreenBuffer::alternate); parser.feed("A\r\nB\r\nC\r\nD\r\nE"); - const auto alternate_last = screen.lines.back().id; - check(screen.history.empty() && screen.discarded == 3, + const auto alternate_last = screen.visible_lines().back().id; + check(screen.history_lines().empty() && screen.discarded == 3, "alternate retention was not bounded independently of primary history"); check(alternate_last > main_last, "alternate scrolling recycled line identities"); screen.activate_buffer(ScreenBuffer::primary); - check(screen.history.size() == 2 && screen.discarded == 1 && - row_text(screen.history[0]) == "TWO " && row_text(screen.history[1]) == "THREE " && - row_text(screen.lines[0]) == "FOUR " && row_text(screen.lines[1]) == "FIVE ", + check(screen.history_lines().size() == 2 && screen.discarded == 1 && + row_text(screen.history_lines()[0]) == "TWO " && row_text(screen.history_lines()[1]) == "THREE " && + row_text(screen.visible_lines()[0]) == "FOUR " && row_text(screen.visible_lines()[1]) == "FIVE ", "alternate scrolling changed saved primary history or eviction state"); parser.feed("\r\nSIX"); - check(screen.history.size() == 2 && screen.discarded == 2 && - row_text(screen.history.front()) == "THREE " && screen.lines.back().id > alternate_last, + check(screen.history_lines().size() == 2 && screen.discarded == 2 && + row_text(screen.history_lines().front()) == "THREE " && screen.visible_lines().back().id > alternate_last, "primary scrolling lost its budget or global identity monotonicity"); } static void resize_both_frames_and_rollback() { Parser parser(3, 6, 2); parser.feed("A\xe4\xb8\xad" "B\r\nCDEF\r\nGHIJ"); auto& screen = parser.screen; - const auto main_first = screen.lines.front().id; - const auto main_second = screen.lines[1].id; - const auto main_third = screen.lines[2].id; + const auto main_first = screen.visible_lines().front().id; + const auto main_second = screen.visible_lines()[1].id; + const auto main_third = screen.visible_lines()[2].id; screen.activate_buffer(ScreenBuffer::alternate); parser.feed("ALT"); screen.move(2, 5); screen.resize(2, 3); - check(screen.height() == 2 && screen.width() == 3 && screen.history.empty() && screen.discarded == 1, + check(screen.height() == 2 && screen.width() == 3 && screen.history_lines().empty() && screen.discarded == 1, "alternate resize used primary retention"); screen.activate_buffer(ScreenBuffer::primary); check(screen.height() == 2 && screen.width() == 3 && screen.row == 1 && screen.column == 2 && - screen.history.size() == 1 && screen.history[0].id == main_first && - screen.lines[0].id == main_second && screen.lines[1].id == main_third && - row_text(screen.history[0]) == "A\xe4\xb8\xad" && - row_text(screen.lines[0]) == "CDE" && row_text(screen.lines[1]) == "GHI", + screen.history_lines().size() == 1 && screen.history_lines()[0].id == main_first && + screen.visible_lines()[0].id == main_second && screen.visible_lines()[1].id == main_third && + row_text(screen.history_lines()[0]) == "A\xe4\xb8\xad" && + row_text(screen.visible_lines()[0]) == "CDE" && row_text(screen.visible_lines()[1]) == "GHI", "resize did not preserve the hidden primary frame's physical-row policy"); const auto primary = snapshot(screen); screen.activate_buffer(ScreenBuffer::alternate); @@ -127,34 +127,34 @@ static void resize_both_frames_and_rollback() { screen.activate_buffer(ScreenBuffer::alternate); screen.resize(2, 2); screen.activate_buffer(ScreenBuffer::primary); - check(screen.history[0].size() == 2 && screen.history[0][1].width == 1 && - screen.history[0][1].text == " " && screen.history[0].cropped, + check(screen.history_lines()[0].size() == 2 && screen.history_lines()[0][1].width == 1 && + screen.history_lines()[0][1].text == " " && screen.history_lines()[0].cropped, "hidden-frame width crop left half of a wide glyph"); } static void growth_preserves_metadata_and_unique_ids() { Parser parser(3, 4, 2); parser.feed("ABCDxy\r\nZ"); auto& screen = parser.screen; - const auto soft = screen.lines[0], hard = screen.lines[1]; + const auto soft = screen.visible_lines()[0], hard = screen.visible_lines()[1]; screen.activate_buffer(ScreenBuffer::alternate); parser.feed("ALT"); - const auto alternate_last = screen.lines.back().id; + const auto alternate_last = screen.visible_lines().back().id; screen.resize(5, 6); - const auto alternate_new_first = screen.lines[3].id, alternate_new_last = screen.lines[4].id; + const auto alternate_new_first = screen.visible_lines()[3].id, alternate_new_last = screen.visible_lines()[4].id; check(alternate_new_first > alternate_last && alternate_new_last > alternate_new_first && - row_text(screen.lines[0]) == "ALT " && row_text(screen.lines.back()) == " ", + row_text(screen.visible_lines()[0]) == "ALT " && row_text(screen.visible_lines().back()) == " ", "alternate growth did not append fresh default rows"); screen.activate_buffer(ScreenBuffer::primary); TextDocument document(screen); - check(screen.lines[0].id == soft.id && screen.lines[0].version == soft.version + 1 && - screen.lines[0].ending == LineEnding::soft_wrap && screen.lines[0].next_id == soft.next_id && - screen.lines[0].wrap_columns == soft.wrap_columns && document.meta(0).joins_next && - screen.lines[1].id == hard.id && screen.lines[1].ending == LineEnding::hard_break && + check(screen.visible_lines()[0].id == soft.id && screen.visible_lines()[0].version == soft.version + 1 && + screen.visible_lines()[0].ending == LineEnding::soft_wrap && screen.visible_lines()[0].next_id == soft.next_id && + screen.visible_lines()[0].wrap_columns == soft.wrap_columns && document.meta(0).joins_next && + screen.visible_lines()[1].id == hard.id && screen.visible_lines()[1].ending == LineEnding::hard_break && !document.meta(1).joins_next, "hidden growth changed soft-wrap links or hard-break metadata"); - check(screen.lines[3].id > alternate_new_last && screen.lines[4].id > screen.lines[3].id && - row_text(screen.lines[0]) == "ABCD " && row_text(screen.lines[1]) == "xy " && - row_text(screen.lines.back()) == " ", + check(screen.visible_lines()[3].id > alternate_new_last && screen.visible_lines()[4].id > screen.visible_lines()[3].id && + row_text(screen.visible_lines()[0]) == "ABCD " && row_text(screen.visible_lines()[1]) == "xy " && + row_text(screen.visible_lines().back()) == " ", "two-frame growth reused ids or reflowed physical rows"); } static void hidden_resize_failure_rolls_back_both_frames() { @@ -168,7 +168,7 @@ static void hidden_resize_failure_rolls_back_both_frames() { const auto primary = snapshot(screen); // Reserve imported identities near exhaustion. Active growth can prepare // one row, but hidden growth cannot allocate its own row afterwards. - auto imported = screen.lines; + auto imported = screen.visible_lines(); imported.resize(1); imported[0].id = (std::numeric_limits::max)() - 2; screen.normalize_identities(imported); @@ -181,7 +181,7 @@ static void hidden_resize_failure_rolls_back_both_frames() { "hidden resize failure changed the alternate frame"); screen.activate_buffer(ScreenBuffer::primary); parser.feed("\r\nA\r\nB"); - check(screen.lines.back().id == (std::numeric_limits::max)() - 1, + check(screen.visible_lines().back().id == (std::numeric_limits::max)() - 1, "failed hidden resize consumed the shared identity allocator"); } static void positions_do_not_revive() { @@ -195,7 +195,7 @@ static void positions_do_not_revive() { screen.activate_buffer(ScreenBuffer::alternate); parser.feed("ALT"); screen.activate_buffer(ScreenBuffer::primary); - check(screen.lines[0].id == original.line_id && screen.lines[0].version == original.version, + check(screen.visible_lines()[0].id == original.line_id && screen.visible_lines()[0].version == original.version, "activation changed primary content identity to invalidate positions"); check(!document.valid(original), "old primary position revived after a buffer roundtrip"); const auto current = *document.position(0, 0); diff --git a/experiments/core/candidate_terminal.cpp b/experiments/core/candidate_terminal.cpp index 6633dbba..1397993c 100644 --- a/experiments/core/candidate_terminal.cpp +++ b/experiments/core/candidate_terminal.cpp @@ -147,14 +147,14 @@ std::unordered_set CandidateTerminal::reconcile_replay(std::vector{}, retained); std::unordered_map old_counts, new_counts; for (const auto& key : expected) ++old_counts[key]; for (const auto& key : actual) ++new_counts[key]; std::vector origin; - origin.insert(origin.end(), source.history.begin(), source.history.end()); - origin.insert(origin.end(), source.lines.begin(), source.lines.end()); + origin.insert(origin.end(), source.history_lines().begin(), source.history_lines().end()); + origin.insert(origin.end(), source.visible_lines().begin(), source.visible_lines().end()); std::unordered_set mapped; size_t origin_index = 0; for (auto& line : retained) { @@ -173,7 +173,7 @@ std::unordered_set CandidateTerminal::reconcile_replay(std::vector& retained, std::vector& original_ids, size_t& history, uint64_t& discarded) { if (!preserve_history_) return; const auto& source = replay_origin_ ? *replay_origin_ : screen; - std::deque previous = source.history; + std::deque previous = source.history_lines(); if (discarded > source.discarded) { const auto removed = discarded - source.discarded; if (removed >= previous.size()) previous.clear(); @@ -288,8 +288,8 @@ void CandidateTerminal::synchronize() { mode_bits_ = meta.mode_bits; grapheme_overflows_ = meta.grapheme_overflows; std::unordered_map previous; - for (const auto& line : screen.history) previous.emplace(line.id, &line); - for (const auto& line : screen.lines) previous.emplace(line.id, &line); + for (const auto& line : screen.history_lines()) previous.emplace(line.id, &line); + for (const auto& line : screen.visible_lines()) previous.emplace(line.id, &line); std::vector retained(meta.history + meta.rows); std::vector raw_ids; diff --git a/experiments/core/candidate_terminal_test.cpp b/experiments/core/candidate_terminal_test.cpp index 8f1082c0..d5d5a78f 100644 --- a/experiments/core/candidate_terminal_test.cpp +++ b/experiments/core/candidate_terminal_test.cpp @@ -35,17 +35,17 @@ void fragmentation() { for (char value : data) byte.feed(std::string_view(&value, 1)); whole.finish(); byte.finish(); require(state(whole) == state(byte), "fragmented candidate state differs"); - require(whole.screen.history[0][0].width == 1, "unexpected narrow cell"); - require(whole.screen.history[0][1].width == 2, "unexpected wide cell"); - require(whole.screen.history[0][2].width == 0 && whole.screen.history[0][2].written, + require(whole.screen.history_lines()[0][0].width == 1, "unexpected narrow cell"); + require(whole.screen.history_lines()[0][1].width == 2, "unexpected wide cell"); + require(whole.screen.history_lines()[0][2].width == 0 && whole.screen.history_lines()[0][2].written, "wide character spacer was not preserved as written"); - require(whole.screen.history[0][3].marks == 1, "combining mark missing"); - require(termforge::project_line(whole.screen.history[0]).text == "A中e\xCC\x81😀", + require(whole.screen.history_lines()[0][3].marks == 1, "combining mark missing"); + require(termforge::project_line(whole.screen.history_lines()[0]).text == "A中e\xCC\x81😀", "canonical text projection must omit wide spacer payloads and preserve literal scalars"); - for (const auto& cell : whole.screen.history[0]) + for (const auto& cell : whole.screen.history_lines()[0]) require(cell.width != 0 || cell.written, "unused cell was incorrectly treated as a wide spacer"); - require(whole.screen.history[0][7].width == 1 && whole.screen.history[0][7].text == " " && - !whole.screen.history[0][7].written, "unused cell must retain its paintable width"); + require(whole.screen.history_lines()[0][7].width == 1 && whole.screen.history_lines()[0][7].text == " " && + !whole.screen.history_lines()[0][7].written, "unused cell must retain its paintable width"); } void streaming_graphemes_selection_and_history() { @@ -57,7 +57,7 @@ void streaming_graphemes_selection_and_history() { else terminal.feed(input); }; feed("A" + zwj + keycap + "Z\r\n"); - const auto& row = terminal.screen.lines[0]; + const auto& row = terminal.screen.visible_lines()[0]; require(row[1].text == zwj && row[1].width == 2 && row[2].width == 0 && row[3].text == keycap && row[3].width == 2 && row[4].width == 0 && row[5].text == "Z", "streaming whole-cluster cell layout differs"); @@ -74,28 +74,28 @@ void streaming_graphemes_selection_and_history() { require(selection.reconcile(document) && selection.copy(document, 1024).text == zwj + keycap, "width-only resize lost whole grapheme selection"); feed("B\r\nC\r\nD"); - require(terminal.screen.history.size() == 1 && - termforge::project_line(terminal.screen.history[0]).text == "A" + zwj + keycap + "Z" && + require(terminal.screen.history_lines().size() == 1 && + termforge::project_line(terminal.screen.history_lines()[0]).text == "A" + zwj + keycap + "Z" && selection.reconcile(document) && selection.copy(document, 1024).text == zwj + keycap, "complex grapheme history or retained selection differs"); } termforge::CandidateTerminal clipped(3, 4, 8); clipped.feed("\x1b[?7lABC" + zwj); - require(clipped.screen.lines[0][3].text == zwj && clipped.screen.lines[0][3].width == 1 && - clipped.screen.lines[0][3].clipped, "clipped grapheme metadata or scalar retention differs"); + require(clipped.screen.visible_lines()[0][3].text == zwj && clipped.screen.visible_lines()[0][3].width == 1 && + clipped.screen.visible_lines()[0][3].clipped, "clipped grapheme metadata or scalar retention differs"); termforge::CandidateTerminal resized_one(3, 4, 8); resized_one.feed(zwj); resized_one.resize(3, 1); - require(resized_one.screen.lines[0][0].text == zwj && resized_one.screen.lines[0][0].width == 1 && - resized_one.screen.lines[0][0].clipped && resized_one.screen.row == 0 && + require(resized_one.screen.visible_lines()[0][0].text == zwj && resized_one.screen.visible_lines()[0][0].width == 1 && + resized_one.screen.visible_lines()[0][0].clipped && resized_one.screen.row == 0 && resized_one.screen.column == 0 && resized_one.screen.wrap_pending, "resize to one column lost text, clipping metadata or cursor"); termforge::CandidateTerminal bounded(3, 16, 8); std::string long_marks = "e"; for (int index = 0; index < 200; ++index) long_marks += "\xcc\x81"; bounded.feed(long_marks); - require(termforge::project_line(bounded.screen.lines[0]).text == long_marks && - bounded.grapheme_overflows() == 3 && bounded.screen.lines[0][1].grapheme_fragment, + require(termforge::project_line(bounded.screen.visible_lines()[0]).text == long_marks && + bounded.grapheme_overflows() == 3 && bounded.screen.visible_lines()[0][1].grapheme_fragment, "bounded grapheme spill discarded text or diagnostics"); termforge::TextDocument document(bounded.screen); termforge::TextSelection selection; @@ -111,7 +111,7 @@ void streaming_margin_padding_is_not_copied_as_an_inserted_space() { const std::string literal = prefix + keycap; if (fragmented) for (const char byte : literal) terminal.feed(std::string_view(&byte, 1)); else terminal.feed(literal); - require(terminal.screen.lines[0][3].leading_spacer == (prefix == "ABC"), + require(terminal.screen.visible_lines()[0][3].leading_spacer == (prefix == "ABC"), "actual written space and leading-wide padding were conflated"); termforge::TextDocument document(terminal.screen); termforge::TextSelection selection; @@ -119,12 +119,12 @@ void streaming_margin_padding_is_not_copied_as_an_inserted_space() { selection.copy(document, 1024).text == literal, "margin growth copy inserted padding or removed a literal space"); terminal.resize(3, 8); - require(termforge::project_line(terminal.screen.lines[0]).text == literal && + require(termforge::project_line(terminal.screen.visible_lines()[0]).text == literal && selection.select_line(document, 0) && selection.copy(document, 1024).text == literal, "resize copied padding or lost a true space"); terminal.feed("\r\nD\r\nE\r\nF"); - require(terminal.screen.history.size() == 1 && - termforge::project_line(terminal.screen.history[0]).text == literal && + require(terminal.screen.history_lines().size() == 1 && + termforge::project_line(terminal.screen.history_lines()[0]).text == literal && selection.select_line(document, 0) && selection.copy(document, 1024).text == literal, "history copied padding or lost original margin text"); } @@ -140,7 +140,7 @@ void prepend_and_inserted_wide_trailer_copy() { else prefix.feed(literal); termforge::TextDocument prefix_document(prefix.screen); termforge::TextSelection prefix_selection; - require(prefix.screen.lines[0][0].text == literal && prefix_selection.select_line(prefix_document,0) && + require(prefix.screen.visible_lines()[0][0].text == literal && prefix_selection.select_line(prefix_document,0) && prefix_selection.copy(prefix_document,1024).text == literal, "zero-width Prepend scalar disappeared from terminal or copied text"); for (const auto& pair : std::vector>{ @@ -149,7 +149,7 @@ void prepend_and_inserted_wide_trailer_copy() { termforge::CandidateTerminal terminal(3,8,8); terminal.feed("中ABCDEF\x1b[1;2H\x1b[4h"); const auto check_pairs = [&] { - const auto& line = terminal.screen.lines[0]; + const auto& line = terminal.screen.visible_lines()[0]; for (size_t column=0;column expected = { "TOP_FIXED", "REGION_C", " REGION_D", "BOTTOM_FIXED", "", "TF_MARGIN_DONE", }; for (size_t row = 0; row < expected.size(); ++row) - require(text(terminal.screen.lines[row]) == expected[row], "main scroll region was not restored"); - for (size_t row = expected.size(); row < terminal.screen.lines.size(); ++row) - require(text(terminal.screen.lines[row]).empty(), "unexpected content after margin fixture"); - require(terminal.screen.history.empty(), "per-buffer margins polluted main history"); + require(text(terminal.screen.visible_lines()[row]) == expected[row], "main scroll region was not restored"); + for (size_t row = expected.size(); row < terminal.screen.visible_lines().size(); ++row) + require(text(terminal.screen.visible_lines()[row]).empty(), "unexpected content after margin fixture"); + require(terminal.screen.history_lines().empty(), "per-buffer margins polluted main history"); require(terminal.screen.row == 6 && terminal.screen.column == 0 && !(terminal.mode_bits() & (1 << 12)), "margin fixture cursor or alternate mode differs"); } @@ -215,18 +215,18 @@ void margin_resize_and_reset() { resized.feed("\x1b[2;3r\x1b[?1049h\x1b[4;5r"); resized.resize(8, 8); resized.feed("\x1b[?1049l\x1b[1;1H1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9"); - require(resized.screen.history.size() == 1 && text(resized.screen.history[0]) == "1", + require(resized.screen.history_lines().size() == 1 && text(resized.screen.history_lines()[0]) == "1", "resize retained an inactive margin or incorrect history"); - for (size_t row = 0; row < resized.screen.lines.size(); ++row) - require(text(resized.screen.lines[row]) == std::to_string(row + 2), + for (size_t row = 0; row < resized.screen.visible_lines().size(); ++row) + require(text(resized.screen.visible_lines()[row]) == std::to_string(row + 2), "resize did not reset both scrolling regions"); termforge::CandidateTerminal reset(4, 8, 4); reset.feed("\x1b[2;3r\x1b[?1049h\x1b[3;4r\x1b" "c"); reset.feed("\x1b[?1049h1\r\n2\r\n3\r\n4\r\n5"); - require(reset.screen.history.empty() && text(reset.screen.lines[0]) == "2" && - text(reset.screen.lines[1]) == "3" && text(reset.screen.lines[2]) == "4" && - text(reset.screen.lines[3]) == "5", "RIS did not reset both scrolling regions"); + require(reset.screen.history_lines().empty() && text(reset.screen.visible_lines()[0]) == "2" && + text(reset.screen.visible_lines()[1]) == "3" && text(reset.screen.visible_lines()[2]) == "4" && + text(reset.screen.visible_lines()[3]) == "5", "RIS did not reset both scrolling regions"); } void empty_payload_events() { @@ -243,19 +243,19 @@ void empty_payload_events() { void identity_versions_and_eviction() { termforge::CandidateTerminal terminal(2, 4, 1); - const auto first = terminal.screen.lines[0].id; - const auto second = terminal.screen.lines[1].id; + const auto first = terminal.screen.visible_lines()[0].id; + const auto second = terminal.screen.visible_lines()[1].id; terminal.feed("A"); - require(terminal.screen.lines[0].id == first && terminal.screen.lines[0].version == 2, + require(terminal.screen.visible_lines()[0].id == first && terminal.screen.visible_lines()[0].version == 2, "cell update did not preserve identity and increment version"); terminal.feed(""); - require(terminal.screen.lines[0].version == 2, "empty feed incremented version"); + require(terminal.screen.visible_lines()[0].version == 2, "empty feed incremented version"); terminal.feed("\r\nB\r\nC\r\n"); - require(terminal.screen.history.size() == 1 && text(terminal.screen.history[0]) == "B", + require(terminal.screen.history_lines().size() == 1 && text(terminal.screen.history_lines()[0]) == "B", "bounded history retained wrong row"); require(terminal.screen.discarded == 1, "history eviction count is wrong"); - require(terminal.screen.history[0].id == second, "scrolled row lost its identity"); - require(terminal.screen.lines[1].id != first && terminal.screen.lines[1].version == 1, + require(terminal.screen.history_lines()[0].id == second, "scrolled row lost its identity"); + require(terminal.screen.visible_lines()[1].id != first && terminal.screen.visible_lines()[1].version == 1, "reused row did not receive a fresh identity"); termforge::TextDocument document(terminal.screen); require(document.index_of(second) == std::optional(0), "text document cannot resolve scrolled identity"); @@ -297,7 +297,7 @@ void replay_case(const std::filesystem::path& path, bool fragmented) { all_expected += "TF_FIND_READY 080\r\n"; const std::string expected = "TF_FIND_077 needle \xE4\xB8\xAD\xE6\x96\x87 e\xCC\x81"; const auto target = [&] { - for (const auto& line : terminal.screen.lines) + for (const auto& line : terminal.screen.visible_lines()) if (text(line).rfind("TF_FIND_077", 0) == 0) return line.id; return uint64_t{}; }; @@ -364,7 +364,7 @@ void replay_case(const std::filesystem::path& path, bool fragmented) { "restored history did not survive a subsequent synchronization"); terminal.feed(expected + "\r\n"); uint64_t duplicate = 0; - for (const auto& line : terminal.screen.lines) + for (const auto& line : terminal.screen.visible_lines()) if (text(line).rfind("TF_FIND_077", 0) == 0) duplicate = line.id; require(duplicate != 0 && duplicate != original, "duplicate text reused the original row identity"); } @@ -395,7 +395,7 @@ void incomplete_replay_is_bounded() { ending.feed("C"); ending.finish(); termforge::TextDocument after_finish(ending.screen); - require(text(ending.screen.lines[0]) == "ABC" && + require(text(ending.screen.visible_lines()[0]) == "ABC" && finish_selection.copy(after_finish, 1024).status == termforge::SelectionCopyStatus::invalidated, "finish did not release the actual state after an incomplete replay"); } @@ -403,12 +403,12 @@ void incomplete_replay_is_bounded() { void history_clear_does_not_resurrect() { termforge::CandidateTerminal terminal(2, 8, 4); terminal.feed("A\r\nB\r\nC\r\nD"); - require(terminal.screen.history.size() == 2, "clear-history fixture is missing retained rows"); + require(terminal.screen.history_lines().size() == 2, "clear-history fixture is missing retained rows"); terminal.resize(2, 9); terminal.feed("\x1b[?25l\x1b[?25h"); - require(terminal.screen.history.size() == 2, "completed resize did not establish retained history"); + require(terminal.screen.history_lines().size() == 2, "completed resize did not establish retained history"); terminal.feed("\x1b[3J"); - require(terminal.screen.history.empty(), "explicit history clear resurrected retained rows"); + require(terminal.screen.history_lines().empty(), "explicit history clear resurrected retained rows"); terminal.feed("E\r\nF"); termforge::TextDocument document(terminal.screen); for (size_t index = 0; index < document.size(); ++index) { @@ -429,8 +429,8 @@ void selection_search_and_viewport() { terminal.feed("alpha\r\nbeta\r\ngamma"); termforge::TextDocument document(terminal.screen); require(document.line(0).projection.text == "alpha", "candidate text projection differs"); - require(terminal.screen.history[0].id < terminal.screen.lines[0].id && - terminal.screen.lines[0].id < terminal.screen.lines[1].id, "candidate retained IDs are not ordered"); + require(terminal.screen.history_lines()[0].id < terminal.screen.visible_lines()[0].id && + terminal.screen.visible_lines()[0].id < terminal.screen.visible_lines()[1].id, "candidate retained IDs are not ordered"); termforge::TextSelection selection; const auto begin = document.position(0, 0), end = document.position(0, 5); require(begin && end, "candidate text positions were not generated"); @@ -446,49 +446,49 @@ void selection_search_and_viewport() { search.step(document, generation, 1); require(search.status() == termforge::SearchStatus::complete && search.matches().size() == 1 && search.match(document, 0), "candidate search did not resolve gamma"); - const auto alpha = terminal.screen.history[0].id, gamma = terminal.screen.lines[1].id; + const auto alpha = terminal.screen.history_lines()[0].id, gamma = terminal.screen.visible_lines()[1].id; terminal.resize(2, 16); const auto widened = search.match(document, 0); - require(terminal.screen.history[0].id == alpha && terminal.screen.lines[1].id == gamma && + require(terminal.screen.history_lines()[0].id == alpha && terminal.screen.visible_lines()[1].id == gamma && widened && widened->range.begin.line_id == gamma && selection.copy(document, 1024).text == "alpha", "width-only candidate resize invalidated unchanged hard-row text or identity"); termforge::HistoryViewport viewport; const auto top = viewport.scroll_to(terminal.screen, 0); - require(top.anchor == terminal.screen.history[0].id && !top.following, + require(top.anchor == terminal.screen.history_lines()[0].id && !top.following, "candidate history top anchor differs"); const auto bottom = viewport.bottom(terminal.screen); - require(bottom.following && bottom.anchor == terminal.screen.lines[0].id, + require(bottom.following && bottom.anchor == terminal.screen.visible_lines()[0].id, "candidate history bottom anchor differs"); } void linebreaks() { termforge::CandidateTerminal terminal(2, 3, 2); terminal.feed("abcd"); - const auto& row = terminal.screen.lines[0]; + const auto& row = terminal.screen.visible_lines()[0]; require(row.ending == termforge::LineEnding::soft_wrap && row.wrap_columns == 3, "soft wrap metadata missing"); - require(row.next_id == terminal.screen.lines[1].id && - termforge::continues_into(row, terminal.screen.lines[1]), "soft wrap continuation is broken"); + require(row.next_id == terminal.screen.visible_lines()[1].id && + termforge::continues_into(row, terminal.screen.visible_lines()[1]), "soft wrap continuation is broken"); terminal.feed("\r\n"); - require(terminal.screen.lines[0].ending == termforge::LineEnding::hard_break, + require(terminal.screen.visible_lines()[0].ending == termforge::LineEnding::hard_break, "hard break metadata missing"); } void scrolling_and_erase() { termforge::CandidateTerminal terminal(3, 8, 2); terminal.feed("A\r\nB\r\nC\r\nD\x1b[2;3r\x1b[3;1H\nX\x1b[r\x1b[1;1H"); - require(text(terminal.screen.lines[0]) == "B" && text(terminal.screen.lines[1]) == "D" && - text(terminal.screen.lines[2]) == "X", "scroll region result differs"); + require(text(terminal.screen.visible_lines()[0]) == "B" && text(terminal.screen.visible_lines()[1]) == "D" && + text(terminal.screen.visible_lines()[2]) == "X", "scroll region result differs"); termforge::CandidateTerminal erase(3, 12, 3); erase.feed("one\r\ntwo\x1b[2;1H\x1b[1J"); - require(text(erase.screen.lines[0]).empty() && text(erase.screen.lines[1]) == " wo", + require(text(erase.screen.visible_lines()[0]).empty() && text(erase.screen.visible_lines()[1]) == " wo", "ED1 did not clear page start through cursor"); termforge::CandidateTerminal clear(3, 12, 3); clear.feed("A\r\nB\r\nC\r\nD\x1b[2J"); - require(clear.screen.history.size() == 1 && text(clear.screen.history[0]) == "A", + require(clear.screen.history_lines().size() == 1 && text(clear.screen.history_lines()[0]) == "A", "ED2 unexpectedly migrated or cleared history"); } @@ -497,7 +497,7 @@ void resize_and_errors() { terminal.feed("abc中\r\n1234567890"); terminal.resize(2, 6); require(terminal.screen.height() == 2 && terminal.screen.width() == 6, "candidate resize dimensions differ"); - require(text(terminal.screen.lines[0]) == "123456" && text(terminal.screen.lines[1]) == "7890", + require(text(terminal.screen.visible_lines()[0]) == "123456" && text(terminal.screen.visible_lines()[1]) == "7890", "candidate reflow projection differs"); bool rejected = false; diff --git a/experiments/core/decawm_mode_test.cpp b/experiments/core/decawm_mode_test.cpp index 5ebed733..b09467a4 100644 --- a/experiments/core/decawm_mode_test.cpp +++ b/experiments/core/decawm_mode_test.cpp @@ -19,8 +19,8 @@ static void disabled_edge_overwrites(ParseMode mode) { const auto& screen = parser.screen; check(screen.row == 9 && screen.column == 90, "disabled edge CD must end at (10,91)"); - check(project_line(screen.lines[9]).text == std::string(90, ' ') + "D" && - project_line(screen.lines[10]).text.empty() && screen.history.empty(), + check(project_line(screen.visible_lines()[9]).text == std::string(90, ' ') + "D" && + project_line(screen.visible_lines()[10]).text.empty() && screen.history_lines().empty(), "disabled edge CD must retain only D without a wrapped successor"); } @@ -57,13 +57,13 @@ static void observed_prefix(ParseMode mode, size_t count, size_t split, bool byt const auto& last = phases[count - 1]; check(screen.row + 1 == last.cursor_row && screen.column + 1 == last.cursor_column, "observed prefix cursor differs from its literal DSR coordinates"); - check(screen.active_buffer() == ScreenBuffer::primary && screen.history.empty() && + check(screen.active_buffer() == ScreenBuffer::primary && screen.history_lines().empty() && screen.discarded == 0 && parser.diagnostics().ignored_sequences == 0, "a supported main-screen source changed buffers/history or was ignored"); for (size_t index = 0; index < count; ++index) { const auto& phase = phases[index]; - const auto& first = screen.lines[phase.first_row]; - const auto& second = screen.lines[phase.first_row + 1]; + const auto& first = screen.visible_lines()[phase.first_row]; + const auto& second = screen.visible_lines()[phase.first_row + 1]; const auto first_projection = project_line(first).text; const auto second_projection = project_line(second).text; check(first_projection == std::string(90, ' ') + phase.first_text && @@ -104,15 +104,15 @@ static void private_commands_are_atomic(ParseMode mode) { if (mode == ParseMode::strict) { check(rejected && parser.screen.active_buffer() == ScreenBuffer::primary && parser.screen.row == 9 && parser.screen.column == 90 && - project_line(parser.screen.lines[9]).text.empty(), + project_line(parser.screen.visible_lines()[9]).text.empty(), "strict private input was accepted or partially changed visible state"); } else { check(!rejected, "recovery failed instead of consuming unsupported private input"); parser.feed("CD"); parser.finish(); check(parser.screen.active_buffer() == ScreenBuffer::primary && parser.screen.row == 10 && parser.screen.column == 1 && - project_line(parser.screen.lines[9]).text == std::string(90, ' ') + "C" && - project_line(parser.screen.lines[10]).text == "D" && + project_line(parser.screen.visible_lines()[9]).text == std::string(90, ' ') + "C" && + project_line(parser.screen.visible_lines()[10]).text == "D" && parser.diagnostics().ignored_sequences == 1, "recovery partially applied private modes or contaminated the next printable bytes"); } @@ -125,7 +125,7 @@ static void recovery_drops_incomplete_mode_commands() { parser.feed(std::string("\x1b[?7") + cancellation + "\x1b[10;91HCD"); parser.finish(); check(parser.screen.row == 10 && parser.screen.column == 1 && - project_line(parser.screen.lines[10]).text == "D", + project_line(parser.screen.visible_lines()[10]).text == "D", "cancellation or ESC resynchronization applied an incomplete private mode"); } Parser parser(25, 91, 2, ParseMode::recovery); @@ -133,7 +133,7 @@ static void recovery_drops_incomplete_mode_commands() { parser.feed("\x1b[10;91HCD"); parser.finish(); check(parser.screen.row == 10 && parser.screen.column == 1 && parser.diagnostics().truncated_sequences == 1 && - project_line(parser.screen.lines[10]).text == "D", + project_line(parser.screen.visible_lines()[10]).text == "D", "EOF truncation applied a private mode or contaminated the next CSI"); } @@ -144,11 +144,11 @@ static void wide_fit_policy_is_preserved(ParseMode mode) { parser.feed("\x1b[?7l\x1b[1;3H\xe4\xb8\xad"); parser.finish(); check(parser.screen.row == 1 && parser.screen.column == 2 && - parser.screen.lines[1].size() == 3 && - parser.screen.lines[1][0].text == "\xe4\xb8\xad" && - parser.screen.lines[1][0].width == 2 && parser.screen.lines[1][1].width == 0 && - parser.screen.lines[1][2].text == " " && - project_line(parser.screen.lines[1]).text == "\xe4\xb8\xad" && + parser.screen.visible_lines()[1].size() == 3 && + parser.screen.visible_lines()[1][0].text == "\xe4\xb8\xad" && + parser.screen.visible_lines()[1][0].width == 2 && parser.screen.visible_lines()[1][1].width == 0 && + parser.screen.visible_lines()[1][2].text == " " && + project_line(parser.screen.visible_lines()[1]).text == "\xe4\xb8\xad" && parser.diagnostics().replaced_scalars == 0, "wide scalar fit fallback replaced text, overflowed a row, or lost its trailing cell"); } diff --git a/experiments/core/history_test.cpp b/experiments/core/history_test.cpp index 479cfef8..50495caf 100644 --- a/experiments/core/history_test.cpp +++ b/experiments/core/history_test.cpp @@ -21,76 +21,76 @@ static std::string metadata(const Screen& screen) { } static void identity_and_versions() { Parser p(2, 6, 2); - const auto first = p.screen.lines[0].id, second = p.screen.lines[1].id; + const auto first = p.screen.visible_lines()[0].id, second = p.screen.visible_lines()[1].id; check(first != 0 && second > first, "initial identities"); - auto version = p.screen.lines[0].version; - p.feed("abc"); check(p.screen.lines[0].version > version, "writes update version"); - version = p.screen.lines[0].version; - p.feed("\rX"); check(text(p.screen.lines[0]) == "Xbc" && p.screen.lines[0].version > version, + auto version = p.screen.visible_lines()[0].version; + p.feed("abc"); check(p.screen.visible_lines()[0].version > version, "writes update version"); + version = p.screen.visible_lines()[0].version; + p.feed("\rX"); check(text(p.screen.visible_lines()[0]) == "Xbc" && p.screen.visible_lines()[0].version > version, "CR overwrite invalidates old text without replacing row identity"); - check(p.screen.lines[0].id == first, "overwrite preserves identity"); - version = p.screen.lines[0].version; - p.feed("\x1b[K"); check(text(p.screen.lines[0]) == "X" && p.screen.lines[0].version > version, + check(p.screen.visible_lines()[0].id == first, "overwrite preserves identity"); + version = p.screen.visible_lines()[0].version; + p.feed("\x1b[K"); check(text(p.screen.visible_lines()[0]) == "X" && p.screen.visible_lines()[0].version > version, "erase updates text version"); - p.feed("e"); version = p.screen.lines[0].version; - p.feed("\xcc\x81"); check(p.screen.lines[0].version > version, "combining mark updates base row"); + p.feed("e"); version = p.screen.visible_lines()[0].version; + p.feed("\xcc\x81"); check(p.screen.visible_lines()[0].version > version, "combining mark updates base row"); p.feed("\r\nB\r\nC"); - check(p.screen.history[0].id == first && p.screen.lines[0].id == second, + check(p.screen.history_lines()[0].id == first && p.screen.visible_lines()[0].id == second, "scroll keeps existing row identities"); - const auto moved_version = p.screen.history[0].version; + const auto moved_version = p.screen.history_lines()[0].version; p.feed("\r\nD"); - check(p.screen.history[0].version == moved_version, "move into history never edits content"); + check(p.screen.history_lines()[0].version == moved_version, "move into history never edits content"); p.feed("\r\nE"); - check(p.screen.find_line(first) == nullptr && p.screen.history[0].id == second, + check(p.screen.find_line(first) == nullptr && p.screen.history_lines()[0].id == second, "evicted ids are absent and not recycled"); - const auto last = p.screen.lines.back().id; + const auto last = p.screen.visible_lines().back().id; p.screen.move(0, 0); p.screen.resize(1, 6); p.screen.resize(3, 6); - check(p.screen.lines[1].id > last && p.screen.lines[2].id > p.screen.lines[1].id, + check(p.screen.visible_lines()[1].id > last && p.screen.visible_lines()[2].id > p.screen.visible_lines()[1].id, "bottom-row clipping and growth never reuse identities"); } static void endings_and_projection() { Parser full(2, 4, 2); full.feed("abcd"); - check(full.screen.wrap_pending && full.screen.lines[0].ending == LineEnding::none, + check(full.screen.wrap_pending && full.screen.visible_lines()[0].ending == LineEnding::none, "full row with delayed wrap is not a soft line ending"); full.feed("\r\n"); - check(full.screen.lines[0].ending == LineEnding::hard_break && !continues_into(full.screen.lines[0], full.screen.lines[1]), + check(full.screen.visible_lines()[0].ending == LineEnding::hard_break && !continues_into(full.screen.visible_lines()[0], full.screen.visible_lines()[1]), "explicit CRLF after full row is hard newline"); Parser soft(3, 4, 2); soft.feed("abcdE"); - check(continues_into(soft.screen.lines[0], soft.screen.lines[1]) && soft.screen.lines[0].wrap_columns == 4, + check(continues_into(soft.screen.visible_lines()[0], soft.screen.visible_lines()[1]) && soft.screen.visible_lines()[0].wrap_columns == 4, "actual delayed wrap records successor and used cells"); - check(text(soft.screen.lines[0]) + text(soft.screen.lines[1]) == "abcdE", "soft row projection"); + check(text(soft.screen.visible_lines()[0]) + text(soft.screen.visible_lines()[1]) == "abcdE", "soft row projection"); Parser wide(2, 4, 2); wide.feed("abc\xe4\xb8\xad" "e\xcc\x81 "); - check(wide.screen.lines[0].ending == LineEnding::soft_wrap && wide.screen.lines[0].wrap_columns == 3, + check(wide.screen.visible_lines()[0].ending == LineEnding::soft_wrap && wide.screen.visible_lines()[0].wrap_columns == 3, "wide scalar wraps before unused final cell"); - auto projected = project_line(wide.screen.lines[1]); - check(text(wide.screen.lines[0]) == "abc" && projected.text == "\xe4\xb8\xad" "e\xcc\x81 ", + auto projected = project_line(wide.screen.visible_lines()[1]); + check(text(wide.screen.visible_lines()[0]) == "abc" && projected.text == "\xe4\xb8\xad" "e\xcc\x81 ", "explicit trailing spaces survive while wide-wrap gap is omitted"); check(projected.spans.size() == 3 && projected.spans[0].column_begin == 0 && projected.spans[0].column_end == 2 && projected.spans[0].byte_end == 3 && projected.spans[1].byte_begin == 3 && projected.spans[1].byte_end == 6, "UTF8 offsets map wide and combining text to whole cells"); Parser erased(2, 4); erased.feed("abcdE\x1b[1;1H\x1b[2K"); - check(erased.screen.lines[0].ending == LineEnding::none && text(erased.screen.lines[0]).empty(), + check(erased.screen.visible_lines()[0].ending == LineEnding::none && text(erased.screen.visible_lines()[0]).empty(), "erasing row end invalidates continuation"); - const auto identity = soft.screen.lines[0].id, version = soft.screen.lines[0].version; + const auto identity = soft.screen.visible_lines()[0].id, version = soft.screen.visible_lines()[0].version; soft.screen.resize(3, 3); - check(soft.screen.lines[0].cropped && soft.screen.lines[0].id == identity && soft.screen.lines[0].version > version && - !continues_into(soft.screen.lines[0], soft.screen.lines[1]), "resize clipping explicitly breaks logical continuity"); + check(soft.screen.visible_lines()[0].cropped && soft.screen.visible_lines()[0].id == identity && soft.screen.visible_lines()[0].version > version && + !continues_into(soft.screen.visible_lines()[0], soft.screen.visible_lines()[1]), "resize clipping explicitly breaks logical continuity"); soft.screen.resize(3, 8); - check(text(soft.screen.lines[0]) == "abc" && soft.screen.lines[0].cropped, "growth cannot restore cropped bytes"); - Line unrelated = soft.screen.lines[1]; unrelated.id += 1000; - check(!continues_into(soft.screen.lines[0], unrelated), "continuation requires exact successor"); + check(text(soft.screen.visible_lines()[0]) == "abc" && soft.screen.visible_lines()[0].cropped, "growth cannot restore cropped bytes"); + Line unrelated = soft.screen.visible_lines()[1]; unrelated.id += 1000; + check(!continues_into(soft.screen.visible_lines()[0], unrelated), "continuation requires exact successor"); } static void overwritten_wrap_gap() { Parser p(2, 4, 2); p.feed("abc\xe4\xb8\xad"); - const auto identity = p.screen.lines[0].id, version = p.screen.lines[0].version; + const auto identity = p.screen.visible_lines()[0].id, version = p.screen.visible_lines()[0].version; p.feed("\x1b[1;4HX"); - check(text(p.screen.lines[0]) == "abcX" && text(p.screen.lines[1]) == "\xe4\xb8\xad", + check(text(p.screen.visible_lines()[0]) == "abcX" && text(p.screen.visible_lines()[1]) == "\xe4\xb8\xad", "writing the former wide-wrap gap must remain visible in text projection"); - check(p.screen.lines[0].id == identity && p.screen.lines[0].version > version && - continues_into(p.screen.lines[0], p.screen.lines[1]), + check(p.screen.visible_lines()[0].id == identity && p.screen.visible_lines()[0].version > version && + continues_into(p.screen.visible_lines()[0], p.screen.visible_lines()[1]), "cursor-addressed edits change content without inventing or removing a wrap event"); - const auto projected = project_line(p.screen.lines[0]); + const auto projected = project_line(p.screen.visible_lines()[0]); check(projected.spans.size() == 4 && projected.spans[3].column_begin == 3 && projected.spans[3].column_end == 4 && projected.spans[3].byte_begin == 3 && projected.spans[3].byte_end == 4, "rewritten gap retains its byte-to-cell mapping"); @@ -122,11 +122,11 @@ static void wrap_projection_across_chunks() { Parser p(2, 4, 2, mode); for (size_t i = 0; i < input.size(); i += chunk) p.feed(input.substr(i, chunk)); p.finish(); - check(text(p.screen.lines[0]) == item.first && - project_line(p.screen.lines[0], false).text == item.untrimmed && - text(p.screen.lines[1]) == "\xe4\xb8\xad", + check(text(p.screen.visible_lines()[0]) == item.first && + project_line(p.screen.visible_lines()[0], false).text == item.untrimmed && + text(p.screen.visible_lines()[1]) == "\xe4\xb8\xad", "wrap/edit/erase projection differs from literal text across input chunks"); - check(continues_into(p.screen.lines[0], p.screen.lines[1]) == item.continues, + check(continues_into(p.screen.visible_lines()[0], p.screen.visible_lines()[1]) == item.continues, "partial erase preserves continuation; end erase or explicit LF removes it"); } } @@ -137,37 +137,37 @@ static void resized_wrap_projection() { grown.screen.resize(2, 4); check(metadata(grown.screen) == before, "no-op resize preserves wrap metadata"); grown.screen.resize(2, 7); - check(project_line(grown.screen.lines[0], false).text == "abc" && - continues_into(grown.screen.lines[0], grown.screen.lines[1]), + check(project_line(grown.screen.visible_lines()[0], false).text == "abc" && + continues_into(grown.screen.visible_lines()[0], grown.screen.visible_lines()[1]), "growth adds padding without changing the observed wrap boundary"); grown.feed("\x1b[1;7HZ"); - check(text(grown.screen.lines[0]) == "abc Z" && text(grown.screen.lines[1]) == "\xe4\xb8\xad" && - continues_into(grown.screen.lines[0], grown.screen.lines[1]), + check(text(grown.screen.visible_lines()[0]) == "abc Z" && text(grown.screen.visible_lines()[1]) == "\xe4\xb8\xad" && + continues_into(grown.screen.visible_lines()[0], grown.screen.visible_lines()[1]), "writing beyond a former row width stays visible without reflow"); grown.feed("\x1b[1;7H "); - check(text(grown.screen.lines[0]) == "abc ", "explicit spaces in grown columns survive projection"); + check(text(grown.screen.visible_lines()[0]) == "abc ", "explicit spaces in grown columns survive projection"); grown.feed("\x1b[1;7H\x1b[K"); - check(text(grown.screen.lines[0]) == "abc" && !continues_into(grown.screen.lines[0], grown.screen.lines[1]), + check(text(grown.screen.visible_lines()[0]) == "abc" && !continues_into(grown.screen.visible_lines()[0], grown.screen.visible_lines()[1]), "erasing the grown row end drops the continuation and unused padding"); Parser clipped(2, 4, 2); clipped.feed("abc\xe4\xb8\xad\x1b[1;3H\xe7\x95\x8c"); clipped.screen.resize(2, 3); - check(text(clipped.screen.lines[0]) == "ab" && project_line(clipped.screen.lines[0], false).text == "ab " && - !continues_into(clipped.screen.lines[0], clipped.screen.lines[1]), + check(text(clipped.screen.visible_lines()[0]) == "ab" && project_line(clipped.screen.visible_lines()[0], false).text == "ab " && + !continues_into(clipped.screen.visible_lines()[0], clipped.screen.visible_lines()[1]), "physical clipping removes the whole wide glyph and invalidates continuation"); clipped.screen.resize(2, 6); clipped.feed("\x1b[1;6HY"); - check(text(clipped.screen.lines[0]) == "ab Y" && text(clipped.screen.lines[1]) == "\xe4\xb8\xad" && - !continues_into(clipped.screen.lines[0], clipped.screen.lines[1]), + check(text(clipped.screen.visible_lines()[0]) == "ab Y" && text(clipped.screen.visible_lines()[1]) == "\xe4\xb8\xad" && + !continues_into(clipped.screen.visible_lines()[0], clipped.screen.visible_lines()[1]), "growth and later writes neither restore cropped text nor infer continuity"); Parser partial(2, 4, 2); partial.feed("abc\xe4\xb8\xad\x1b[1;"); partial.screen.resize(2, 6); partial.feed("5H\xe7"); partial.screen.resize(2, 7); partial.feed("\x95\x8c"); partial.finish(); - const auto projected = project_line(partial.screen.lines[0]); + const auto projected = project_line(partial.screen.visible_lines()[0]); check(projected.text == "abc \xe7\x95\x8c" && projected.spans.size() == 5 && projected.spans.back().column_begin == 4 && projected.spans.back().column_end == 6 && projected.spans.back().byte_begin == 4 && projected.spans.back().byte_end == 7 && - continues_into(partial.screen.lines[0], partial.screen.lines[1]), + continues_into(partial.screen.visible_lines()[0], partial.screen.visible_lines()[1]), "resize during partial CSI/UTF8 preserves edited wide text and coordinate spans"); } static void viewport_content() { @@ -204,7 +204,7 @@ static void viewport_content() { p.screen.resize(5, 9); reading = view.snapshot(p.screen); check(reading.lines.size() == 5 && text(reading.lines[0]) == "L4", "row and column growth preserve surviving anchor"); view.bottom(p.screen); reading = view.snapshot(p.screen); - check(reading.viewport.following && reading.viewport.top == p.screen.history.size() && reading.cursor_row == p.screen.row, + check(reading.viewport.following && reading.viewport.top == p.screen.history_lines().size() && reading.cursor_row == p.screen.row, "explicit bottom returns live cursor and latest viewport"); Parser no_history(1, 3, 0); HistoryViewport empty; no_history.feed("A\r\nB"); @@ -226,7 +226,7 @@ static void boundaries() { bounded.feed(std::to_string(i) + "\r\n"); if (i == 5) view.scroll_to(bounded.screen, 0); const auto state = view.reconcile(bounded.screen); - check(bounded.screen.history.size() <= 7 && state.total <= 9 && state.top <= state.maximum_top, + check(bounded.screen.history_lines().size() <= 7 && state.total <= 9 && state.top <= state.maximum_top, "bounded retention and viewport range under continuous output"); } std::set identities; @@ -234,7 +234,7 @@ static void boundaries() { const auto& line = bounded.screen.retained_line(i); check(identities.insert(line.id).second && line.id > 4990, "no identity reuse after sustained eviction"); } - check(text(bounded.screen.history.front()) == "4992" && text(bounded.screen.lines.front()) == "4999", + check(text(bounded.screen.history_lines().front()) == "4992" && text(bounded.screen.visible_lines().front()) == "4999", "independent retained-number oracle"); const auto before = metadata(bounded.screen); bool rejected = false; try { bounded.screen.resize(0, 1); } catch (const std::invalid_argument&) { rejected = true; } diff --git a/experiments/core/history_view.hpp b/experiments/core/history_view.hpp index e3c91ac2..2bb50091 100644 --- a/experiments/core/history_view.hpp +++ b/experiments/core/history_view.hpp @@ -27,20 +27,20 @@ class HistoryViewport { // A live anchor uses its exact rank; an evicted anchor keeps the existing // ordered-history fallback (first retained row at or after the id). size_t anchor_top(const Screen& screen) const { - if (const auto index = screen.locate(anchor_)) return (std::min)(*index, screen.history.size()); - return (std::min)(screen.lower_bound_line(anchor_), screen.history.size()); + if (const auto index = screen.locate(anchor_)) return (std::min)(*index, screen.history_lines().size()); + return (std::min)(screen.lower_bound_line(anchor_), screen.history_lines().size()); } ViewportState reconcile(const Screen& screen) { sync_epoch(screen); - const size_t maximum = screen.history.size(); + const size_t maximum = screen.history_lines().size(); const size_t top = following_ ? maximum : anchor_top(screen); anchor_ = screen.retained_line(top).id; return {top, screen.retained_size(), static_cast(screen.height()), maximum, anchor_, following_}; } ViewportState scroll_to(const Screen& screen, size_t top) { sync_epoch(screen); - top = (std::min)(top, screen.history.size()); - following_ = top == screen.history.size(); + top = (std::min)(top, screen.history_lines().size()); + following_ = top == screen.history_lines().size(); anchor_ = screen.retained_line(top).id; return reconcile(screen); } @@ -57,7 +57,7 @@ class HistoryViewport { } return scroll_to(screen, top); } - ViewportState bottom(const Screen& screen) { return scroll_to(screen, screen.history.size()); } + ViewportState bottom(const Screen& screen) { return scroll_to(screen, screen.history_lines().size()); } ViewportState reveal(const Screen& screen, uint64_t identity) { if (const auto index = screen.locate(identity)) return scroll_to(screen, *index); return scroll_to(screen, screen.lower_bound_line(identity)); diff --git a/experiments/core/order_query_test.cpp b/experiments/core/order_query_test.cpp index 80826388..0e21bc50 100644 --- a/experiments/core/order_query_test.cpp +++ b/experiments/core/order_query_test.cpp @@ -95,7 +95,7 @@ static void publication_keeps_the_cursor_inside_the_frame() { screen.publish({}, {row(4, 40, 'a')}); check(screen.row == 0 && screen.column == 3, "a shorter published frame clamps only the lost row"); screen.put('b', "b"); - check(screen.lines[0][3].text == "b", "writes after a shrinking publication stay in range"); + check(screen.visible_lines()[0][3].text == "b", "writes after a shrinking publication stay in range"); } static void publication_respects_the_active_history_budget() { diff --git a/experiments/core/resize_repaint_witness.cpp b/experiments/core/resize_repaint_witness.cpp index e46edfd5..fe13b667 100644 --- a/experiments/core/resize_repaint_witness.cpp +++ b/experiments/core/resize_repaint_witness.cpp @@ -135,7 +135,7 @@ CaseResult run_case(Case item, std::string_view transport) { out << ",\"transport_consumed_bytes\":" << offset << ",\"grid_columns\":" << parser.screen.width() << ",\"grid_rows\":" << parser.screen.height() << ",\"cursor_row\":" << parser.screen.row << ",\"cursor_column\":" << parser.screen.column << ",\"wrap_pending\":" << (parser.screen.wrap_pending ? "true" : "false") - << ",\"history_rows\":" << parser.screen.history.size() << ",\"document_rows\":" << document.size() + << ",\"history_rows\":" << parser.screen.history_lines().size() << ",\"document_rows\":" << document.size() << ",\"discarded_rows\":" << parser.screen.discarded << ",\"buffer_epoch\":" << document.epoch() << ",\"range_before_reconcile\":"; range(out, before); out << ",\"reconciled\":" << (reconciled ? "true" : "false") << ",\"range_after_reconcile\":"; @@ -160,7 +160,7 @@ CaseResult run_case(Case item, std::string_view transport) { }; parser.feed("\x1b[2J\x1b[H" + expected); checkpoint("seed-feed", "original-fixture-body"); - require(expected.size() == 2499 && parser.screen.history.size() == 75 && + require(expected.size() == 2499 && parser.screen.history_lines().size() == 75 && parser.screen.retained_size() == 82 && parser.screen.row == 6 && parser.screen.column == 0, "initial fixture shape differs; stop without altering seed or identities"); require(select_all(selection, TextDocument(parser.screen)), "initial Select All range was rejected"); diff --git a/experiments/core/resize_test.cpp b/experiments/core/resize_test.cpp index 75421fc8..46fec42a 100644 --- a/experiments/core/resize_test.cpp +++ b/experiments/core/resize_test.cpp @@ -21,8 +21,8 @@ static void invariant(const termforge::Screen& s) { } } }; - lines(s.lines); lines(s.history); - check(s.history.size() <= s.history_limit, "history bound"); + lines(s.visible_lines()); lines(s.history_lines()); + check(s.history_lines().size() <= s.history_limit, "history bound"); } static std::string first_two_rows(termforge::Parser& parser, size_t end_column) { termforge::TextDocument document(parser.screen); @@ -58,36 +58,36 @@ int main() { complete_erasure_removes_only_obsolete_crop_provenance(); Parser p(3, 4, 2); p.feed("abcdEF\r\nGH"); p.screen.resize(3, 3); - check(text(p.screen.lines[0]) == "abc" && text(p.screen.lines[1]) == "EF " && - text(p.screen.lines[2]) == "GH ", "soft and hard rows do not reflow"); + check(text(p.screen.visible_lines()[0]) == "abc" && text(p.screen.visible_lines()[1]) == "EF " && + text(p.screen.visible_lines()[2]) == "GH ", "soft and hard rows do not reflow"); p.screen.resize(4, 6); - check(text(p.screen.lines[0]) == "abc " && text(p.screen.lines[3]) == " ", "growth blanks, no restore"); + check(text(p.screen.visible_lines()[0]) == "abc " && text(p.screen.visible_lines()[3]) == " ", "growth blanks, no restore"); p.screen.resize(1, 6); - check(text(p.screen.lines[0]) == "GH " && p.screen.row == 0 && p.screen.history.size() == 2, "retain cursor via top history"); + check(text(p.screen.visible_lines()[0]) == "GH " && p.screen.row == 0 && p.screen.history_lines().size() == 2, "retain cursor via top history"); p.feed("\r\nnext"); p.screen.resize(2, 2); - check(p.screen.discarded == 1 && text(p.screen.history[0]) == "EF" && text(p.screen.history[1]) == "GH", "history eviction and width"); - check(text(p.screen.lines[1]) == " ", "growth never pulls history"); + check(p.screen.discarded == 1 && text(p.screen.history_lines()[0]) == "EF" && text(p.screen.history_lines()[1]) == "GH", "history eviction and width"); + check(text(p.screen.visible_lines()[1]) == " ", "growth never pulls history"); Parser top(3, 4, 0); top.feed("AB\r\nCD\r\nEF\x1b[1;2H"); top.screen.resize(1, 4); - check(text(top.screen.lines[0]) == "AB " && top.screen.column == 1 && top.screen.discarded == 0, "drop bottom without scroll history"); + check(text(top.screen.visible_lines()[0]) == "AB " && top.screen.column == 1 && top.screen.discarded == 0, "drop bottom without scroll history"); Parser zero(3, 2, 0); zero.feed("A\r\nB\r\nC"); zero.screen.resize(1, 2); - check(zero.screen.discarded == 2 && text(zero.screen.lines[0]) == "C ", "zero history"); + check(zero.screen.discarded == 2 && text(zero.screen.visible_lines()[0]) == "C ", "zero history"); Parser wide(2, 4, 2); wide.feed("A\xe4\xb8\xad\xcc\x81"); wide.screen.resize(2, 2); - check(text(wide.screen.lines[0]) == "A " && wide.screen.lines[0][1].marks == 0, "clip wide including mark"); + check(text(wide.screen.visible_lines()[0]) == "A " && wide.screen.visible_lines()[0][1].marks == 0, "clip wide including mark"); wide.screen.resize(2, 1); invariant(wide.screen); Parser historical(1, 4, 2); historical.feed("A\xe4\xb8\xad\r\nZ"); historical.screen.resize(1, 2); - check(text(historical.screen.history[0]) == "A ", "wide history truncation"); invariant(historical.screen); + check(text(historical.screen.history_lines()[0]) == "A ", "wide history truncation"); invariant(historical.screen); Parser narrow(2, 3, 0, termforge::ParseMode::recovery); narrow.screen.resize(2, 1); narrow.feed("\xe4\xb8\xad"); narrow.finish(); invariant(narrow.screen); check(narrow.diagnostics().replaced_scalars == 1, "one-column wide recovery"); Parser keep(2, 4, 2); keep.feed("\x1b[31m\xe4\xb8\xad\xcc\x81"); keep.screen.resize(3, 5); - check(keep.screen.lines[0][0].width == 2 && keep.screen.lines[0][0].marks == 1 && - keep.screen.lines[0][0].style.fg == 1 && keep.screen.lines[2][0].style.plain(), "preserve style and marks, default blanks"); + check(keep.screen.visible_lines()[0][0].width == 2 && keep.screen.visible_lines()[0][0].marks == 1 && + keep.screen.visible_lines()[0][0].style.fg == 1 && keep.screen.visible_lines()[2][0].style.plain(), "preserve style and marks, default blanks"); Parser wrap(2, 2); wrap.feed("ab"); wrap.screen.resize(2, 2); check(wrap.screen.wrap_pending, "same size is no-op"); wrap.screen.resize(2, 3); wrap.feed("X"); - check(text(wrap.screen.lines[0]) == "aX " && !wrap.screen.wrap_pending, "clear delayed wrap on actual resize"); + check(text(wrap.screen.visible_lines()[0]) == "aX " && !wrap.screen.wrap_pending, "clear delayed wrap on actual resize"); Parser split(2, 4); split.feed("\xe4"); split.screen.resize(3, 5); split.feed("\xb8\xad\x1b[2;"); split.screen.resize(4, 6); split.feed("3HZ"); split.finish(); - check(text(split.screen.lines[1]) == " Z ", "preserve partial UTF8 and CSI parser state"); + check(text(split.screen.visible_lines()[1]) == " Z ", "preserve partial UTF8 and CSI parser state"); Parser attach(2, 4); attach.feed("a"); attach.screen.resize(2, 5); bool rejected = false; try { attach.feed("\xcc\x81"); } catch (const termforge::InputError&) { rejected = true; } check(rejected, "resize clears combining attachment"); diff --git a/experiments/core/results/2026-10-29-order-query/README.md b/experiments/core/results/2026-10-29-order-query/README.md index 4dfb5749..7f43cd59 100644 --- a/experiments/core/results/2026-10-29-order-query/README.md +++ b/experiments/core/results/2026-10-29-order-query/README.md @@ -1,5 +1,7 @@ # 行结构发布、精确身份查询与文档序(#169 首片) +> 后续切片(2026-10-29):存储已封为私有,`Screen::lines/history` 不再是公开成员;本片为“绕过发布口写入”保留的 `mark_order_unknown()` 因此没有调用者并被移除。见[封存行存储](../2026-10-29-sealed-row-storage/README.md)。 + 本片把“稳定行 ID 被当作文档顺序”的隐含假设收敛到 `Screen` 的 owner 发布口与精确查询:行身份只表示身份,位置由当前保留序列决定。没有新增 VT 模式、区域 LF 或任何用户可触发行为;`Screen::lines/history` 仍是公开成员,封住直接写入属于下一片。 ## 交付内容 diff --git a/experiments/core/results/2026-10-29-sealed-row-storage/README.md b/experiments/core/results/2026-10-29-sealed-row-storage/README.md new file mode 100644 index 00000000..7db1ddce --- /dev/null +++ b/experiments/core/results/2026-10-29-sealed-row-storage/README.md @@ -0,0 +1,34 @@ +# 封存行存储:结构只经 owner 发布(#169 第二片) + +首片(`85eb5a0`,PR #170)给出了 `publish()` / `locate()` / `order_revision()`,但 `Screen::lines` 与 `history` 仍是公开成员:任何调用方仍可直接改存储,绕过发布口的写入会留下陈旧 rank。本片把这条路径关掉。 + +## 交付内容 + +| 变更 | 说明 | +|---|---| +| 存储私有化 | `Screen::lines` / `history` 移入 private;新增只读口 `visible_lines()`、`history_lines()`、`history_size()`,函数注释明确“短生命周期、不可跨结构变化保留” | +| 移除逃生口 | 首片为“绕过发布口的直接写入”保留的 `mark_order_unknown()` 在存储私有后没有调用者,已删除;结构只经 `resize` / `newline` / `activate_buffer` / `publish` / `restore_frame` 改变 | +| 调用方迁移 | 23 个文件、363 处只读访问改经只读口;`selection_test` 的 3 处身份替换与 r164 乱序夹具改经 `publish()`;`measure`、`render`、`trace`、shell 场景与 Qt 源码同步迁移 | +| 真实 caller:Find overlay | `ShellWindow::search_overlay` 的“匹配是否落在可见范围/覆盖哪些行”由数值 ID 比较改为文档 rank(可见首行 rank = `view.viewport.top`,匹配端点用 `TextDocument::index_of`)。这是 issue 明确列出的数值 ID 裁剪迁移 | +| 回放/导入 | `CandidateTerminal` 的回放恢复仍经 `restore_frame()`、导入仍经 `publish()`;本片只跟随只读口迁移,不改其策略 | + +r164 的冻结研究树(`experiments/core/line_order`,由 `prepare.py` 以固定源码再生)**不随主线迁移**:它编译对照的是冻结的旧 `screen.hpp`,改动会破坏可复现性,故保持原样。 + +## 验证 + +环境:Windows 11 10.0.26200、Visual Studio 17 2022 / MSVC 19.42、Release、Qt 与 candidate 关闭;隔离 worktree 构建。 + +- 默认 Release 全量构建 exit 0(`/W4` 无告警输出)。 +- 核心 CTest 8/8:`order_query_core`、`resize_core`、`history_core`、`selection_core`、`search_core`、`buffer_state_core`、`alternate_mode_core`、`decawm_mode_core`。 +- 扩展快测 20/20(另含 `core`、`recovery`、`binary_paths`、`text_actions`、`keyboard`、`paste`、`clipboard_policy`、`text_action_policy`、`presentation`、`measure`、`scenario_tick_gate`、`transport_completion`)。 +- `candidate_terminal.cpp` 以 MSVC `/W4 /std:c++17` 独立编译 exit 0(本机未启用候选适配器;候选链接与运行由 PR 的 `alacritty-headless` 作业复核)。 +- Qt 相关三处只读访问已迁移,但 Qt 目标未在本机构建(隔离 worktree 内无 SDK),由 PR 的 `qt-cpp-windows` 作业复核。 + +固定身份见 [validation.json](validation.json)。 + +## 边界 + +- 仍无用户可触发的重排、无区域 LF、无新 VT 模式;运行中跨重排的增量 Search 未验收(issue 已明确该条件另行验收)。 +- 有序增量 Find 的 `emit_from_id_` / `prune` / `rewind` 区段假设未改。 +- 本片仅内部正确性改造:不声明 GUI/TUI、固定 ConPTY 运输、原版等价或任何性能结论。 +- 私有化不改变 `Screen` 的可复制语义;副本仍各自拥有存储与顺序。 diff --git a/experiments/core/results/2026-10-29-sealed-row-storage/validation.json b/experiments/core/results/2026-10-29-sealed-row-storage/validation.json new file mode 100644 index 00000000..cdf2ee5e --- /dev/null +++ b/experiments/core/results/2026-10-29-sealed-row-storage/validation.json @@ -0,0 +1,44 @@ +{ + "schema": "termforge-core-sealed-row-storage-v1", + "issue": 169, + "base_commit": "85eb5a0bccfa085e6b85a07b1831df326a886163", + "scope": "Screen 行存储私有化、只读访问口、调用方迁移与 Find overlay 文档 rank 化;无新 VT/重排行为", + "configuration": { + "platform": "Windows-11-10.0.26200", + "generator": "Visual Studio 17 2022", + "compiler": "MSVC 19.42", + "build_type": "Release", + "qt_shell": false, + "candidate_adapter": false + }, + "commands": [ + "cmake -S . -B build -G \"Visual Studio 17 2022\" -A x64", + "cmake --build build --config Release --parallel 4", + "ctest --test-dir build -C Release -R \"^(order_query|resize|history|selection|search|buffer_state|alternate_mode|decawm_mode)_core$\" --output-on-failure", + "ctest --test-dir build -C Release -R \"^(order_query|resize|history|selection|search|buffer_state|alternate_mode|decawm_mode)_core$|^core$|^recovery$|^binary_paths$|^text_actions$|^keyboard$|^paste$|^clipboard_policy$|^text_action_policy$|^presentation$|^measure$|^scenario_tick_gate$|^transport_completion$\" --output-on-failure", + "cl /nologo /std:c++17 /W4 /EHsc /DNOMINMAX /DWIN32_LEAN_AND_MEAN /I experiments/core /c experiments/core/candidate_terminal.cpp" + ], + "results": { + "default_release_build": "exit 0, no /W4 diagnostics", + "core_ctest_8": "8/8 passed", + "extended_fast_ctest_20": "20/20 passed", + "candidate_terminal_compile_only": "exit 0 (/W4)", + "qt_build": "not run locally; covered by PR qt-cpp-windows" + }, + "source_sha256": { + "experiments/core/screen.hpp": "382cb73ba66a01166b56d58bd84bc587017dd38c8a0011d1bbd3bb785bfede8c", + "experiments/core/history_view.hpp": "0ffcb7a2af554cbb7767f0cb8d1b95962470f67b5d6cbf8057c75703f96ad29e", + "experiments/core/candidate_terminal.cpp": "452808b39c2eaac1dcca0b4164c58cff44f13ef8e27eae7d8e23a22895ec13ff", + "experiments/shell/search_interactions.hpp": "1a4523174c7999c720eff22bf9043df9f165c1ee487d7ecc9944c82432350ddc", + "experiments/measure/measure.cpp": "bb4cbfd8bb6620deb48ab510e5948190443d2a6bbfa6d4d8f16eebf404dbc517", + "experiments/presentation/render.hpp": "0966402f84784b2da95ae2d722fdc724621a121dd9a31710088a1f554d44ffa4" + }, + "limitations": [ + "无用户可达重排;运行中跨重排的增量 Search 另行验收", + "有序增量 Find 的 emit_from_id_/prune/rewind 区段假设未改", + "r164 冻结研究树 experiments/core/line_order 未迁移,保持与冻结源码一致", + "Qt 目标与候选适配器未在本机构建运行;由 PR 的宿主作业复核", + "不构成 GUI/TUI、固定 ConPTY、原版等价或性能结论" + ], + "performance_claim": false +} diff --git a/experiments/core/screen.hpp b/experiments/core/screen.hpp index cee00fe4..74deab6c 100644 --- a/experiments/core/screen.hpp +++ b/experiments/core/screen.hpp @@ -75,6 +75,10 @@ class Screen { }; // Permission belongs to the terminal; each frame retains its own edge state. bool autowrap_enabled_ = true; + // Retained storage. Only the owner changes its membership or order: + // resize, newline, activate_buffer, publish() and restore_frame(). + std::vector lines; + std::deque history; int columns_; uint64_t next_id_ = 1; uint64_t buffer_epoch_ = 0; @@ -173,8 +177,6 @@ class Screen { bool wrap_pending = false; size_t discarded = 0; Style style; - std::vector lines; - std::deque history; const size_t history_limit; Screen(int rows, int columns, size_t limit) : columns_(columns), history_limit(limit) { if (rows < 1 || rows > 200 || columns < 1 || columns > 500 || limit > 10000) @@ -219,6 +221,11 @@ class Screen { ++buffer_epoch_; advance_order(ordered_); } + // Short-lived, read-only views. Callers must not retain them across a + // structure change; copy rows they need as values. + const std::vector& visible_lines() const { return lines; } + const std::deque& history_lines() const { return history; } + size_t history_size() const { return history.size(); } size_t retained_size() const { return history.size() + lines.size(); } const Line& retained_line(size_t index) const { if (index >= retained_size()) throw std::out_of_range("retained line index"); @@ -257,9 +264,6 @@ class Screen { if (retained_line(index).id == identity) return index; return std::nullopt; } - // A direct structural write that bypasses publish() must call this so later - // lookups scan instead of trusting a bisect that no longer holds. - void mark_order_unknown() { ordered_ = false; } // Restore a complete frame image captured from this same screen family // (replay). The restored order is not provably the active sequence any more, // so lookups fall back to scanning and readers lose their rank-derived state. diff --git a/experiments/core/search_test.cpp b/experiments/core/search_test.cpp index 38bc4fd9..8d12a48e 100644 --- a/experiments/core/search_test.cpp +++ b/experiments/core/search_test.cpp @@ -19,10 +19,10 @@ static void basic_matches() { const auto generation = search.start(doc, "needle"); finish(search, doc, generation); const auto& matches = search.matches(); check(search.status() == SearchStatus::complete && matches.size() == 2, "two literal matches"); - check(matches[0].range.begin.line_id == parser.screen.lines[0].id && + check(matches[0].range.begin.line_id == parser.screen.visible_lines()[0].id && matches[0].range.begin.column == 4 && matches[0].range.end.column == 10, "first literal match maps to known cells"); - check(matches[1].range.begin.line_id == parser.screen.lines[1].id && + check(matches[1].range.begin.line_id == parser.screen.visible_lines()[1].id && matches[1].range.begin.column == 0 && matches[1].range.end.column == 6, "second literal match maps to known cells"); } @@ -31,7 +31,7 @@ static void wraps_newlines_and_unicode() { const TextDocument soft_doc(soft.screen); TextSearch search; finish(search, soft_doc, search.start(soft_doc, "LEFTRIGHT"), 1); check(search.matches().size() == 1 && search.matches()[0].range.begin.column == 0 && - search.matches()[0].range.end.line_id == soft.screen.lines[2].id && + search.matches()[0].range.end.line_id == soft.screen.visible_lines()[2].id && search.matches()[0].range.end.column == 1, "literal search crosses actual soft wraps"); Parser hard(3, 4, 2); hard.feed("LEFT\r\nRIGHT"); const TextDocument hard_doc(hard.screen); @@ -158,7 +158,7 @@ static void changing_text_and_eviction() { Parser moved(3, 8, 2); moved.feed("needle\r\nlast\r\nthird"); const TextDocument moved_doc(moved.screen); finish(search, moved_doc, search.start(moved_doc, "needle"), 1); - const auto retained_id = moved.screen.lines[0].id, removed_id = moved.screen.lines[1].id; + const auto retained_id = moved.screen.visible_lines()[0].id, removed_id = moved.screen.visible_lines()[1].id; moved.feed("\x1b[1;1H"); moved.screen.resize(1, 8); moved.screen.resize(2, 8); moved.feed("\x1b[2;1H"); moved.screen.resize(1, 8); search.notify_change(moved_doc); @@ -206,13 +206,13 @@ static void incremental_non_overlap_phase() { parser.feed("\x1b[3;1Haab"); finish(search, doc, search.notify_change(doc), 1); check(search.matches().size() == 4, "incremental rewind preserves original non-overlap phase"); const auto& matches = search.matches(); - check(matches[2].range.begin.line_id == parser.screen.lines[1].id && matches[2].range.begin.column == 1 && - matches[3].range.begin.line_id == parser.screen.lines[2].id && matches[3].range.begin.column == 0, + check(matches[2].range.begin.line_id == parser.screen.visible_lines()[1].id && matches[2].range.begin.column == 1 && + matches[3].range.begin.line_id == parser.screen.visible_lines()[2].id && matches[3].range.begin.column == 0, "tail mutation does not shift valid earlier or later matches by a byte"); Parser rolling(2, 3, 1); rolling.feed("aaaaaaaaaa"); const TextDocument rolling_doc(rolling.screen); finish(search, rolling_doc, search.start(rolling_doc, "aa"), 1); rolling.feed("aaa"); finish(search, rolling_doc, search.notify_change(rolling_doc), 1); - check(search.matches().size() == 3 && search.matches()[0].range.begin.line_id == rolling.screen.history[0].id && + check(search.matches().size() == 3 && search.matches()[0].range.begin.line_id == rolling.screen.history_lines()[0].id && search.matches()[0].range.begin.column == 0, "eviction restarts a partial matching prefix at the new retained document boundary"); } @@ -228,7 +228,7 @@ static void long_query_and_partial_row_replacement() { bytes += progress.bytes_scanned; } check(bytes == 4097 && search.matches().size() == 1 && - search.matches()[0].range.end.line_id == parser.screen.lines[8].id && + search.matches()[0].range.end.line_id == parser.screen.visible_lines()[8].id && search.matches()[0].range.end.column == 96, "maximum query matches 4096 known bytes across nine rows"); // Cancel after loading just one row; no residual UTF-8 or KMP origin from // that partial query may become a result of the replacement generation. diff --git a/experiments/core/selection_test.cpp b/experiments/core/selection_test.cpp index fff199ee..09025742 100644 --- a/experiments/core/selection_test.cpp +++ b/experiments/core/selection_test.cpp @@ -23,6 +23,15 @@ static TextPosition at(const TextDocument& document, size_t row, size_t column) const auto value = document.position(row, column); check(value.has_value(), "fixture position must exist"); return *value; } +// Identity replacement goes through the owner publication path; the screen +// validates the replacement rows before committing them. +static void replace_identity(Screen& screen, size_t index) { + std::vector live(screen.visible_lines().begin(), screen.visible_lines().end()); + live[index].id = 0; + screen.normalize_identities(live); + std::vector history(screen.history_lines().begin(), screen.history_lines().end()); + screen.publish(std::move(history), std::move(live)); +} static void forward_reverse_copy_and_glyph_boundaries() { Parser p(3, 18, 4); p.feed("ALPHA tail \r\nBETA \xe4\xb8\xad\xe6\x96\x87 e\xcc\x81"); @@ -122,23 +131,20 @@ static void half_open_endpoint_boundary() { Parser replaced(4,20,4); replaced.feed("ONE\r\nTWO\r\n"); TextDocument replaced_document(replaced.screen); check(selection.set(replaced_document,{at(replaced_document,0,0),at(replaced_document,2,0)}), "select through an empty endpoint before identity replacement"); - const auto old_endpoint = replaced.screen.lines[2].id; - auto imported = replaced.screen.lines; imported[2].id = 0; - replaced.screen.normalize_identities(imported); replaced.screen.lines = std::move(imported); - const auto new_endpoint = replaced.screen.lines[2].id; + const auto old_endpoint = replaced.screen.visible_lines()[2].id; + replace_identity(replaced.screen, 2); + const auto new_endpoint = replaced.screen.visible_lines()[2].id; check(new_endpoint != old_endpoint && selection.reconcile(replaced_document) && selection.range()->end.line_id == new_endpoint && selection.copy(replaced_document,100).text == "ONE\r\nTWO\r\n", "a still-empty excluded endpoint can be replaced without changing selected bytes"); check(selection.set(replaced_document,{at(replaced_document,0,0),at(replaced_document,2,0)}), "reselect before nonempty endpoint replacement"); - replaced.feed("\x1b[3;1HX"); imported = replaced.screen.lines; imported[2].id = 0; - replaced.screen.normalize_identities(imported); replaced.screen.lines = std::move(imported); + replaced.feed("\x1b[3;1HX"); replace_identity(replaced.screen, 2); check(!selection.reconcile(replaced_document) && selection.empty(), "replacement of an excluded empty endpoint must remain empty"); check(selection.set(replaced_document,{at(replaced_document,1,0),at(replaced_document,1,3)}), "select a nonempty endpoint before identity replacement"); - imported = replaced.screen.lines; imported[1].id = 0; - replaced.screen.normalize_identities(imported); replaced.screen.lines = std::move(imported); + replace_identity(replaced.screen, 1); check(!selection.reconcile(replaced_document) && selection.empty(), "a nonempty endpoint cannot borrow a replacement row identity"); } diff --git a/experiments/core/trace.cpp b/experiments/core/trace.cpp index 5c57c9e5..68be4277 100644 --- a/experiments/core/trace.cpp +++ b/experiments/core/trace.cpp @@ -48,14 +48,14 @@ int main(int argc, char** argv) { size_t traced_cells = 0; const auto snapshot = [&]() { const auto& screen = parser.screen; - const size_t count = (screen.lines.size() + screen.history.size()) * screen.width(); + const size_t count = (screen.visible_lines().size() + screen.history_lines().size()) * screen.width(); if (count > 1000000 - traced_cells) throw std::runtime_error("trace cell budget exceeded"); traced_cells += count; std::cout << "{\"offset\":" << offset << ",\"screen\":{\"row\":" << screen.row << ",\"column\":" << screen.column << ",\"wrap_pending\":" << (screen.wrap_pending ? "true" : "false") << ",\"cells\":"; - cells(std::cout, screen.lines); - std::cout << ",\"history\":"; cells(std::cout, screen.history); + cells(std::cout, screen.visible_lines()); + std::cout << ",\"history\":"; cells(std::cout, screen.history_lines()); std::cout << ",\"current_style\":"; screen.style.json(std::cout); std::cout << "}}\n"; }; diff --git a/experiments/measure/measure.cpp b/experiments/measure/measure.cpp index e44f46a1..6534cf55 100644 --- a/experiments/measure/measure.cpp +++ b/experiments/measure/measure.cpp @@ -134,7 +134,7 @@ static uint64_t hash_payload(std::string_view bytes) { static void check_recovery_mode() { termforge::Parser probe(1, 3, 0, parser_mode); probe.feed(std::string_view("\xff" "A", 2)); probe.finish(); - if (probe.screen.lines[0][0].text != "\xef\xbf\xbd" || probe.screen.lines[0][1].text != "A" || + if (probe.screen.visible_lines()[0][0].text != "\xef\xbf\xbd" || probe.screen.visible_lines()[0][1].text != "A" || probe.screen.column != 2 || probe.diagnostics().malformed_utf8 != 1) throw std::runtime_error("recovery parser identity probe failed"); } @@ -250,7 +250,7 @@ int main(int argc, char** argv) { } }; auto correct = [&line, &route](const termforge::Screen& s) { - if (s.row != 23 || s.column != 0 || s.wrap_pending || s.discarded != 877 || s.history.size() != 100) return false; + if (s.row != 23 || s.column != 0 || s.wrap_pending || s.discarded != 877 || s.history_lines().size() != 100) return false; if (route == "interactive") { const auto matches = [](const termforge::Line& row) { const std::string expected = "abcdeQgh"; @@ -258,17 +258,17 @@ int main(int argc, char** argv) { row[c].width != 1 || !row[c].style.plain()) return false; return true; }; - for (int r = 0; r < 23; ++r) if (!matches(s.lines[r])) return false; - for (const auto& row : s.history) if (!matches(row)) return false; - for (int c = 0; c < 80; ++c) if (s.lines.back()[c].text != " " || - s.lines.back()[c].width != 1 || !s.lines.back()[c].style.plain()) return false; + for (int r = 0; r < 23; ++r) if (!matches(s.visible_lines()[r])) return false; + for (const auto& row : s.history_lines()) if (!matches(row)) return false; + for (int c = 0; c < 80; ++c) if (s.visible_lines().back()[c].text != " " || + s.visible_lines().back()[c].width != 1 || !s.visible_lines().back()[c].style.plain()) return false; return true; } for (int r = 0; r < 24; ++r) for (int c = 0; c < 80; ++c) { - const auto& cell = s.lines[r][c]; + const auto& cell = s.visible_lines()[r][c]; if (cell.text != (r < 23 && c < static_cast(line.size()) ? "x" : " ") || cell.width != 1 || !cell.style.plain()) return false; } - for (const auto& row : s.history) for (int c = 0; c < 80; ++c) + for (const auto& row : s.history_lines()) for (int c = 0; c < 80; ++c) if (row[c].text != (c < 76 ? "x" : " ") || row[c].width != 1 || !row[c].style.plain()) return false; return true; }; diff --git a/experiments/presentation/render.hpp b/experiments/presentation/render.hpp index 7728b020..6dc55adf 100644 --- a/experiments/presentation/render.hpp +++ b/experiments/presentation/render.hpp @@ -83,7 +83,7 @@ static COLORREF gdi_color(unsigned rgb) { return RGB((rgb>>16)&255, (rgb>>8)&255 struct Snapshot { const std::vector lines; const int cursor_row, cursor_column, revision; - explicit Snapshot(const termforge::Screen& s, int rev) : lines(s.lines), cursor_row(s.row), cursor_column(s.column), revision(rev) {} + explicit Snapshot(const termforge::Screen& s, int rev) : lines(s.visible_lines()), cursor_row(s.row), cursor_column(s.column), revision(rev) {} explicit Snapshot(termforge::ViewSnapshot view, int rev) : lines(std::move(view.lines)), cursor_row(view.cursor_row), cursor_column(view.cursor_column), revision(rev) {} int rows() const { return static_cast(lines.size()); } diff --git a/experiments/shell/search_interactions.hpp b/experiments/shell/search_interactions.hpp index 2c028c5e..1a645c94 100644 --- a/experiments/shell/search_interactions.hpp +++ b/experiments/shell/search_interactions.hpp @@ -135,15 +135,22 @@ inline void ShellWindow::poll_find() { inline void ShellWindow::search_overlay(termforge::ViewSnapshot& view) { if(view.lines.empty()) return; termforge::TextDocument document(parser_.screen); + // Overlay decisions use the document rank, never the numeric identity: a + // retained row may hold a larger id while sitting earlier in the document. + const size_t top=view.viewport.top; + const size_t visible_end=top+view.lines.size(); for(size_t index=0;indexview.lines.back().id) continue; const auto match=search_.match(document,index); if(!match) continue; const auto& range=match->range; - for(auto& row:view.lines) { - if(row.idrange.end.line_id) continue; - const size_t first=row.id==range.begin.line_id?range.begin.column:0; - const size_t last=row.id==range.end.line_id?range.end.column:row.size(); + const auto begin_rank=document.index_of(range.begin.line_id); + const auto end_rank=document.index_of(range.end.line_id); + if(!begin_rank || !end_rank || *end_rank=visible_end) continue; + for(size_t row_index=0;row_index*end_rank) continue; + const size_t first=rank==*begin_rank?range.begin.column:0; + const size_t last=rank==*end_rank?range.end.column:row.size(); for(size_t column=first;columnindex==index?11:3; row[column].style.fg=0; diff --git a/experiments/shell/shell.cpp b/experiments/shell/shell.cpp index 70decfd9..6b10af73 100644 --- a/experiments/shell/shell.cpp +++ b/experiments/shell/shell.cpp @@ -1050,7 +1050,7 @@ class ShellWindow { // Each worker takes and releases its own mutex. This is an interval // observation, not an atomic snapshot across queues or paste progress. value.output=output_.snapshot(); value.input=input_.snapshot(); value.resize=resize_.snapshot(); - value.history_rows=parser_.screen.history.size(); value.history_limit=parser_.screen.history_limit; + value.history_rows=parser_.screen.history_lines().size(); value.history_limit=parser_.screen.history_limit; value.retained_rows=parser_.screen.retained_size(); value.discarded_history_rows=parser_.screen.discarded; value.viewport=runtime_published_viewport_; value.consumed_bytes=result_.consumed; value.max_consumed_per_tick=result_.max_consumed_per_tick; @@ -1205,7 +1205,7 @@ class ShellWindow { } bool contains_line(const std::string& text) const { return line_is(text); } bool live_contains_line(const std::string& expected) const { - for(const auto& line:parser_.screen.lines) { + for(const auto& line:parser_.screen.visible_lines()) { std::string text = termforge::project_line(line, false).text; while(!text.empty() && text.back()==' ') text.pop_back(); if(text==expected) return true; diff --git a/src/qt/resize_smoke.cpp b/src/qt/resize_smoke.cpp index 37770b22..7a21bc9c 100644 --- a/src/qt/resize_smoke.cpp +++ b/src/qt/resize_smoke.cpp @@ -323,7 +323,7 @@ static std::pair output_continues_during_resize(cons return {model.received_bytes(), output.high_water_bytes}; } static bool has_line(const termforge::Screen& screen, std::string_view expected) { - for (const auto& row : screen.lines) { + for (const auto& row : screen.visible_lines()) { std::string text; for (const auto& cell : row) if (cell.width) text += cell.text; while (!text.empty() && text.back() == ' ') text.pop_back(); @@ -426,7 +426,7 @@ int main(int argc, char** argv) { for (const auto size : sizes) { ++stage; const auto before = model.resize_state(); - const auto first_line_id = model.screen().lines.front().id; + const auto first_line_id = model.screen().visible_lines().front().id; bool event_responded = false; QTimer::singleShot(0, &terminal, [&] { event_responded = true; ++heartbeat_events; }); const QSize requested_pixels = size == QSize(1, 1) ? QSize(1, 1) : @@ -449,7 +449,7 @@ int main(int argc, char** argv) { const auto after = model.resize_state(); require(after.requested_revision == before.requested_revision && after.transport_revision == before.transport_revision && - model.screen().lines.front().id == first_line_id, + model.screen().visible_lines().front().id == first_line_id, "same-size repeat changed the core frame or native resize revision"); } record_state(stage, size.width(), size.height(), terminal, model); diff --git a/src/qt/shell_smoke.cpp b/src/qt/shell_smoke.cpp index d763b2a2..7f25cc78 100644 --- a/src/qt/shell_smoke.cpp +++ b/src/qt/shell_smoke.cpp @@ -33,7 +33,7 @@ static void await(Predicate predicate, const termforge::qt::SessionModel& model, require(predicate(), message); } static bool has_line(const termforge::Screen& screen, std::string_view expected) { - for (const auto& row : screen.lines) { + for (const auto& row : screen.visible_lines()) { std::string text; for (const auto& cell : row) if (cell.width != 0) text += cell.text; while (!text.empty() && text.back() == ' ') text.pop_back(); @@ -42,7 +42,7 @@ static bool has_line(const termforge::Screen& screen, std::string_view expected) return false; } static bool has_text(const termforge::Screen& screen, std::string_view expected) { - for (const auto& row : screen.lines) { + for (const auto& row : screen.visible_lines()) { std::string text; for (const auto& cell : row) if (cell.width != 0) text += cell.text; if (text.find(expected) != std::string::npos) return true; @@ -64,7 +64,7 @@ static void require_answer_ink(QWidget& window, termforge::qt::TerminalWidget& t int answer_row = -1; for (int row = 0; row < screen.height(); ++row) { std::string text; - for (const auto& cell : screen.lines[static_cast(row)]) if (cell.width) text += cell.text; + for (const auto& cell : screen.visible_lines()[static_cast(row)]) if (cell.width) text += cell.text; while (!text.empty() && text.back() == ' ') text.pop_back(); if (text == expected) answer_row = row; } @@ -346,7 +346,7 @@ int main(int argc, char** argv) { // and status line; a blank terminal renderer must fail this check. int answer_row = -1; for (int row = 0; row < model.screen().height(); ++row) { - const auto& line = model.screen().lines[static_cast(row)]; + const auto& line = model.screen().visible_lines()[static_cast(row)]; if (line[0].text == "7" && line[1].text == "5" && line[2].text == "0") answer_row = row; } require(answer_row >= 0, "arithmetic answer has no display row"); diff --git a/src/qt/terminal_widget.hpp b/src/qt/terminal_widget.hpp index aa05babe..eb41fa7f 100644 --- a/src/qt/terminal_widget.hpp +++ b/src/qt/terminal_widget.hpp @@ -126,7 +126,7 @@ class TerminalWidget final : public QWidget { const auto& screen = model_.screen(); const auto base_font = font(); for (int row = 0; row < screen.height(); ++row) { - const auto& line = screen.lines[static_cast(row)]; + const auto& line = screen.visible_lines()[static_cast(row)]; for (int column = 0; column < screen.width(); ++column) { const auto& cell = line[static_cast(column)]; const QRectF area(margin + column * cell_width_, margin + row * cell_height_, cell_width_, cell_height_); diff --git a/src/qt/unicode_input_test.cpp b/src/qt/unicode_input_test.cpp index f89a1bd4..8bdf691f 100644 --- a/src/qt/unicode_input_test.cpp +++ b/src/qt/unicode_input_test.cpp @@ -33,7 +33,7 @@ static void await(Predicate predicate, const char* message) { require(predicate(), message); } static bool has_text(const termforge::Screen& screen, std::string_view text) { - for (const auto& row : screen.lines) { + for (const auto& row : screen.visible_lines()) { std::string line; for (const auto& cell : row) if (cell.width) line += cell.text; if (line.find(text) != std::string::npos) return true;