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