1 /*
2     SPDX-FileCopyrightText: 2013-2016 Simon St James <kdedevel@etotheipiplusone.com>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "searchmode.h"
8 
9 #include "../globalstate.h"
10 #include "../history.h"
11 #include "katedocument.h"
12 #include "kateview.h"
13 #include <vimode/inputmodemanager.h>
14 #include <vimode/modes/modebase.h>
15 
16 #include <KColorScheme>
17 
18 #include <QApplication>
19 #include <QLineEdit>
20 
21 using namespace KateVi;
22 
23 namespace
24 {
isCharEscaped(const QString & string,int charPos)25 bool isCharEscaped(const QString &string, int charPos)
26 {
27     if (charPos == 0) {
28         return false;
29     }
30     int numContiguousBackslashesToLeft = 0;
31     charPos--;
32     while (charPos >= 0 && string[charPos] == QLatin1Char('\\')) {
33         numContiguousBackslashesToLeft++;
34         charPos--;
35     }
36     return ((numContiguousBackslashesToLeft % 2) == 1);
37 }
38 
toggledEscaped(const QString & originalString,QChar escapeChar)39 QString toggledEscaped(const QString &originalString, QChar escapeChar)
40 {
41     int searchFrom = 0;
42     QString toggledEscapedString = originalString;
43     do {
44         const int indexOfEscapeChar = toggledEscapedString.indexOf(escapeChar, searchFrom);
45         if (indexOfEscapeChar == -1) {
46             break;
47         }
48         if (!isCharEscaped(toggledEscapedString, indexOfEscapeChar)) {
49             // Escape.
50             toggledEscapedString.replace(indexOfEscapeChar, 1, QLatin1String("\\") + escapeChar);
51             searchFrom = indexOfEscapeChar + 2;
52         } else {
53             // Unescape.
54             toggledEscapedString.remove(indexOfEscapeChar - 1, 1);
55             searchFrom = indexOfEscapeChar;
56         }
57     } while (true);
58 
59     return toggledEscapedString;
60 }
61 
findPosOfSearchConfigMarker(const QString & searchText,const bool isSearchBackwards)62 int findPosOfSearchConfigMarker(const QString &searchText, const bool isSearchBackwards)
63 {
64     const QChar searchConfigMarkerChar = (isSearchBackwards ? QLatin1Char('?') : QLatin1Char('/'));
65     for (int pos = 0; pos < searchText.length(); pos++) {
66         if (searchText.at(pos) == searchConfigMarkerChar) {
67             if (!isCharEscaped(searchText, pos)) {
68                 return pos;
69             }
70         }
71     }
72     return -1;
73 }
74 
isRepeatLastSearch(const QString & searchText,const bool isSearchBackwards)75 bool isRepeatLastSearch(const QString &searchText, const bool isSearchBackwards)
76 {
77     const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards);
78     if (posOfSearchConfigMarker != -1) {
79         if (QStringView(searchText).left(posOfSearchConfigMarker).isEmpty()) {
80             return true;
81         }
82     }
83     return false;
84 }
85 
shouldPlaceCursorAtEndOfMatch(const QString & searchText,const bool isSearchBackwards)86 bool shouldPlaceCursorAtEndOfMatch(const QString &searchText, const bool isSearchBackwards)
87 {
88     const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards);
89     if (posOfSearchConfigMarker != -1) {
90         if (searchText.length() > posOfSearchConfigMarker + 1 && searchText.at(posOfSearchConfigMarker + 1) == QLatin1Char('e')) {
91             return true;
92         }
93     }
94     return false;
95 }
96 
withSearchConfigRemoved(const QString & originalSearchText,const bool isSearchBackwards)97 QString withSearchConfigRemoved(const QString &originalSearchText, const bool isSearchBackwards)
98 {
99     const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(originalSearchText, isSearchBackwards);
100     if (posOfSearchConfigMarker == -1) {
101         return originalSearchText;
102     } else {
103         return originalSearchText.left(posOfSearchConfigMarker);
104     }
105 }
106 }
107 
vimRegexToQtRegexPattern(const QString & vimRegexPattern)108 QString KateVi::vimRegexToQtRegexPattern(const QString &vimRegexPattern)
109 {
110     QString qtRegexPattern = vimRegexPattern;
111     qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('('));
112     qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char(')'));
113     qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('+'));
114     qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('|'));
115     qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char('?'));
116     {
117         // All curly brackets, except the closing curly bracket of a matching pair where the opening bracket is escaped,
118         // must have their escaping toggled.
119         bool lookingForMatchingCloseBracket = false;
120         QList<int> matchingClosedCurlyBracketPositions;
121         for (int i = 0; i < qtRegexPattern.length(); i++) {
122             if (qtRegexPattern[i] == QLatin1Char('{') && isCharEscaped(qtRegexPattern, i)) {
123                 lookingForMatchingCloseBracket = true;
124             }
125             if (qtRegexPattern[i] == QLatin1Char('}') && lookingForMatchingCloseBracket && qtRegexPattern[i - 1] != QLatin1Char('\\')) {
126                 matchingClosedCurlyBracketPositions.append(i);
127             }
128         }
129         if (matchingClosedCurlyBracketPositions.isEmpty()) {
130             // Escape all {'s and }'s - there are no matching pairs.
131             qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('{'));
132             qtRegexPattern = toggledEscaped(qtRegexPattern, QLatin1Char('}'));
133         } else {
134             // Ensure that every chunk of qtRegexPattern that does *not* contain a curly closing bracket
135             // that is matched have their { and } escaping toggled.
136             QString qtRegexPatternNonMatchingCurliesToggled;
137             int previousNonMatchingClosedCurlyPos = 0; // i.e. the position of the last character which is either
138             // not a curly closing bracket, or is a curly closing bracket
139             // that is not matched.
140             for (int matchingClosedCurlyPos : std::as_const(matchingClosedCurlyBracketPositions)) {
141                 QString chunkExcludingMatchingCurlyClosed =
142                     qtRegexPattern.mid(previousNonMatchingClosedCurlyPos, matchingClosedCurlyPos - previousNonMatchingClosedCurlyPos);
143                 chunkExcludingMatchingCurlyClosed = toggledEscaped(chunkExcludingMatchingCurlyClosed, QLatin1Char('{'));
144                 chunkExcludingMatchingCurlyClosed = toggledEscaped(chunkExcludingMatchingCurlyClosed, QLatin1Char('}'));
145                 qtRegexPatternNonMatchingCurliesToggled += chunkExcludingMatchingCurlyClosed + qtRegexPattern[matchingClosedCurlyPos];
146                 previousNonMatchingClosedCurlyPos = matchingClosedCurlyPos + 1;
147             }
148             QString chunkAfterLastMatchingClosedCurly = qtRegexPattern.mid(matchingClosedCurlyBracketPositions.last() + 1);
149             chunkAfterLastMatchingClosedCurly = toggledEscaped(chunkAfterLastMatchingClosedCurly, QLatin1Char('{'));
150             chunkAfterLastMatchingClosedCurly = toggledEscaped(chunkAfterLastMatchingClosedCurly, QLatin1Char('}'));
151             qtRegexPatternNonMatchingCurliesToggled += chunkAfterLastMatchingClosedCurly;
152 
153             qtRegexPattern = qtRegexPatternNonMatchingCurliesToggled;
154         }
155     }
156 
157     // All square brackets, *except* for those that are a) unescaped; and b) form a matching pair, must be
158     // escaped.
159     bool lookingForMatchingCloseBracket = false;
160     int openingBracketPos = -1;
161     QList<int> matchingSquareBracketPositions;
162     for (int i = 0; i < qtRegexPattern.length(); i++) {
163         if (qtRegexPattern[i] == QLatin1Char('[') && !isCharEscaped(qtRegexPattern, i) && !lookingForMatchingCloseBracket) {
164             lookingForMatchingCloseBracket = true;
165             openingBracketPos = i;
166         }
167         if (qtRegexPattern[i] == QLatin1Char(']') && lookingForMatchingCloseBracket && !isCharEscaped(qtRegexPattern, i)) {
168             lookingForMatchingCloseBracket = false;
169             matchingSquareBracketPositions.append(openingBracketPos);
170             matchingSquareBracketPositions.append(i);
171         }
172     }
173 
174     if (matchingSquareBracketPositions.isEmpty()) {
175         // Escape all ['s and ]'s - there are no matching pairs.
176         qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char('['));
177         qtRegexPattern = ensuredCharEscaped(qtRegexPattern, QLatin1Char(']'));
178     } else {
179         // Ensure that every chunk of qtRegexPattern that does *not* contain one of the matching pairs of
180         // square brackets have their square brackets escaped.
181         QString qtRegexPatternNonMatchingSquaresMadeLiteral;
182         int previousNonMatchingSquareBracketPos = 0; // i.e. the position of the last character which is
183         // either not a square bracket, or is a square bracket but
184         // which is not matched.
185         for (int matchingSquareBracketPos : std::as_const(matchingSquareBracketPositions)) {
186             QString chunkExcludingMatchingSquareBrackets =
187                 qtRegexPattern.mid(previousNonMatchingSquareBracketPos, matchingSquareBracketPos - previousNonMatchingSquareBracketPos);
188             chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(chunkExcludingMatchingSquareBrackets, QLatin1Char('['));
189             chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(chunkExcludingMatchingSquareBrackets, QLatin1Char(']'));
190             qtRegexPatternNonMatchingSquaresMadeLiteral += chunkExcludingMatchingSquareBrackets + qtRegexPattern[matchingSquareBracketPos];
191             previousNonMatchingSquareBracketPos = matchingSquareBracketPos + 1;
192         }
193         QString chunkAfterLastMatchingSquareBracket = qtRegexPattern.mid(matchingSquareBracketPositions.last() + 1);
194         chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(chunkAfterLastMatchingSquareBracket, QLatin1Char('['));
195         chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(chunkAfterLastMatchingSquareBracket, QLatin1Char(']'));
196         qtRegexPatternNonMatchingSquaresMadeLiteral += chunkAfterLastMatchingSquareBracket;
197 
198         qtRegexPattern = qtRegexPatternNonMatchingSquaresMadeLiteral;
199     }
200 
201     qtRegexPattern.replace(QLatin1String("\\>"), QLatin1String("\\b"));
202     qtRegexPattern.replace(QLatin1String("\\<"), QLatin1String("\\b"));
203 
204     return qtRegexPattern;
205 }
206 
ensuredCharEscaped(const QString & originalString,QChar charToEscape)207 QString KateVi::ensuredCharEscaped(const QString &originalString, QChar charToEscape)
208 {
209     QString escapedString = originalString;
210     for (int i = 0; i < escapedString.length(); i++) {
211         if (escapedString[i] == charToEscape && !isCharEscaped(escapedString, i)) {
212             escapedString.replace(i, 1, QLatin1String("\\") + charToEscape);
213         }
214     }
215     return escapedString;
216 }
217 
withCaseSensitivityMarkersStripped(const QString & originalSearchTerm)218 QString KateVi::withCaseSensitivityMarkersStripped(const QString &originalSearchTerm)
219 {
220     // Only \C is handled, for now - I'll implement \c if someone asks for it.
221     int pos = 0;
222     QString caseSensitivityMarkersStripped = originalSearchTerm;
223     while (pos < caseSensitivityMarkersStripped.length()) {
224         if (caseSensitivityMarkersStripped.at(pos) == QLatin1Char('C') && isCharEscaped(caseSensitivityMarkersStripped, pos)) {
225             caseSensitivityMarkersStripped.remove(pos - 1, 2);
226             pos--;
227         }
228         pos++;
229     }
230     return caseSensitivityMarkersStripped;
231 }
232 
reversed(const QStringList & originalList)233 QStringList KateVi::reversed(const QStringList &originalList)
234 {
235     QStringList reversedList = originalList;
236     std::reverse(reversedList.begin(), reversedList.end());
237     return reversedList;
238 }
239 
SearchMode(EmulatedCommandBar * emulatedCommandBar,MatchHighlighter * matchHighlighter,InputModeManager * viInputModeManager,KTextEditor::ViewPrivate * view,QLineEdit * edit)240 SearchMode::SearchMode(EmulatedCommandBar *emulatedCommandBar,
241                        MatchHighlighter *matchHighlighter,
242                        InputModeManager *viInputModeManager,
243                        KTextEditor::ViewPrivate *view,
244                        QLineEdit *edit)
245     : ActiveMode(emulatedCommandBar, matchHighlighter, viInputModeManager, view)
246     , m_edit(edit)
247 {
248 }
249 
init(SearchMode::SearchDirection searchDirection)250 void SearchMode::init(SearchMode::SearchDirection searchDirection)
251 {
252     m_searchDirection = searchDirection;
253     m_startingCursorPos = view()->cursorPosition();
254 }
255 
handleKeyPress(const QKeyEvent * keyEvent)256 bool SearchMode::handleKeyPress(const QKeyEvent *keyEvent)
257 {
258     Q_UNUSED(keyEvent);
259     return false;
260 }
261 
editTextChanged(const QString & newText)262 void SearchMode::editTextChanged(const QString &newText)
263 {
264     QString qtRegexPattern = newText;
265     const bool searchBackwards = (m_searchDirection == SearchDirection::Backward);
266     const bool placeCursorAtEndOfMatch = shouldPlaceCursorAtEndOfMatch(qtRegexPattern, searchBackwards);
267     if (isRepeatLastSearch(qtRegexPattern, searchBackwards)) {
268         qtRegexPattern = viInputModeManager()->searcher()->getLastSearchPattern();
269     } else {
270         qtRegexPattern = withSearchConfigRemoved(qtRegexPattern, searchBackwards);
271         qtRegexPattern = vimRegexToQtRegexPattern(qtRegexPattern);
272     }
273 
274     // Decide case-sensitivity via SmartCase (note: if the expression contains \C, the "case-sensitive" marker, then
275     // we will be case-sensitive "by coincidence", as it were.).
276     bool caseSensitive = true;
277     if (qtRegexPattern.toLower() == qtRegexPattern) {
278         caseSensitive = false;
279     }
280 
281     qtRegexPattern = withCaseSensitivityMarkersStripped(qtRegexPattern);
282 
283     m_currentSearchParams.pattern = qtRegexPattern;
284     m_currentSearchParams.isCaseSensitive = caseSensitive;
285     m_currentSearchParams.isBackwards = searchBackwards;
286     m_currentSearchParams.shouldPlaceCursorAtEndOfMatch = placeCursorAtEndOfMatch;
287 
288     // The "count" for the current search is not shared between Visual & Normal mode, so we need to pick
289     // the right one to handle the counted search.
290     int c = viInputModeManager()->getCurrentViModeHandler()->getCount();
291     KTextEditor::Range match = viInputModeManager()->searcher()->findPattern(m_currentSearchParams,
292                                                                              m_startingCursorPos,
293                                                                              c,
294                                                                              false /* Don't add incremental searches to search history */);
295 
296     if (match.isValid()) {
297         // The returned range ends one past the last character of the match, so adjust.
298         KTextEditor::Cursor realMatchEnd = KTextEditor::Cursor(match.end().line(), match.end().column() - 1);
299         if (realMatchEnd.column() == -1) {
300             realMatchEnd = KTextEditor::Cursor(realMatchEnd.line() - 1, view()->doc()->lineLength(realMatchEnd.line() - 1));
301         }
302         moveCursorTo(placeCursorAtEndOfMatch ? realMatchEnd : match.start());
303         setBarBackground(SearchMode::MatchFound);
304     } else {
305         moveCursorTo(m_startingCursorPos);
306         if (!m_edit->text().isEmpty()) {
307             setBarBackground(SearchMode::NoMatchFound);
308         } else {
309             setBarBackground(SearchMode::Normal);
310         }
311     }
312 
313     updateMatchHighlight(match);
314 }
315 
deactivate(bool wasAborted)316 void SearchMode::deactivate(bool wasAborted)
317 {
318     // "Deactivate" can be called multiple times between init()'s, so only reset the cursor once!
319     if (m_startingCursorPos.isValid()) {
320         if (wasAborted) {
321             moveCursorTo(m_startingCursorPos);
322         }
323     }
324     m_startingCursorPos = KTextEditor::Cursor::invalid();
325     setBarBackground(SearchMode::Normal);
326     // Send a synthetic keypress through the system that signals whether the search was aborted or
327     // not.  If not, the keypress will "complete" the search motion, thus triggering it.
328     // We send to KateViewInternal as it updates the status bar and removes the "?".
329     const Qt::Key syntheticSearchCompletedKey = (wasAborted ? static_cast<Qt::Key>(0) : Qt::Key_Enter);
330     QKeyEvent syntheticSearchCompletedKeyPress(QEvent::KeyPress, syntheticSearchCompletedKey, Qt::NoModifier);
331     m_isSendingSyntheticSearchCompletedKeypress = true;
332     QApplication::sendEvent(view()->focusProxy(), &syntheticSearchCompletedKeyPress);
333     m_isSendingSyntheticSearchCompletedKeypress = false;
334     if (!wasAborted) {
335         // Search was actually executed, so store it as the last search.
336         viInputModeManager()->searcher()->setLastSearchParams(m_currentSearchParams);
337     }
338     // Append the raw text of the search to the search history (i.e. without conversion
339     // from Vim-style regex; without case-sensitivity markers stripped; etc.
340     // Vim does this even if the search was aborted, so we follow suit.
341     viInputModeManager()->globalState()->searchHistory()->append(m_edit->text());
342 }
343 
completionInvoked(Completer::CompletionInvocation invocationType)344 CompletionStartParams SearchMode::completionInvoked(Completer::CompletionInvocation invocationType)
345 {
346     Q_UNUSED(invocationType);
347     return activateSearchHistoryCompletion();
348 }
349 
completionChosen()350 void SearchMode::completionChosen()
351 {
352     // Choose completion with Enter/ Return -> close bar (the search will have already taken effect at this point), marking as not aborted .
353     close(false);
354 }
355 
activateSearchHistoryCompletion()356 CompletionStartParams SearchMode::activateSearchHistoryCompletion()
357 {
358     return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->searchHistory()->items()), 0);
359 }
360 
setBarBackground(SearchMode::BarBackgroundStatus status)361 void SearchMode::setBarBackground(SearchMode::BarBackgroundStatus status)
362 {
363     QPalette barBackground(m_edit->palette());
364     switch (status) {
365     case MatchFound: {
366         KColorScheme::adjustBackground(barBackground, KColorScheme::PositiveBackground);
367         break;
368     }
369     case NoMatchFound: {
370         KColorScheme::adjustBackground(barBackground, KColorScheme::NegativeBackground);
371         break;
372     }
373     case Normal: {
374         barBackground = QPalette();
375         break;
376     }
377     }
378     m_edit->setPalette(barBackground);
379 }
380