1/*
2 SPDX-FileCopyrightText: KDE Developers
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "searcher.h"
8#include "globalstate.h"
9#include "history.h"
10#include "kateconfig.h"
11#include "katedocument.h"
12#include "kateview.h"
13#include <vimode/inputmodemanager.h>
14#include <vimode/modes/modebase.h>
15
16using namespace KateVi;
17
18Searcher::Searcher(InputModeManager *manager)
19 : m_viInputModeManager(manager)
20 , m_view(manager->view())
21 , m_lastHlSearchRange(KTextEditor::Range::invalid())
22 , highlightMatchAttribute(new KTextEditor::Attribute())
23{
24 updateHighlightColors();
25
26 if (m_hlMode == HighlightMode::Enable) {
27 connectSignals();
28 }
29}
30
31Searcher::~Searcher()
32{
33 disconnectSignals();
34 clearHighlights();
35}
36
37const QString Searcher::getLastSearchPattern() const
38{
39 return m_lastSearchConfig.pattern;
40}
41
42void Searcher::setLastSearchParams(const SearchParams &searchParams)
43{
44 if (!searchParams.pattern.isEmpty())
45 m_lastSearchConfig = searchParams;
46}
47
48bool Searcher::lastSearchWrapped() const
49{
50 return m_lastSearchWrapped;
51}
52
53void Searcher::findNext()
54{
55 const Range r = motionFindNext();
56 if (r.valid) {
57 m_viInputModeManager->getCurrentViModeHandler()->goToPos(r);
58 }
59}
60
61void Searcher::findPrevious()
62{
63 const Range r = motionFindPrev();
64 if (r.valid) {
65 m_viInputModeManager->getCurrentViModeHandler()->goToPos(r);
66 }
67}
68
69Range Searcher::motionFindNext(int count)
70{
71 Range match = findPatternForMotion(searchParams: m_lastSearchConfig, startFrom: m_view->cursorPosition(), count);
72
73 if (!match.valid) {
74 return match;
75 }
76 if (!m_lastSearchConfig.shouldPlaceCursorAtEndOfMatch) {
77 return Range(match.startLine, match.startColumn, ExclusiveMotion);
78 }
79 return Range(match.endLine, match.endColumn - 1, ExclusiveMotion);
80}
81
82Range Searcher::motionFindPrev(int count)
83{
84 SearchParams lastSearchReversed = m_lastSearchConfig;
85 lastSearchReversed.isBackwards = !lastSearchReversed.isBackwards;
86 Range match = findPatternForMotion(searchParams: lastSearchReversed, startFrom: m_view->cursorPosition(), count);
87
88 if (!match.valid) {
89 return match;
90 }
91 if (!m_lastSearchConfig.shouldPlaceCursorAtEndOfMatch) {
92 return Range(match.startLine, match.startColumn, ExclusiveMotion);
93 }
94 return Range(match.endLine, match.endColumn - 1, ExclusiveMotion);
95}
96
97Range Searcher::findPatternForMotion(const SearchParams &searchParams, const KTextEditor::Cursor startFrom, int count)
98{
99 if (searchParams.pattern.isEmpty()) {
100 return Range::invalid();
101 }
102
103 KTextEditor::Range match = findPatternWorker(searchParams, startFrom, count);
104
105 if (m_hlMode != HighlightMode::Disable) {
106 if (m_hlMode == HighlightMode::HideCurrent) {
107 m_hlMode = HighlightMode::Enable;
108 highlightVisibleResults(searchParams, force: true);
109 } else {
110 highlightVisibleResults(searchParams);
111 }
112 }
113
114 return Range(match.start(), match.end(), ExclusiveMotion);
115}
116
117Range Searcher::findWordForMotion(const QString &word, bool backwards, const KTextEditor::Cursor startFrom, int count)
118{
119 m_lastSearchConfig.isBackwards = backwards;
120 m_lastSearchConfig.isCaseSensitive = false;
121 m_lastSearchConfig.shouldPlaceCursorAtEndOfMatch = false;
122
123 m_viInputModeManager->globalState()->searchHistory()->append(QStringLiteral("\\<%1\\>").arg(a: word));
124 QString pattern = QStringLiteral("\\b%1\\b").arg(a: word);
125 m_lastSearchConfig.pattern = pattern;
126 if (m_hlMode == HighlightMode::HideCurrent)
127 m_hlMode = HighlightMode::Enable;
128
129 return findPatternForMotion(searchParams: m_lastSearchConfig, startFrom, count);
130}
131
132KTextEditor::Range Searcher::findPattern(const SearchParams &searchParams, const KTextEditor::Cursor startFrom, int count, bool addToSearchHistory)
133{
134 if (addToSearchHistory) {
135 m_viInputModeManager->globalState()->searchHistory()->append(historyItem: searchParams.pattern);
136 m_lastSearchConfig = searchParams;
137 }
138
139 KTextEditor::Range r = findPatternWorker(searchParams, startFrom, count);
140
141 if (m_hlMode != HighlightMode::Disable)
142 highlightVisibleResults(searchParams);
143
144 newPattern = false;
145 return r;
146}
147
148void Searcher::highlightVisibleResults(const SearchParams &searchParams, bool force)
149{
150 if (newPattern && searchParams.pattern.isEmpty())
151 return;
152
153 auto vr = m_view->visibleRange();
154
155 const SearchParams &l = searchParams;
156 const SearchParams &r = m_lastHlSearchConfig;
157
158 if (!force && l.pattern == r.pattern && l.isCaseSensitive == r.isCaseSensitive && vr == m_lastHlSearchRange) {
159 return;
160 }
161
162 m_lastHlSearchConfig = searchParams;
163 m_lastHlSearchRange = vr;
164
165 clearHighlights();
166
167 KTextEditor::SearchOptions flags = KTextEditor::Regex;
168 m_lastSearchWrapped = false;
169
170 const QString &pattern = searchParams.pattern;
171
172 if (!searchParams.isCaseSensitive) {
173 flags |= KTextEditor::CaseInsensitive;
174 }
175
176 KTextEditor::Range match;
177 KTextEditor::Cursor current(vr.start());
178
179 do {
180 match = m_view->doc()->searchText(range: KTextEditor::Range(current, vr.end()), pattern, options: flags).first();
181 if (match.isValid()) {
182 if (match.isEmpty())
183 match = KTextEditor::Range(match.start(), 1);
184
185 auto highlight = m_view->doc()->newMovingRange(range: match, insertBehaviors: Kate::TextRange::DoNotExpand);
186 highlight->setView(m_view);
187 highlight->setAttributeOnlyForViews(true);
188 highlight->setZDepth(-10000.0);
189 highlight->setAttribute(highlightMatchAttribute);
190 m_hlRanges.append(t: highlight);
191
192 current = match.end();
193 }
194 } while (match.isValid() && current < vr.end());
195}
196
197void Searcher::clearHighlights()
198{
199 if (!m_hlRanges.empty()) {
200 qDeleteAll(c: m_hlRanges);
201 m_hlRanges.clear();
202 }
203}
204
205void Searcher::hideCurrentHighlight()
206{
207 if (m_hlMode != HighlightMode::Disable) {
208 m_hlMode = HighlightMode::HideCurrent;
209 clearHighlights();
210 }
211}
212
213void Searcher::updateHighlightColors()
214{
215 const QColor foregroundColor = m_view->defaultStyleAttribute(defaultStyle: KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color();
216 const QColor &searchColor = m_view->rendererConfig()->searchHighlightColor();
217 // init match attribute
218 highlightMatchAttribute->setForeground(foregroundColor);
219 highlightMatchAttribute->setBackground(searchColor);
220}
221
222void Searcher::enableHighlightSearch(bool enable)
223{
224 if (enable) {
225 m_hlMode = HighlightMode::Enable;
226
227 connectSignals();
228 highlightVisibleResults(searchParams: m_lastSearchConfig, force: true);
229 } else {
230 m_hlMode = HighlightMode::Disable;
231
232 disconnectSignals();
233 clearHighlights();
234 }
235}
236
237bool Searcher::isHighlightSearchEnabled() const
238{
239 return m_hlMode != HighlightMode::Disable;
240}
241
242void Searcher::disconnectSignals()
243{
244 QObject::disconnect(m_displayRangeChangedConnection);
245 QObject::disconnect(m_textChangedConnection);
246}
247
248void Searcher::connectSignals()
249{
250 disconnectSignals();
251
252 m_displayRangeChangedConnection = QObject::connect(sender: m_view, signal: &KTextEditor::ViewPrivate::displayRangeChanged, slot: [this]() {
253 if (m_hlMode == HighlightMode::Enable)
254 highlightVisibleResults(searchParams: m_lastHlSearchConfig);
255 });
256 m_textChangedConnection = QObject::connect(sender: m_view->doc(), signal: &KTextEditor::Document::textChanged, slot: [this]() {
257 if (m_hlMode == HighlightMode::Enable)
258 highlightVisibleResults(searchParams: m_lastHlSearchConfig, force: true);
259 });
260}
261
262void Searcher::patternDone(bool wasAborted)
263{
264 if (wasAborted) {
265 if (m_hlMode == HighlightMode::HideCurrent || m_lastSearchConfig.pattern.isEmpty())
266 clearHighlights();
267 else if (m_hlMode == HighlightMode::Enable)
268 highlightVisibleResults(searchParams: m_lastSearchConfig);
269
270 } else {
271 if (m_hlMode == HighlightMode::HideCurrent)
272 m_hlMode = HighlightMode::Enable;
273 }
274 newPattern = true;
275}
276
277KTextEditor::Range Searcher::findPatternWorker(const SearchParams &searchParams, const KTextEditor::Cursor startFrom, int count)
278{
279 KTextEditor::Cursor searchBegin = startFrom;
280 KTextEditor::SearchOptions flags = KTextEditor::Regex;
281 m_lastSearchWrapped = false;
282
283 const QString &pattern = searchParams.pattern;
284
285 if (searchParams.isBackwards) {
286 flags |= KTextEditor::Backwards;
287 }
288 if (!searchParams.isCaseSensitive) {
289 flags |= KTextEditor::CaseInsensitive;
290 }
291 KTextEditor::Range finalMatch;
292 for (int i = 0; i < count; i++) {
293 if (!searchParams.isBackwards) {
294 const KTextEditor::Range matchRange =
295 m_view->doc()
296 ->searchText(range: KTextEditor::Range(KTextEditor::Cursor(searchBegin.line(), searchBegin.column() + 1), m_view->doc()->documentEnd()),
297 pattern,
298 options: flags)
299 .first();
300
301 if (matchRange.isValid()) {
302 finalMatch = matchRange;
303 } else {
304 // Wrap around.
305 const KTextEditor::Range wrappedMatchRange =
306 m_view->doc()->searchText(range: KTextEditor::Range(m_view->doc()->documentRange().start(), m_view->doc()->documentEnd()), pattern, options: flags).first();
307 if (wrappedMatchRange.isValid()) {
308 finalMatch = wrappedMatchRange;
309 m_lastSearchWrapped = true;
310 } else {
311 return KTextEditor::Range::invalid();
312 }
313 }
314 } else {
315 // Ok - this is trickier: we can't search in the range from doc start to searchBegin, because
316 // the match might extend *beyond* searchBegin.
317 // We could search through the entire document and then filter out only those matches that are
318 // after searchBegin, but it's more efficient to instead search from the start of the
319 // document until the beginning of the line after searchBegin, and then filter.
320 // Unfortunately, searchText doesn't necessarily turn up all matches (just the first one, sometimes)
321 // so we must repeatedly search in such a way that the previous match isn't found, until we either
322 // find no matches at all, or the first match that is before searchBegin.
323 KTextEditor::Cursor newSearchBegin = KTextEditor::Cursor(searchBegin.line(), m_view->doc()->lineLength(line: searchBegin.line()));
324 KTextEditor::Range bestMatch = KTextEditor::Range::invalid();
325 while (true) {
326 QList<KTextEditor::Range> matchesUnfiltered =
327 m_view->doc()->searchText(range: KTextEditor::Range(newSearchBegin, m_view->doc()->documentRange().start()), pattern, options: flags);
328
329 if (matchesUnfiltered.size() == 1 && !matchesUnfiltered.first().isValid()) {
330 break;
331 }
332
333 // After sorting, the last element in matchesUnfiltered is the last match position.
334 std::sort(first: matchesUnfiltered.begin(), last: matchesUnfiltered.end());
335
336 QList<KTextEditor::Range> filteredMatches;
337 for (KTextEditor::Range unfilteredMatch : std::as_const(t&: matchesUnfiltered)) {
338 if (unfilteredMatch.start() < searchBegin) {
339 filteredMatches.append(t: unfilteredMatch);
340 }
341 }
342 if (!filteredMatches.isEmpty()) {
343 // Want the latest matching range that is before searchBegin.
344 bestMatch = filteredMatches.last();
345 break;
346 }
347
348 // We found some unfiltered matches, but none were suitable. In case matchesUnfiltered wasn't
349 // all matching elements, search again, starting from before the earliest matching range.
350 if (filteredMatches.isEmpty()) {
351 newSearchBegin = matchesUnfiltered.first().start();
352 }
353 }
354
355 KTextEditor::Range matchRange = bestMatch;
356
357 if (matchRange.isValid()) {
358 finalMatch = matchRange;
359 } else {
360 const KTextEditor::Range wrappedMatchRange =
361 m_view->doc()->searchText(range: KTextEditor::Range(m_view->doc()->documentEnd(), m_view->doc()->documentRange().start()), pattern, options: flags).first();
362
363 if (wrappedMatchRange.isValid()) {
364 finalMatch = wrappedMatchRange;
365 m_lastSearchWrapped = true;
366 } else {
367 return KTextEditor::Range::invalid();
368 }
369 }
370 }
371 searchBegin = finalMatch.start();
372 }
373 return finalMatch;
374}
375

source code of ktexteditor/src/vimode/searcher.cpp