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 }