File indexing completed on 2024-04-21 03:58:08

0001 /*
0002     SPDX-FileCopyrightText: KDE Developers
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "searcher.h"
0008 #include "globalstate.h"
0009 #include "history.h"
0010 #include "kateconfig.h"
0011 #include "katedocument.h"
0012 #include "kateview.h"
0013 #include <vimode/inputmodemanager.h>
0014 #include <vimode/modes/modebase.h>
0015 
0016 using namespace KateVi;
0017 
0018 Searcher::Searcher(InputModeManager *manager)
0019     : m_viInputModeManager(manager)
0020     , m_view(manager->view())
0021     , m_lastHlSearchRange(KTextEditor::Range::invalid())
0022     , highlightMatchAttribute(new KTextEditor::Attribute())
0023 {
0024     updateHighlightColors();
0025 
0026     if (m_hlMode == HighlightMode::Enable) {
0027         connectSignals();
0028     }
0029 }
0030 
0031 Searcher::~Searcher()
0032 {
0033     disconnectSignals();
0034     clearHighlights();
0035 }
0036 
0037 const QString Searcher::getLastSearchPattern() const
0038 {
0039     return m_lastSearchConfig.pattern;
0040 }
0041 
0042 void Searcher::setLastSearchParams(const SearchParams &searchParams)
0043 {
0044     if (!searchParams.pattern.isEmpty())
0045         m_lastSearchConfig = searchParams;
0046 }
0047 
0048 bool Searcher::lastSearchWrapped() const
0049 {
0050     return m_lastSearchWrapped;
0051 }
0052 
0053 void Searcher::findNext()
0054 {
0055     const Range r = motionFindNext();
0056     if (r.valid) {
0057         m_viInputModeManager->getCurrentViModeHandler()->goToPos(r);
0058     }
0059 }
0060 
0061 void Searcher::findPrevious()
0062 {
0063     const Range r = motionFindPrev();
0064     if (r.valid) {
0065         m_viInputModeManager->getCurrentViModeHandler()->goToPos(r);
0066     }
0067 }
0068 
0069 Range Searcher::motionFindNext(int count)
0070 {
0071     Range match = findPatternForMotion(m_lastSearchConfig, m_view->cursorPosition(), count);
0072 
0073     if (!match.valid) {
0074         return match;
0075     }
0076     if (!m_lastSearchConfig.shouldPlaceCursorAtEndOfMatch) {
0077         return Range(match.startLine, match.startColumn, ExclusiveMotion);
0078     }
0079     return Range(match.endLine, match.endColumn - 1, ExclusiveMotion);
0080 }
0081 
0082 Range Searcher::motionFindPrev(int count)
0083 {
0084     SearchParams lastSearchReversed = m_lastSearchConfig;
0085     lastSearchReversed.isBackwards = !lastSearchReversed.isBackwards;
0086     Range match = findPatternForMotion(lastSearchReversed, m_view->cursorPosition(), count);
0087 
0088     if (!match.valid) {
0089         return match;
0090     }
0091     if (!m_lastSearchConfig.shouldPlaceCursorAtEndOfMatch) {
0092         return Range(match.startLine, match.startColumn, ExclusiveMotion);
0093     }
0094     return Range(match.endLine, match.endColumn - 1, ExclusiveMotion);
0095 }
0096 
0097 Range Searcher::findPatternForMotion(const SearchParams &searchParams, const KTextEditor::Cursor startFrom, int count)
0098 {
0099     if (searchParams.pattern.isEmpty()) {
0100         return Range::invalid();
0101     }
0102 
0103     KTextEditor::Range match = findPatternWorker(searchParams, startFrom, count);
0104 
0105     if (m_hlMode != HighlightMode::Disable) {
0106         if (m_hlMode == HighlightMode::HideCurrent) {
0107             m_hlMode = HighlightMode::Enable;
0108             highlightVisibleResults(searchParams, true);
0109         } else {
0110             highlightVisibleResults(searchParams);
0111         }
0112     }
0113 
0114     return Range(match.start(), match.end(), ExclusiveMotion);
0115 }
0116 
0117 Range Searcher::findWordForMotion(const QString &word, bool backwards, const KTextEditor::Cursor startFrom, int count)
0118 {
0119     m_lastSearchConfig.isBackwards = backwards;
0120     m_lastSearchConfig.isCaseSensitive = false;
0121     m_lastSearchConfig.shouldPlaceCursorAtEndOfMatch = false;
0122 
0123     m_viInputModeManager->globalState()->searchHistory()->append(QStringLiteral("\\<%1\\>").arg(word));
0124     QString pattern = QStringLiteral("\\b%1\\b").arg(word);
0125     m_lastSearchConfig.pattern = pattern;
0126     if (m_hlMode == HighlightMode::HideCurrent)
0127         m_hlMode = HighlightMode::Enable;
0128 
0129     return findPatternForMotion(m_lastSearchConfig, startFrom, count);
0130 }
0131 
0132 KTextEditor::Range Searcher::findPattern(const SearchParams &searchParams, const KTextEditor::Cursor startFrom, int count, bool addToSearchHistory)
0133 {
0134     if (addToSearchHistory) {
0135         m_viInputModeManager->globalState()->searchHistory()->append(searchParams.pattern);
0136         m_lastSearchConfig = searchParams;
0137     }
0138 
0139     KTextEditor::Range r = findPatternWorker(searchParams, startFrom, count);
0140 
0141     if (m_hlMode != HighlightMode::Disable)
0142         highlightVisibleResults(searchParams);
0143 
0144     newPattern = false;
0145     return r;
0146 }
0147 
0148 void Searcher::highlightVisibleResults(const SearchParams &searchParams, bool force)
0149 {
0150     if (newPattern && searchParams.pattern.isEmpty())
0151         return;
0152 
0153     auto vr = m_view->visibleRange();
0154 
0155     const SearchParams &l = searchParams;
0156     const SearchParams &r = m_lastHlSearchConfig;
0157 
0158     if (!force && l.pattern == r.pattern && l.isCaseSensitive == r.isCaseSensitive && vr == m_lastHlSearchRange) {
0159         return;
0160     }
0161 
0162     m_lastHlSearchConfig = searchParams;
0163     m_lastHlSearchRange = vr;
0164 
0165     clearHighlights();
0166 
0167     KTextEditor::SearchOptions flags = KTextEditor::Regex;
0168     m_lastSearchWrapped = false;
0169 
0170     const QString &pattern = searchParams.pattern;
0171 
0172     if (!searchParams.isCaseSensitive) {
0173         flags |= KTextEditor::CaseInsensitive;
0174     }
0175 
0176     KTextEditor::Range match;
0177     KTextEditor::Cursor current(vr.start());
0178 
0179     do {
0180         match = m_view->doc()->searchText(KTextEditor::Range(current, vr.end()), pattern, flags).first();
0181         if (match.isValid()) {
0182             if (match.isEmpty())
0183                 match = KTextEditor::Range(match.start(), 1);
0184 
0185             auto highlight = m_view->doc()->newMovingRange(match, Kate::TextRange::DoNotExpand);
0186             highlight->setView(m_view);
0187             highlight->setAttributeOnlyForViews(true);
0188             highlight->setZDepth(-10000.0);
0189             highlight->setAttribute(highlightMatchAttribute);
0190             m_hlRanges.append(highlight);
0191 
0192             current = match.end();
0193         }
0194     } while (match.isValid() && current < vr.end());
0195 }
0196 
0197 void Searcher::clearHighlights()
0198 {
0199     if (!m_hlRanges.empty()) {
0200         qDeleteAll(m_hlRanges);
0201         m_hlRanges.clear();
0202     }
0203 }
0204 
0205 void Searcher::hideCurrentHighlight()
0206 {
0207     if (m_hlMode != HighlightMode::Disable) {
0208         m_hlMode = HighlightMode::HideCurrent;
0209         clearHighlights();
0210     }
0211 }
0212 
0213 void Searcher::updateHighlightColors()
0214 {
0215     const QColor foregroundColor = m_view->defaultStyleAttribute(KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color();
0216     const QColor &searchColor = m_view->rendererConfig()->searchHighlightColor();
0217     // init match attribute
0218     highlightMatchAttribute->setForeground(foregroundColor);
0219     highlightMatchAttribute->setBackground(searchColor);
0220 }
0221 
0222 void Searcher::enableHighlightSearch(bool enable)
0223 {
0224     if (enable) {
0225         m_hlMode = HighlightMode::Enable;
0226 
0227         connectSignals();
0228         highlightVisibleResults(m_lastSearchConfig, true);
0229     } else {
0230         m_hlMode = HighlightMode::Disable;
0231 
0232         disconnectSignals();
0233         clearHighlights();
0234     }
0235 }
0236 
0237 bool Searcher::isHighlightSearchEnabled() const
0238 {
0239     return m_hlMode != HighlightMode::Disable;
0240 }
0241 
0242 void Searcher::disconnectSignals()
0243 {
0244     QObject::disconnect(m_displayRangeChangedConnection);
0245     QObject::disconnect(m_textChangedConnection);
0246 }
0247 
0248 void Searcher::connectSignals()
0249 {
0250     disconnectSignals();
0251 
0252     m_displayRangeChangedConnection = QObject::connect(m_view, &KTextEditor::ViewPrivate::displayRangeChanged, [this]() {
0253         if (m_hlMode == HighlightMode::Enable)
0254             highlightVisibleResults(m_lastHlSearchConfig);
0255     });
0256     m_textChangedConnection = QObject::connect(m_view->doc(), &KTextEditor::Document::textChanged, [this]() {
0257         if (m_hlMode == HighlightMode::Enable)
0258             highlightVisibleResults(m_lastHlSearchConfig, true);
0259     });
0260 }
0261 
0262 void Searcher::patternDone(bool wasAborted)
0263 {
0264     if (wasAborted) {
0265         if (m_hlMode == HighlightMode::HideCurrent || m_lastSearchConfig.pattern.isEmpty())
0266             clearHighlights();
0267         else if (m_hlMode == HighlightMode::Enable)
0268             highlightVisibleResults(m_lastSearchConfig);
0269 
0270     } else {
0271         if (m_hlMode == HighlightMode::HideCurrent)
0272             m_hlMode = HighlightMode::Enable;
0273     }
0274     newPattern = true;
0275 }
0276 
0277 KTextEditor::Range Searcher::findPatternWorker(const SearchParams &searchParams, const KTextEditor::Cursor startFrom, int count)
0278 {
0279     KTextEditor::Cursor searchBegin = startFrom;
0280     KTextEditor::SearchOptions flags = KTextEditor::Regex;
0281     m_lastSearchWrapped = false;
0282 
0283     const QString &pattern = searchParams.pattern;
0284 
0285     if (searchParams.isBackwards) {
0286         flags |= KTextEditor::Backwards;
0287     }
0288     if (!searchParams.isCaseSensitive) {
0289         flags |= KTextEditor::CaseInsensitive;
0290     }
0291     KTextEditor::Range finalMatch;
0292     for (int i = 0; i < count; i++) {
0293         if (!searchParams.isBackwards) {
0294             const KTextEditor::Range matchRange =
0295                 m_view->doc()
0296                     ->searchText(KTextEditor::Range(KTextEditor::Cursor(searchBegin.line(), searchBegin.column() + 1), m_view->doc()->documentEnd()),
0297                                  pattern,
0298                                  flags)
0299                     .first();
0300 
0301             if (matchRange.isValid()) {
0302                 finalMatch = matchRange;
0303             } else {
0304                 // Wrap around.
0305                 const KTextEditor::Range wrappedMatchRange =
0306                     m_view->doc()->searchText(KTextEditor::Range(m_view->doc()->documentRange().start(), m_view->doc()->documentEnd()), pattern, flags).first();
0307                 if (wrappedMatchRange.isValid()) {
0308                     finalMatch = wrappedMatchRange;
0309                     m_lastSearchWrapped = true;
0310                 } else {
0311                     return KTextEditor::Range::invalid();
0312                 }
0313             }
0314         } else {
0315             // Ok - this is trickier: we can't search in the range from doc start to searchBegin, because
0316             // the match might extend *beyond* searchBegin.
0317             // We could search through the entire document and then filter out only those matches that are
0318             // after searchBegin, but it's more efficient to instead search from the start of the
0319             // document until the beginning of the line after searchBegin, and then filter.
0320             // Unfortunately, searchText doesn't necessarily turn up all matches (just the first one, sometimes)
0321             // so we must repeatedly search in such a way that the previous match isn't found, until we either
0322             // find no matches at all, or the first match that is before searchBegin.
0323             KTextEditor::Cursor newSearchBegin = KTextEditor::Cursor(searchBegin.line(), m_view->doc()->lineLength(searchBegin.line()));
0324             KTextEditor::Range bestMatch = KTextEditor::Range::invalid();
0325             while (true) {
0326                 QList<KTextEditor::Range> matchesUnfiltered =
0327                     m_view->doc()->searchText(KTextEditor::Range(newSearchBegin, m_view->doc()->documentRange().start()), pattern, flags);
0328 
0329                 if (matchesUnfiltered.size() == 1 && !matchesUnfiltered.first().isValid()) {
0330                     break;
0331                 }
0332 
0333                 // After sorting, the last element in matchesUnfiltered is the last match position.
0334                 std::sort(matchesUnfiltered.begin(), matchesUnfiltered.end());
0335 
0336                 QList<KTextEditor::Range> filteredMatches;
0337                 for (KTextEditor::Range unfilteredMatch : std::as_const(matchesUnfiltered)) {
0338                     if (unfilteredMatch.start() < searchBegin) {
0339                         filteredMatches.append(unfilteredMatch);
0340                     }
0341                 }
0342                 if (!filteredMatches.isEmpty()) {
0343                     // Want the latest matching range that is before searchBegin.
0344                     bestMatch = filteredMatches.last();
0345                     break;
0346                 }
0347 
0348                 // We found some unfiltered matches, but none were suitable. In case matchesUnfiltered wasn't
0349                 // all matching elements, search again, starting from before the earliest matching range.
0350                 if (filteredMatches.isEmpty()) {
0351                     newSearchBegin = matchesUnfiltered.first().start();
0352                 }
0353             }
0354 
0355             KTextEditor::Range matchRange = bestMatch;
0356 
0357             if (matchRange.isValid()) {
0358                 finalMatch = matchRange;
0359             } else {
0360                 const KTextEditor::Range wrappedMatchRange =
0361                     m_view->doc()->searchText(KTextEditor::Range(m_view->doc()->documentEnd(), m_view->doc()->documentRange().start()), pattern, flags).first();
0362 
0363                 if (wrappedMatchRange.isValid()) {
0364                     finalMatch = wrappedMatchRange;
0365                     m_lastSearchWrapped = true;
0366                 } else {
0367                     return KTextEditor::Range::invalid();
0368                 }
0369             }
0370         }
0371         searchBegin = finalMatch.start();
0372     }
0373     return finalMatch;
0374 }