From c43600c8006952349ee9c33a9d3c92cba049490b Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:19:03 +0800 Subject: [PATCH 1/8] feat(qt): add bounded literal Find to the main screen and history view Add Ctrl+Shift+F, a current-session Find panel with query focus, Previous/Next/ Close buttons, a Match case toggle and status text. Reuse TextSearch's bounded literal policy (4096 UTF-8 byte query, 1024-result cap, ASCII case folding, existing loop/truncation statuses) in incremental timer slices. Hits stay source-positioned; navigation reveals the source row through HistoryViewport and highlights via the current ViewMapping, so display row numbers are never stored. Escape closes the panel and restores terminal focus. Add a formal Qt regression with the deterministic selection fixture: public shortcut/query, history reveal, source hit and painted spans, Previous/Next, no-match status, Escape focus restoration and query limit. No system clipboard, paste, BCD/VM or new search semantics are changed. --- .github/workflows/qt-cpp.yml | 6 +- CMakeLists.txt | 14 +++ src/qt/find_test.cpp | 115 ++++++++++++++++++++ src/qt/terminal_widget.hpp | 196 +++++++++++++++++++++++++++++++++++ 4 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 src/qt/find_test.cpp diff --git a/.github/workflows/qt-cpp.yml b/.github/workflows/qt-cpp.yml index f76add41..cac61bf4 100644 --- a/.github/workflows/qt-cpp.yml +++ b/.github/workflows/qt-cpp.yml @@ -42,7 +42,7 @@ jobs: "-DTERMFORGE_CONPTY_BACKEND_DIR=$dependencyRoot/conpty" ` "-DTERMFORGE_MSVC_RUNTIME_DIR=$dependencyRoot/crt" - name: Build Qt product and independent smoke - run: cmake --build build/qt-cpp --config Release --target termforge_qt_shell qt_cpp_shell_smoke qt_cpp_resize_smoke qt_unicode_input_test qt_unicode_render_test qt_terminal_modes_test qt_hidden_retention_test qt_history_navigation_test qt_selection_migration_test qt_clipboard_system_test --parallel 2 + run: cmake --build build/qt-cpp --config Release --target termforge_qt_shell qt_cpp_shell_smoke qt_cpp_resize_smoke qt_unicode_input_test qt_unicode_render_test qt_terminal_modes_test qt_hidden_retention_test qt_history_navigation_test qt_selection_migration_test qt_find_test qt_clipboard_system_test --parallel 2 - name: Record complete path-free runtime manifests run: | python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-bin/Release --executable termforge_qt_shell.exe @@ -63,6 +63,8 @@ jobs: if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-selection-bin/Release --executable qt_selection_migration_test.exe if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-find-bin/Release --executable qt_find_test.exe + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } python tools/msvc_runtime.py manifest --directory build/qt-cpp/qt-clipboard-bin/Release --executable qt_clipboard_system_test.exe if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Preserve runtime file identities without binaries @@ -84,7 +86,7 @@ jobs: build/qt-cpp/qt-selection-bin/Release/runtime-manifest.json build/qt-cpp/qt-clipboard-bin/Release/runtime-manifest.json - name: Run Qt offscreen smoke in its owned Job - run: ctest --test-dir build/qt-cpp -C Release -R '^(qt_cpp_(shell|resize)|qt_unicode_input|qt_unicode_render|qt_terminal_modes(_retained)?|qt_hidden_retention|qt_history_navigation|qt_selection_migration)$' --no-tests=error --verbose + run: ctest --test-dir build/qt-cpp -C Release -R '^(qt_cpp_(shell|resize)|qt_unicode_input|qt_unicode_render|qt_terminal_modes(_retained)?|qt_hidden_retention|qt_history_navigation|qt_selection_migration|qt_find)$' --no-tests=error --verbose - name: Run hosted-only Qt system clipboard copy run: python src/qt/run_smoke.py build/qt-cpp/qt-clipboard-bin/Release/qt_clipboard_system_test.exe --hosted-copy - name: Run hosted Windows QPA product-writer clipboard copy diff --git a/CMakeLists.txt b/CMakeLists.txt index c0f5cb94..fa58ee01 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -818,6 +818,20 @@ if(TERMFORGE_BUILD_QT_SHELL) "${PROJECT_SOURCE_DIR}/src/qt/run_smoke.py" "$") set_tests_properties(qt_selection_migration PROPERTIES TIMEOUT 40 ENVIRONMENT "QT_QPA_PLATFORM=offscreen") + add_executable(qt_find_test src/qt/find_test.cpp) + set_target_properties(qt_find_test PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qt-find-bin/$") + termforge_qt_entry(qt_find_test) + target_link_libraries(qt_find_test PRIVATE Qt6::Test) + add_custom_command(TARGET qt_find_test POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" + "$/platforms/" + VERBATIM) + add_test(NAME qt_find COMMAND "${Python3_EXECUTABLE}" + "${PROJECT_SOURCE_DIR}/src/qt/run_smoke.py" "$") + set_tests_properties(qt_find PROPERTIES TIMEOUT 45 + ENVIRONMENT "QT_QPA_PLATFORM=offscreen") add_executable(qt_clipboard_system_test src/qt/clipboard_system_test.cpp) set_target_properties(qt_clipboard_system_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qt-clipboard-bin/$") diff --git a/src/qt/find_test.cpp b/src/qt/find_test.cpp new file mode 100644 index 00000000..3bede2fb --- /dev/null +++ b/src/qt/find_test.cpp @@ -0,0 +1,115 @@ +// Formal Qt Find entry regression: public shortcut/query, source-anchored +// matches, history reveal, navigation and Escape focus restoration. +#include "terminal_widget.hpp" +#include "selection_fixture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using termforge::qt::SessionModel; +using termforge::qt::TerminalWidget; +using termforge::qt::test::selection_child; + +namespace { +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} +void await(const std::function& predicate, SessionModel& model, QElapsedTimer& lifecycle, const char* message) { + QElapsedTimer phase; phase.start(); + while (!predicate() && phase.elapsed() < 10000 && lifecycle.elapsed() < 25000) { + QTest::qWait(10); + if (!model.error().isEmpty()) throw std::runtime_error(model.error().toStdString()); + } + require(predicate(), message); +} +} // namespace + +int main(int argc, char** argv) { + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + if (argc == 2 && std::string_view(argv[1]) == "--selection-child") { + try { return selection_child(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 3; } + } + if (argc != 1) { std::cerr << "usage: qt_find_test\n"; return 2; } + char gate[3]{}; DWORD count = 0; BOOL owned = FALSE; + if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), gate, sizeof(gate), &count, nullptr) || + count != sizeof(gate) || std::string_view(gate, count) != "GO\n" || + !IsProcessInJob(GetCurrentProcess(), nullptr, &owned) || !owned) { + std::cerr << "find test requires its owned Job supervisor\n"; return 2; + } + QApplication app(argc, argv); + QApplication::setQuitOnLastWindowClosed(false); + try { + QElapsedTimer lifecycle; lifecycle.start(); + wchar_t executable[32768]{}; + const DWORD length = GetModuleFileNameW(nullptr, executable, 32768); + require(length && length < 32768, "find executable unavailable"); + SessionModel model; QWidget window; TerminalWidget terminal(model, &window); + window.resize(1200, 900); window.show(); terminal.setFocus(); + require(model.start(executable, L"--selection-child", {}), "find child did not start"); + QTest::qWait(700); + require(model.running(), "find child exited before search"); + + // Public shortcut opens the current-session Find panel with query focus. + QTest::keyClick(&terminal, Qt::Key_F, Qt::ControlModifier | Qt::ShiftModifier); + require(terminal.find_visible_for_test(), "Ctrl+Shift+F did not open Find"); + auto* query = window.findChild(QStringLiteral("findQuery")); + require(query && query->hasFocus(), "Find query did not take focus"); + QTest::keyClicks(query, QStringLiteral("SEL_SOFT_")); + await([&] { return terminal.find_match_count_for_test() > 0 && + !terminal.find_status_for_test().startsWith(QStringLiteral("Searching")); }, + model, lifecycle, "Find did not complete with a match"); + require(terminal.find_status_for_test().contains(QStringLiteral("match")), + "Find status does not report a match"); + const auto hit = terminal.find_current_range_for_test(); + require(hit.has_value() && hit->begin.line_id == hit->end.line_id, "Find hit is not source-located"); + const auto spans = terminal.find_spans_for_test(); + require(!spans.empty(), "Find hit did not project onto the painted page"); + require(!terminal.history_following(), "revealing a history hit did not leave the live bottom"); + + // Next/previous public buttons keep the hit source-anchored; single hit wraps. + auto* next = window.findChild(QStringLiteral("findNext")); + auto* previous = window.findChild(QStringLiteral("findPrevious")); + require(next && previous, "Find navigation buttons missing"); + QTest::mouseClick(next, Qt::LeftButton); + require(terminal.find_current_range_for_test().has_value(), "next lost the Find hit"); + QTest::mouseClick(previous, Qt::LeftButton); + require(terminal.find_current_range_for_test().has_value(), "previous lost the Find hit"); + + // No-match state is explicit. + terminal.set_find_query_for_test(QStringLiteral("TF_NO_SUCH_LITERAL")); + await([&] { return terminal.find_status_for_test() == QStringLiteral("No matches"); }, + model, lifecycle, "no-match state missing"); + + // Escape closes and returns focus without sending the query to the child. + query->setFocus(); + QTest::keyClick(query, Qt::Key_Escape); + require(!terminal.find_visible_for_test(), "Escape did not close Find"); + require(terminal.hasFocus(), "Escape did not restore terminal focus"); + require(terminal.find_query_text_for_test() == QStringLiteral("TF_NO_SUCH_LITERAL"), + "closing Find unexpectedly changed the query"); + + // The query field enforces the existing 4096-unit limit at the entry. + terminal.set_find_query_for_test(QString(4097, QLatin1Char('x'))); + require(terminal.find_query_text_for_test().size() == 4096, + "query field did not enforce the existing 4096-unit limit"); + + std::cout << "{\"schema\":\"termforge-qt-find-v1\",\"status\":\"passed\"," + "\"shortcut_open\":true,\"query_focus\":true,\"source_hit\":true," + "\"history_reveal\":true,\"next_previous\":true,\"no_match\":true," + "\"escape_focus\":true,\"limit\":true,\"offscreen_only\":true," + "\"query_sent_to_child\":false}\n"; + require(model.send(std::string(1, 'q')), "find child quit rejected"); + await([&] { return model.finished(); }, model, lifecycle, "find child did not exit"); + require(model.exit_code() == 37 && model.cleanup_verified(), "find child cleanup was not verified"); + return 0; + } catch (const std::exception& error) { + std::cerr << "Qt Find: " << error.what() << '\n'; + return 1; + } +} diff --git a/src/qt/terminal_widget.hpp b/src/qt/terminal_widget.hpp index 795e65d2..91d4c578 100644 --- a/src/qt/terminal_widget.hpp +++ b/src/qt/terminal_widget.hpp @@ -6,11 +6,20 @@ #include "../../experiments/core/display_cursor.hpp" #include "../../experiments/core/history_view.hpp" #include "../../experiments/core/text_selection.hpp" +#include "../../experiments/core/text_search.hpp" #include "../../experiments/shell/keyboard.hpp" #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -55,6 +64,15 @@ class TerminalWidget final : public QWidget { int display_cursor_column_ = 0; TextSelection selection_; std::optional selection_anchor_; + QFrame* find_panel_ = nullptr; + QLineEdit* find_query_ = nullptr; + QLabel* find_status_ = nullptr; + QCheckBox* find_case_ = nullptr; + TextSearch find_search_; + std::optional find_current_; + size_t find_current_index_ = 0; + uint64_t find_generation_ = 0; + QTimer find_timer_; std::unique_ptr clipboard_; ClipboardWrite last_copy_; QString copy_notice_; @@ -85,6 +103,7 @@ class TerminalWidget final : public QWidget { } void resizeEvent(QResizeEvent* event) override { QWidget::resizeEvent(event); + if (find_panel_) find_panel_->setGeometry(margin, margin, (std::max)(1, width() - margin * 2), 62); synchronize_grid(); } void showEvent(QShowEvent* event) override { @@ -298,6 +317,10 @@ class TerminalWidget final : public QWidget { model_.send(std::string{}); event->ignore(); return; } + if (event->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier) && + event->key() == Qt::Key_F) { + open_find(); event->accept(); return; + } if (event->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier) && event->key() == Qt::Key_C) { copy_selection_to_clipboard(); @@ -505,6 +528,139 @@ class TerminalWidget final : public QWidget { display_cursor_row_ = screen.row; display_cursor_column_ = screen.column; } + void set_find_status(const QString& text) { + if (find_status_) find_status_->setText(text); + } + TextPosition find_anchor() { + const auto& screen = model_.screen(); + const auto state = viewport_.reconcile(screen); + const size_t total = screen.retained_size(); + const size_t row = total ? (std::min)(state.top, total - 1) : 0; + const auto& line = screen.retained_line(row); + return {line.id, line.version, 0, screen.buffer_epoch()}; + } + void start_find() { + if (!find_query_) return; + const QByteArray query = find_query_->text().toUtf8(); + if (query.isEmpty()) { + find_search_.cancel(); find_current_.reset(); set_find_status(QStringLiteral("No query")); + update(); return; + } + try { + find_generation_ = find_search_.start(TextDocument(model_.screen()), query.toStdString(), + {find_case_->isChecked(), 1024}); + } catch (const std::exception& error) { + find_current_.reset(); + set_find_status(QStringLiteral("Find: ") + QString::fromUtf8(error.what())); + return; + } + find_current_.reset(); find_current_index_ = 0; + set_find_status(QStringLiteral("Searching...")); + find_timer_.start(0); + } + void step_find() { + const TextDocument document(model_.screen()); + const auto progress = find_search_.step(document, find_generation_, 4096); + if (progress.status == SearchStatus::running) { + set_find_status(QStringLiteral("Searching... %1").arg(find_search_.matches().size())); + update(); return; + } + find_timer_.stop(); + if (progress.status == SearchStatus::capped) + set_find_status(QStringLiteral("%1 matches (capped)").arg(find_search_.matches().size())); + else if (progress.status == SearchStatus::complete || progress.status == SearchStatus::idle) + set_find_status(find_search_.matches().empty() ? QStringLiteral("No matches") : + QStringLiteral("%1 matches").arg(find_search_.matches().size())); + else if (progress.status == SearchStatus::invalidated) + set_find_status(QStringLiteral("Search invalidated")); + else if (progress.status == SearchStatus::cancelled) + set_find_status(QStringLiteral("No query")); + if (!find_current_ && !find_search_.matches().empty()) { + if (const auto nav = find_search_.navigate(document, SearchDirection::next, find_anchor(), true)) { + find_current_ = nav->match; find_current_index_ = nav->index; + viewport_.reveal(model_.screen(), nav->match.range.begin.line_id); + } + } + update(); + } + void navigate_find(SearchDirection direction) { + const TextDocument document(model_.screen()); + const std::optional anchor = find_current_ + ? std::optional{find_current_->range.begin} : std::optional{find_anchor()}; + const auto nav = find_search_.navigate(document, direction, anchor, true); + if (!nav) { + set_find_status(QStringLiteral("No matches")); + find_current_.reset(); update(); return; + } + find_current_ = nav->match; find_current_index_ = nav->index; + viewport_.reveal(model_.screen(), nav->match.range.begin.line_id); + set_find_status(QStringLiteral("%1 / %2%3").arg(nav->index + 1) + .arg(find_search_.matches().size()).arg(nav->wrapped ? QStringLiteral(" (wrapped)") : QString())); + update(); + } + void open_find() { + if (!find_panel_) return; + find_panel_->show(); + find_panel_->raise(); + find_query_->setFocus(Qt::ShortcutFocusReason); + find_query_->selectAll(); + if (!find_query_->text().isEmpty()) start_find(); + } + void close_find() { + find_timer_.stop(); + find_search_.close(); + find_current_.reset(); + if (find_panel_) find_panel_->hide(); + setFocus(Qt::OtherFocusReason); + update(); + } + void build_find_panel() { + find_panel_ = new QFrame(this); + find_panel_->setObjectName(QStringLiteral("findPanel")); + find_panel_->setStyleSheet(QStringLiteral("QFrame#findPanel { background: palette(window); border: 1px solid palette(mid); }")); + auto* outer = new QVBoxLayout(find_panel_); + outer->setContentsMargins(6, 4, 6, 4); + auto* controls = new QHBoxLayout(); + find_query_ = new QLineEdit(find_panel_); + find_query_->setObjectName(QStringLiteral("findQuery")); + find_query_->setPlaceholderText(QStringLiteral("Find literal text")); + find_query_->setMaxLength(4096); + controls->addWidget(find_query_, 1); + auto* previous = new QPushButton(QStringLiteral("Previous"), find_panel_); + previous->setObjectName(QStringLiteral("findPrevious")); + auto* next = new QPushButton(QStringLiteral("Next"), find_panel_); + next->setObjectName(QStringLiteral("findNext")); + auto* close = new QPushButton(QStringLiteral("Close"), find_panel_); + close->setObjectName(QStringLiteral("findClose")); + find_case_ = new QCheckBox(QStringLiteral("Match case"), find_panel_); + find_case_->setObjectName(QStringLiteral("findCase")); + find_case_->setChecked(true); + controls->addWidget(previous); controls->addWidget(next); controls->addWidget(close); controls->addWidget(find_case_); + outer->addLayout(controls); + find_status_ = new QLabel(QStringLiteral("Ctrl+Shift+F: literal Find, max 4096 UTF-8 bytes"), find_panel_); + find_status_->setObjectName(QStringLiteral("findStatus")); + outer->addWidget(find_status_); + find_panel_->hide(); + connect(find_query_, &QLineEdit::textChanged, this, [this] { start_find(); }); + connect(find_case_, &QCheckBox::toggled, this, [this] { start_find(); }); + connect(previous, &QPushButton::clicked, this, [this] { navigate_find(SearchDirection::previous); }); + connect(next, &QPushButton::clicked, this, [this] { navigate_find(SearchDirection::next); }); + connect(close, &QPushButton::clicked, this, [this] { close_find(); }); + connect(&find_timer_, &QTimer::timeout, this, [this] { step_find(); }); + find_timer_.setSingleShot(false); + find_query_->installEventFilter(this); + } + bool eventFilter(QObject* watched, QEvent* event) override { + if (watched == find_query_ && event->type() == QEvent::KeyPress) { + auto* key = static_cast(event); + if (key->key() == Qt::Key_Escape) { close_find(); return true; } + if (key->key() == Qt::Key_Return || key->key() == Qt::Key_Enter) { + navigate_find(key->modifiers().testFlag(Qt::ShiftModifier) ? SearchDirection::previous : SearchDirection::next); + return true; + } + } + return QWidget::eventFilter(watched, event); + } void paintEvent(QPaintEvent*) override { QPainter painter(this); painter.fillRect(rect(), colour(-1, false)); @@ -513,6 +669,20 @@ class TerminalWidget final : public QWidget { selection_.reconcile(document); refresh_painted_page(screen); auto& page = *painted_page_; + if (find_current_ && find_search_.match(document, find_current_index_)) { + for (const auto& span : page.mapping.project(document, find_current_->range, + page.mapping.layout_revision())) { + if (span.row < painted_row_offset_) continue; + const size_t row = span.row - painted_row_offset_; + if (row >= page.lines.size()) continue; + for (size_t column = span.column_begin; column < span.column_end && column < page.lines[row].size(); ++column) { + auto& style = page.lines[row][column].style; + style.bg = 3; style.fg = 15; style.reverse = false; + } + } + } else if (find_current_) { + find_current_.reset(); + } const size_t columns = page.mapping.columns() ? page.mapping.columns() : static_cast(screen.width()); const size_t visible_rows = (std::min)(static_cast(screen.height()), page.lines.size() > painted_row_offset_ ? page.lines.size() - painted_row_offset_ : size_t{0}); @@ -600,6 +770,7 @@ class TerminalWidget final : public QWidget { resize(sizeHint()); grid_ready_ = true; clipboard_ = std::make_unique(*this); + build_find_panel(); model_.on_changed = [this] { refresh_session(); }; } ~TerminalWidget() override { model_.on_changed = {}; } @@ -608,6 +779,13 @@ class TerminalWidget final : public QWidget { const TextDocument document(model_.screen()); selection_.reconcile(document); if (selection_anchor_ && !document.valid(*selection_anchor_)) selection_anchor_.reset(); + if (find_panel_ && find_panel_->isVisible() && !find_query_->text().isEmpty()) { + if (find_search_.status() == SearchStatus::running || find_search_.status() == SearchStatus::complete || + find_search_.status() == SearchStatus::capped) { + find_generation_ = find_search_.notify_change(document); + find_timer_.start(0); + } + } // Keep the display page and caret current before Qt asks the input // method for geometry, not only on the next paint. if (grid_ready_) refresh_painted_page(model_.screen()); @@ -632,6 +810,24 @@ class TerminalWidget final : public QWidget { return selection_.copy(TextDocument(model_.screen()), maximum_bytes); } ClipboardWrite copy_to_clipboard() { return copy_selection_to_clipboard(); } + void open_find_for_test() { open_find(); } + void close_find_for_test() { close_find(); } + void set_find_query_for_test(const QString& text) { if (find_query_) find_query_->setText(text); } + QString find_query_text_for_test() const { return find_query_ ? find_query_->text() : QString{}; } + bool find_visible_for_test() const { return find_panel_ && find_panel_->isVisible(); } + QString find_status_for_test() const { return find_status_ ? find_status_->text() : QString{}; } + size_t find_match_count_for_test() const { return find_search_.matches().size(); } + std::optional find_current_range_for_test() const { + return find_current_ ? std::optional{find_current_->range} : std::nullopt; + } + void find_next_for_test() { navigate_find(SearchDirection::next); } + void find_previous_for_test() { navigate_find(SearchDirection::previous); } + std::vector find_spans_for_test() { + if (!find_current_ || !painted_page_) return {}; + const TextDocument document(model_.screen()); + return painted_page_->mapping.project(document, find_current_->range, + painted_page_->mapping.layout_revision()); + } void setClipboardWriterForTest(std::unique_ptr writer) { clipboard_ = std::move(writer); } From 3888d841f03483595416f33945082f37ea7d5705 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:19:53 +0800 Subject: [PATCH 2/8] test(qt): capture the Find panel for review --- src/qt/find_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qt/find_test.cpp b/src/qt/find_test.cpp index 3bede2fb..212a8c38 100644 --- a/src/qt/find_test.cpp +++ b/src/qt/find_test.cpp @@ -71,6 +71,7 @@ int main(int argc, char** argv) { const auto spans = terminal.find_spans_for_test(); require(!spans.empty(), "Find hit did not project onto the painted page"); require(!terminal.history_following(), "revealing a history hit did not leave the live bottom"); + window.grab().save(QStringLiteral("find-panel.png")); // Next/previous public buttons keep the hit source-anchored; single hit wraps. auto* next = window.findChild(QStringLiteral("findNext")); From 25e3995deec1eae8842658accf9354af16846f9b Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:20:22 +0800 Subject: [PATCH 3/8] docs(qt): record the bounded Find slice and review screenshot Add the Find implementation/verification record, source and test identity, the captured offscreen panel image, and converge FEATURES/EVIDENCE on the bounded capability. Keep selection, TUI/alternate-screen Find, physical presentation, DPI and performance outside the claim. --- docs/EVIDENCE.md | 1 + docs/FEATURES.md | 2 +- .../results/2026-11-06-qt-find/README.md | 22 +++++++ .../results/2026-11-06-qt-find/find-panel.png | Bin 0 -> 38909 bytes .../2026-11-06-qt-find/validation.json | 54 ++++++++++++++++++ 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 experiments/shell/results/2026-11-06-qt-find/README.md create mode 100644 experiments/shell/results/2026-11-06-qt-find/find-panel.png create mode 100644 experiments/shell/results/2026-11-06-qt-find/validation.json diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index e4de5411..efe2e4e1 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -24,6 +24,7 @@ | Qt 正式路径恢复 Windows 原生滚轮设置(#29 收尾) | [原生合同、注入回归与边界](../experiments/shell/results/2026-11-05-qt-wheel-native/README.md):每个真实滚轮事件读取 `SPI_GETWHEELSCROLLLINES`,0 禁用、正整数行数、`WHEEL_PAGESCROLL` 页步长=可见高度−1 至少1,查询失败回退3行;注入覆盖原生/0/3/页标记/半 detent/页高 24→20/查询失败/设置更新,真实 Qt+受控 child 历史与锚点回归 8/8 | 仅 Windows 11 x64 offscreen Qt 6.10.3;不修改宿主设置;页步长为 R1 产品合同非 WindTerm oracle;查询失败分支由注入验证而非真实策略变更;滚动条/翻页键/粘贴/呈现/DPI/Unicode/性能未覆盖;#317 待决、#27/#29 不关闭 | | Qt 历史/重排页选择源位置迁移(#29 切片) | [真实 Qt 鼠标/历史浏览、受控 child 与截图](../experiments/shell/results/2026-11-06-qt-selection-migration/README.md):反向跨软续行、部分列+硬行边界、60×20 重排/恢复、浏览页变化、无关输出、真实淘汰/ED3/RIS 失效、双会话隔离;字面文本与高亮列数独立核对,Qt 正式路径 9/9 | 单屏 offscreen、固定短 ASCII/单一软续行;宽字/结合字选择列、系统剪贴板/复制 UI、粘贴、选词选行、自动滚动、Find、物理呈现/DPI/其他尺寸/性能未覆盖;内部提取≠系统剪贴板;旧失败/A/B/C/#27/#29 状态不变 | | Qt 有界系统复制入口(#29/#30 切片) | [实现、fake 入口回归与 hosted 阻塞](../experiments/shell/results/2026-11-06-qt-system-copy/README.md):Ctrl+Shift+C/实际 Copy 菜单接入 CF_UNICODETEXT,空/失效/容量/失败分列;本地 fake writer 入口测试通过,Qt 正式路径 9/9;hosted-only CF 入口通过(QPA/offscreen 分层) | **系统 CF 已在一次性 GitHub-hosted Windows run `42160062253` 分层验证**:offscreen 注入 owner 与 Windows QPA 默认 writer/真实控件 HWND 均独立读回键盘/实际菜单/空 sentinel/占用失败/释放后成功/淘汰;首次 offscreen winId 非 HWND 的 `OpenClipboard:1400` 只修正测试 owner,不改产品;宽字/结合字复制列、占用重试、粘贴、Find、物理呈现/性能未覆盖;不把 offscreen 缓存当 CF;旧 B/CD/A/B/C/#27/#29/#30 状态不变 | +| Qt 有界字面量 Find(#29 切片) | [实现、本地确定性 child 与截图](../experiments/shell/results/2026-11-06-qt-find/README.md):Ctrl+Shift+F/查询/Previous/Next/Close,复用 TextSearch 的 4096 字节/1024 结果/字面量/ASCII case folding;源位置经 ViewMapping 在正常主屏与历史页高亮导航;本地 Qt 正式路径 10/10(qt_find 1/1) | 固定 ASCII 样例、离屏 Qt;中文/NFD 只声明字面标量匹配,不宣称字素/glyph 等价;不含选词/选行、滚动条/翻页键历史、TUI/备用屏 Find、物理呈现/DPI/性能阈值;不代表 #29/#30/R2 完成 | | 正式 Qt / 固定 ConPTY 固定 Unicode 样本贯通呈现与独立接收(#30 切片) | [链路、样本与实测](../experiments/shell/results/2026-11-04-qt-unicode-presentation/README.md):新增 `qt_unicode_render`,5 条固定样本经公开 Qt 事件→真实 child `ReadConsoleW` 精确接收→同字节 CR→CRLF 回显→核心解析→`TerminalWidget` 离屏抓图;receiver/echo SHA-256 与源字节逐字节一致,CJK 实测 fallback `Alibaba PuHuiTi`、emoji `Segoe UI Emoji`(advance 21>span 18 但无裁剪参考墨迹 3..17 未越格),结合标 `marks=1`,第二条空会话 0 字节且无样本文字;Qt 正式路径 6/6 | U+200D/U+FE0F 仍被 recovery parser 替换为 U+FFFD(只记录,不批准新聚类/宽度策略);offscreen + 宿主 fallback 依赖、`presented_done=null`,不覆盖物理呈现/原生 IME/真实 DPI/完整字素或 #30/#155 验收 | | 隐藏主屏纯高度变化的正文保留(#27 工程合同) | [RED/GREEN、命令与边界](../experiments/core/results/2026-11-04-hidden-height-retention/README.md):修复前 `resize_hidden_height_test` 证明隐藏 live/历史被按未变宽度裁剪并丢失保留尾;`resize_preserving` 改为只在宽度实际变化时适配隐藏帧后,三场景通过并注册 `retained_resize_hidden_height_core`;组合回放 20/20、事务检查不变,正式 Qt CTest 5/5 | 工程不变量;原版对照见下一行。2026-11-04 用户确认保留完整正文、接受原版返回裁剪差异,工程身份不变;固定七阶段正式 Qt 路径已离屏核对(见下方正式路径行),物理呈现/DPI 未覆盖。未覆盖备用屏后续输出、保存光标交叉、Unicode、历史淘汰、Qt 物理呈现/DPI 或性能;#27/#29 不关闭 | | 正式 Qt / 固定 ConPTY 隐藏帧七阶段完整正文(#27/#29 正式路径切片) | [场景、阶段与显示/命中](../experiments/shell/results/2026-11-04-qt-hidden-retention/README.md):真实 child 写入 91×20 固定 75 B 正文→公开 Qt 缩窄 61→`1049h`→仅高度 20→14→20→`1049l`→恢复宽度 91;58 检查 0 失败,2 次运行除 `child_pid` 外一致;阶段 1/2/6/7 逻辑正文 SHA `0e683ff0…` 相同,保留行 id `12`、91-Cell 源行与历史 5 行前缀保持,显示页/光标/命中均映射同一源身份;Qt 正式路径 7/7(8.99 s)、核心 41/41(23.05 s) | 仅 offscreen 固定样本;非物理呈现(`presented_done=null`)、DPI、IME 或性能验收;child 原生行单独记录,不作为保留文本 oracle;原严格失败、补充审计与 `semantic_acceptance=false` 不改写;#27/#29/#30/#7 不关闭 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index c988297d..96ebad97 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -23,7 +23,7 @@ | 新路线迁移入口 | 当前状态与边界 | 目标差距 | 路标 | |---|---|---|---| -| 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供默认 80×24、随可用区域变化的 cmd 标签、基本 ASCII 与[限定 Unicode 文本字节](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、16 色及各会话独立有界队列、[输入法组合/提交/取消(事件级 + 固定拼音验证)](../research/ui/results/2026-10-30-qt-ime-fixed-pinyin/README.md),并接入[无修饰导航键与基础鼠标正反拖选高亮](../experiments/shell/results/2026-11-01-r2-core-input/README.md);[基础动态尺寸验证](../experiments/shell/results/2026-10-29-qt-resize/README.md)分离窗口/核心/transport 与真实 child,覆盖极小、重复、隐藏页和关闭交叉,并复验原标签算术及事件循环回收。678 原生极窄失败保留;0ff 仅修订字面子区域/顶层实际区域夹具,另有[冻结包一次隔离原生成功](../experiments/shell/results/2026-10-29-qt-resize/native-validation.json):实际顶层区域、core/transport/child 一致,窗口仅 DPI 96。旧尺寸结果保留各自源码;[Unicode 联合源码的一次完整61项](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json)分开 Qt3项与其余58项既有/独立诊断,13条内部skip保留。正式尺寸事务已改走显式保留入口并绘制独立显示投影/显示光标:[只缩宽的保留行契约](../experiments/shell/results/2026-11-03-qt-retained-display/README.md)在真实 child/固定 ConPTY 核对 12/12 检查,Qt 正式路径 CTest 5/5;旧六阶段 native-sync 失败保留并归类为原生重排网格与保留行合同的坐标差。Qt 已接入有限历史浏览:滚轮与 `Ctrl+Shift+Home/End` 通过同一 `HistoryViewport` 源锚点浏览保留历史,窄/高 resize、新输出、淘汰/ED3/RIS 下的锚点规则由真实 Qt 事件和受控 child 回归核对([固定结果](../experiments/shell/results/2026-11-05-qt-history-anchor/README.md));Windows 滚轮直接读取 `SPI_GETWHEELSCROLLLINES`,恢复 0 禁用、正整数行数与 `WHEEL_PAGESCROLL` 页步长,查询失败按 R1 回退 3 行([原生滚轮结果](../experiments/shell/results/2026-11-05-qt-wheel-native/README.md));历史/软续接/显示重排页的选区源位置迁移已端到端覆盖(正反/部分列/跨软硬行、重排/浏览/无关输出、淘汰/ED3/RIS 失效、双会话隔离;内部提取非系统剪贴板,见[固定结果](../experiments/shell/results/2026-11-06-qt-selection-migration/README.md));系统复制入口已接入 Ctrl+Shift+C/实际 Copy 菜单的有界 CF_UNICODETEXT 写入,并在 hosted Windows CI 以独立 CF 读回验证键盘/实际菜单/空选区/占用失败与释放后成功/淘汰 sentinel(见[系统复制结果](../experiments/shell/results/2026-11-06-qt-system-copy/README.md));滚动条/翻页键历史与粘贴回底未迁移 | 分屏、其他 shell、工作区恢复、同时缩宽+缩高的原版视口/滚动与后续 CUP 证据(已有固定**写后端点**样本:一次动作 91×20→61×14 后执行 CUP 7;2+DQ,观测 scroll 5、DSR 7;4、62 字符行;无 pre-write 原版正文,裁剪归因未知;见[双轴结果](../research/ui/results/2026-11-04-resize-dual-axis-reference/README.md))、完整 reflow、Qt 滚动条/翻页键历史与自动滚动、选词/选行与系统复制/粘贴、Find、完整 Unicode 字素/绘制、候选窗与真实 IME 交叉(已有固定拼音;[固定时间点对照](research/qt-ime-candidate-panel-reference-2026-11-01.md)中输出后两侧都保持旧位置、resize 后延后点都更新;字体阶段原版未触发,面板身份未证明,且对照 Qt 源为历史 5fab30b)、真实 DPI、应用兼容与性能仍需实现或迁移;完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 | +| 自主 C++ / Qt / 固定微软 ConPTY | **实现中,未验收**:[显式 Qt 入口](../src/qt/README.md)提供默认 80×24、随可用区域变化的 cmd 标签、基本 ASCII 与[限定 Unicode 文本字节](../experiments/shell/results/2026-10-29-qt-unicode-input/README.md)、16 色及各会话独立有界队列、[输入法组合/提交/取消(事件级 + 固定拼音验证)](../research/ui/results/2026-10-30-qt-ime-fixed-pinyin/README.md),并接入[无修饰导航键与基础鼠标正反拖选高亮](../experiments/shell/results/2026-11-01-r2-core-input/README.md);[基础动态尺寸验证](../experiments/shell/results/2026-10-29-qt-resize/README.md)分离窗口/核心/transport 与真实 child,覆盖极小、重复、隐藏页和关闭交叉,并复验原标签算术及事件循环回收。678 原生极窄失败保留;0ff 仅修订字面子区域/顶层实际区域夹具,另有[冻结包一次隔离原生成功](../experiments/shell/results/2026-10-29-qt-resize/native-validation.json):实际顶层区域、core/transport/child 一致,窗口仅 DPI 96。旧尺寸结果保留各自源码;[Unicode 联合源码的一次完整61项](../experiments/shell/results/2026-10-29-qt-unicode-input/validation.json)分开 Qt3项与其余58项既有/独立诊断,13条内部skip保留。正式尺寸事务已改走显式保留入口并绘制独立显示投影/显示光标:[只缩宽的保留行契约](../experiments/shell/results/2026-11-03-qt-retained-display/README.md)在真实 child/固定 ConPTY 核对 12/12 检查,Qt 正式路径 CTest 5/5;旧六阶段 native-sync 失败保留并归类为原生重排网格与保留行合同的坐标差。Qt 已接入有限历史浏览:滚轮与 `Ctrl+Shift+Home/End` 通过同一 `HistoryViewport` 源锚点浏览保留历史,窄/高 resize、新输出、淘汰/ED3/RIS 下的锚点规则由真实 Qt 事件和受控 child 回归核对([固定结果](../experiments/shell/results/2026-11-05-qt-history-anchor/README.md));Windows 滚轮直接读取 `SPI_GETWHEELSCROLLLINES`,恢复 0 禁用、正整数行数与 `WHEEL_PAGESCROLL` 页步长,查询失败按 R1 回退 3 行([原生滚轮结果](../experiments/shell/results/2026-11-05-qt-wheel-native/README.md));历史/软续接/显示重排页的选区源位置迁移已端到端覆盖(正反/部分列/跨软硬行、重排/浏览/无关输出、淘汰/ED3/RIS 失效、双会话隔离;内部提取非系统剪贴板,见[固定结果](../experiments/shell/results/2026-11-06-qt-selection-migration/README.md));系统复制入口已接入 Ctrl+Shift+C/实际 Copy 菜单的有界 CF_UNICODETEXT 写入,并在 hosted Windows CI 以独立 CF 读回验证键盘/实际菜单/空选区/占用失败与释放后成功/淘汰 sentinel(见[系统复制结果](../experiments/shell/results/2026-11-06-qt-system-copy/README.md));正式 Qt Find 已接 Ctrl+Shift+F/查询/上一项/下一项/关闭,复用 TextSearch 的 4096 字节/1024 结果/字面量/ASCII case folding,并通过源位置+ViewMapping 在正常主屏与历史页高亮导航(本地确定性 child 10/10,见[Find 结果](../experiments/shell/results/2026-11-06-qt-find/README.md);不含选词/选行/物理呈现/性能阈值);滚动条/翻页键历史与粘贴回底未迁移 | 分屏、其他 shell、工作区恢复、同时缩宽+缩高的原版视口/滚动与后续 CUP 证据(已有固定**写后端点**样本:一次动作 91×20→61×14 后执行 CUP 7;2+DQ,观测 scroll 5、DSR 7;4、62 字符行;无 pre-write 原版正文,裁剪归因未知;见[双轴结果](../research/ui/results/2026-11-04-resize-dual-axis-reference/README.md))、完整 reflow、Qt 滚动条/翻页键历史与自动滚动、选词/选行与系统复制/粘贴、Find、完整 Unicode 字素/绘制、候选窗与真实 IME 交叉(已有固定拼音;[固定时间点对照](research/qt-ime-candidate-panel-reference-2026-11-01.md)中输出后两侧都保持旧位置、resize 后延后点都更新;字体阶段原版未触发,面板身份未证明,且对照 Qt 源为历史 5fab30b)、真实 DPI、应用兼容与性能仍需实现或迁移;完整 WindTerm 标签/尺寸行为对照未完成;当前不是可发布开发包 | R2 | 自主 C++ 核心已具备双缓冲与限定 `?1049`、DECAWM、DECOM、DECCKM/DECTCEM、每屏 DECSTBM 边距、字符/行插删、ED3/RIS、DSR 6n 和部分 SGR 状态。依据分别见 [1049](../experiments/core/results/2026-10-29-cpp-1049/README.md)、[DECAWM](../experiments/core/results/2026-10-29-cpp-decawm/README.md)及[支持表](../experiments/core/README.md)。[原版每屏 origin 对照](../research/ui/results/2026-11-02-mode-state-reference/README.md)确认备用屏进入时关闭 origin、主屏返回恢复自身状态及固定样本的 margin 下界;核心已改为[每屏保存 origin](../experiments/core/results/2026-11-02-origin-frame/README.md)。区域 LF、IL/DL 后的精确行序,以及[区域自动换行续接、RIS/ED3 历史结构发布](../experiments/core/results/2026-11-02-r27-mode-cross/README.md)已有修正。完整网格/样式、resize 和运输交叉仍由 #27/#29 验收,不能据此声明完整 TUI 通过。 diff --git a/experiments/shell/results/2026-11-06-qt-find/README.md b/experiments/shell/results/2026-11-06-qt-find/README.md new file mode 100644 index 00000000..b5d6c6fe --- /dev/null +++ b/experiments/shell/results/2026-11-06-qt-find/README.md @@ -0,0 +1,22 @@ +# 正式 Qt:有界字面量 Find(正常主屏 + 历史显示页) + +本切片把既有 `TextSearch` 的有界字面量搜索接入正式 Qt:`Ctrl+Shift+F` 打开当前会话 Find 面板,复用 4096 UTF-8 字节查询上限、1024 结果上限、字面量/无规范化/ASCII case folding、循环/截断/持续扫描策略;命中保存源 `TextPosition`,通过 `HistoryViewport` 与当前 `ViewMapping` 导航和高亮,不保存显示行号。 + +## 实现 + +- `TerminalWidget`:Find 面板含查询框、Previous/Next/Close、Match case 和状态行;`Ctrl+Shift+F` 打开并聚焦,Escape 关闭并恢复终端焦点。 +- 搜索按 `TextSearch::step` 的 4096 单位预算分片,计时器继续;空/超限/无命中/截断/失效状态来自既有合同。 +- 输出变化调用 `TextSearch::notify_change`,重扫/失效;当前命中在 `TextDocument` 上以源位置 live-validate,再经当前显示页 `ViewMapping` 投影;`reveal` 定位源行,历史/当前页均可用。 +- 查询只在 Find 面板内处理,不作为终端输入;复制仍走已合入的源选择/系统 Copy。 + +## 验证 + +- `qt_find` 1/1,Qt 正式路径 10/10;确定性 selection child:`Ctrl+Shift+F` 实际按键、查询焦点、`SEL_SOFT_` 源命中、历史 reveal、高亮投影、Previous/Next、无命中、Escape 焦点恢复、4096 查询上限;未触个人剪贴板/桌面。 +- 截图:[find-panel.png](find-panel.png)(同一真实 Qt 测试窗口,历史命中态;离屏,不代表物理 DPI/呈现)。 +- 当前 head CI 与源码/构建身份见 [validation.json](validation.json);本切片不代表 #29/#30/R2 完成。 + +## 边界 + +- 固定样例/ASCII 字面量与离屏 Qt;中文/NFD 只声明字面标量匹配,不宣称字素/glyph/宽度等价。 +- 不覆盖选词/选行、滚动条/翻页键历史、备用屏/TUI Find、物理呈现/DPI、完整 Unicode 字素或性能阈值;不新增数值性能验收。 +- 旧 B/CD、192.168.3.210 校准/v2、A/B/C、旧六阶段失败、#27/#29/#30/#35/#7 状态不变。 diff --git a/experiments/shell/results/2026-11-06-qt-find/find-panel.png b/experiments/shell/results/2026-11-06-qt-find/find-panel.png new file mode 100644 index 0000000000000000000000000000000000000000..6b07bc4a2b4e6bb3e8d16353fe9b832931ec9f24 GIT binary patch literal 38909 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 RqZ(lp&Z+*8DtG Date: Sat, 7 Nov 2026 00:14:57 +0800 Subject: [PATCH 4/8] test(qt): close the bounded Find acceptance gaps Add multiple-match source/order/wrap assertions with exact painted width, history and current-page reveal, narrow/wide source migration, eviction non-resurrection, epoch invalidation, two real sessions with independent input recording, zero query leakage and post-Escape input targeting. Exercise empty query, actual Match case, 4097-byte/Chinese/NFD over-limit rejection without silent truncation, and Return/F3/Shift+F3. Make the query field stop silently truncating at 4096 units; TextSearch now rejects the exact UTF-8 byte limit with an explicit status. Add F3/Shift+F3 to the existing Next/Previous contract. The test-only selection child can record received input bytes; no product clipboard/desktop is touched. --- src/qt/find_test.cpp | 269 +++++++++++++++++++++++++++-------- src/qt/selection_fixture.hpp | 9 +- src/qt/terminal_widget.hpp | 16 ++- 3 files changed, 233 insertions(+), 61 deletions(-) diff --git a/src/qt/find_test.cpp b/src/qt/find_test.cpp index 212a8c38..2ceeee42 100644 --- a/src/qt/find_test.cpp +++ b/src/qt/find_test.cpp @@ -1,17 +1,22 @@ -// Formal Qt Find entry regression: public shortcut/query, source-anchored -// matches, history reveal, navigation and Escape focus restoration. +// Formal Qt Find acceptance: public shortcut/query, multiple source-ordered +// matches, wrap, history/current-page highlight and scroll, resize migration, +// invalidation/eviction/epoch, two real sessions and actual child input bytes. #include "terminal_widget.hpp" #include "selection_fixture.hpp" +#include "../../experiments/core/text_document.hpp" #include +#include #include -#include +#include #include #include #include +#include #include #include #include +using termforge::TextDocument; using termforge::qt::SessionModel; using termforge::qt::TerminalWidget; using termforge::qt::test::selection_child; @@ -20,20 +25,47 @@ namespace { void require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } +std::string read_input(const QString& path) { + QFile file(path); + if (!file.exists()) return {}; + require(file.open(QIODevice::ReadOnly), "find input record unavailable"); + return file.readAll().toStdString(); +} +std::string text_at(SessionModel& model, uint64_t line_id) { + const TextDocument document(model.screen()); + const auto index = document.index_of(line_id); + require(index.has_value(), "find hit line is not in the document"); + return document.line(*index).projection.text; +} void await(const std::function& predicate, SessionModel& model, QElapsedTimer& lifecycle, const char* message) { QElapsedTimer phase; phase.start(); - while (!predicate() && phase.elapsed() < 10000 && lifecycle.elapsed() < 25000) { + while (!predicate() && phase.elapsed() < 10000 && lifecycle.elapsed() < 30000) { QTest::qWait(10); if (!model.error().isEmpty()) throw std::runtime_error(model.error().toStdString()); } require(predicate(), message); } +void await_find_done(TerminalWidget& terminal, SessionModel& model, QElapsedTimer& lifecycle, const char* message) { + await([&] { return !terminal.find_status_for_test().startsWith(QStringLiteral("Searching")); }, + model, lifecycle, message); +} +size_t line_rank(SessionModel& model, uint64_t id) { + const TextDocument document(model.screen()); + return document.index_of(id).value_or(std::numeric_limits::max()); +} +void start_child(SessionModel& model, TerminalWidget& terminal, QWidget& window, + const std::wstring& executable, const QString& record, const std::wstring& working, const char* message) { + const QString args = QStringLiteral("--selection-child \"%1\"").arg(record); + require(model.start(executable, args.toStdWString(), working), message); + window.show(); terminal.setFocus(); +} } // namespace int main(int argc, char** argv) { SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); - if (argc == 2 && std::string_view(argv[1]) == "--selection-child") { - try { return selection_child(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 3; } + if (argc >= 2 && std::string_view(argv[1]) == "--selection-child") { + try { return selection_child(argc == 3 ? QString::fromLocal8Bit(argv[2]).toStdWString() : std::wstring{}); } + catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 3; } } if (argc != 1) { std::cerr << "usage: qt_find_test\n"; return 2; } char gate[3]{}; DWORD count = 0; BOOL owned = FALSE; @@ -46,68 +78,189 @@ int main(int argc, char** argv) { QApplication::setQuitOnLastWindowClosed(false); try { QElapsedTimer lifecycle; lifecycle.start(); + QTemporaryDir workspace; + require(workspace.isValid(), "find workspace unavailable"); wchar_t executable[32768]{}; const DWORD length = GetModuleFileNameW(nullptr, executable, 32768); require(length && length < 32768, "find executable unavailable"); - SessionModel model; QWidget window; TerminalWidget terminal(model, &window); - window.resize(1200, 900); window.show(); terminal.setFocus(); - require(model.start(executable, L"--selection-child", {}), "find child did not start"); + const QString first_input = workspace.path() + "/first-input.bin"; + const QString second_input = workspace.path() + "/second-input.bin"; + + SessionModel first; QWidget first_window; TerminalWidget terminal(first, &first_window); + first_window.resize(1200, 900); + start_child(first, terminal, first_window, executable, first_input, + workspace.path().toStdWString(), "find first child did not start"); + QTest::qWait(700); + require(first.running(), "find first child exited before search"); + + SessionModel second; QWidget second_window; TerminalWidget second_terminal(second, &second_window); + second_window.resize(1200, 900); + start_child(second, second_terminal, second_window, executable, second_input, + workspace.path().toStdWString(), "find second child did not start"); QTest::qWait(700); - require(model.running(), "find child exited before search"); + require(second.running(), "find second child exited before search"); - // Public shortcut opens the current-session Find panel with query focus. + first_window.activateWindow(); terminal.setFocus(); QTest::qWait(30); QTest::keyClick(&terminal, Qt::Key_F, Qt::ControlModifier | Qt::ShiftModifier); require(terminal.find_visible_for_test(), "Ctrl+Shift+F did not open Find"); - auto* query = window.findChild(QStringLiteral("findQuery")); - require(query && query->hasFocus(), "Find query did not take focus"); - QTest::keyClicks(query, QStringLiteral("SEL_SOFT_")); - await([&] { return terminal.find_match_count_for_test() > 0 && - !terminal.find_status_for_test().startsWith(QStringLiteral("Searching")); }, - model, lifecycle, "Find did not complete with a match"); - require(terminal.find_status_for_test().contains(QStringLiteral("match")), - "Find status does not report a match"); - const auto hit = terminal.find_current_range_for_test(); - require(hit.has_value() && hit->begin.line_id == hit->end.line_id, "Find hit is not source-located"); - const auto spans = terminal.find_spans_for_test(); - require(!spans.empty(), "Find hit did not project onto the painted page"); - require(!terminal.history_following(), "revealing a history hit did not leave the live bottom"); - window.grab().save(QStringLiteral("find-panel.png")); - - // Next/previous public buttons keep the hit source-anchored; single hit wraps. - auto* next = window.findChild(QStringLiteral("findNext")); - auto* previous = window.findChild(QStringLiteral("findPrevious")); - require(next && previous, "Find navigation buttons missing"); - QTest::mouseClick(next, Qt::LeftButton); - require(terminal.find_current_range_for_test().has_value(), "next lost the Find hit"); - QTest::mouseClick(previous, Qt::LeftButton); - require(terminal.find_current_range_for_test().has_value(), "previous lost the Find hit"); - - // No-match state is explicit. - terminal.set_find_query_for_test(QStringLiteral("TF_NO_SUCH_LITERAL")); - await([&] { return terminal.find_status_for_test() == QStringLiteral("No matches"); }, - model, lifecycle, "no-match state missing"); - - // Escape closes and returns focus without sending the query to the child. - query->setFocus(); - QTest::keyClick(query, Qt::Key_Escape); - require(!terminal.find_visible_for_test(), "Escape did not close Find"); - require(terminal.hasFocus(), "Escape did not restore terminal focus"); - require(terminal.find_query_text_for_test() == QStringLiteral("TF_NO_SUCH_LITERAL"), - "closing Find unexpectedly changed the query"); - - // The query field enforces the existing 4096-unit limit at the entry. + auto* first_query = first_window.findChild(QStringLiteral("findQuery")); + require(first_query && first_query->hasFocus(), "Find query did not take focus"); + QTest::keyClicks(first_query, QStringLiteral("SEL_N_")); + await_find_done(terminal, first, lifecycle, "multi-match Find did not complete"); + const size_t total = terminal.find_match_count_for_test(); + require(total > 4, "Find did not return multiple matches"); + auto range = terminal.find_current_range_for_test(); + require(range.has_value(), "Find did not select a current match"); + auto* prev_button = first_window.findChild(QStringLiteral("findPrevious")); + for (int guard = 0; guard < static_cast(total) + 2 && terminal.find_current_index_for_test() != 0; ++guard) + QTest::mouseClick(prev_button, Qt::LeftButton); + require(terminal.find_current_index_for_test() == 0, "could not position at the first match"); + range = terminal.find_current_range_for_test(); + require(range.has_value(), "first match lost after positioning"); + + size_t expected_index = 0; + uint64_t prior_rank = line_rank(first, range->begin.line_id); + for (int step = 0; step < 3; ++step) { + auto* next_button = first_window.findChild(QStringLiteral("findNext")); + require(next_button, "Find Next button missing"); + QTest::mouseClick(next_button, Qt::LeftButton); + require(terminal.find_current_index_for_test() == ++expected_index, "Next index is not sequential"); + range = terminal.find_current_range_for_test(); + require(range.has_value(), "Next lost the current match"); + const std::string line = text_at(first, range->begin.line_id); + require(line.substr(range->begin.column, range->end.column - range->begin.column) == "SEL_N_", + "Next hit does not point at the expected source literal"); + const uint64_t rank = line_rank(first, range->begin.line_id); + require(rank > prior_rank, "Next hit is not in document order"); + prior_rank = rank; + } + auto spans = terminal.find_spans_for_test(); + require(!spans.empty(), "current hit has no painted projection"); + size_t projected = 0; + for (const auto& span : spans) projected += span.column_end - span.column_begin; + require(projected == range->end.column - range->begin.column, + "painted highlight width does not match the source hit"); + require(!terminal.history_following(), "history hit did not leave the live bottom"); + first_window.grab().save(QStringLiteral("find-panel.png")); + + for (int guard = 0; guard < static_cast(total) + 2 && terminal.find_current_index_for_test() != 0; ++guard) + QTest::mouseClick(first_window.findChild(QStringLiteral("findPrevious")), Qt::LeftButton); + require(terminal.find_current_index_for_test() == 0, "could not return to the first hit"); + QTest::mouseClick(first_window.findChild(QStringLiteral("findPrevious")), Qt::LeftButton); + require(terminal.find_last_wrapped_for_test(), "Previous from the first hit did not wrap"); + require(terminal.find_current_index_for_test() == total - 1, "wrapped Previous index is not the last hit"); + auto* next_button = first_window.findChild(QStringLiteral("findNext")); + QTest::mouseClick(next_button, Qt::LeftButton); + require(terminal.find_last_wrapped_for_test(), "Next from the last hit did not wrap"); + require(terminal.find_current_index_for_test() == 0, "wrapped Next index is not the first hit"); + + QTest::keyClick(first_query, Qt::Key_Return); + require(terminal.find_current_index_for_test() == 1, "Return did not move to Next"); + QTest::keyClick(first_query, Qt::Key_F3); + require(terminal.find_current_index_for_test() == 2, "F3 did not move to Next"); + QTest::keyClick(first_query, Qt::Key_F3, Qt::ShiftModifier); + require(terminal.find_current_index_for_test() == 1, "Shift+F3 did not move to Previous"); + + const auto before_resize = *terminal.find_current_range_for_test(); + const std::string before_text = text_at(first, before_resize.begin.line_id); + first_window.resize(760, 720); QTest::qWait(80); + const auto narrow = terminal.find_current_range_for_test(); + require(narrow && narrow->begin == before_resize.begin && narrow->end == before_resize.end, + "narrow resize changed the source hit"); + first_window.resize(1200, 900); QTest::qWait(80); + const auto restored = terminal.find_current_range_for_test(); + require(restored && restored->begin == before_resize.begin && restored->end == before_resize.end, + "restored resize changed the source hit"); + require(text_at(first, before_resize.begin.line_id) == before_text, "resize changed the hit line text"); + require(!terminal.find_spans_for_test().empty(), "resized hit has no projection"); + + QTest::keyClick(first_query, Qt::Key_A, Qt::ControlModifier); + QTest::keyClick(first_query, Qt::Key_Backspace); + require(terminal.find_match_count_for_test() == 0, "empty query retained matches"); + require(terminal.find_status_for_test() == QStringLiteral("No query"), "empty query lacks explicit status"); + + terminal.set_find_query_for_test(QStringLiteral("sel_head")); + await_find_done(terminal, first, lifecycle, "case-sensitive no-match did not finish"); + require(terminal.find_match_count_for_test() == 0, "case-sensitive query unexpectedly matched"); + auto* match_case = first_window.findChild(QStringLiteral("findCase")); + require(match_case && match_case->isChecked(), "Match case is not the explicit default"); + QTest::mouseClick(match_case, Qt::LeftButton); + await([&] { return terminal.find_match_count_for_test() > 0; }, first, lifecycle, + "unchecked Match case did not find the folded hit"); + QTest::mouseClick(match_case, Qt::LeftButton); + terminal.set_find_query_for_test(QString(4097, QLatin1Char('x'))); - require(terminal.find_query_text_for_test().size() == 4096, - "query field did not enforce the existing 4096-unit limit"); + require(terminal.find_query_text_for_test().size() == 4097, "query was silently truncated"); + require(terminal.find_status_for_test().contains(QStringLiteral("4096")), + "4097-byte query did not report the existing limit"); + terminal.set_find_query_for_test(QString(1366, QChar(0x4e2d))); + require(terminal.find_status_for_test().contains(QStringLiteral("4096")), + "over-limit Chinese query did not report the limit"); + terminal.set_find_query_for_test(QString(QString::fromUtf8("e\xcc\x81")).repeated(2049)); + require(terminal.find_status_for_test().contains(QStringLiteral("4096")), + "over-limit NFD query did not report the limit"); + + terminal.set_find_query_for_test(QStringLiteral("SEL_HEAD")); + await([&] { return terminal.find_current_range_for_test().has_value(); }, first, lifecycle, + "SEL_HEAD history hit missing"); + const uint64_t evicted_id = terminal.find_current_range_for_test()->begin.line_id; + require(first.send(std::string(1, 'a')), "eviction command rejected"); + QTest::qWait(200); + require(!terminal.find_current_range_for_test().has_value() || + terminal.find_current_range_for_test()->begin.line_id != evicted_id, + "evicted source hit was resurrected"); + const TextDocument after_evict(first.screen()); + require(!after_evict.index_of(evicted_id).has_value(), "evicted line still present in the document"); + terminal.set_find_query_for_test(QStringLiteral("SEL_HEAD")); + await_find_done(terminal, first, lifecycle, "post-eviction rescan did not finish"); + require(terminal.find_match_count_for_test() == 0, "evicted SEL_HEAD still matches"); + + terminal.set_find_query_for_test(QStringLiteral("SEL_HEAD")); + await_find_done(terminal, first, lifecycle, "pre-epoch query did not finish"); + require(first.send(std::string(1, 'r')), "RIS command rejected"); + QTest::qWait(200); + { + const auto current = terminal.find_current_range_for_test(); + const TextDocument document(first.screen()); + require(!current || document.index_of(current->begin.line_id).has_value(), + "pre-RIS hit survived the epoch change"); + } + terminal.set_find_query_for_test(QStringLiteral("SEL_RIS")); + await([&] { return terminal.find_match_count_for_test() > 0; }, first, lifecycle, + "post-RIS query did not find the new epoch text"); + + require(read_input(first_input) == "a" || read_input(first_input) == "ar", + "query text leaked into the first child input"); + require(read_input(second_input).empty(), "query text leaked into the second child input"); + second_window.activateWindow(); second_terminal.setFocus(); QTest::qWait(30); + QTest::keyClick(&second_terminal, Qt::Key_F, Qt::ControlModifier | Qt::ShiftModifier); + auto* second_query = second_window.findChild(QStringLiteral("findQuery")); + require(second_query && second_query->hasFocus(), "second session Find did not focus its query"); + QTest::keyClicks(second_query, QStringLiteral("SEL_HEAD")); + await_find_done(second_terminal, second, lifecycle, "second-session Find did not finish"); + require(second_terminal.find_match_count_for_test() > 0, "second session has no independent hit"); + require(read_input(second_input).empty(), "second-session query leaked into its child"); + QTest::keyClick(second_query, Qt::Key_Escape); + require(second_terminal.hasFocus(), "second session Escape did not restore terminal focus"); + QTest::keyClicks(&second_terminal, QStringLiteral("Z")); + await([&] { return read_input(second_input) == "Z"; }, second, lifecycle, + "post-Escape input was not received exactly once by the correct child"); + require(read_input(first_input) == "a" || read_input(first_input) == "ar", + "post-Escape input reached the wrong child"); std::cout << "{\"schema\":\"termforge-qt-find-v1\",\"status\":\"passed\"," - "\"shortcut_open\":true,\"query_focus\":true,\"source_hit\":true," - "\"history_reveal\":true,\"next_previous\":true,\"no_match\":true," - "\"escape_focus\":true,\"limit\":true,\"offscreen_only\":true," - "\"query_sent_to_child\":false}\n"; - require(model.send(std::string(1, 'q')), "find child quit rejected"); - await([&] { return model.finished(); }, model, lifecycle, "find child did not exit"); - require(model.exit_code() == 37 && model.cleanup_verified(), "find child cleanup was not verified"); + "\"multi_match_order\":true,\"wrap\":true,\"exact_highlight\":true," + "\"history_reveal\":true,\"resize_source_migration\":true," + "\"eviction_no_resurrection\":true,\"epoch_invalidation\":true," + "\"two_session_isolation\":true,\"query_zero_leak\":true," + "\"escape_input_once\":true,\"empty_query\":true,\"case_toggle\":true," + "\"limit_bytes\":true,\"return_f3\":true,\"offscreen_only\":true}\n"; + require(first.send(std::string(1, 'q')), "first child quit rejected"); + require(second.send(std::string(1, 'q')), "second child quit rejected"); + await([&] { return first.finished(); }, first, lifecycle, "first child did not exit"); + await([&] { return second.finished(); }, second, lifecycle, "second child did not exit"); + require(first.exit_code() == 37 && first.cleanup_verified(), "first child cleanup was not verified"); + require(second.exit_code() == 37 && second.cleanup_verified(), "second child cleanup was not verified"); return 0; } catch (const std::exception& error) { std::cerr << "Qt Find: " << error.what() << '\n'; diff --git a/src/qt/selection_fixture.hpp b/src/qt/selection_fixture.hpp index d2d80d71..a3f83dfd 100644 --- a/src/qt/selection_fixture.hpp +++ b/src/qt/selection_fixture.hpp @@ -4,6 +4,7 @@ #pragma once #include "../../experiments/conpty/session.hpp" #include +#include #include #include #include @@ -31,7 +32,7 @@ inline void selection_numbered(HANDLE output, int first, int last) { selection_write(output, batch); } -inline int selection_child() { +inline int selection_child(const std::wstring& record_path = {}) { BOOL owned = FALSE; if (!IsProcessInJob(GetCurrentProcess(), nullptr, &owned) || !owned) throw std::runtime_error("selection child requires its owned Session Job"); @@ -52,10 +53,16 @@ inline int selection_child() { "COPY_UNICODE:\xe4\xb8\xad\xe6\x96\x87 e\xcc\x81 \r\nCOPY_TAIL\r\n"); selection_numbered(output.get(), 126, 165); selection_write(output.get(), "SEL>"); + std::ofstream received; + if (!record_path.empty()) { + received.open(record_path, std::ios::binary | std::ios::trunc); + if (!received) tf_conpty::fail("selection child input record"); + } for (;;) { char key = 0; DWORD count = 0; if (!ReadFile(input.get(), &key, 1, &count, nullptr) || count != 1) tf_conpty::fail("ReadFile(selection child input)"); + if (received) { received.put(key); received.flush(); } if (key == 'q') return 37; if (key == 'o') { selection_numbered(output.get(), 121, 125); diff --git a/src/qt/terminal_widget.hpp b/src/qt/terminal_widget.hpp index 91d4c578..622738a8 100644 --- a/src/qt/terminal_widget.hpp +++ b/src/qt/terminal_widget.hpp @@ -71,6 +71,7 @@ class TerminalWidget final : public QWidget { TextSearch find_search_; std::optional find_current_; size_t find_current_index_ = 0; + bool find_last_wrapped_ = false; uint64_t find_generation_ = 0; QTimer find_timer_; std::unique_ptr clipboard_; @@ -317,6 +318,11 @@ class TerminalWidget final : public QWidget { model_.send(std::string{}); event->ignore(); return; } + if (find_panel_ && find_panel_->isVisible() && + (event->key() == Qt::Key_F3)) { + navigate_find(event->modifiers().testFlag(Qt::ShiftModifier) ? SearchDirection::previous : SearchDirection::next); + event->accept(); return; + } if (event->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier) && event->key() == Qt::Key_F) { open_find(); event->accept(); return; @@ -590,9 +596,10 @@ class TerminalWidget final : public QWidget { const auto nav = find_search_.navigate(document, direction, anchor, true); if (!nav) { set_find_status(QStringLiteral("No matches")); - find_current_.reset(); update(); return; + find_current_.reset(); find_last_wrapped_ = false; update(); return; } find_current_ = nav->match; find_current_index_ = nav->index; + find_last_wrapped_ = nav->wrapped; viewport_.reveal(model_.screen(), nav->match.range.begin.line_id); set_find_status(QStringLiteral("%1 / %2%3").arg(nav->index + 1) .arg(find_search_.matches().size()).arg(nav->wrapped ? QStringLiteral(" (wrapped)") : QString())); @@ -624,7 +631,6 @@ class TerminalWidget final : public QWidget { find_query_ = new QLineEdit(find_panel_); find_query_->setObjectName(QStringLiteral("findQuery")); find_query_->setPlaceholderText(QStringLiteral("Find literal text")); - find_query_->setMaxLength(4096); controls->addWidget(find_query_, 1); auto* previous = new QPushButton(QStringLiteral("Previous"), find_panel_); previous->setObjectName(QStringLiteral("findPrevious")); @@ -654,6 +660,10 @@ class TerminalWidget final : public QWidget { if (watched == find_query_ && event->type() == QEvent::KeyPress) { auto* key = static_cast(event); if (key->key() == Qt::Key_Escape) { close_find(); return true; } + if (key->key() == Qt::Key_F3) { + navigate_find(key->modifiers().testFlag(Qt::ShiftModifier) ? SearchDirection::previous : SearchDirection::next); + return true; + } if (key->key() == Qt::Key_Return || key->key() == Qt::Key_Enter) { navigate_find(key->modifiers().testFlag(Qt::ShiftModifier) ? SearchDirection::previous : SearchDirection::next); return true; @@ -817,6 +827,8 @@ class TerminalWidget final : public QWidget { bool find_visible_for_test() const { return find_panel_ && find_panel_->isVisible(); } QString find_status_for_test() const { return find_status_ ? find_status_->text() : QString{}; } size_t find_match_count_for_test() const { return find_search_.matches().size(); } + size_t find_current_index_for_test() const { return find_current_index_; } + bool find_last_wrapped_for_test() const { return find_last_wrapped_; } std::optional find_current_range_for_test() const { return find_current_ ? std::optional{find_current_->range} : std::nullopt; } From fe7abe45acd0395f4be8585300d22b89a520148a Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:15:30 +0800 Subject: [PATCH 5/8] docs(qt): close the Find acceptance gap map --- .../results/2026-11-06-qt-find/README.md | 19 +++++++- .../results/2026-11-06-qt-find/find-panel.png | Bin 38909 -> 40085 bytes .../2026-11-06-qt-find/validation.json | 41 ++++++++++++++---- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/experiments/shell/results/2026-11-06-qt-find/README.md b/experiments/shell/results/2026-11-06-qt-find/README.md index b5d6c6fe..f6d75ec3 100644 --- a/experiments/shell/results/2026-11-06-qt-find/README.md +++ b/experiments/shell/results/2026-11-06-qt-find/README.md @@ -11,7 +11,8 @@ ## 验证 -- `qt_find` 1/1,Qt 正式路径 10/10;确定性 selection child:`Ctrl+Shift+F` 实际按键、查询焦点、`SEL_SOFT_` 源命中、历史 reveal、高亮投影、Previous/Next、无命中、Escape 焦点恢复、4096 查询上限;未触个人剪贴板/桌面。 +- `qt_find` 1/1,Qt 正式路径 10/10;two real selection children with independent input recording:multiple `SEL_N_` matches 的顺序/index、双向 wrap、精确源文本/列与显示宽度、历史 reveal、缩窄/扩大同源迁移、淘汰不复活、RIS 旧 epoch 失效;两个会话查询隔离,查询零泄漏到 child,Escape 后 `Z` 恰好一次只到正确 child;空查询、实际 Match case、4097-byte/中文/NFD 超限拒绝、Return/F3/Shift+F3。未触个人剪贴板/桌面。 +- 1024 结果上限由既有 TextSearch 合同/核心 capped 测试覆盖;固定 child 的 100 行历史无法在真实入口达到 1024,未用伪造大文档冒充用户证据。 - 截图:[find-panel.png](find-panel.png)(同一真实 Qt 测试窗口,历史命中态;离屏,不代表物理 DPI/呈现)。 - 当前 head CI 与源码/构建身份见 [validation.json](validation.json);本切片不代表 #29/#30/R2 完成。 @@ -19,4 +20,20 @@ - 固定样例/ASCII 字面量与离屏 Qt;中文/NFD 只声明字面标量匹配,不宣称字素/glyph/宽度等价。 - 不覆盖选词/选行、滚动条/翻页键历史、备用屏/TUI Find、物理呈现/DPI、完整 Unicode 字素或性能阈值;不新增数值性能验收。 +- 1024 结果截断仅有核心合同覆盖;真实 child 保留上限 100 行,不能到达 1024,未伪造。 - 旧 B/CD、192.168.3.210 校准/v2、A/B/C、旧六阶段失败、#27/#29/#30/#35/#7 状态不变。 + + +## 原派单缺口 → 证据 + +| 原要求 | 本次证据 | +|---|---| +| 多命中准确顺序/循环/当前页与历史页 | 多个 `SEL_N_` 顺序/index、双向 wrap、历史 reveal、精确源文本/列和显示宽度 | +| 缩窄扩大同源迁移、写入/淘汰失效、旧 epoch | resize 前后源 range 不变;淘汰后无复活;RIS 后旧位置拒绝、新 epoch 可查 | +| 双会话隔离、Escape 后输入只到正确 child | 两个真实 child + 独立输入文件;查询零泄漏,Escape 后 `Z` 恰好一次到目标 | +| 空查询、大小写、1024 截断/有限循环、4096 字节边界 | 空查询/Match case/Return/F3/Shift+F3/ASCII+中文+NFD 超限明确拒绝;1024 由核心 capped 测试覆盖,真实 child 100 行无法到达,明确记录 | +| 公开入口而非测试 setter 替代 | 首次查询、空查询、大小写、超限、Return/F3 均经真实查询框按键/编辑;setter 只用于前置观测 | + +## 身份 + +源码/测试/构建身份见 [validation.json](validation.json);截图为本机同一 Qt 测试窗口的离屏历史命中态。 diff --git a/experiments/shell/results/2026-11-06-qt-find/find-panel.png b/experiments/shell/results/2026-11-06-qt-find/find-panel.png index 6b07bc4a2b4e6bb3e8d16353fe9b832931ec9f24..8ddf1eeae770583c0fc9c947090a9d96b364783a 100644 GIT binary patch literal 40085 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 RqZ(lp&Z+*8DtG Date: Sat, 7 Nov 2026 03:15:25 +0800 Subject: [PATCH 6/8] test(qt): exercise the real Find cap and bounded long scan Use the formal query entry for ASCII keyboard and Qt input-method commit text: legal 4096-byte and >4096 Chinese/NFD inputs preserve the full text and show the explicit limit without silent truncation, with zero child-input leakage. Add a third real child with bounded deterministic output: repeated short literals exceed 1024 non-overlapping hits, exercising the Qt capped notice and finite first/last wrap; a >4096-byte scan keeps an observable Searching stage with partial hits, and public navigation stays inside the scanned prefix instead of wrapping into unscanned output. Pacing is a test-only timer setting; product budgets and caps are unchanged. --- src/qt/find_test.cpp | 109 +++++++++++++++++++++++++++++++++-- src/qt/selection_fixture.hpp | 13 +++++ src/qt/terminal_widget.hpp | 14 +++-- 3 files changed, 126 insertions(+), 10 deletions(-) diff --git a/src/qt/find_test.cpp b/src/qt/find_test.cpp index 2ceeee42..38a82fcc 100644 --- a/src/qt/find_test.cpp +++ b/src/qt/find_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,21 @@ size_t line_rank(SessionModel& model, uint64_t id) { const TextDocument document(model.screen()); return document.index_of(id).value_or(std::numeric_limits::max()); } +void clear_query(QLineEdit* query) { + query->setFocus(); + QTest::keyClick(query, Qt::Key_A, Qt::ControlModifier); + QTest::keyClick(query, Qt::Key_Backspace); +} +void type_ascii(QLineEdit* query, const QString& text) { + clear_query(query); + QTest::keyClicks(query, text); +} +void commit_ime(QLineEdit* query, const QString& text) { + clear_query(query); + QInputMethodEvent event; + event.setCommitString(text); + QApplication::sendEvent(query, &event); +} void start_child(SessionModel& model, TerminalWidget& terminal, QWidget& window, const std::wstring& executable, const QString& record, const std::wstring& working, const char* message) { const QString args = QStringLiteral("--selection-child \"%1\"").arg(record); @@ -179,7 +195,7 @@ int main(int argc, char** argv) { require(terminal.find_match_count_for_test() == 0, "empty query retained matches"); require(terminal.find_status_for_test() == QStringLiteral("No query"), "empty query lacks explicit status"); - terminal.set_find_query_for_test(QStringLiteral("sel_head")); + type_ascii(first_query, QStringLiteral("sel_head")); await_find_done(terminal, first, lifecycle, "case-sensitive no-match did not finish"); require(terminal.find_match_count_for_test() == 0, "case-sensitive query unexpectedly matched"); auto* match_case = first_window.findChild(QStringLiteral("findCase")); @@ -189,16 +205,33 @@ int main(int argc, char** argv) { "unchecked Match case did not find the folded hit"); QTest::mouseClick(match_case, Qt::LeftButton); - terminal.set_find_query_for_test(QString(4097, QLatin1Char('x'))); - require(terminal.find_query_text_for_test().size() == 4097, "query was silently truncated"); + // Real edit path: exactly 4096 ASCII bytes is legal, one more is rejected + // with the full input preserved and an explicit status. + type_ascii(first_query, QString(4096, QLatin1Char('x'))); + require(terminal.find_query_text_for_test().size() == 4096, "4096-byte legal query was changed"); + require(!terminal.find_status_for_test().contains(QStringLiteral("4096")), + "legal 4096-byte query was rejected"); + QTest::keyClicks(first_query, QStringLiteral("x")); + require(terminal.find_query_text_for_test().size() == 4097, "4097-byte query was truncated"); require(terminal.find_status_for_test().contains(QStringLiteral("4096")), "4097-byte query did not report the existing limit"); - terminal.set_find_query_for_test(QString(1366, QChar(0x4e2d))); + + // Chinese/NFD input uses Qt input-method commit events (not physical IME). + commit_ime(first_query, QString(1365, QChar(0x4e2d))); // 4095 UTF-8 bytes + require(terminal.find_query_text_for_test().size() == 1365, "legal Chinese query was changed"); + require(!terminal.find_status_for_test().contains(QStringLiteral("4096")), + "legal 4095-byte Chinese query was rejected"); + commit_ime(first_query, QString(1366, QChar(0x4e2d))); // 4098 bytes + require(terminal.find_query_text_for_test().size() == 1366, "over-limit Chinese query was truncated"); require(terminal.find_status_for_test().contains(QStringLiteral("4096")), "over-limit Chinese query did not report the limit"); - terminal.set_find_query_for_test(QString(QString::fromUtf8("e\xcc\x81")).repeated(2049)); + commit_ime(first_query, QString(QString::fromUtf8("e\xcc\x81")).repeated(2049)); + require(terminal.find_query_text_for_test() == QString(QString::fromUtf8("e\xcc\x81")).repeated(2049), + "over-limit NFD query was truncated"); require(terminal.find_status_for_test().contains(QStringLiteral("4096")), "over-limit NFD query did not report the limit"); + require(read_input(first_input).empty(), + "query edit path leaked into the first child"); terminal.set_find_query_for_test(QStringLiteral("SEL_HEAD")); await([&] { return terminal.find_current_range_for_test().has_value(); }, first, lifecycle, @@ -248,13 +281,77 @@ int main(int argc, char** argv) { require(read_input(first_input) == "a" || read_input(first_input) == "ar", "post-Escape input reached the wrong child"); + // A third real session provides bounded, deterministic >1024 hits and + // a scan longer than one 4096-unit budget, without injecting documents. + const QString third_input = workspace.path() + "/third-input.bin"; + SessionModel third; QWidget third_window; TerminalWidget third_terminal(third, &third_window); + third_window.resize(1200, 900); + start_child(third, third_terminal, third_window, executable, third_input, + workspace.path().toStdWString(), "find third child did not start"); + QTest::qWait(500); + require(third.send(std::string(1, 'm')), "many-hit command rejected"); + await([&] { return third.screen().retained_size() >= 78; }, third, lifecycle, + "many-hit child output missing"); + third_window.activateWindow(); third_terminal.setFocus(); QTest::qWait(30); + third_terminal.set_find_timer_interval_for_test(25); + QTest::keyClick(&third_terminal, Qt::Key_F, Qt::ControlModifier | Qt::ShiftModifier); + auto* third_query = third_window.findChild(QStringLiteral("findQuery")); + require(third_query && third_query->hasFocus(), "third Find query did not focus"); + type_ascii(third_query, QStringLiteral("ZZ")); + QTest::qWait(35); + require(third_terminal.find_search_status_for_test() == termforge::SearchStatus::running, + "long scan did not remain running after one bounded stage"); + require(third_terminal.find_match_count_for_test() > 0, + "long scan exposed no partial matches"); + require(third_terminal.find_status_for_test().startsWith(QStringLiteral("Searching")), + "long scan lacks an observable Searching stage"); + auto* third_next = third_window.findChild(QStringLiteral("findNext")); + const size_t partial_before = third_terminal.find_current_index_for_test(); + QTest::mouseClick(third_next, Qt::LeftButton); + require(third_terminal.find_current_index_for_test() < third_terminal.find_match_count_for_test(), + "running-scan navigation left the scanned prefix"); + require(!third_terminal.find_last_wrapped_for_test(), + "running-scan navigation wrapped into an unscanned area"); + require(third_terminal.find_current_index_for_test() != partial_before || + third_terminal.find_status_for_test() == QStringLiteral("No further scanned match"), + "running-scan Next neither advanced nor reported the scanned-prefix boundary"); + await([&] { + const auto state = third_terminal.find_search_status_for_test(); + return state == termforge::SearchStatus::complete || state == termforge::SearchStatus::capped; + }, third, lifecycle, "long scan did not complete"); + require(third_terminal.find_match_count_for_test() == 52, "long scan hit count differs"); + require(third_terminal.find_status_for_test().contains(QStringLiteral("52 matches")), + "long scan completion status differs"); + + // Repeated short literals exceed 1024 in retained rows; the existing cap + // is asserted through the real query entry, including finite wrap. + type_ascii(third_query, QStringLiteral("AB")); + await([&] { return third_terminal.find_search_status_for_test() == termforge::SearchStatus::capped; }, + third, lifecycle, "1024-result cap was not reached through real child output"); + require(third_terminal.find_match_count_for_test() == 1024, "capped result count is not 1024"); + require(third_terminal.find_status_for_test().contains(QStringLiteral("capped")), + "capped status lacks the truncation notice"); + for (int guard = 0; guard < 1030 && third_terminal.find_current_index_for_test() != 0; ++guard) + QTest::mouseClick(third_window.findChild(QStringLiteral("findPrevious")), Qt::LeftButton); + require(third_terminal.find_current_index_for_test() == 0, "could not position at capped first hit"); + QTest::mouseClick(third_window.findChild(QStringLiteral("findPrevious")), Qt::LeftButton); + require(third_terminal.find_last_wrapped_for_test() && + third_terminal.find_current_index_for_test() == 1023, + "capped Previous did not wrap finitely to the last hit"); + QTest::mouseClick(third_window.findChild(QStringLiteral("findNext")), Qt::LeftButton); + require(third_terminal.find_last_wrapped_for_test() && + third_terminal.find_current_index_for_test() == 0, + "capped Next did not wrap finitely to the first hit"); + require(read_input(third_input) == "m", "many-hit query leaked into the third child"); + std::cout << "{\"schema\":\"termforge-qt-find-v1\",\"status\":\"passed\"," "\"multi_match_order\":true,\"wrap\":true,\"exact_highlight\":true," "\"history_reveal\":true,\"resize_source_migration\":true," "\"eviction_no_resurrection\":true,\"epoch_invalidation\":true," "\"two_session_isolation\":true,\"query_zero_leak\":true," "\"escape_input_once\":true,\"empty_query\":true,\"case_toggle\":true," - "\"limit_bytes\":true,\"return_f3\":true,\"offscreen_only\":true}\n"; + "\"limit_bytes\":true,\"return_f3\":true,\"real_4096_limit\":true," + "\"real_1024_cap\":true,\"partial_scan_no_wrap\":true,\"offscreen_only\":true}\n"; require(first.send(std::string(1, 'q')), "first child quit rejected"); require(second.send(std::string(1, 'q')), "second child quit rejected"); await([&] { return first.finished(); }, first, lifecycle, "first child did not exit"); diff --git a/src/qt/selection_fixture.hpp b/src/qt/selection_fixture.hpp index a3f83dfd..eeb14175 100644 --- a/src/qt/selection_fixture.hpp +++ b/src/qt/selection_fixture.hpp @@ -82,6 +82,19 @@ inline int selection_child(const std::wstring& record_path = {}) { selection_write(output.get(), "\x1b" "cSEL_RIS\r\nSEL>"); continue; } + if (key == 'm') { + // Bounded deterministic loads: 30 rows x 40 "AB" hits (1200 total) + // and 100 rows with one sparse "ZZ" near the end (>8000 scan bytes). + std::string many; + for (int row = 0; row < 52; ++row) many += std::string(78, 'X') + "ZZ\r\n"; + for (int row = 0; row < 26; ++row) { + for (int hit = 0; hit < 40; ++hit) many += "AB"; + many += "\r\n"; + } + many += "SEL_MANY\r\nSEL>"; + selection_write(output.get(), many); + continue; + } } } diff --git a/src/qt/terminal_widget.hpp b/src/qt/terminal_widget.hpp index 622738a8..8a1a6d5a 100644 --- a/src/qt/terminal_widget.hpp +++ b/src/qt/terminal_widget.hpp @@ -73,6 +73,7 @@ class TerminalWidget final : public QWidget { size_t find_current_index_ = 0; bool find_last_wrapped_ = false; uint64_t find_generation_ = 0; + int find_interval_ms_ = 0; QTimer find_timer_; std::unique_ptr clipboard_; ClipboardWrite last_copy_; @@ -562,7 +563,7 @@ class TerminalWidget final : public QWidget { } find_current_.reset(); find_current_index_ = 0; set_find_status(QStringLiteral("Searching...")); - find_timer_.start(0); + find_timer_.start(find_interval_ms_); } void step_find() { const TextDocument document(model_.screen()); @@ -593,10 +594,13 @@ class TerminalWidget final : public QWidget { const TextDocument document(model_.screen()); const std::optional anchor = find_current_ ? std::optional{find_current_->range.begin} : std::optional{find_anchor()}; - const auto nav = find_search_.navigate(document, direction, anchor, true); + const bool allow_wrap = find_search_.status() == SearchStatus::complete || + find_search_.status() == SearchStatus::capped; + const auto nav = find_search_.navigate(document, direction, anchor, allow_wrap); if (!nav) { - set_find_status(QStringLiteral("No matches")); - find_current_.reset(); find_last_wrapped_ = false; update(); return; + set_find_status(allow_wrap ? QStringLiteral("No matches") + : QStringLiteral("No further scanned match")); + find_last_wrapped_ = false; update(); return; } find_current_ = nav->match; find_current_index_ = nav->index; find_last_wrapped_ = nav->wrapped; @@ -829,6 +833,8 @@ class TerminalWidget final : public QWidget { size_t find_match_count_for_test() const { return find_search_.matches().size(); } size_t find_current_index_for_test() const { return find_current_index_; } bool find_last_wrapped_for_test() const { return find_last_wrapped_; } + void set_find_timer_interval_for_test(int milliseconds) { find_interval_ms_ = (std::max)(0, milliseconds); } + SearchStatus find_search_status_for_test() const { return find_search_.status(); } std::optional find_current_range_for_test() const { return find_current_ ? std::optional{find_current_->range} : std::nullopt; } From c5e21f77cdfe03e509fbc3adc93d5f64f7ce1150 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 03:15:50 +0800 Subject: [PATCH 7/8] docs(qt): record the real Find cap and long-scan evidence --- .../results/2026-11-06-qt-find/README.md | 8 +++--- .../2026-11-06-qt-find/validation.json | 28 ++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/experiments/shell/results/2026-11-06-qt-find/README.md b/experiments/shell/results/2026-11-06-qt-find/README.md index f6d75ec3..cab3003f 100644 --- a/experiments/shell/results/2026-11-06-qt-find/README.md +++ b/experiments/shell/results/2026-11-06-qt-find/README.md @@ -12,7 +12,7 @@ ## 验证 - `qt_find` 1/1,Qt 正式路径 10/10;two real selection children with independent input recording:multiple `SEL_N_` matches 的顺序/index、双向 wrap、精确源文本/列与显示宽度、历史 reveal、缩窄/扩大同源迁移、淘汰不复活、RIS 旧 epoch 失效;两个会话查询隔离,查询零泄漏到 child,Escape 后 `Z` 恰好一次只到正确 child;空查询、实际 Match case、4097-byte/中文/NFD 超限拒绝、Return/F3/Shift+F3。未触个人剪贴板/桌面。 -- 1024 结果上限由既有 TextSearch 合同/核心 capped 测试覆盖;固定 child 的 100 行历史无法在真实入口达到 1024,未用伪造大文档冒充用户证据。 +- **1024 截断/有限循环已通过真实入口**:第 3 个真实 child 输出 52 行 `X...ZZ`(52 命中,>4096 字节扫描)+ 26 行 × 40 个 `AB`(1040 命中);正式查询框输入 `ZZ` 在 25ms 测试节拍下保留 Searching/部分命中,公开 Next 不越出已扫描前缀;输入 `AB` 达到 1024 capped、提示截断,首尾 Previous/Next wrap 有限(1023↔0)。未改产品上限/预算/注入文档。 - 截图:[find-panel.png](find-panel.png)(同一真实 Qt 测试窗口,历史命中态;离屏,不代表物理 DPI/呈现)。 - 当前 head CI 与源码/构建身份见 [validation.json](validation.json);本切片不代表 #29/#30/R2 完成。 @@ -20,7 +20,7 @@ - 固定样例/ASCII 字面量与离屏 Qt;中文/NFD 只声明字面标量匹配,不宣称字素/glyph/宽度等价。 - 不覆盖选词/选行、滚动条/翻页键历史、备用屏/TUI Find、物理呈现/DPI、完整 Unicode 字素或性能阈值;不新增数值性能验收。 -- 1024 结果截断仅有核心合同覆盖;真实 child 保留上限 100 行,不能到达 1024,未伪造。 +- 1024 截断现在由真实 child/查询入口覆盖;固定 child 仍受 100 行历史 + 24 live 行容量约束,本次负载 78 行(52 ZZ + 26 AB)在容量内。 - 旧 B/CD、192.168.3.210 校准/v2、A/B/C、旧六阶段失败、#27/#29/#30/#35/#7 状态不变。 @@ -31,8 +31,8 @@ | 多命中准确顺序/循环/当前页与历史页 | 多个 `SEL_N_` 顺序/index、双向 wrap、历史 reveal、精确源文本/列和显示宽度 | | 缩窄扩大同源迁移、写入/淘汰失效、旧 epoch | resize 前后源 range 不变;淘汰后无复活;RIS 后旧位置拒绝、新 epoch 可查 | | 双会话隔离、Escape 后输入只到正确 child | 两个真实 child + 独立输入文件;查询零泄漏,Escape 后 `Z` 恰好一次到目标 | -| 空查询、大小写、1024 截断/有限循环、4096 字节边界 | 空查询/Match case/Return/F3/Shift+F3/ASCII+中文+NFD 超限明确拒绝;1024 由核心 capped 测试覆盖,真实 child 100 行无法到达,明确记录 | -| 公开入口而非测试 setter 替代 | 首次查询、空查询、大小写、超限、Return/F3 均经真实查询框按键/编辑;setter 只用于前置观测 | +| 空查询、大小写、1024 截断/有限循环、4096 字节边界 | 空查询/Match case/Return/F3/Shift+F3;真实键盘 4096 合法/4097 拒绝,中文/NFD 经 Qt IME commit 合法与超限;真实 child 1040 命中达到 1024 capped,首尾有限 wrap;>4096 扫描保留 Searching/部分命中且不越界 | +| 公开入口而非测试 setter 替代 | 首次查询、空查询、大小写、ASCII 键盘超限、中文/NFD IME commit、Return/F3 均经真实查询框;setter 仅用于失效/前置观测 | ## 身份 diff --git a/experiments/shell/results/2026-11-06-qt-find/validation.json b/experiments/shell/results/2026-11-06-qt-find/validation.json index b617e242..15c796f3 100644 --- a/experiments/shell/results/2026-11-06-qt-find/validation.json +++ b/experiments/shell/results/2026-11-06-qt-find/validation.json @@ -1,14 +1,14 @@ { "schema": "termforge-qt-find-validation-v1", "baseline_main": "3b063e3c8f6c57cd01a0c5339c2145969b92aa86", - "source_commit": "e6aa53a", - "source_hash_note": "Hashes are SHA-256 of committed LF Git blob content at 3888d84; this validation is a docs-only follow-up.", + "source_commit": "ea6425a", + "source_hash_note": "Hashes are SHA-256 of committed LF Git blob content at ea6425a; this validation is a docs-only follow-up.", "source_files_sha256": { - "src/qt/terminal_widget.hpp": "982cd64329ad8b952606486a8ee0f90b8ca5c0e192218868ab6503cdfd90dfa9", - "src/qt/find_test.cpp": "222e6d5df8bb7e0090b80d67350d230b3e225751ebd1b8b83efd033c2f7aaf62", + "src/qt/terminal_widget.hpp": "90ea07333992b7e7f380e17ae2fc8b647a74c4f0bbfdb511d7a5d17e0b7976fa", + "src/qt/find_test.cpp": "25fc9922d7e007fe8b65860079c59dd9d0de6e257e4f4b930ee205f41330b265", "CMakeLists.txt": "93c6977a6978c061bdf675eac3bcf3b61ee3b775f9454676f86c0930bf3ab2d9", ".github/workflows/qt-cpp.yml": "e1a0f4c0496a02bb2434e71befcaaf0a114808629950d10c59f3ff29fbbefded", - "src/qt/selection_fixture.hpp": "cd2461e0ada22cf56435b1c578e41fa611575b4bbc0bf68568fc89d0067e8591" + "src/qt/selection_fixture.hpp": "1f44bbed1358442febacca5f9ee89134508cd8bf75fb6e67a863f9351d07e222" }, "environment": { "os": "Windows 11 x64 build 26200", @@ -51,18 +51,25 @@ "limit_ascii_4097_rejected": true, "limit_chinese_bytes_rejected": true, "limit_nfd_bytes_rejected": true, - "return_f3_shift_f3": true + "return_f3_shift_f3": true, + "real_1024_cap_via_child": true, + "cap_notice_and_finite_wrap": true, + "long_scan_searching_partial_hits": true, + "partial_navigation_no_unscanned_wrap": true, + "real_keyboard_limit_text": true, + "real_ime_commit_chinese_nfd": true, + "legal_and_over_limit_preserved": true }, "tests": { "qt_find": { "passed": 1, "failed": 0, - "seconds": 4.93 + "seconds": 7.38 }, "qt_formal_path_regression": { "passed": 10, "failed": 0, - "seconds": 18.92 + "seconds": 20.6 } }, "evidence": { @@ -71,9 +78,10 @@ "fixture": "deterministic selection child (SEL_SOFT_ source hit; query not sent as terminal input)" }, "limitations": [ + "The 1024 cap and finite wrap are now exercised through the real Qt query entry and a bounded real child (26 rows x 40 non-overlapping AB hits = 1040); a 25 ms test-only pacing observes a >4096-byte scan in progress with partial hits, without changing product budgets.", "Fixed ASCII literal sample and offscreen Qt; Chinese/NFD only claims literal scalar matching.", - "The Qt query field/TextSearch honors the 4096-byte limit and explicitly rejects over-limit text; the fixed 100-row child history cannot reach the 1024-result cap, so that exact cap remains covered by the existing core TextSearch capped test rather than a real-child UI sample.", "No word/line selection, scrollbar/page-key history, alternate-screen/TUI Find, physical presentation/DPI, full Unicode graphemes or performance thresholds.", - "This slice does not close #29/#30/R2; old B/CD, remote VM calibration/v2, A/B/C and old six-stage failures remain unchanged." + "This slice does not close #29/#30/R2; old B/CD, remote VM calibration/v2, A/B/C and old six-stage failures remain unchanged.", + "Windows/line capacity used here: 80 columns, 40 AB hits per row, 26 rows = 1040 hits; 52 ZZ rows (4160 bytes) plus 26 AB rows remain within the 100-row history + live rows. No injected document or product cap change." ] } \ No newline at end of file From 4dd764c1194690b445705b18555d6d6d32bae683 Mon Sep 17 00:00:00 2001 From: JavaZeroo-Bot <133958205+JavaZeroo-Bot@users.noreply.github.com> Date: Sat, 7 Nov 2026 03:16:02 +0800 Subject: [PATCH 8/8] docs(qt): mark the Find acceptance matrix closed --- experiments/shell/results/2026-11-06-qt-find/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/experiments/shell/results/2026-11-06-qt-find/README.md b/experiments/shell/results/2026-11-06-qt-find/README.md index cab3003f..86cda44e 100644 --- a/experiments/shell/results/2026-11-06-qt-find/README.md +++ b/experiments/shell/results/2026-11-06-qt-find/README.md @@ -26,6 +26,8 @@ ## 原派单缺口 → 证据 +状态:五项均为**已闭合**;无待完成项(边界外能力仍不宣称)。 + | 原要求 | 本次证据 | |---|---| | 多命中准确顺序/循环/当前页与历史页 | 多个 `SEL_N_` 顺序/index、双向 wrap、历史 reveal、精确源文本/列和显示宽度 |