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;