KTextEditor

searchmode.cpp
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(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
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(pos) == searchConfigMarkerChar) {
67 if (!isCharEscaped(searchText, 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(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(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(originalSearchText, isSearchBackwards);
100 if (posOfSearchConfigMarker == -1) {
101 return originalSearchText;
102 } else {
103 return originalSearchText.left(posOfSearchConfigMarker);
104 }
105}
106}
107
108QString 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
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(escapedString, i)) {
212 escapedString.replace(i, 1, 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(pos) == QLatin1Char('C') && isCharEscaped(caseSensitivityMarkersStripped, pos)) {
225 caseSensitivityMarkersStripped.remove(pos - 1, 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(reversedList.begin(), 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(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 if (!viInputModeManager()->searcher()->isHighlightSearchEnabled())
314 updateMatchHighlight(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(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(view()->focusProxy(), &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(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(false);
356}
357
358CompletionStartParams SearchMode::activateSearchHistoryCompletion()
359{
360 return CompletionStartParams::createModeSpecific(reversed(viInputModeManager()->globalState()->searchHistory()->items()), 0);
361}
362
363void SearchMode::setBarBackground(SearchMode::BarBackgroundStatus status)
364{
365 QPalette barBackground(m_edit->palette());
366 switch (status) {
367 case MatchFound: {
369 break;
370 }
371 case NoMatchFound: {
373 break;
374 }
375 case Normal: {
376 barBackground = QPalette();
377 break;
378 }
379 }
380 m_edit->setPalette(barBackground);
381}
static void adjustBackground(QPalette &, BackgroundRole newRole=NormalBackground, QPalette::ColorRole color=QPalette::Base, ColorSet set=View, KSharedConfigPtr=KSharedConfigPtr())
The Cursor represents a position in a Document.
Definition cursor.h:75
constexpr int column() const noexcept
Retrieve the column on which this cursor is situated.
Definition cursor.h:192
constexpr bool isValid() const noexcept
Returns whether the current position of this cursor is a valid position (line + column must both be >...
Definition cursor.h:102
constexpr int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
static constexpr Cursor invalid() noexcept
Returns an invalid cursor.
Definition cursor.h:112
An object representing a section of text, from one Cursor to another.
A KateViewBarWidget that attempts to emulate some of the features of Vim's own command bar,...
Q_SCRIPTABLE Q_NOREPLY void start()
Q_SCRIPTABLE CaptureState status()
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
bool sendEvent(QObject *receiver, QEvent *event)
void append(QList< T > &&value)
iterator begin()
iterator end()
bool isEmpty() const const
T & last()
const QChar at(qsizetype position) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString toLower() const const
NoModifier
QTextStream & left(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:15:44 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.