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 }