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 | |
16 | using namespace KateVi; |
17 | |
18 | Searcher::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 | |
31 | Searcher::~Searcher() |
32 | { |
33 | disconnectSignals(); |
34 | clearHighlights(); |
35 | } |
36 | |
37 | const QString Searcher::getLastSearchPattern() const |
38 | { |
39 | return m_lastSearchConfig.pattern; |
40 | } |
41 | |
42 | void Searcher::setLastSearchParams(const SearchParams &searchParams) |
43 | { |
44 | if (!searchParams.pattern.isEmpty()) |
45 | m_lastSearchConfig = searchParams; |
46 | } |
47 | |
48 | bool Searcher::lastSearchWrapped() const |
49 | { |
50 | return m_lastSearchWrapped; |
51 | } |
52 | |
53 | void Searcher::findNext() |
54 | { |
55 | const Range r = motionFindNext(); |
56 | if (r.valid) { |
57 | m_viInputModeManager->getCurrentViModeHandler()->goToPos(r); |
58 | } |
59 | } |
60 | |
61 | void Searcher::findPrevious() |
62 | { |
63 | const Range r = motionFindPrev(); |
64 | if (r.valid) { |
65 | m_viInputModeManager->getCurrentViModeHandler()->goToPos(r); |
66 | } |
67 | } |
68 | |
69 | Range 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 | |
82 | Range 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 | |
97 | Range 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 | |
117 | Range 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 | |
132 | KTextEditor::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 | |
148 | void 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 | |
197 | void Searcher::clearHighlights() |
198 | { |
199 | if (!m_hlRanges.empty()) { |
200 | qDeleteAll(c: m_hlRanges); |
201 | m_hlRanges.clear(); |
202 | } |
203 | } |
204 | |
205 | void Searcher::hideCurrentHighlight() |
206 | { |
207 | if (m_hlMode != HighlightMode::Disable) { |
208 | m_hlMode = HighlightMode::HideCurrent; |
209 | clearHighlights(); |
210 | } |
211 | } |
212 | |
213 | void 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 | |
222 | void 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 | |
237 | bool Searcher::isHighlightSearchEnabled() const |
238 | { |
239 | return m_hlMode != HighlightMode::Disable; |
240 | } |
241 | |
242 | void Searcher::disconnectSignals() |
243 | { |
244 | QObject::disconnect(m_displayRangeChangedConnection); |
245 | QObject::disconnect(m_textChangedConnection); |
246 | } |
247 | |
248 | void 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 | |
262 | void 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 | |
277 | KTextEditor::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 | |