Index: autotests/src/searchbar_test.cpp =================================================================== --- autotests/src/searchbar_test.cpp +++ autotests/src/searchbar_test.cpp @@ -76,7 +76,7 @@ QCOMPARE(view.selectionRange(), Range(0, 8, 0, 9)); - bar.setSearchPattern("a"); + bar.setSearchPattern("a"); QTest::qWait(0); QCOMPARE(view.selectionRange(), Range(0, 0, 0, 1)); @@ -106,22 +106,22 @@ QVERIFY(!view.selection()); bar.setMatchCase(false); - bar.setSearchPattern("A"); + bar.setSearchPattern("A"); QTest::qWait(0); QVERIFY(!bar.matchCase()); QCOMPARE(view.selectionRange(), Range(0, 0, 0, 1)); - bar.setMatchCase(true); + bar.setMatchCase(true); QTest::qWait(0); QVERIFY(bar.matchCase()); QCOMPARE(view.selectionRange(), Range(0, 2, 0, 3)); - bar.setMatchCase(false); + bar.setMatchCase(false); QTest::qWait(0); QVERIFY(!bar.matchCase()); QCOMPARE(view.selectionRange(), Range(0, 0, 0, 1)); - bar.setMatchCase(true); + bar.setMatchCase(true); QTest::qWait(0); QVERIFY(bar.matchCase()); QCOMPARE(view.selectionRange(), Range(0, 2, 0, 3)); Index: src/search/katesearchbar.h =================================================================== --- src/search/katesearchbar.h +++ src/search/katesearchbar.h @@ -133,6 +133,7 @@ private Q_SLOTS: void onIncPatternChanged(const QString &pattern); + void incrementalSearch(); void onMatchCaseToggled(bool matchCase); void onReturnPressed(); @@ -154,7 +155,6 @@ */ void findOrReplaceAll(); - /** * Restore needed settings when signal @ref findOrReplaceAllFinished() * was received. @@ -169,6 +169,7 @@ private: // Helpers + bool hasRangeMatch(const KTextEditor::Range &range); bool find(SearchDirection searchDirection = SearchForward) { return findOrReplace(searchDirection, nullptr); }; bool findOrReplace(SearchDirection searchDirection, const QString *replacement); @@ -216,6 +217,9 @@ // Incremental search related Ui::IncrementalSearchBar *m_incUi; KTextEditor::Cursor m_incInitCursor; + QVector m_incSearchRanges; + int m_currentSearchRange; + int m_chunkOverlap; // Power search related Ui::PowerSearchBar *m_powerUi = nullptr; Index: src/search/katesearchbar.cpp =================================================================== --- src/search/katesearchbar.cpp +++ src/search/katesearchbar.cpp @@ -375,7 +375,10 @@ if (m_incUi != nullptr) { QPalette foreground(m_incUi->status->palette()); switch (matchResult) { - case MatchFound: // FALLTHROUGH + case MatchFound: + m_incUi->next->setDisabled(false); + m_incUi->prev->setDisabled(false); + [[fallthrough]]; case MatchNothing: KColorScheme::adjustForeground(foreground, KColorScheme::NormalText, QPalette::WindowText, KColorScheme::Window); m_incUi->status->clear(); @@ -425,39 +428,110 @@ // clear prior highlightings (deletes info message if present) clearHighlights(); - m_incUi->next->setDisabled(pattern.isEmpty()); - m_incUi->prev->setDisabled(pattern.isEmpty()); + m_incUi->next->setDisabled(true); + m_incUi->prev->setDisabled(true); - KateMatch match(m_view->doc(), searchOptions()); + indicateMatch(MatchNothing); - if (!pattern.isEmpty()) { - // Find, first try - const Range inputRange = KTextEditor::Range(m_incInitCursor, m_view->document()->documentEnd()); - match.searchText(inputRange, pattern); + if (pattern.isEmpty()) { + selectRange2(Range(m_incInitCursor, m_incInitCursor)); + return; } - const bool wrap = !match.isValid() && !pattern.isEmpty(); + m_currentSearchRange = 0; - if (wrap) { - // Find, second try - const KTextEditor::Range inputRange = m_view->document()->documentRange(); - match.searchText(inputRange, pattern); + const int newChunkOverlap = pattern.count(QLatin1Char('\n')); + if (newChunkOverlap != m_chunkOverlap) { + m_incSearchRanges.clear(); + } + + if (!m_incSearchRanges.isEmpty()) { + incrementalSearch(); + return; + } + + m_chunkOverlap = newChunkOverlap; + + // Build search range chunks in advance... + const KTextEditor::Cursor chunkSize(2000, 0); + const KTextEditor::Cursor chunkOverlap(m_chunkOverlap, 0); // Consider cases with multi line pattern + const int fuzzyChunk = chunkSize.line() / 3; // To avoid too small chunks, so expand the last by 30% when possible + // ...starting from current cursor position down to document end... + KTextEditor::Cursor fromPos = m_incInitCursor; + KTextEditor::Cursor toPos = KTextEditor::Cursor(m_incInitCursor.line() + chunkSize.line(), 0); + int lastLine = m_view->document()->documentEnd().line() - fuzzyChunk - 1; + forever { + if (toPos.line() > lastLine) { + toPos = m_view->document()->documentEnd(); + m_incSearchRanges.append(KTextEditor::Range(fromPos, toPos)); + break; + } + m_incSearchRanges.append(KTextEditor::Range(fromPos, toPos)); + fromPos = toPos - chunkOverlap; + toPos += chunkSize; + } + + // ...continuing from top of document down to current cursor position + fromPos = KTextEditor::Cursor(0, 0); + toPos = chunkSize; + lastLine = m_incInitCursor.line() - fuzzyChunk - 1; + forever { + if (toPos.line() > lastLine) { + // Expand last chunk to end of current line to ensure we have a match when cursor is inside of pattern + toPos = KTextEditor::Cursor(m_incInitCursor.line(), m_view->document()->lineLength(m_incInitCursor.line())); + toPos += chunkOverlap; + toPos = qMin(toPos, m_view->document()->documentEnd()); + m_incSearchRanges.append(KTextEditor::Range(fromPos, toPos)); + break; + } + m_incSearchRanges.append(KTextEditor::Range(fromPos, toPos)); + fromPos = toPos - chunkOverlap; + toPos += chunkSize; + } + + incrementalSearch(); +} + +void KateSearchBar::incrementalSearch() +{ + if (m_incSearchRanges.size() <= m_currentSearchRange) { + // Cursor was changed or user typed too fast + return; + } + + const Range range = m_incSearchRanges.at(m_currentSearchRange++); + + if (hasRangeMatch(range)) { + // We are done, we have a winner + return; } - const MatchResult matchResult = match.isValid() ? (wrap ? MatchWrappedForward : MatchFound) : - pattern.isEmpty() ? MatchNothing : - MatchMismatch; + if (m_incSearchRanges.size() == m_currentSearchRange) { + // We are done, nothing found + selectRange2(Range::invalid()); + indicateMatch(MatchMismatch); + } else { + QTimer::singleShot(0, this, &KateSearchBar::incrementalSearch); + } +} + +// TODO parameter not needed, func could merged with incrementalSearch() +bool KateSearchBar::hasRangeMatch(const KTextEditor::Range &range) +{ + KateMatch match(m_view->doc(), searchOptions()); + match.searchText(range, searchPattern()); - const Range selectionRange = pattern.isEmpty() ? Range(m_incInitCursor, m_incInitCursor) : - match.isValid() ? match.range() : - Range::invalid(); + if (!match.isValid()) { + return false; + } + indicateMatch(MatchFound); // don't update m_incInitCursor when we move the cursor disconnect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateSearchBar::updateIncInitCursor); - selectRange2(selectionRange); + selectRange2(match.range()); connect(m_view, &KTextEditor::View::cursorPositionChanged, this, &KateSearchBar::updateIncInitCursor); - indicateMatch(matchResult); + return true; } void KateSearchBar::setMatchCase(bool matchCase) @@ -713,6 +787,9 @@ void KateSearchBar::addCurrentTextToHistory(QComboBox *combo) { + // Don't trigger editTextChanged/onIncPatternChanged + const QSignalBlocker blocker(combo); + const QString text = combo->currentText(); const int index = combo->findText(text); @@ -722,10 +799,10 @@ if (index != 0) { combo->insertItem(0, text); combo->setCurrentIndex(0); - } - // sync to application config - KTextEditor::EditorPrivate::self()->saveSearchReplaceHistoryModels(); + // sync to application config + KTextEditor::EditorPrivate::self()->saveSearchReplaceHistoryModels(); + } } void KateSearchBar::backupConfig(bool ofPower) @@ -1663,6 +1740,7 @@ // Update init cursor if (m_incUi != nullptr) { m_incInitCursor = m_view->cursorPosition(); + m_incSearchRanges.clear(); } updateSelectionOnly(); @@ -1691,8 +1769,9 @@ return; } - // Update init cursor + // Update init cursor, ensure to update search ranges too m_incInitCursor = m_view->cursorPosition(); + m_incSearchRanges.clear(); } void KateSearchBar::onPowerPatternContextMenuRequest(const QPoint &pos)