diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp index b9dd26f5fc..4a20f0573f 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.cpp @@ -1069,25 +1069,26 @@ void Controller::hideSearchBar() { SetMarkdownPreviewSearchMatches(_preview.get(), {}, -1); _preview->setFocus(); } + invalidateSearchSession(); _searchEntries.clear(); _searchCurrentEntry = -1; } -auto Controller::collectSearchEntries() const --> std::vector { +std::vector Controller::ScanSearchEntries( + const SearchSources &sources, + const QString &query) { auto result = std::vector(); - if (_searchQuery.isEmpty() || !_preview) { + if (query.isEmpty()) { return result; } - const auto sources = MarkdownPreviewSearchSources(_preview.get()); const auto scan = [&](const QString &text, auto &&push) { auto from = 0; while ((from = int(text.indexOf( - _searchQuery, + query, from, Qt::CaseInsensitive))) >= 0) { - push(from, from + int(_searchQuery.size())); - from += int(_searchQuery.size()); + push(from, from + int(query.size())); + from += int(query.size()); } }; for (auto i = 0; i != int(sources.size()); ++i) { @@ -1106,6 +1107,31 @@ auto Controller::collectSearchEntries() const return result; } +void Controller::ensureSearchSnapshot() { + if (_searchSnapshot || !_preview) { + return; + } + _searchSnapshot = std::make_shared( + MarkdownPreviewSearchSources(_preview.get())); +} + +void Controller::invalidateSearchSession() { + _searchSnapshot = nullptr; + _searchCache.clear(); + ++_searchGeneration; +} + +std::vector Controller::rescanSearchEntries() { + invalidateSearchSession(); + ensureSearchSnapshot(); + if (!_searchSnapshot || _searchQuery.isEmpty()) { + return {}; + } + auto result = ScanSearchEntries(*_searchSnapshot, _searchQuery); + _searchCache.emplace(_searchQuery, result); + return result; +} + void Controller::applySearchQuery(const QString &query) { _searchQuery = query; if (!_searchBar) { @@ -1115,13 +1141,40 @@ void Controller::applySearchQuery(const QString &query) { } void Controller::refreshSearchResults() { + invalidateSearchSession(); if (_searchBar && _searchBar->shown()) { rebuildSearchResults(_searchCurrentEntry, false); } } void Controller::rebuildSearchResults(int preferredCurrent, bool activate) { - _searchEntries = collectSearchEntries(); + _searchDesiredCurrent = preferredCurrent; + _searchDesiredActivate = activate; + if (_searchQuery.isEmpty() || !_preview) { + applySearchEntries({}, preferredCurrent, activate); + return; + } + const auto i = _searchCache.find(_searchQuery); + if (i != end(_searchCache)) { + DEBUG_LOG(("Native Markdown IV: search cache hit: %1" + ).arg(_searchQuery)); + auto entries = i->second; + applySearchEntries(std::move(entries), preferredCurrent, activate); + return; + } + if (_searchScanInFlight) { + DEBUG_LOG(("Native Markdown IV: search coalesced: %1" + ).arg(_searchQuery)); + return; + } + startSearchScan(); +} + +void Controller::applySearchEntries( + std::vector &&entries, + int preferredCurrent, + bool activate) { + _searchEntries = std::move(entries); const auto total = int(_searchEntries.size()); _searchCurrentEntry = total ? std::clamp(preferredCurrent, 0, total - 1) @@ -1129,6 +1182,72 @@ void Controller::rebuildSearchResults(int preferredCurrent, bool activate) { applyCurrentSearchEntry(activate); } +void Controller::startSearchScan() { + Expects(!_searchScanInFlight); + + ensureSearchSnapshot(); + if (!_searchSnapshot) { + return; + } + _searchScanInFlight = true; + DEBUG_LOG(("Native Markdown IV: search request: %1" + ).arg(_searchQuery)); + const auto weak = base::make_weak(this); + crl::async([ + weak, + query = _searchQuery, + generation = _searchGeneration, + snapshot = _searchSnapshot + ] { + if (!weak) { + return; + } + auto entries = ScanSearchEntries(*snapshot, query); + crl::on_main([ + weak, + query, + generation, + entries = std::move(entries) + ]() mutable { + if (const auto strong = weak.get()) { + strong->finishSearchScan( + query, + generation, + std::move(entries)); + } + }); + }); +} + +void Controller::finishSearchScan( + const QString &query, + int generation, + std::vector &&entries) { + _searchScanInFlight = false; + if (generation != _searchGeneration) { + DEBUG_LOG(("Native Markdown IV: search response dropped: %1" + ).arg(query)); + } else { + DEBUG_LOG(("Native Markdown IV: search response: %1 (%2 matches)" + ).arg(query + ).arg(int(entries.size()))); + _searchCache[query] = entries; + } + if (!_searchBar || !_searchBar->shown() || _searchQuery.isEmpty()) { + return; + } + if (generation == _searchGeneration && query == _searchQuery) { + applySearchEntries( + std::move(entries), + _searchDesiredCurrent, + _searchDesiredActivate); + } else { + rebuildSearchResults( + _searchDesiredCurrent, + _searchDesiredActivate); + } +} + void Controller::resolveCurrentSearchEntry() { if (!_preview) { return; @@ -1156,7 +1275,7 @@ void Controller::resolveCurrentSearchEntry() { if (!ExpandMarkdownPreviewDetails(_preview.get(), anchorId)) { return; } - _searchEntries = collectSearchEntries(); + _searchEntries = rescanSearchEntries(); const auto newTotal = int(_searchEntries.size()); const auto materialized = newTotal - (oldTotal - (runEnd - runStart)); diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.h index 34b2298c2b..400aa1a546 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_controller.h @@ -7,8 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/flat_map.h" #include "base/object_ptr.h" #include "base/unique_qptr.h" +#include "base/weak_ptr.h" #include "iv/markdown/iv_markdown_document.h" #include "iv/markdown/iv_markdown_prepare.h" #include "ui/effects/animations.h" @@ -35,7 +37,9 @@ class SearchBar; namespace Iv::Markdown { -class Controller final { +struct MarkdownArticleSearchSource; + +class Controller final : public base::has_weak_ptr { public: Controller( not_null delegate, @@ -75,6 +79,8 @@ private: struct HistoryEntry; struct SearchEntry; + using SearchSources = std::vector; + void close(); void createWindow(); void createLayerManager(); @@ -99,7 +105,21 @@ private: void resolveCurrentSearchEntry(); void applyCurrentSearchEntry(bool activate); void stepSearchResult(int delta); - [[nodiscard]] std::vector collectSearchEntries() const; + [[nodiscard]] static std::vector ScanSearchEntries( + const SearchSources &sources, + const QString &query); + void ensureSearchSnapshot(); + void invalidateSearchSession(); + [[nodiscard]] std::vector rescanSearchEntries(); + void applySearchEntries( + std::vector &&entries, + int preferredCurrent, + bool activate); + void startSearchScan(); + void finishSearchScan( + const QString &query, + int generation, + std::vector &&entries); void openSource(); [[nodiscard]] ViewerKind viewerKind() const; [[nodiscard]] QString subtitleText() const; @@ -180,6 +200,12 @@ private: QString _searchQuery; std::vector _searchEntries; int _searchCurrentEntry = -1; + std::shared_ptr _searchSnapshot; + base::flat_map> _searchCache; + int _searchGeneration = 0; + int _searchDesiredCurrent = 0; + bool _searchDesiredActivate = false; + bool _searchScanInFlight = false; std::vector _history; int _historyIndex = -1; int _shownHistoryIndex = -1;