File indexing completed on 2024-05-05 03:58:35

0001 /*
0002     SPDX-FileCopyrightText: 2013-2016 Simon St James <kdedevel@etotheipiplusone.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "searchmode.h"
0008 
0009 #include "../globalstate.h"
0010 #include "../history.h"
0011 #include "katedocument.h"
0012 #include "kateview.h"
0013 #include <vimode/inputmodemanager.h>
0014 #include <vimode/modes/modebase.h>
0015 
0016 #include <KColorScheme>
0017 
0018 #include <QApplication>
0019 #include <QLineEdit>
0020 
0021 using namespace KateVi;
0022 
0023 namespace
0024 {
0025 bool isCharEscaped(const QString &string, int charPos)
0026 {
0027     if (charPos == 0) {
0028         return false;
0029     }
0030     int numContiguousBackslashesToLeft = 0;
0031     charPos--;
0032     while (charPos >= 0 && string[charPos] == QLatin1Char('\\')) {
0033         numContiguousBackslashesToLeft++;
0034         charPos--;
0035     }
0036     return ((numContiguousBackslashesToLeft % 2) == 1);
0037 }
0038 
0039 QString toggledEscaped(const QString &originalString, QChar escapeChar)
0040 {
0041     int searchFrom = 0;
0042     QString toggledEscapedString = originalString;
0043     do {
0044         const int indexOfEscapeChar = toggledEscapedString.indexOf(escapeChar, searchFrom);
0045         if (indexOfEscapeChar == -1) {
0046             break;
0047         }
0048         if (!isCharEscaped(toggledEscapedString, indexOfEscapeChar)) {
0049             // Escape.
0050             toggledEscapedString.replace(indexOfEscapeChar, 1, QLatin1String("\\") + escapeChar);
0051             searchFrom = indexOfEscapeChar + 2;
0052         } else {
0053             // Unescape.
0054             toggledEscapedString.remove(indexOfEscapeChar - 1, 1);
0055             searchFrom = indexOfEscapeChar;
0056         }
0057     } while (true);
0058 
0059     return toggledEscapedString;
0060 }
0061 
0062 int findPosOfSearchConfigMarker(const QString &searchText, const bool isSearchBackwards)
0063 {
0064     const QChar searchConfigMarkerChar = (isSearchBackwards ? QLatin1Char('?') : QLatin1Char('/'));
0065     for (int pos = 0; pos < searchText.length(); pos++) {
0066         if (searchText.at(pos) == searchConfigMarkerChar) {
0067             if (!isCharEscaped(searchText, pos)) {
0068                 return pos;
0069             }
0070         }
0071     }
0072     return -1;
0073 }
0074 
0075 bool isRepeatLastSearch(const QString &searchText, const bool isSearchBackwards)
0076 {
0077     const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards);
0078     if (posOfSearchConfigMarker != -1) {
0079         if (QStringView(searchText).left(posOfSearchConfigMarker).isEmpty()) {
0080             return true;
0081         }
0082     }
0083     return false;
0084 }
0085 
0086 bool shouldPlaceCursorAtEndOfMatch(const QString &searchText, const bool isSearchBackwards)
0087 {
0088     const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards);
0089     if (posOfSearchConfigMarker != -1) {
0090         if (searchText.length() > posOfSearchConfigMarker + 1 && searchText.at(posOfSearchConfigMarker + 1) == QLatin1Char('e')) {
0091             return true;
0092         }
0093     }
0094     return false;
0095 }
0096 
0097 QString withSearchConfigRemoved(const QString &originalSearchText, const bool isSearchBackwards)
0098 {
0099     const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(originalSearchText, isSearchBackwards);
0100     if (posOfSearchConfigMarker == -1) {
0101         return originalSearchText;
0102     } else {
0103         return originalSearchText.left(posOfSearchConfigMarker);
0104     }
0105 }
0106 }
0107 
0108 QString KateVi::vimRegexToQtRegexPattern(const QString &vimRegexPattern)
0109 {
0110     QString qtRegexPattern = vimRegexPattern;
0111     qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('('));
0112     qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char(')'));
0113     qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('+'));
0114     qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('|'));
0115     qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char('?'));
0116     {
0117         // All curly brackets, except the closing curly bracket of a matching pair where the opening bracket is escaped,
0118         // must have their escaping toggled.
0119         bool lookingForMatchingCloseBracket = false;
0120         QList<int> matchingClosedCurlyBracketPositions;
0121         for (int i = 0; i < qtRegexPattern.length(); i++) {
0122             if (qtRegexPattern[i] == QLatin1Char('{') && isCharEscaped(qtRegexPattern, i)) {
0123                 lookingForMatchingCloseBracket = true;
0124             }
0125             if (qtRegexPattern[i] == QLatin1Char('}') && lookingForMatchingCloseBracket && qtRegexPattern[i - 1] != QLatin1Char('\\')) {
0126                 matchingClosedCurlyBracketPositions.append(i);
0127             }
0128         }
0129         if (matchingClosedCurlyBracketPositions.isEmpty()) {
0130             // Escape all {'s and }'s - there are no matching pairs.
0131             qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('{'));
0132             qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('}'));
0133         } else {
0134             // Ensure that every chunk of qtRegexPattern that does *not* contain a curly closing bracket
0135             // that is matched have their { and } escaping toggled.
0136             QString qtRegexPatternNonMatchingCurliesToggled;
0137             int previousNonMatchingClosedCurlyPos = 0; // i.e. the position of the last character which is either
0138             // not a curly closing bracket, or is a curly closing bracket
0139             // that is not matched.
0140             for (int matchingClosedCurlyPos : std::as_const(matchingClosedCurlyBracketPositions)) {
0141                 QString chunkExcludingMatchingCurlyClosed =
0142                     qtRegexPattern.mid(previousNonMatchingClosedCurlyPos, matchingClosedCurlyPos - previousNonMatchingClosedCurlyPos);
0143                 chunkExcludingMatchingCurlyClosed = toggledEscaped(chunkExcludingMatchingCurlyClosed, QLatin1Char('{'));
0144                 chunkExcludingMatchingCurlyClosed = toggledEscaped(chunkExcludingMatchingCurlyClosed, QLatin1Char('}'));
0145                 qtRegexPatternNonMatchingCurliesToggled += chunkExcludingMatchingCurlyClosed + qtRegexPattern[matchingClosedCurlyPos];
0146                 previousNonMatchingClosedCurlyPos = matchingClosedCurlyPos + 1;
0147             }
0148             QString chunkAfterLastMatchingClosedCurly = qtRegexPattern.mid(matchingClosedCurlyBracketPositions.last() + 1);
0149             chunkAfterLastMatchingClosedCurly = toggledEscaped(chunkAfterLastMatchingClosedCurly, QLatin1Char('{'));
0150             chunkAfterLastMatchingClosedCurly = toggledEscaped(chunkAfterLastMatchingClosedCurly, QLatin1Char('}'));
0151             qtRegexPatternNonMatchingCurliesToggled += chunkAfterLastMatchingClosedCurly;
0152 
0153             qtRegexPattern = qtRegexPatternNonMatchingCurliesToggled;
0154         }
0155     }
0156 
0157     // All square brackets, *except* for those that are a) unescaped; and b) form a matching pair, must be
0158     // escaped.
0159     bool lookingForMatchingCloseBracket = false;
0160     int openingBracketPos = -1;
0161     QList<int> matchingSquareBracketPositions;
0162     for (int i = 0; i < qtRegexPattern.length(); i++) {
0163         if (qtRegexPattern[i] == QLatin1Char('[') && !isCharEscaped(qtRegexPattern, i) && !lookingForMatchingCloseBracket) {
0164             lookingForMatchingCloseBracket = true;
0165             openingBracketPos = i;
0166         }
0167         if (qtRegexPattern[i] == QLatin1Char(']') && lookingForMatchingCloseBracket && !isCharEscaped(qtRegexPattern, i)) {
0168             lookingForMatchingCloseBracket = false;
0169             matchingSquareBracketPositions.append(openingBracketPos);
0170             matchingSquareBracketPositions.append(i);
0171         }
0172     }
0173 
0174     if (matchingSquareBracketPositions.isEmpty()) {
0175         // Escape all ['s and ]'s - there are no matching pairs.
0176         qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char('['));
0177         qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char(']'));
0178     } else {
0179         // Ensure that every chunk of qtRegexPattern that does *not* contain one of the matching pairs of
0180         // square brackets have their square brackets escaped.
0181         QString qtRegexPatternNonMatchingSquaresMadeLiteral;
0182         int previousNonMatchingSquareBracketPos = 0; // i.e. the position of the last character which is
0183         // either not a square bracket, or is a square bracket but
0184         // which is not matched.
0185         for (int matchingSquareBracketPos : std::as_const(matchingSquareBracketPositions)) {
0186             QString chunkExcludingMatchingSquareBrackets =
0187                 qtRegexPattern.mid(previousNonMatchingSquareBracketPos, matchingSquareBracketPos - previousNonMatchingSquareBracketPos);
0188             chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(chunkExcludingMatchingSquareBrackets, QLatin1Char('['));
0189             chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(chunkExcludingMatchingSquareBrackets, QLatin1Char(']'));
0190             qtRegexPatternNonMatchingSquaresMadeLiteral += chunkExcludingMatchingSquareBrackets + qtRegexPattern[matchingSquareBracketPos];
0191             previousNonMatchingSquareBracketPos = matchingSquareBracketPos + 1;
0192         }
0193         QString chunkAfterLastMatchingSquareBracket = qtRegexPattern.mid(matchingSquareBracketPositions.last() + 1);
0194         chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(chunkAfterLastMatchingSquareBracket, QLatin1Char('['));
0195         chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(chunkAfterLastMatchingSquareBracket, QLatin1Char(']'));
0196         qtRegexPatternNonMatchingSquaresMadeLiteral += chunkAfterLastMatchingSquareBracket;
0197 
0198         qtRegexPattern = qtRegexPatternNonMatchingSquaresMadeLiteral;
0199     }
0200 
0201     qtRegexPattern.replace(QLatin1String("\\>"), QLatin1String("\\b"));
0202     qtRegexPattern.replace(QLatin1String("\\<"), QLatin1String("\\b"));
0203 
0204     return qtRegexPattern;
0205 }
0206 
0207 QString KateVi::ensuredCharEscaped(const QString &originalString, QChar charToEscape)
0208 {
0209     QString escapedString = originalString;
0210     for (int i = 0; i < escapedString.length(); i++) {
0211         if (escapedString[i] == charToEscape && !isCharEscaped(escapedString, i)) {
0212             escapedString.replace(i, 1, QLatin1String("\\") + charToEscape);
0213         }
0214     }
0215     return escapedString;
0216 }
0217 
0218 QString KateVi::withCaseSensitivityMarkersStripped(const QString &originalSearchTerm)
0219 {
0220     // Only \C is handled, for now - I'll implement \c if someone asks for it.
0221     int pos = 0;
0222     QString caseSensitivityMarkersStripped = originalSearchTerm;
0223     while (pos < caseSensitivityMarkersStripped.length()) {
0224         if (caseSensitivityMarkersStripped.at(pos) == QLatin1Char('C') && isCharEscaped(caseSensitivityMarkersStripped, pos)) {
0225             caseSensitivityMarkersStripped.remove(pos - 1, 2);
0226             pos--;
0227         }
0228         pos++;
0229     }
0230     return caseSensitivityMarkersStripped;
0231 }
0232 
0233 QStringList KateVi::reversed(const QStringList &originalList)
0234 {
0235     QStringList reversedList = originalList;
0236     std::reverse(reversedList.begin(), reversedList.end());
0237     return reversedList;
0238 }
0239 
0240 SearchMode::SearchMode(EmulatedCommandBar *emulatedCommandBar,
0241                        MatchHighlighter *matchHighlighter,
0242                        InputModeManager *viInputModeManager,
0243                        KTextEditor::ViewPrivate *view,
0244                        QLineEdit *edit)
0245     : ActiveMode(emulatedCommandBar, matchHighlighter, viInputModeManager, view)
0246     , m_edit(edit)
0247 {
0248 }
0249 
0250 void SearchMode::init(SearchMode::SearchDirection searchDirection)
0251 {
0252     m_searchDirection = searchDirection;
0253     m_startingCursorPos = view()->cursorPosition();
0254 }
0255 
0256 bool SearchMode::handleKeyPress(const QKeyEvent *keyEvent)
0257 {
0258     Q_UNUSED(keyEvent);
0259     return false;
0260 }
0261 
0262 void SearchMode::editTextChanged(const QString &newText)
0263 {
0264     QString qtRegexPattern = newText;
0265     const bool searchBackwards = (m_searchDirection == SearchDirection::Backward);
0266     const bool placeCursorAtEndOfMatch = shouldPlaceCursorAtEndOfMatch(qtRegexPattern, searchBackwards);
0267     if (isRepeatLastSearch(qtRegexPattern, searchBackwards)) {
0268         qtRegexPattern = viInputModeManager()->searcher()->getLastSearchPattern();
0269     } else {
0270         qtRegexPattern = withSearchConfigRemoved(qtRegexPattern, searchBackwards);
0271         qtRegexPattern = vimRegexToQtRegexPattern(qtRegexPattern);
0272     }
0273 
0274     // Decide case-sensitivity via SmartCase (note: if the expression contains \C, the "case-sensitive" marker, then
0275     // we will be case-sensitive "by coincidence", as it were.).
0276     bool caseSensitive = true;
0277     if (qtRegexPattern.toLower() == qtRegexPattern) {
0278         caseSensitive = false;
0279     }
0280 
0281     qtRegexPattern = withCaseSensitivityMarkersStripped(qtRegexPattern);
0282 
0283     m_currentSearchParams.pattern = qtRegexPattern;
0284     m_currentSearchParams.isCaseSensitive = caseSensitive;
0285     m_currentSearchParams.isBackwards = searchBackwards;
0286     m_currentSearchParams.shouldPlaceCursorAtEndOfMatch = placeCursorAtEndOfMatch;
0287 
0288     // The "count" for the current search is not shared between Visual & Normal mode, so we need to pick
0289     // the right one to handle the counted search.
0290     int c = viInputModeManager()->getCurrentViModeHandler()->getCount();
0291     KTextEditor::Range match = viInputModeManager()->searcher()->findPattern(m_currentSearchParams,
0292                                                                              m_startingCursorPos,
0293                                                                              c,
0294                                                                              false /* Don't add incremental searches to search history */);
0295 
0296     if (match.isValid()) {
0297         // The returned range ends one past the last character of the match, so adjust.
0298         KTextEditor::Cursor realMatchEnd = KTextEditor::Cursor(match.end().line(), match.end().column() - 1);
0299         if (realMatchEnd.column() == -1) {
0300             realMatchEnd = KTextEditor::Cursor(realMatchEnd.line() - 1, view()->doc()->lineLength(realMatchEnd.line() - 1));
0301         }
0302         moveCursorTo(placeCursorAtEndOfMatch ? realMatchEnd : match.start());
0303         setBarBackground(SearchMode::MatchFound);
0304     } else {
0305         moveCursorTo(m_startingCursorPos);
0306         if (!m_edit->text().isEmpty()) {
0307             setBarBackground(SearchMode::NoMatchFound);
0308         } else {
0309             setBarBackground(SearchMode::Normal);
0310         }
0311     }
0312 
0313     if (!viInputModeManager()->searcher()->isHighlightSearchEnabled())
0314         updateMatchHighlight(match);
0315 }
0316 
0317 void SearchMode::deactivate(bool wasAborted)
0318 {
0319     // "Deactivate" can be called multiple times between init()'s, so only reset the cursor once!
0320     if (m_startingCursorPos.isValid()) {
0321         if (wasAborted) {
0322             moveCursorTo(m_startingCursorPos);
0323         }
0324     }
0325     m_startingCursorPos = KTextEditor::Cursor::invalid();
0326     setBarBackground(SearchMode::Normal);
0327     // Send a synthetic keypress through the system that signals whether the search was aborted or
0328     // not.  If not, the keypress will "complete" the search motion, thus triggering it.
0329     // We send to KateViewInternal as it updates the status bar and removes the "?".
0330     const Qt::Key syntheticSearchCompletedKey = (wasAborted ? static_cast<Qt::Key>(0) : Qt::Key_Enter);
0331     QKeyEvent syntheticSearchCompletedKeyPress(QEvent::KeyPress, syntheticSearchCompletedKey, Qt::NoModifier);
0332     m_isSendingSyntheticSearchCompletedKeypress = true;
0333     QApplication::sendEvent(view()->focusProxy(), &syntheticSearchCompletedKeyPress);
0334     m_isSendingSyntheticSearchCompletedKeypress = false;
0335     if (!wasAborted) {
0336         // Search was actually executed, so store it as the last search.
0337         viInputModeManager()->searcher()->setLastSearchParams(m_currentSearchParams);
0338     }
0339     // Append the raw text of the search to the search history (i.e. without conversion
0340     // from Vim-style regex; without case-sensitivity markers stripped; etc.
0341     // Vim does this even if the search was aborted, so we follow suit.
0342     viInputModeManager()->globalState()->searchHistory()->append(m_edit->text());
0343     viInputModeManager()->searcher()->patternDone(wasAborted);
0344 }
0345 
0346 CompletionStartParams SearchMode::completionInvoked(Completer::CompletionInvocation invocationType)
0347 {
0348     Q_UNUSED(invocationType);
0349     return activateSearchHistoryCompletion();
0350 }
0351 
0352 void SearchMode::completionChosen()
0353 {
0354     // Choose completion with Enter/ Return -> close bar (the search will have already taken effect at this point), marking as not aborted .
0355     close(false);
0356 }
0357 
0358 CompletionStartParams SearchMode::activateSearchHistoryCompletion()
0359 {
0360     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->searchHistory()->items()), 0);
0361 }
0362 
0363 void SearchMode::setBarBackground(SearchMode::BarBackgroundStatus status)
0364 {
0365     QPalette barBackground(m_edit->palette());
0366     switch (status) {
0367     case MatchFound: {
0368         KColorScheme::adjustBackground(barBackground, KColorScheme::PositiveBackground);
0369         break;
0370     }
0371     case NoMatchFound: {
0372         KColorScheme::adjustBackground(barBackground, KColorScheme::NegativeBackground);
0373         break;
0374     }
0375     case Normal: {
0376         barBackground = QPalette();
0377         break;
0378     }
0379     }
0380     m_edit->setPalette(barBackground);
0381 }