KTextEditor

searcher.cpp
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(m_lastSearchConfig, 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(lastSearchReversed, 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, 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(word));
124 QString pattern = QStringLiteral("\\b%1\\b").arg(word);
125 m_lastSearchConfig.pattern = pattern;
126 if (m_hlMode == HighlightMode::HideCurrent)
127 m_hlMode = HighlightMode::Enable;
128
129 return findPatternForMotion(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(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
168 m_lastSearchWrapped = false;
169
170 const QString &pattern = searchParams.pattern;
171
172 if (!searchParams.isCaseSensitive) {
174 }
175
177 KTextEditor::Cursor current(vr.start());
178
179 do {
180 match = m_view->doc()->searchText(KTextEditor::Range(current, vr.end()), pattern, 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(match, 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(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(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(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(m_lastSearchConfig, 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(m_view, &KTextEditor::ViewPrivate::displayRangeChanged, [this]() {
253 if (m_hlMode == HighlightMode::Enable)
254 highlightVisibleResults(m_lastHlSearchConfig);
255 });
256 m_textChangedConnection = QObject::connect(m_view->doc(), &KTextEditor::Document::textChanged, [this]() {
257 if (m_hlMode == HighlightMode::Enable)
258 highlightVisibleResults(m_lastHlSearchConfig, 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(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;
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) {
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(KTextEditor::Range(KTextEditor::Cursor(searchBegin.line(), searchBegin.column() + 1), m_view->doc()->documentEnd()),
297 pattern,
298 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(KTextEditor::Range(m_view->doc()->documentRange().start(), m_view->doc()->documentEnd()), pattern, flags).first();
307 if (wrappedMatchRange.isValid()) {
308 finalMatch = wrappedMatchRange;
309 m_lastSearchWrapped = true;
310 } else {
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(searchBegin.line()));
325 while (true) {
326 QList<KTextEditor::Range> matchesUnfiltered =
327 m_view->doc()->searchText(KTextEditor::Range(newSearchBegin, m_view->doc()->documentRange().start()), pattern, 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(matchesUnfiltered.begin(), matchesUnfiltered.end());
335
336 QList<KTextEditor::Range> filteredMatches;
337 for (KTextEditor::Range unfilteredMatch : std::as_const(matchesUnfiltered)) {
338 if (unfilteredMatch.start() < searchBegin) {
339 filteredMatches.append(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(KTextEditor::Range(m_view->doc()->documentEnd(), m_view->doc()->documentRange().start()), pattern, flags).first();
362
363 if (wrappedMatchRange.isValid()) {
364 finalMatch = wrappedMatchRange;
365 m_lastSearchWrapped = true;
366 } else {
368 }
369 }
370 }
371 searchBegin = finalMatch.start();
372 }
373 return finalMatch;
374}
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 int line() const noexcept
Retrieve the line on which this cursor is situated.
Definition cursor.h:174
void textChanged(KTextEditor::Document *document)
The document emits this signal whenever its text changes.
@ DoNotExpand
Don't expand to encapsulate new characters in either direction. This is the default.
An object representing a section of text, from one Cursor to another.
constexpr Cursor start() const noexcept
Get the start position of this range.
static constexpr Range invalid() noexcept
Returns an invalid range.
constexpr bool isValid() const noexcept
Validity check.
@ CaseInsensitive
Ignores cases, e.g. "a" matches "A".
Definition document.h:54
@ Regex
Treats the pattern as a regular expression.
Definition document.h:51
@ Backwards
Searches in backward direction.
Definition document.h:55
QFlags< SearchOption > SearchOptions
Stores a combination of #SearchOption values.
Definition document.h:65
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
The KTextEditor namespace contains all the public API that is required to use the KTextEditor compone...
void append(QList< T > &&value)
iterator begin()
void clear()
bool empty() const const
iterator end()
T & first()
bool isEmpty() const const
T & last()
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QString arg(Args &&... args) const const
bool isEmpty() const const
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.