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
21using namespace KateVi;
22
23namespace
24{
25bool 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
39QString toggledEscaped(const QString &originalString, QChar escapeChar)
40{
41 int searchFrom = 0;
42 QString toggledEscapedString = originalString;
43 do {
44 const int indexOfEscapeChar = toggledEscapedString.indexOf(c: escapeChar, from: searchFrom);
45 if (indexOfEscapeChar == -1) {
46 break;
47 }
48 if (!isCharEscaped(string: toggledEscapedString, charPos: indexOfEscapeChar)) {
49 // Escape.
50 toggledEscapedString.replace(i: indexOfEscapeChar, len: 1, after: QLatin1String("\\") + escapeChar);
51 searchFrom = indexOfEscapeChar + 2;
52 } else {
53 // Unescape.
54 toggledEscapedString.remove(i: indexOfEscapeChar - 1, len: 1);
55 searchFrom = indexOfEscapeChar;
56 }
57 } while (true);
58
59 return toggledEscapedString;
60}
61
62int 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(i: pos) == searchConfigMarkerChar) {
67 if (!isCharEscaped(string: searchText, charPos: pos)) {
68 return pos;
69 }
70 }
71 }
72 return -1;
73}
74
75bool isRepeatLastSearch(const QString &searchText, const bool isSearchBackwards)
76{
77 const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText, isSearchBackwards);
78 if (posOfSearchConfigMarker != -1) {
79 if (QStringView(searchText).left(n: posOfSearchConfigMarker).isEmpty()) {
80 return true;
81 }
82 }
83 return false;
84}
85
86bool 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(i: posOfSearchConfigMarker + 1) == QLatin1Char('e')) {
91 return true;
92 }
93 }
94 return false;
95}
96
97QString withSearchConfigRemoved(const QString &originalSearchText, const bool isSearchBackwards)
98{
99 const int posOfSearchConfigMarker = findPosOfSearchConfigMarker(searchText: originalSearchText, isSearchBackwards);
100 if (posOfSearchConfigMarker == -1) {
101 return originalSearchText;
102 } else {
103 return originalSearchText.left(n: posOfSearchConfigMarker);
104 }
105}
106}
107
108QString KateVi::vimRegexToQtRegexPattern(const QString &vimRegexPattern)
109{
110 QString qtRegexPattern = vimRegexPattern;
111 qtRegexPattern = toggledEscaped(originalString: qtRegexPattern, escapeChar: QLatin1Char('('));
112 qtRegexPattern = toggledEscaped(originalString: qtRegexPattern, escapeChar: QLatin1Char(')'));
113 qtRegexPattern = toggledEscaped(originalString: qtRegexPattern, escapeChar: QLatin1Char('+'));
114 qtRegexPattern = toggledEscaped(originalString: qtRegexPattern, escapeChar: QLatin1Char('|'));
115 qtRegexPattern = ensuredCharEscaped(originalString: qtRegexPattern, charToEscape: 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(string: qtRegexPattern, charPos: i)) {
123 lookingForMatchingCloseBracket = true;
124 }
125 if (qtRegexPattern[i] == QLatin1Char('}') && lookingForMatchingCloseBracket && qtRegexPattern[i - 1] != QLatin1Char('\\')) {
126 matchingClosedCurlyBracketPositions.append(t: i);
127 }
128 }
129 if (matchingClosedCurlyBracketPositions.isEmpty()) {
130 // Escape all {'s and }'s - there are no matching pairs.
131 qtRegexPattern = toggledEscaped(originalString: qtRegexPattern, escapeChar: QLatin1Char('{'));
132 qtRegexPattern = toggledEscaped(originalString: qtRegexPattern, escapeChar: 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(t&: matchingClosedCurlyBracketPositions)) {
141 QString chunkExcludingMatchingCurlyClosed =
142 qtRegexPattern.mid(position: previousNonMatchingClosedCurlyPos, n: matchingClosedCurlyPos - previousNonMatchingClosedCurlyPos);
143 chunkExcludingMatchingCurlyClosed = toggledEscaped(originalString: chunkExcludingMatchingCurlyClosed, escapeChar: QLatin1Char('{'));
144 chunkExcludingMatchingCurlyClosed = toggledEscaped(originalString: chunkExcludingMatchingCurlyClosed, escapeChar: QLatin1Char('}'));
145 qtRegexPatternNonMatchingCurliesToggled += chunkExcludingMatchingCurlyClosed + qtRegexPattern[matchingClosedCurlyPos];
146 previousNonMatchingClosedCurlyPos = matchingClosedCurlyPos + 1;
147 }
148 QString chunkAfterLastMatchingClosedCurly = qtRegexPattern.mid(position: matchingClosedCurlyBracketPositions.last() + 1);
149 chunkAfterLastMatchingClosedCurly = toggledEscaped(originalString: chunkAfterLastMatchingClosedCurly, escapeChar: QLatin1Char('{'));
150 chunkAfterLastMatchingClosedCurly = toggledEscaped(originalString: chunkAfterLastMatchingClosedCurly, escapeChar: 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(string: qtRegexPattern, charPos: i) && !lookingForMatchingCloseBracket) {
164 lookingForMatchingCloseBracket = true;
165 openingBracketPos = i;
166 }
167 if (qtRegexPattern[i] == QLatin1Char(']') && lookingForMatchingCloseBracket && !isCharEscaped(string: qtRegexPattern, charPos: i)) {
168 lookingForMatchingCloseBracket = false;
169 matchingSquareBracketPositions.append(t: openingBracketPos);
170 matchingSquareBracketPositions.append(t: i);
171 }
172 }
173
174 if (matchingSquareBracketPositions.isEmpty()) {
175 // Escape all ['s and ]'s - there are no matching pairs.
176 qtRegexPattern = ensuredCharEscaped(originalString: qtRegexPattern, charToEscape: QLatin1Char('['));
177 qtRegexPattern = ensuredCharEscaped(originalString: qtRegexPattern, charToEscape: 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(t&: matchingSquareBracketPositions)) {
186 QString chunkExcludingMatchingSquareBrackets =
187 qtRegexPattern.mid(position: previousNonMatchingSquareBracketPos, n: matchingSquareBracketPos - previousNonMatchingSquareBracketPos);
188 chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(originalString: chunkExcludingMatchingSquareBrackets, charToEscape: QLatin1Char('['));
189 chunkExcludingMatchingSquareBrackets = ensuredCharEscaped(originalString: chunkExcludingMatchingSquareBrackets, charToEscape: QLatin1Char(']'));
190 qtRegexPatternNonMatchingSquaresMadeLiteral += chunkExcludingMatchingSquareBrackets + qtRegexPattern[matchingSquareBracketPos];
191 previousNonMatchingSquareBracketPos = matchingSquareBracketPos + 1;
192 }
193 QString chunkAfterLastMatchingSquareBracket = qtRegexPattern.mid(position: matchingSquareBracketPositions.last() + 1);
194 chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(originalString: chunkAfterLastMatchingSquareBracket, charToEscape: QLatin1Char('['));
195 chunkAfterLastMatchingSquareBracket = ensuredCharEscaped(originalString: chunkAfterLastMatchingSquareBracket, charToEscape: QLatin1Char(']'));
196 qtRegexPatternNonMatchingSquaresMadeLiteral += chunkAfterLastMatchingSquareBracket;
197
198 qtRegexPattern = qtRegexPatternNonMatchingSquaresMadeLiteral;
199 }
200
201 qtRegexPattern.replace(before: QLatin1String("\\>"), after: QLatin1String("\\b"));
202 qtRegexPattern.replace(before: QLatin1String("\\<"), after: QLatin1String("\\b"));
203
204 return qtRegexPattern;
205}
206
207QString 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(string: escapedString, charPos: i)) {
212 escapedString.replace(i, len: 1, after: QLatin1String("\\") + charToEscape);
213 }
214 }
215 return escapedString;
216}
217
218QString 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(i: pos) == QLatin1Char('C') && isCharEscaped(string: caseSensitivityMarkersStripped, charPos: pos)) {
225 caseSensitivityMarkersStripped.remove(i: pos - 1, len: 2);
226 pos--;
227 }
228 pos++;
229 }
230 return caseSensitivityMarkersStripped;
231}
232
233QStringList KateVi::reversed(const QStringList &originalList)
234{
235 QStringList reversedList = originalList;
236 std::reverse(first: reversedList.begin(), last: reversedList.end());
237 return reversedList;
238}
239
240SearchMode::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
250void SearchMode::init(SearchMode::SearchDirection searchDirection)
251{
252 m_searchDirection = searchDirection;
253 m_startingCursorPos = view()->cursorPosition();
254}
255
256bool SearchMode::handleKeyPress(const QKeyEvent *keyEvent)
257{
258 Q_UNUSED(keyEvent);
259 return false;
260}
261
262void SearchMode::editTextChanged(const QString &newText)
263{
264 QString qtRegexPattern = newText;
265 const bool searchBackwards = (m_searchDirection == SearchDirection::Backward);
266 const bool placeCursorAtEndOfMatch = shouldPlaceCursorAtEndOfMatch(searchText: qtRegexPattern, isSearchBackwards: searchBackwards);
267 if (isRepeatLastSearch(searchText: qtRegexPattern, isSearchBackwards: searchBackwards)) {
268 qtRegexPattern = viInputModeManager()->searcher()->getLastSearchPattern();
269 } else {
270 qtRegexPattern = withSearchConfigRemoved(originalSearchText: qtRegexPattern, isSearchBackwards: searchBackwards);
271 qtRegexPattern = vimRegexToQtRegexPattern(vimRegexPattern: 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(originalSearchTerm: 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(searchParams: m_currentSearchParams,
292 startFrom: m_startingCursorPos,
293 count: c,
294 addToSearchHistory: 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(line: realMatchEnd.line() - 1));
301 }
302 moveCursorTo(cursorPos: placeCursorAtEndOfMatch ? realMatchEnd : match.start());
303 setBarBackground(SearchMode::MatchFound);
304 } else {
305 moveCursorTo(cursorPos: m_startingCursorPos);
306 if (!m_edit->text().isEmpty()) {
307 setBarBackground(SearchMode::NoMatchFound);
308 } else {
309 setBarBackground(SearchMode::Normal);
310 }
311 }
312
313 if (!viInputModeManager()->searcher()->isHighlightSearchEnabled())
314 updateMatchHighlight(matchRange: match);
315}
316
317void SearchMode::deactivate(bool wasAborted)
318{
319 // "Deactivate" can be called multiple times between init()'s, so only reset the cursor once!
320 if (m_startingCursorPos.isValid()) {
321 if (wasAborted) {
322 moveCursorTo(cursorPos: m_startingCursorPos);
323 }
324 }
325 m_startingCursorPos = KTextEditor::Cursor::invalid();
326 setBarBackground(SearchMode::Normal);
327 // Send a synthetic keypress through the system that signals whether the search was aborted or
328 // not. If not, the keypress will "complete" the search motion, thus triggering it.
329 // We send to KateViewInternal as it updates the status bar and removes the "?".
330 const Qt::Key syntheticSearchCompletedKey = (wasAborted ? static_cast<Qt::Key>(0) : Qt::Key_Enter);
331 QKeyEvent syntheticSearchCompletedKeyPress(QEvent::KeyPress, syntheticSearchCompletedKey, Qt::NoModifier);
332 m_isSendingSyntheticSearchCompletedKeypress = true;
333 QApplication::sendEvent(receiver: view()->focusProxy(), event: &syntheticSearchCompletedKeyPress);
334 m_isSendingSyntheticSearchCompletedKeypress = false;
335 if (!wasAborted) {
336 // Search was actually executed, so store it as the last search.
337 viInputModeManager()->searcher()->setLastSearchParams(m_currentSearchParams);
338 }
339 // Append the raw text of the search to the search history (i.e. without conversion
340 // from Vim-style regex; without case-sensitivity markers stripped; etc.
341 // Vim does this even if the search was aborted, so we follow suit.
342 viInputModeManager()->globalState()->searchHistory()->append(historyItem: m_edit->text());
343 viInputModeManager()->searcher()->patternDone(wasAborted);
344}
345
346CompletionStartParams SearchMode::completionInvoked(Completer::CompletionInvocation invocationType)
347{
348 Q_UNUSED(invocationType);
349 return activateSearchHistoryCompletion();
350}
351
352void SearchMode::completionChosen()
353{
354 // Choose completion with Enter/ Return -> close bar (the search will have already taken effect at this point), marking as not aborted .
355 close(wasAborted: false);
356}
357
358CompletionStartParams SearchMode::activateSearchHistoryCompletion()
359{
360 return CompletionStartParams::createModeSpecific(completions: reversed(originalList: viInputModeManager()->globalState()->searchHistory()->items()), wordStartPos: 0);
361}
362
363void SearchMode::setBarBackground(SearchMode::BarBackgroundStatus status)
364{
365 QPalette barBackground(m_edit->palette());
366 switch (status) {
367 case MatchFound: {
368 KColorScheme::adjustBackground(barBackground, newRole: KColorScheme::PositiveBackground);
369 break;
370 }
371 case NoMatchFound: {
372 KColorScheme::adjustBackground(barBackground, newRole: KColorScheme::NegativeBackground);
373 break;
374 }
375 case Normal: {
376 barBackground = QPalette();
377 break;
378 }
379 }
380 m_edit->setPalette(barBackground);
381}
382

source code of ktexteditor/src/vimode/emulatedcommandbar/searchmode.cpp