diff --git a/src/ui/windows/console_output_model.cpp b/src/ui/windows/console_output_model.cpp index ba2eaaf..bac5550 100644 --- a/src/ui/windows/console_output_model.cpp +++ b/src/ui/windows/console_output_model.cpp @@ -32,5 +32,18 @@ bool consoleLinePassesFilter(const std::string& lineText, return true; } +bool ExtendFlatVisibleIndices(std::vector& visible, std::uint64_t oldFirst, std::uint64_t oldEnd, + std::uint64_t newFirst, int n, const std::function& passes) +{ + const std::uint64_t newEnd = newFirst + static_cast(n < 0 ? 0 : n); + if (newFirst < oldFirst || newFirst > oldEnd || newEnd < oldEnd) return false; + const int shift = static_cast(newFirst - oldFirst); + visible.erase(visible.begin(), std::lower_bound(visible.begin(), visible.end(), shift)); + for (int& idx : visible) idx -= shift; + for (int i = static_cast(oldEnd - newFirst); i < n; ++i) + if (passes(i)) visible.push_back(i); + return true; +} + } // namespace ui } // namespace dragonx diff --git a/src/ui/windows/console_output_model.h b/src/ui/windows/console_output_model.h index 485a93f..121744a 100644 --- a/src/ui/windows/console_output_model.h +++ b/src/ui/windows/console_output_model.h @@ -2,7 +2,10 @@ #include "console_channel.h" +#include +#include #include +#include namespace dragonx { namespace ui { @@ -20,5 +23,13 @@ bool consoleLinePassesFilter(const std::string& lineText, ConsoleChannel channel, const ConsoleOutputFilter& filter); +// Incrementally maintain a FLAT (filtered, fold-free) visible set across model appends/evictions. `visible` holds +// ascending raw indices computed for the absolute window [oldFirst, oldEnd); the model now spans +// [newFirst, newFirst + n). Drops evicted indices, rebases the survivors and appends every new raw index in +// [oldEnd - newFirst, n) for which passes(i) is true. Returns false (visible untouched) when the windows do not +// chain; the caller then rescans. Only valid while the filter is unchanged (line text/channel are immutable). +bool ExtendFlatVisibleIndices(std::vector& visible, std::uint64_t oldFirst, std::uint64_t oldEnd, + std::uint64_t newFirst, int n, const std::function& passes); + } // namespace ui } // namespace dragonx diff --git a/src/ui/windows/console_tab.cpp b/src/ui/windows/console_tab.cpp index 16f4a3c..b025800 100644 --- a/src/ui/windows/console_tab.cpp +++ b/src/ui/windows/console_tab.cpp @@ -948,15 +948,28 @@ void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLow // fold triangles aren't drawn/clickable while filtering (else clicks would silently mutate the // collapsed flag with no visible effect and the glyph would disagree with the rendered block). folding_active_ = !has_filter; - visible_indices_.clear(); const int n = static_cast(model_.size()); - if (has_filter) { + const std::uint64_t seqFirst = model_.firstSeq(); + const int flatFlags = (outputFilter.daemonMessagesEnabled ? 1 : 0) | (outputFilter.errorsOnly ? 2 : 0) | + (outputFilter.rpcTraceEnabled ? 4 : 0) | (outputFilter.appMessagesEnabled ? 8 : 0); + // Filtered (flat) view with the exact same filter as the last pass and a model that only appended/evicted + // since: drop evicted indices, rebase, test only the new lines. Identical to the full scan below (line text + + // channel are immutable; the flat view ignores folds) without its per-drain O(total chars) lowercase pass. + const bool extended = has_filter && vis_flat_valid_ && vis_flat_flags_ == flatFlags && + vis_flat_filter_ == outputFilter.text && + ExtendFlatVisibleIndices(visible_indices_, vis_flat_first_, vis_flat_end_, seqFirst, n, + [&](int i) { return consoleLinePassesFilter(model_[i].text, model_[i].channel, outputFilter); }); + if (extended) { + // visible_indices_ updated in place + } else if (has_filter) { + visible_indices_.clear(); // Filtered view is flat — folding is bypassed so every match is reachable. for (int i = 0; i < n; i++) { if (!consoleLinePassesFilter(model_[i].text, model_[i].channel, outputFilter)) continue; visible_indices_.push_back(i); } } else { + visible_indices_.clear(); // No filter — show all lines, but hide the interior of any collapsed JSON block // (its opener stays visible; the opener + foldSpan lines through the closer are skipped). for (int i = 0; i < n; ) { @@ -966,6 +979,11 @@ void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLow else i++; } } + vis_flat_valid_ = has_filter; + vis_flat_flags_ = flatFlags; + vis_flat_filter_ = outputFilter.text; + vis_flat_first_ = seqFirst; + vis_flat_end_ = seqFirst + static_cast(n); filter_match_count_ = hasTextFilter ? static_cast(visible_indices_.size()) : 0; } @@ -2129,6 +2147,7 @@ void ConsoleTab::clear() ++vis_generation_; layout_ = ConsoleLayout{}; // drop the refs into wrap_cache_ BEFORE resetting it (no dangling segments) wrap_cache_.reset(); + vis_flat_valid_ = false; stop_confirm_pending_ = false; // a pending 'stop' confirmation is cancelled by clearing addLine(TR("console_cleared"), ConsoleChannel::Info); } diff --git a/src/ui/windows/console_tab.h b/src/ui/windows/console_tab.h index 1c0f84c..c4dbf7d 100644 --- a/src/ui/windows/console_tab.h +++ b/src/ui/windows/console_tab.h @@ -188,6 +188,13 @@ private: bool folding_active_ = true; // fold UI shown only in the unfiltered (foldable) view bool has_text_filter_ = false; // computed once per frame (before the toolbar draws it) std::string filter_lower_; // lowercased filter needle for match highlighting + // Flat (filtered) view bookkeeping for the incremental computeVisibleLines path: the exact filter and the + // absolute model window [vis_flat_first_, vis_flat_end_) the current visible_indices_ were computed for. + bool vis_flat_valid_ = false; + int vis_flat_flags_ = 0; + std::string vis_flat_filter_; + std::uint64_t vis_flat_first_ = 0; + std::uint64_t vis_flat_end_ = 0; // Memoization keys so the two expensive per-frame passes rebuild only on change (not every frame): // computeVisibleLines (filter scan over the whole model) is keyed on the model revision + filter diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 4814b37..f2415fc 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -5858,6 +5858,35 @@ void testConsoleWrapCache() } } +// ExtendFlatVisibleIndices (console_output_model.h): the incremental filtered visible set equals a full rescan. +void testConsoleFlatVisibleIndices() +{ + using dragonx::ui::ExtendFlatVisibleIndices; + std::deque mdl; + std::uint64_t first = 0; + auto passes = [&mdl](int i) { return mdl[static_cast(i)].find('x') != std::string::npos; }; + auto rescan = [&]() { + std::vector v; + for (int i = 0; i < static_cast(mdl.size()); ++i) if (passes(i)) v.push_back(i); + return v; + }; + for (int i = 0; i < 20; ++i) mdl.push_back(i % 3 == 0 ? "x" + std::to_string(i) : std::string("y")); + std::vector vis = rescan(); + std::uint64_t oldFirst = first, oldEnd = first + mdl.size(); + for (int k = 0; k < 4; ++k) { mdl.pop_front(); ++first; } // evict 4 (incl. matches 0 and 3) + for (int i = 20; i < 25; ++i) mdl.push_back(i % 2 == 0 ? "x" : "y"); // append 5 + EXPECT_TRUE(ExtendFlatVisibleIndices(vis, oldFirst, oldEnd, first, static_cast(mdl.size()), passes)); + EXPECT_TRUE(vis == rescan()); + const std::vector untouched = vis; // non-chaining window => refused + EXPECT_FALSE(ExtendFlatVisibleIndices(vis, first, first + mdl.size(), first + mdl.size() + 1, 3, passes)); + EXPECT_TRUE(vis == untouched); + oldFirst = first; oldEnd = first + mdl.size(); // clear(): firstSeq jumps to old end + first += mdl.size(); + mdl.clear(); mdl.push_back("x"); mdl.push_back("y"); + EXPECT_TRUE(ExtendFlatVisibleIndices(vis, oldFirst, oldEnd, first, static_cast(mdl.size()), passes)); + EXPECT_TRUE(vis == rescan()); +} + // ConsoleScrollController — auto-scroll state machine (pin, wheel-up cooldown, backlog). void testConsoleScrollController() { @@ -15098,6 +15127,7 @@ int main() testDaemonLifecycleAdapters(); testConsoleTextLayout(); testConsoleWrapCache(); + testConsoleFlatVisibleIndices(); testConsoleModel(); testPortfolioHelpers(); testMarketSeries();