1/*
2 SPDX-FileCopyrightText: 2009-2010 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
3 SPDX-FileCopyrightText: 2007 Sebastian Pipping <webmaster@hartwork.org>
4 SPDX-FileCopyrightText: 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
5 SPDX-FileCopyrightText: 2007 Thomas Friedrichsmeier <thomas.friedrichsmeier@ruhr-uni-bochum.de>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "katesearchbar.h"
11
12#include "kateconfig.h"
13#include "katedocument.h"
14#include "kateglobal.h"
15#include "katematch.h"
16#include "kateundomanager.h"
17#include "kateview.h"
18
19#include <KTextEditor/DocumentCursor>
20#include <KTextEditor/Message>
21#include <KTextEditor/MovingRange>
22
23#include "ui_searchbarincremental.h"
24#include "ui_searchbarpower.h"
25
26#include <KColorScheme>
27#include <KLocalizedString>
28#include <KMessageBox>
29
30#include <QCheckBox>
31#include <QComboBox>
32#include <QCompleter>
33#include <QElapsedTimer>
34#include <QKeyEvent>
35#include <QMenu>
36#include <QRegularExpression>
37#include <QShortcut>
38#include <QStringListModel>
39#include <QVBoxLayout>
40
41#include <vector>
42
43// Turn debug messages on/off here
44// #define FAST_DEBUG_ENABLE
45
46#ifdef FAST_DEBUG_ENABLE
47#define FAST_DEBUG(x) qCDebug(LOG_KTE) << x
48#else
49#define FAST_DEBUG(x)
50#endif
51
52using namespace KTextEditor;
53
54namespace
55{
56class AddMenuManager
57{
58private:
59 QList<QString> m_insertBefore;
60 QList<QString> m_insertAfter;
61 QSet<QAction *> m_actionPointers;
62 uint m_indexWalker;
63 QMenu *m_menu;
64
65public:
66 AddMenuManager(QMenu *parent, int expectedItemCount)
67 : m_insertBefore(QList<QString>(expectedItemCount))
68 , m_insertAfter(QList<QString>(expectedItemCount))
69 , m_indexWalker(0)
70 , m_menu(nullptr)
71 {
72 Q_ASSERT(parent != nullptr);
73 m_menu = parent->addMenu(i18n("Add..."));
74 if (m_menu == nullptr) {
75 return;
76 }
77 m_menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
78 }
79
80 void enableMenu(bool enabled)
81 {
82 if (m_menu == nullptr) {
83 return;
84 }
85 m_menu->setEnabled(enabled);
86 }
87
88 void addEntry(const QString &before,
89 const QString &after,
90 const QString &description,
91 const QString &realBefore = QString(),
92 const QString &realAfter = QString())
93 {
94 if (m_menu == nullptr) {
95 return;
96 }
97 QAction *const action = m_menu->addAction(text: before + after + QLatin1Char('\t') + description);
98 m_insertBefore[m_indexWalker] = QString(realBefore.isEmpty() ? before : realBefore);
99 m_insertAfter[m_indexWalker] = QString(realAfter.isEmpty() ? after : realAfter);
100 action->setData(QVariant(m_indexWalker++));
101 m_actionPointers.insert(value: action);
102 }
103
104 void addSeparator()
105 {
106 if (m_menu == nullptr) {
107 return;
108 }
109 m_menu->addSeparator();
110 }
111
112 void handle(QAction *action, QLineEdit *lineEdit)
113 {
114 if (!m_actionPointers.contains(value: action)) {
115 return;
116 }
117
118 const int cursorPos = lineEdit->cursorPosition();
119 const int index = action->data().toUInt();
120 const QString &before = m_insertBefore[index];
121 const QString &after = m_insertAfter[index];
122 lineEdit->insert(before + after);
123 lineEdit->setCursorPosition(cursorPos + before.size());
124 lineEdit->setFocus();
125 }
126};
127
128} // anon namespace
129
130KateSearchBar::KateSearchBar(bool initAsPower, KTextEditor::ViewPrivate *view, KateViewConfig *config)
131 : KateViewBarWidget(true, view)
132 , m_view(view)
133 , m_config(config)
134 , m_layout(new QVBoxLayout())
135 , m_widget(nullptr)
136 , m_incUi(nullptr)
137 , m_incInitCursor(view->cursorPosition())
138 , m_powerUi(nullptr)
139 , highlightMatchAttribute(new Attribute())
140 , highlightReplacementAttribute(new Attribute())
141 , m_incHighlightAll(false)
142 , m_incFromCursor(true)
143 , m_incMatchCase(false)
144 , m_powerMatchCase(true)
145 , m_powerFromCursor(false)
146 , m_powerHighlightAll(false)
147 , m_powerMode(0)
148{
149 connect(sender: view, signal: &KTextEditor::View::cursorPositionChanged, context: this, slot: &KateSearchBar::updateIncInitCursor);
150 connect(sender: view, signal: &KTextEditor::View::selectionChanged, context: this, slot: &KateSearchBar::updateSelectionOnly);
151 connect(sender: this, signal: &KateSearchBar::findOrReplaceAllFinished, context: this, slot: &KateSearchBar::endFindOrReplaceAll);
152
153 auto setSelectionChangedByUndoRedo = [this]() {
154 m_selectionChangedByUndoRedo = true;
155 };
156 auto unsetSelectionChangedByUndoRedo = [this]() {
157 m_selectionChangedByUndoRedo = false;
158 };
159 KateUndoManager *docUndoManager = view->doc()->undoManager();
160 connect(sender: docUndoManager, signal: &KateUndoManager::undoStart, context: this, slot&: setSelectionChangedByUndoRedo);
161 connect(sender: docUndoManager, signal: &KateUndoManager::undoEnd, context: this, slot&: unsetSelectionChangedByUndoRedo);
162 connect(sender: docUndoManager, signal: &KateUndoManager::redoStart, context: this, slot&: setSelectionChangedByUndoRedo);
163 connect(sender: docUndoManager, signal: &KateUndoManager::redoEnd, context: this, slot&: unsetSelectionChangedByUndoRedo);
164
165 // When document is reloaded, disable selection-only search so that the search won't be stuck after the first search
166 connect(sender: view->doc(), signal: &KTextEditor::Document::reloaded, context: this, slot: [this]() {
167 setSelectionOnly(false);
168 });
169
170 // init match attribute
171 Attribute::Ptr mouseInAttribute(new Attribute());
172 mouseInAttribute->setFontBold(true);
173 highlightMatchAttribute->setDynamicAttribute(type: Attribute::ActivateMouseIn, attribute: mouseInAttribute);
174
175 Attribute::Ptr caretInAttribute(new Attribute());
176 caretInAttribute->setFontItalic(true);
177 highlightMatchAttribute->setDynamicAttribute(type: Attribute::ActivateCaretIn, attribute: caretInAttribute);
178
179 updateHighlightColors();
180
181 // Modify parent
182 QWidget *const widget = centralWidget();
183 widget->setLayout(m_layout);
184 m_layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
185
186 // allow to have small size, for e.g. Kile
187 setMinimumWidth(100);
188
189 // Copy global to local config backup
190 const auto searchFlags = m_config->searchFlags();
191 m_incHighlightAll = (searchFlags & KateViewConfig::IncHighlightAll) != 0;
192 m_incFromCursor = (searchFlags & KateViewConfig::IncFromCursor) != 0;
193 m_incMatchCase = (searchFlags & KateViewConfig::IncMatchCase) != 0;
194 m_powerMatchCase = (searchFlags & KateViewConfig::PowerMatchCase) != 0;
195 m_powerFromCursor = (searchFlags & KateViewConfig::PowerFromCursor) != 0;
196 m_powerHighlightAll = (searchFlags & KateViewConfig::PowerHighlightAll) != 0;
197 m_powerMode = ((searchFlags & KateViewConfig::PowerModeRegularExpression) != 0)
198 ? MODE_REGEX
199 : (((searchFlags & KateViewConfig::PowerModeEscapeSequences) != 0)
200 ? MODE_ESCAPE_SEQUENCES
201 : (((searchFlags & KateViewConfig::PowerModeWholeWords) != 0) ? MODE_WHOLE_WORDS : MODE_PLAIN_TEXT));
202
203 // Load one of either dialogs
204 if (initAsPower) {
205 enterPowerMode();
206 } else {
207 enterIncrementalMode();
208 }
209
210 updateSelectionOnly();
211}
212
213KateSearchBar::~KateSearchBar()
214{
215 if (!m_cancelFindOrReplace) {
216 // Finish/Cancel the still running job to avoid a crash
217 endFindOrReplaceAll();
218 }
219
220 clearHighlights();
221 delete m_layout;
222 delete m_widget;
223
224 delete m_incUi;
225 delete m_powerUi;
226 if (m_workingRange) {
227 delete m_workingRange;
228 }
229}
230
231void KateSearchBar::closed()
232{
233 // remove search from the view bar, because it vertically bloats up the
234 // stacked layout in KateViewBar.
235 if (viewBar()) {
236 viewBar()->removeBarWidget(barWidget: this);
237 }
238
239 clearHighlights();
240 m_replacement.clear();
241 m_unfinishedSearchText.clear();
242}
243
244void KateSearchBar::setReplacementPattern(const QString &replacementPattern)
245{
246 Q_ASSERT(isPower());
247
248 if (this->replacementPattern() == replacementPattern) {
249 return;
250 }
251
252 m_powerUi->replacement->setEditText(replacementPattern);
253}
254
255QString KateSearchBar::replacementPattern() const
256{
257 Q_ASSERT(isPower());
258
259 return m_powerUi->replacement->currentText();
260}
261
262void KateSearchBar::setSearchMode(KateSearchBar::SearchMode mode)
263{
264 Q_ASSERT(isPower());
265
266 m_powerUi->searchMode->setCurrentIndex(mode);
267}
268
269void KateSearchBar::findNext()
270{
271 const bool found = find();
272
273 if (found) {
274 QComboBox *combo = m_powerUi != nullptr ? m_powerUi->pattern : m_incUi->pattern;
275
276 // Add to search history
277 addCurrentTextToHistory(combo);
278 }
279}
280
281void KateSearchBar::findPrevious()
282{
283 const bool found = find(searchDirection: SearchBackward);
284
285 if (found) {
286 QComboBox *combo = m_powerUi != nullptr ? m_powerUi->pattern : m_incUi->pattern;
287
288 // Add to search history
289 addCurrentTextToHistory(combo);
290 }
291}
292
293void KateSearchBar::showResultMessage()
294{
295 QString text;
296
297 if (m_replaceMode) {
298 text = i18ncp("short translation", "1 replacement made", "%1 replacements made", m_matchCounter);
299 } else {
300 text = i18ncp("short translation", "1 match found", "%1 matches found", m_matchCounter);
301 }
302
303 if (m_infoMessage) {
304 m_infoMessage->setText(text);
305 } else {
306 m_infoMessage = new KTextEditor::Message(text, KTextEditor::Message::Positive);
307 m_infoMessage->setPosition(KTextEditor::Message::BottomInView);
308 m_infoMessage->setAutoHide(3000); // 3 seconds
309 m_infoMessage->setView(m_view);
310 m_view->doc()->postMessage(message: m_infoMessage);
311 }
312}
313
314void KateSearchBar::highlightMatch(Range range)
315{
316 KTextEditor::MovingRange *const highlight = m_view->doc()->newMovingRange(range, insertBehaviors: Kate::TextRange::DoNotExpand);
317 highlight->setView(m_view); // show only in this view
318 highlight->setAttributeOnlyForViews(true);
319 // use z depth defined in moving ranges interface
320 highlight->setZDepth(-10000.0);
321 highlight->setAttribute(highlightMatchAttribute);
322 m_hlRanges.append(t: highlight);
323}
324
325void KateSearchBar::highlightReplacement(Range range)
326{
327 KTextEditor::MovingRange *const highlight = m_view->doc()->newMovingRange(range, insertBehaviors: Kate::TextRange::DoNotExpand);
328 highlight->setView(m_view); // show only in this view
329 highlight->setAttributeOnlyForViews(true);
330 // use z depth defined in moving ranges interface
331 highlight->setZDepth(-10000.0);
332 highlight->setAttribute(highlightReplacementAttribute);
333 m_hlRanges.append(t: highlight);
334}
335
336void KateSearchBar::indicateMatch(MatchResult matchResult)
337{
338 QLineEdit *const lineEdit = isPower() ? m_powerUi->pattern->lineEdit() : m_incUi->pattern->lineEdit();
339 QPalette background(lineEdit->palette());
340
341 switch (matchResult) {
342 case MatchFound: // FALLTHROUGH
343 case MatchWrappedForward:
344 case MatchWrappedBackward:
345 // Green background for line edit
346 KColorScheme::adjustBackground(background, newRole: KColorScheme::PositiveBackground);
347 break;
348 case MatchMismatch:
349 // Red background for line edit
350 KColorScheme::adjustBackground(background, newRole: KColorScheme::NegativeBackground);
351 break;
352 case MatchNothing:
353 // Reset background of line edit
354 background = QPalette();
355 break;
356 case MatchNeutral:
357 KColorScheme::adjustBackground(background, newRole: KColorScheme::NeutralBackground);
358 break;
359 }
360
361 // Update status label
362 if (m_incUi != nullptr) {
363 QPalette foreground(m_incUi->status->palette());
364 switch (matchResult) {
365 case MatchFound: // FALLTHROUGH
366 case MatchNothing:
367 KColorScheme::adjustForeground(foreground, newRole: KColorScheme::NormalText, color: QPalette::WindowText, set: KColorScheme::Window);
368 m_incUi->status->clear();
369 break;
370 case MatchWrappedForward:
371 case MatchWrappedBackward:
372 KColorScheme::adjustForeground(foreground, newRole: KColorScheme::NormalText, color: QPalette::WindowText, set: KColorScheme::Window);
373 if (matchResult == MatchWrappedBackward) {
374 m_incUi->status->setText(i18n("Reached top, continued from bottom"));
375 } else {
376 m_incUi->status->setText(i18n("Reached bottom, continued from top"));
377 }
378 break;
379 case MatchMismatch:
380 KColorScheme::adjustForeground(foreground, newRole: KColorScheme::NegativeText, color: QPalette::WindowText, set: KColorScheme::Window);
381 m_incUi->status->setText(i18n("Not found"));
382 break;
383 case MatchNeutral:
384 /* do nothing */
385 break;
386 }
387 m_incUi->status->setPalette(foreground);
388 }
389
390 lineEdit->setPalette(background);
391}
392
393/*static*/ void KateSearchBar::selectRange(KTextEditor::ViewPrivate *view, KTextEditor::Range range)
394{
395 view->setCursorPositionInternal(position: range.end());
396 view->setSelection(range);
397}
398
399void KateSearchBar::selectRange2(KTextEditor::Range range)
400{
401 disconnect(sender: m_view, signal: &KTextEditor::View::selectionChanged, receiver: this, slot: &KateSearchBar::updateSelectionOnly);
402 selectRange(view: m_view, range);
403 connect(sender: m_view, signal: &KTextEditor::View::selectionChanged, context: this, slot: &KateSearchBar::updateSelectionOnly);
404}
405
406void KateSearchBar::onIncPatternChanged(const QString &pattern)
407{
408 if (!m_incUi) {
409 return;
410 }
411
412 // clear prior highlightings (deletes info message if present)
413 clearHighlights();
414
415 m_incUi->next->setDisabled(pattern.isEmpty());
416 m_incUi->prev->setDisabled(pattern.isEmpty());
417
418 KateMatch match(m_view->doc(), searchOptions());
419
420 if (!pattern.isEmpty()) {
421 // Find, first try
422 const Range inputRange = KTextEditor::Range(m_incInitCursor, m_view->document()->documentEnd());
423 match.searchText(range: inputRange, pattern);
424 }
425
426 const bool wrap = !match.isValid() && !pattern.isEmpty();
427
428 if (wrap) {
429 // Find, second try
430 const KTextEditor::Range inputRange = m_view->document()->documentRange();
431 match.searchText(range: inputRange, pattern);
432 }
433
434 const MatchResult matchResult = match.isValid() ? (wrap ? MatchWrappedForward : MatchFound) : pattern.isEmpty() ? MatchNothing : MatchMismatch;
435
436 const Range selectionRange = pattern.isEmpty() ? Range(m_incInitCursor, m_incInitCursor) : match.isValid() ? match.range() : Range::invalid();
437
438 // don't update m_incInitCursor when we move the cursor
439 disconnect(sender: m_view, signal: &KTextEditor::View::cursorPositionChanged, receiver: this, slot: &KateSearchBar::updateIncInitCursor);
440 selectRange2(range: selectionRange);
441 connect(sender: m_view, signal: &KTextEditor::View::cursorPositionChanged, context: this, slot: &KateSearchBar::updateIncInitCursor);
442
443 indicateMatch(matchResult);
444}
445
446void KateSearchBar::setMatchCase(bool matchCase)
447{
448 if (this->matchCase() == matchCase) {
449 return;
450 }
451
452 if (isPower()) {
453 m_powerUi->matchCase->setChecked(matchCase);
454 } else {
455 m_incUi->matchCase->setChecked(matchCase);
456 }
457}
458
459void KateSearchBar::onMatchCaseToggled(bool /*matchCase*/)
460{
461 sendConfig();
462
463 if (m_incUi != nullptr) {
464 // Re-search with new settings
465 const QString pattern = m_incUi->pattern->currentText();
466 onIncPatternChanged(pattern);
467 } else {
468 indicateMatch(matchResult: MatchNothing);
469 }
470}
471
472bool KateSearchBar::matchCase() const
473{
474 return isPower() ? m_powerUi->matchCase->isChecked() : m_incUi->matchCase->isChecked();
475}
476
477void KateSearchBar::onReturnPressed()
478{
479 const Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers();
480 const bool shiftDown = (modifiers & Qt::ShiftModifier) != 0;
481 const bool controlDown = (modifiers & Qt::ControlModifier) != 0;
482
483 if (shiftDown) {
484 // Shift down, search backwards
485 findPrevious();
486 } else {
487 // Shift up, search forwards
488 findNext();
489 }
490
491 if (controlDown) {
492 Q_EMIT hideMe();
493 }
494}
495
496bool KateSearchBar::findOrReplace(SearchDirection searchDirection, const QString *replacement)
497{
498 // What to find?
499 if (searchPattern().isEmpty()) {
500 return false; // == Pattern error
501 }
502
503 // don't let selectionChanged signal mess around in this routine
504 disconnect(sender: m_view, signal: &KTextEditor::View::selectionChanged, receiver: this, slot: &KateSearchBar::updateSelectionOnly);
505
506 // clear previous highlights if there are any
507 clearHighlights();
508
509 const SearchOptions enabledOptions = searchOptions(searchDirection);
510
511 // Where to find?
512 Range inputRange;
513 const Range selection = m_view->selection() ? m_view->selectionRange() : Range::invalid();
514 if (selection.isValid()) {
515 if (selectionOnly()) {
516 if (m_workingRange == nullptr) {
517 m_workingRange =
518 m_view->doc()->newMovingRange(range: KTextEditor::Range::invalid(), insertBehaviors: KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
519 }
520 if (!m_workingRange->toRange().isValid()) {
521 // First match in selection
522 inputRange = selection;
523 // Remember selection for succeeding selection-only searches
524 // Elsewhere, make sure m_workingRange is invalidated when selection/search range changes
525 m_workingRange->setRange(selection);
526 } else {
527 // The selection wasn't changed/updated by user, so we use the previous selection
528 // We use the selection's start/end so that the search can move forward/backward
529 if (searchDirection == SearchBackward) {
530 inputRange.setRange(start: m_workingRange->start(), end: selection.end());
531 } else {
532 inputRange.setRange(start: selection.start(), end: m_workingRange->end());
533 }
534 }
535 } else {
536 // Next match after/before selection if a match was selected before
537 if (searchDirection == SearchForward) {
538 inputRange.setRange(start: selection.start(), end: m_view->document()->documentEnd());
539 } else {
540 inputRange.setRange(start: Cursor(0, 0), end: selection.end());
541 }
542
543 // Discard selection/search range previously remembered
544 if (m_workingRange) {
545 delete m_workingRange;
546 m_workingRange = nullptr;
547 }
548 }
549 } else {
550 // No selection
551 setSelectionOnly(false);
552 const Cursor cursorPos = m_view->cursorPosition();
553 if (searchDirection == SearchForward) {
554 inputRange.setRange(start: cursorPos, end: m_view->document()->documentEnd());
555 } else {
556 inputRange.setRange(start: Cursor(0, 0), end: cursorPos);
557 }
558 }
559 FAST_DEBUG("Search range is" << inputRange);
560
561 KateMatch match(m_view->doc(), enabledOptions);
562 Range afterReplace = Range::invalid();
563
564 // Find, first try
565 match.searchText(range: inputRange, pattern: searchPattern());
566 if (match.isValid()) {
567 if (match.range() == selection) {
568 // Same match again
569 if (replacement != nullptr) {
570 // Selection is match -> replace
571 KTextEditor::MovingRange *smartInputRange =
572 m_view->doc()->newMovingRange(range: inputRange, insertBehaviors: KTextEditor::MovingRange::ExpandLeft | KTextEditor::MovingRange::ExpandRight);
573 afterReplace = match.replace(replacement: *replacement, blockMode: m_view->blockSelection());
574 inputRange = *smartInputRange;
575 delete smartInputRange;
576 }
577
578 // Find, second try after old selection
579 if (searchDirection == SearchForward) {
580 const Cursor start = (replacement != nullptr) ? afterReplace.end() : selection.end();
581 inputRange.setRange(start, end: inputRange.end());
582 } else {
583 const Cursor end = (replacement != nullptr) ? afterReplace.start() : selection.start();
584 inputRange.setRange(start: inputRange.start(), end);
585 }
586
587 match.searchText(range: inputRange, pattern: searchPattern());
588
589 } else if (match.isEmpty() && match.range().end() == m_view->cursorPosition()) {
590 // valid zero-length match, e.g.: '^', '$', '\b'
591 // advance the range to avoid looping
592 KTextEditor::DocumentCursor zeroLenMatch(m_view->doc(), match.range().end());
593
594 if (searchDirection == SearchForward) {
595 zeroLenMatch.move(chars: 1);
596 inputRange.setRange(start: zeroLenMatch.toCursor(), end: inputRange.end());
597 } else { // SearchBackward
598 zeroLenMatch.move(chars: -1);
599 inputRange.setRange(start: inputRange.start(), end: zeroLenMatch.toCursor());
600 }
601
602 match.searchText(range: inputRange, pattern: searchPattern());
603 }
604 }
605
606 bool askWrap = !match.isValid() && (!afterReplace.isValid() || !selectionOnly());
607 bool wrap = false;
608 if (askWrap) {
609 askWrap = false;
610 wrap = true;
611 }
612
613 if (askWrap) {
614 QString question =
615 searchDirection == SearchForward ? i18n("Bottom of file reached. Continue from top?") : i18n("Top of file reached. Continue from bottom?");
616 wrap = (KMessageBox::questionTwoActions(parent: nullptr,
617 text: question,
618 i18n("Continue search?"),
619 primaryAction: KStandardGuiItem::cont(),
620 secondaryAction: KStandardGuiItem::cancel(),
621 QStringLiteral("DoNotShowAgainContinueSearchDialog"))
622 == KMessageBox::PrimaryAction);
623 }
624 if (wrap) {
625 m_view->showSearchWrappedHint(isReverseSearch: searchDirection == SearchBackward);
626 if (selectionOnly() && m_workingRange != nullptr && m_workingRange->toRange().isValid()) {
627 inputRange = m_workingRange->toRange();
628 } else {
629 inputRange = m_view->document()->documentRange();
630 }
631 match.searchText(range: inputRange, pattern: searchPattern());
632 }
633
634 if (match.isValid()) {
635 selectRange2(range: match.range());
636 }
637
638 const MatchResult matchResult = !match.isValid() ? MatchMismatch
639 : !wrap ? MatchFound
640 : searchDirection == SearchForward ? MatchWrappedForward
641 : MatchWrappedBackward;
642 indicateMatch(matchResult);
643
644 // highlight replacements if applicable
645 if (afterReplace.isValid()) {
646 highlightReplacement(range: afterReplace);
647 }
648
649 // restore connection
650 connect(sender: m_view, signal: &KTextEditor::View::selectionChanged, context: this, slot: &KateSearchBar::updateSelectionOnly);
651
652 return true; // == No pattern error
653}
654
655void KateSearchBar::findAll()
656{
657 // clear highlightings of prior search&replace action
658 clearHighlights();
659
660 Range inputRange = (m_view->selection() && selectionOnly()) ? m_view->selectionRange() : m_view->document()->documentRange();
661
662 beginFindAll(inputRange);
663}
664
665void KateSearchBar::onPowerPatternChanged(const QString & /*pattern*/)
666{
667 givePatternFeedback();
668 indicateMatch(matchResult: MatchNothing);
669}
670
671bool KateSearchBar::isPatternValid() const
672{
673 if (searchPattern().isEmpty()) {
674 return false;
675 }
676
677 return searchOptions().testFlag(flag: WholeWords) ? searchPattern().trimmed() == searchPattern()
678 : searchOptions().testFlag(flag: Regex) ? QRegularExpression(searchPattern(), QRegularExpression::UseUnicodePropertiesOption).isValid()
679 : true;
680}
681
682void KateSearchBar::givePatternFeedback()
683{
684 // Enable/disable next/prev and replace next/all
685 m_powerUi->findNext->setEnabled(isPatternValid());
686 m_powerUi->findPrev->setEnabled(isPatternValid());
687 m_powerUi->replaceNext->setEnabled(isPatternValid());
688 m_powerUi->replaceAll->setEnabled(isPatternValid());
689 m_powerUi->findAll->setEnabled(isPatternValid());
690}
691
692void KateSearchBar::addCurrentTextToHistory(QComboBox *combo)
693{
694 const QString text = combo->currentText();
695 const int index = combo->findText(text);
696
697 if (index > 0) {
698 combo->removeItem(index);
699 }
700 if (index != 0) {
701 combo->insertItem(aindex: 0, atext: text);
702 combo->setCurrentIndex(0);
703 }
704
705 // sync to application config
706 KTextEditor::EditorPrivate::self()->saveSearchReplaceHistoryModels();
707}
708
709void KateSearchBar::backupConfig(bool ofPower)
710{
711 if (ofPower) {
712 m_powerMatchCase = m_powerUi->matchCase->isChecked();
713 m_powerMode = m_powerUi->searchMode->currentIndex();
714 } else {
715 m_incMatchCase = m_incUi->matchCase->isChecked();
716 }
717}
718
719void KateSearchBar::sendConfig()
720{
721 const auto pastFlags = m_config->searchFlags();
722 auto futureFlags = pastFlags;
723
724 if (m_powerUi != nullptr) {
725 const bool OF_POWER = true;
726 backupConfig(ofPower: OF_POWER);
727
728 // Update power search flags only
729 const auto incFlagsOnly = pastFlags & (KateViewConfig::IncHighlightAll | KateViewConfig::IncFromCursor | KateViewConfig::IncMatchCase);
730
731 futureFlags = incFlagsOnly | (m_powerMatchCase ? KateViewConfig::PowerMatchCase : 0) | (m_powerFromCursor ? KateViewConfig::PowerFromCursor : 0)
732 | (m_powerHighlightAll ? KateViewConfig::PowerHighlightAll : 0)
733 | ((m_powerMode == MODE_REGEX)
734 ? KateViewConfig::PowerModeRegularExpression
735 : ((m_powerMode == MODE_ESCAPE_SEQUENCES)
736 ? KateViewConfig::PowerModeEscapeSequences
737 : ((m_powerMode == MODE_WHOLE_WORDS) ? KateViewConfig::PowerModeWholeWords : KateViewConfig::PowerModePlainText)));
738
739 } else if (m_incUi != nullptr) {
740 const bool OF_INCREMENTAL = false;
741 backupConfig(ofPower: OF_INCREMENTAL);
742
743 // Update incremental search flags only
744 const auto powerFlagsOnly = pastFlags
745 & (KateViewConfig::PowerMatchCase | KateViewConfig::PowerFromCursor | KateViewConfig::PowerHighlightAll | KateViewConfig::PowerModeRegularExpression
746 | KateViewConfig::PowerModeEscapeSequences | KateViewConfig::PowerModeWholeWords | KateViewConfig::PowerModePlainText);
747
748 futureFlags = powerFlagsOnly | (m_incHighlightAll ? KateViewConfig::IncHighlightAll : 0) | (m_incFromCursor ? KateViewConfig::IncFromCursor : 0)
749 | (m_incMatchCase ? KateViewConfig::IncMatchCase : 0);
750 }
751
752 // Adjust global config
753 m_config->setSearchFlags(futureFlags);
754}
755
756void KateSearchBar::replaceNext()
757{
758 const QString replacement = m_powerUi->replacement->currentText();
759
760 if (findOrReplace(searchDirection: SearchForward, replacement: &replacement)) {
761 // Never merge replace actions with other replace actions/user actions
762 m_view->doc()->undoManager()->undoSafePoint();
763
764 // Add to search history
765 addCurrentTextToHistory(combo: m_powerUi->pattern);
766
767 // Add to replace history
768 addCurrentTextToHistory(combo: m_powerUi->replacement);
769 }
770}
771
772// replacement == NULL --> Only highlight all matches
773// replacement != NULL --> Replace and highlight all matches
774void KateSearchBar::beginFindOrReplaceAll(Range inputRange, const QString &replacement, bool replaceMode /* = true*/)
775{
776 // don't let selectionChanged signal mess around in this routine
777 disconnect(sender: m_view, signal: &KTextEditor::View::selectionChanged, receiver: this, slot: &KateSearchBar::updateSelectionOnly);
778 // Cancel job when user close the document to avoid crash
779 connect(sender: m_view->doc(), signal: &KTextEditor::Document::aboutToClose, context: this, slot: &KateSearchBar::endFindOrReplaceAll);
780
781 if (m_powerUi) {
782 // Offer Cancel button and disable not useful buttons
783 m_powerUi->searchCancelStacked->setCurrentIndex(m_powerUi->searchCancelStacked->indexOf(m_powerUi->cancelPage));
784 m_powerUi->findNext->setEnabled(false);
785 m_powerUi->findPrev->setEnabled(false);
786 m_powerUi->replaceNext->setEnabled(false);
787 }
788
789 m_highlightRanges.clear();
790 m_inputRange = inputRange;
791 m_workingRange = m_view->doc()->newMovingRange(range: m_inputRange);
792 m_replacement = replacement;
793 m_replaceMode = replaceMode;
794 m_matchCounter = 0;
795 m_cancelFindOrReplace = false; // Ensure we have a GO!
796
797 findOrReplaceAll();
798}
799
800void KateSearchBar::findOrReplaceAll()
801{
802 const SearchOptions enabledOptions = searchOptions(searchDirection: SearchForward);
803
804 // we highlight all ranges of a replace, up to some hard limit
805 // e.g. if you replace 100000 things, rendering will break down otherwise ;=)
806 const int maxHighlightings = 65536;
807
808 // reuse match object to avoid massive moving range creation
809 KateMatch match(m_view->doc(), enabledOptions);
810
811 // Ignore block mode if selectionOnly option is disabled (see bug 456367)
812 bool block = m_view->selection() && m_view->blockSelection() && selectionOnly();
813
814 int line = m_inputRange.start().line();
815
816 bool timeOut = false;
817 bool done = false;
818
819 // This variable holds the number of lines that we have searched
820 // When it reaches 50K, we break the loop to allow event processing
821 int numLinesSearched = 0;
822 // Use a simple range in the loop to avoid needless work
823 KTextEditor::Range workingRangeCopy = m_workingRange->toRange();
824 do {
825 if (block) {
826 delete m_workingRange; // Never forget that!
827 m_workingRange = m_view->doc()->newMovingRange(range: m_view->doc()->rangeOnLine(range: m_inputRange, line));
828 workingRangeCopy = m_workingRange->toRange();
829 }
830
831 do {
832 int currentSearchLine = workingRangeCopy.start().line();
833 match.searchText(range: workingRangeCopy, pattern: searchPattern());
834 if (!match.isValid()) {
835 done = true;
836 break;
837 }
838 bool const originalMatchEmpty = match.isEmpty();
839
840 // Work with the match
841 Range lastRange;
842 if (m_replaceMode) {
843 if (m_matchCounter == 0) {
844 static_cast<KTextEditor::DocumentPrivate *>(m_view->document())->editStart();
845 }
846
847 // Replace
848 lastRange = match.replace(replacement: m_replacement, blockMode: false, replacementCounter: ++m_matchCounter);
849 // update working range as text must have changed now
850 workingRangeCopy = m_workingRange->toRange();
851 } else {
852 lastRange = match.range();
853 ++m_matchCounter;
854 }
855
856 // remember ranges if limit not reached
857 if (m_matchCounter < maxHighlightings) {
858 m_highlightRanges.push_back(x: lastRange);
859 } else {
860 m_highlightRanges.clear();
861 // TODO Info user that highlighting is disabled
862 }
863
864 // Continue after match
865 if (lastRange.end() >= workingRangeCopy.end()) {
866 done = true;
867 break;
868 }
869
870 KTextEditor::DocumentCursor workingStart(m_view->doc(), lastRange.end());
871
872 if (originalMatchEmpty) {
873 // Can happen for regex patterns with zero-length matches, e.g. ^, $, \b
874 // If we don't advance here we will loop forever...
875 workingStart.move(chars: 1);
876 }
877 workingRangeCopy.setRange(start: workingStart.toCursor(), end: workingRangeCopy.end());
878
879 // Are we done?
880 if (!workingRangeCopy.isValid() || workingStart.atEndOfDocument()) {
881 done = true;
882 break;
883 }
884
885 // Check if we have searched through 50K lines and time out.
886 // We do this to allow the search operation to be cancelled
887 numLinesSearched += workingRangeCopy.start().line() - currentSearchLine;
888 timeOut = numLinesSearched >= 50000;
889
890 } while (!m_cancelFindOrReplace && !timeOut);
891
892 } while (!m_cancelFindOrReplace && !timeOut && block && ++line <= m_inputRange.end().line());
893
894 // update m_workingRange
895 m_workingRange->setRange(workingRangeCopy);
896
897 if (done || m_cancelFindOrReplace) {
898 Q_EMIT findOrReplaceAllFinished();
899 } else if (timeOut) {
900 QTimer::singleShot(interval: 0, receiver: this, slot: &KateSearchBar::findOrReplaceAll);
901 }
902
903 showResultMessage();
904}
905
906void KateSearchBar::endFindOrReplaceAll()
907{
908 // Don't forget to remove our "crash protector"
909 disconnect(sender: m_view->doc(), signal: &KTextEditor::Document::aboutToClose, receiver: this, slot: &KateSearchBar::endFindOrReplaceAll);
910
911 // After last match
912 if (m_matchCounter > 0) {
913 if (m_replaceMode) {
914 static_cast<KTextEditor::DocumentPrivate *>(m_view->document())->editEnd();
915 }
916 }
917
918 // Add ScrollBarMarks
919 if (!m_highlightRanges.empty()) {
920 m_view->document()->setMarkDescription(mark: KTextEditor::Document::SearchMatch, i18n("SearchHighLight"));
921 m_view->document()->setMarkIcon(markType: KTextEditor::Document::SearchMatch, icon: QIcon());
922 for (const Range &r : m_highlightRanges) {
923 m_view->document()->addMark(line: r.start().line(), markType: KTextEditor::Document::SearchMatch);
924 }
925 }
926
927 // Add highlights
928 if (m_replaceMode) {
929 for (const Range &r : std::as_const(t&: m_highlightRanges)) {
930 highlightReplacement(range: r);
931 }
932 // Never merge replace actions with other replace actions/user actions
933 m_view->doc()->undoManager()->undoSafePoint();
934
935 } else {
936 for (const Range &r : std::as_const(t&: m_highlightRanges)) {
937 highlightMatch(range: r);
938 }
939 // indicateMatch(m_matchCounter > 0 ? MatchFound : MatchMismatch); TODO
940 }
941
942 // Clean-Up the still hold MovingRange
943 delete m_workingRange;
944 m_workingRange = nullptr; // m_workingRange is also used elsewhere so we signify that it is now "unused"
945
946 // restore connection
947 connect(sender: m_view, signal: &KTextEditor::View::selectionChanged, context: this, slot: &KateSearchBar::updateSelectionOnly);
948
949 if (m_powerUi) {
950 // Offer Find and Replace buttons and enable again useful buttons
951 m_powerUi->searchCancelStacked->setCurrentIndex(m_powerUi->searchCancelStacked->indexOf(m_powerUi->searchPage));
952 m_powerUi->findNext->setEnabled(true);
953 m_powerUi->findPrev->setEnabled(true);
954 m_powerUi->replaceNext->setEnabled(true);
955
956 // Add to search history
957 addCurrentTextToHistory(combo: m_powerUi->pattern);
958
959 // Add to replace history
960 addCurrentTextToHistory(combo: m_powerUi->replacement);
961 }
962
963 m_cancelFindOrReplace = true; // Indicate we are not running
964}
965
966void KateSearchBar::replaceAll()
967{
968 // clear prior highlightings (deletes info message if present)
969 clearHighlights();
970
971 // What to find/replace?
972 const QString replacement = m_powerUi->replacement->currentText();
973
974 // Where to replace?
975 const bool selected = m_view->selection();
976 Range inputRange = (selected && selectionOnly()) ? m_view->selectionRange() : m_view->document()->documentRange();
977
978 beginFindOrReplaceAll(inputRange, replacement);
979}
980
981void KateSearchBar::setSearchPattern(const QString &searchPattern)
982{
983 if (searchPattern == this->searchPattern()) {
984 return;
985 }
986
987 if (isPower()) {
988 m_powerUi->pattern->setEditText(searchPattern);
989 } else {
990 m_incUi->pattern->setEditText(searchPattern);
991 }
992}
993
994QString KateSearchBar::searchPattern() const
995{
996 return (m_powerUi != nullptr) ? m_powerUi->pattern->currentText() : m_incUi->pattern->currentText();
997}
998
999void KateSearchBar::setSelectionOnly(bool selectionOnly)
1000{
1001 if (this->selectionOnly() == selectionOnly) {
1002 return;
1003 }
1004
1005 if (isPower()) {
1006 m_powerUi->selectionOnly->setChecked(selectionOnly);
1007 }
1008}
1009
1010bool KateSearchBar::selectionOnly() const
1011{
1012 return isPower() ? m_powerUi->selectionOnly->isChecked() : false;
1013}
1014
1015KTextEditor::SearchOptions KateSearchBar::searchOptions(SearchDirection searchDirection) const
1016{
1017 SearchOptions enabledOptions = KTextEditor::Default;
1018
1019 if (!matchCase()) {
1020 enabledOptions |= CaseInsensitive;
1021 }
1022
1023 if (searchDirection == SearchBackward) {
1024 enabledOptions |= Backwards;
1025 }
1026
1027 if (m_powerUi != nullptr) {
1028 switch (m_powerUi->searchMode->currentIndex()) {
1029 case MODE_WHOLE_WORDS:
1030 enabledOptions |= WholeWords;
1031 break;
1032
1033 case MODE_ESCAPE_SEQUENCES:
1034 enabledOptions |= EscapeSequences;
1035 break;
1036
1037 case MODE_REGEX:
1038 enabledOptions |= Regex;
1039 break;
1040
1041 case MODE_PLAIN_TEXT: // FALLTHROUGH
1042 default:
1043 break;
1044 }
1045 }
1046
1047 return enabledOptions;
1048}
1049
1050struct ParInfo {
1051 int openIndex;
1052 bool capturing;
1053 int captureNumber; // 1..9
1054};
1055
1056QList<QString> KateSearchBar::getCapturePatterns(const QString &pattern) const
1057{
1058 QList<QString> capturePatterns;
1059 capturePatterns.reserve(asize: 9);
1060 QStack<ParInfo> parInfos;
1061
1062 const int inputLen = pattern.length();
1063 int input = 0; // walker index
1064 bool insideClass = false;
1065 int captureCount = 0;
1066
1067 while (input < inputLen) {
1068 if (insideClass) {
1069 // Wait for closing, unescaped ']'
1070 if (pattern[input].unicode() == L']') {
1071 insideClass = false;
1072 }
1073 input++;
1074 } else {
1075 switch (pattern[input].unicode()) {
1076 case L'\\':
1077 // Skip this and any next character
1078 input += 2;
1079 break;
1080
1081 case L'(':
1082 ParInfo curInfo;
1083 curInfo.openIndex = input;
1084 curInfo.capturing = (input + 1 >= inputLen) || (pattern[input + 1].unicode() != '?');
1085 if (curInfo.capturing) {
1086 captureCount++;
1087 }
1088 curInfo.captureNumber = captureCount;
1089 parInfos.push(t: curInfo);
1090
1091 input++;
1092 break;
1093
1094 case L')':
1095 if (!parInfos.empty()) {
1096 ParInfo &top = parInfos.top();
1097 if (top.capturing && (top.captureNumber <= 9)) {
1098 const int start = top.openIndex + 1;
1099 const int len = input - start;
1100 if (capturePatterns.size() < top.captureNumber) {
1101 capturePatterns.resize(size: top.captureNumber);
1102 }
1103 capturePatterns[top.captureNumber - 1] = pattern.mid(position: start, n: len);
1104 }
1105 parInfos.pop();
1106 }
1107
1108 input++;
1109 break;
1110
1111 case L'[':
1112 input++;
1113 insideClass = true;
1114 break;
1115
1116 default:
1117 input++;
1118 break;
1119 }
1120 }
1121 }
1122
1123 return capturePatterns;
1124}
1125
1126void KateSearchBar::showExtendedContextMenu(bool forPattern, const QPoint &pos)
1127{
1128 // Make original menu
1129 QComboBox *comboBox = forPattern ? m_powerUi->pattern : m_powerUi->replacement;
1130 QMenu *const contextMenu = comboBox->lineEdit()->createStandardContextMenu();
1131
1132 if (contextMenu == nullptr) {
1133 return;
1134 }
1135
1136 bool extendMenu = false;
1137 bool regexMode = false;
1138 switch (m_powerUi->searchMode->currentIndex()) {
1139 case MODE_REGEX:
1140 regexMode = true;
1141 // FALLTHROUGH
1142
1143 case MODE_ESCAPE_SEQUENCES:
1144 extendMenu = true;
1145 break;
1146
1147 default:
1148 break;
1149 }
1150
1151 AddMenuManager addMenuManager(contextMenu, 37);
1152 if (!extendMenu) {
1153 addMenuManager.enableMenu(enabled: extendMenu);
1154 } else {
1155 // Build menu
1156 if (forPattern) {
1157 if (regexMode) {
1158 addMenuManager.addEntry(QStringLiteral("^"), after: QString(), i18n("Beginning of line"));
1159 addMenuManager.addEntry(QStringLiteral("$"), after: QString(), i18n("End of line"));
1160 addMenuManager.addSeparator();
1161 addMenuManager.addEntry(QStringLiteral("."), after: QString(), i18n("Match any character excluding new line (by default)"));
1162 addMenuManager.addEntry(QStringLiteral("+"), after: QString(), i18n("One or more occurrences"));
1163 addMenuManager.addEntry(QStringLiteral("*"), after: QString(), i18n("Zero or more occurrences"));
1164 addMenuManager.addEntry(QStringLiteral("?"), after: QString(), i18n("Zero or one occurrences"));
1165 addMenuManager.addEntry(QStringLiteral("{a"),
1166 QStringLiteral(",b}"),
1167 i18n("<a> through <b> occurrences"),
1168 QStringLiteral("{"),
1169 QStringLiteral(",}"));
1170
1171 addMenuManager.addSeparator();
1172 addMenuManager.addSeparator();
1173 addMenuManager.addEntry(QStringLiteral("("), QStringLiteral(")"), i18n("Group, capturing"));
1174 addMenuManager.addEntry(QStringLiteral("|"), after: QString(), i18n("Or"));
1175 addMenuManager.addEntry(QStringLiteral("["), QStringLiteral("]"), i18n("Set of characters"));
1176 addMenuManager.addEntry(QStringLiteral("[^"), QStringLiteral("]"), i18n("Negative set of characters"));
1177 addMenuManager.addSeparator();
1178 }
1179 } else {
1180 addMenuManager.addEntry(QStringLiteral("\\0"), after: QString(), i18n("Whole match reference"));
1181 addMenuManager.addSeparator();
1182 if (regexMode) {
1183 const QString pattern = m_powerUi->pattern->currentText();
1184 const QList<QString> capturePatterns = getCapturePatterns(pattern);
1185
1186 const int captureCount = capturePatterns.count();
1187 for (int i = 1; i <= 9; i++) {
1188 const QString number = QString::number(i);
1189 const QString &captureDetails =
1190 (i <= captureCount) ? QLatin1String(" = (") + QStringView(capturePatterns[i - 1]).left(n: 30) + QLatin1Char(')') : QString();
1191 addMenuManager.addEntry(before: QLatin1String("\\") + number, after: QString(), i18n("Reference") + QLatin1Char(' ') + number + captureDetails);
1192 }
1193
1194 addMenuManager.addSeparator();
1195 }
1196 }
1197
1198 addMenuManager.addEntry(QStringLiteral("\\n"), after: QString(), i18n("Line break"));
1199 addMenuManager.addEntry(QStringLiteral("\\t"), after: QString(), i18n("Tab"));
1200
1201 if (forPattern && regexMode) {
1202 addMenuManager.addEntry(QStringLiteral("\\b"), after: QString(), i18n("Word boundary"));
1203 addMenuManager.addEntry(QStringLiteral("\\B"), after: QString(), i18n("Not word boundary"));
1204 addMenuManager.addEntry(QStringLiteral("\\d"), after: QString(), i18n("Digit"));
1205 addMenuManager.addEntry(QStringLiteral("\\D"), after: QString(), i18n("Non-digit"));
1206 addMenuManager.addEntry(QStringLiteral("\\s"), after: QString(), i18n("Whitespace (excluding line breaks)"));
1207 addMenuManager.addEntry(QStringLiteral("\\S"), after: QString(), i18n("Non-whitespace"));
1208 addMenuManager.addEntry(QStringLiteral("\\w"), after: QString(), i18n("Word character (alphanumerics plus '_')"));
1209 addMenuManager.addEntry(QStringLiteral("\\W"), after: QString(), i18n("Non-word character"));
1210 }
1211
1212 addMenuManager.addEntry(QStringLiteral("\\0???"), after: QString(), i18n("Octal character 000 to 377 (2^8-1)"), QStringLiteral("\\0"));
1213 addMenuManager.addEntry(QStringLiteral("\\x{????}"), after: QString(), i18n("Hex character 0000 to FFFF (2^16-1)"), QStringLiteral("\\x{....}"));
1214 addMenuManager.addEntry(QStringLiteral("\\\\"), after: QString(), i18n("Backslash"));
1215
1216 if (forPattern && regexMode) {
1217 addMenuManager.addSeparator();
1218 addMenuManager.addEntry(QStringLiteral("(?:E"), QStringLiteral(")"), i18n("Group, non-capturing"), QStringLiteral("(?:"));
1219 addMenuManager.addEntry(QStringLiteral("(?=E"), QStringLiteral(")"), i18n("Positive Lookahead"), QStringLiteral("(?="));
1220 addMenuManager.addEntry(QStringLiteral("(?!E"), QStringLiteral(")"), i18n("Negative lookahead"), QStringLiteral("(?!"));
1221 // variable length positive/negative lookbehind is an experimental feature in Perl 5.30
1222 // see: https://perldoc.perl.org/perlre.html
1223 // currently QRegularExpression only supports fixed-length positive/negative lookbehind (2020-03-01)
1224 addMenuManager.addEntry(QStringLiteral("(?<=E"), QStringLiteral(")"), i18n("Fixed-length positive lookbehind"), QStringLiteral("(?<="));
1225 addMenuManager.addEntry(QStringLiteral("(?<!E"), QStringLiteral(")"), i18n("Fixed-length negative lookbehind"), QStringLiteral("(?<!"));
1226 }
1227
1228 if (!forPattern) {
1229 addMenuManager.addSeparator();
1230 addMenuManager.addEntry(QStringLiteral("\\L"), after: QString(), i18n("Begin lowercase conversion"));
1231 addMenuManager.addEntry(QStringLiteral("\\U"), after: QString(), i18n("Begin uppercase conversion"));
1232 addMenuManager.addEntry(QStringLiteral("\\E"), after: QString(), i18n("End case conversion"));
1233 addMenuManager.addEntry(QStringLiteral("\\l"), after: QString(), i18n("Lowercase first character conversion"));
1234 addMenuManager.addEntry(QStringLiteral("\\u"), after: QString(), i18n("Uppercase first character conversion"));
1235 addMenuManager.addEntry(QStringLiteral("\\#[#..]"), after: QString(), i18n("Replacement counter (for Replace All)"), QStringLiteral("\\#"));
1236 }
1237 }
1238
1239 // Show menu
1240 QAction *const result = contextMenu->exec(pos: comboBox->mapToGlobal(pos));
1241 if (result != nullptr) {
1242 addMenuManager.handle(action: result, lineEdit: comboBox->lineEdit());
1243 }
1244}
1245
1246void KateSearchBar::onPowerModeChanged(int /*index*/)
1247{
1248 if (m_powerUi->searchMode->currentIndex() == MODE_REGEX) {
1249 m_powerUi->matchCase->setChecked(true);
1250 }
1251
1252 sendConfig();
1253 indicateMatch(matchResult: MatchNothing);
1254
1255 givePatternFeedback();
1256}
1257
1258// static void addSecondarySelection(KTextEditor::ViewPrivate *view, KTextEditor::Range range)
1259// {
1260// view->addSecondaryCursorWithSelection(range);
1261// }
1262
1263void KateSearchBar::nextMatchForSelection(KTextEditor::ViewPrivate *view, SearchDirection searchDirection)
1264{
1265 if (!view->selection()) {
1266 // Select current word so we can search for that
1267 const Cursor cursorPos = view->cursorPosition();
1268 KTextEditor::Range wordRange = view->document()->wordRangeAt(cursor: cursorPos);
1269 if (wordRange.isValid()) {
1270 selectRange(view, range: wordRange);
1271 return;
1272 }
1273 }
1274 if (view->selection()) {
1275 // We only want text of one of the selection ranges
1276 const QString pattern = view->doc()->text(range: view->selectionRange());
1277
1278 // How to find?
1279 SearchOptions enabledOptions(KTextEditor::Default);
1280 if (searchDirection == SearchBackward) {
1281 enabledOptions |= Backwards;
1282 }
1283
1284 // Where to find?
1285 const Range selRange = view->selectionRange();
1286 // const Range selRange = view->lastSelectionRange();
1287 Range inputRange;
1288 if (searchDirection == SearchForward) {
1289 inputRange.setRange(start: selRange.end(), end: view->doc()->documentEnd());
1290 } else {
1291 inputRange.setRange(start: Cursor(0, 0), end: selRange.start());
1292 }
1293
1294 // Find, first try
1295 KateMatch match(view->doc(), enabledOptions);
1296 match.searchText(range: inputRange, pattern);
1297
1298 if (match.isValid()) {
1299 selectRange(view, range: match.range());
1300 // addSecondarySelection(view, match.range());
1301 } else {
1302 // Find, second try
1303 m_view->showSearchWrappedHint(isReverseSearch: searchDirection == SearchBackward);
1304 if (searchDirection == SearchForward) {
1305 inputRange.setRange(start: Cursor(0, 0), end: selRange.start());
1306 } else {
1307 inputRange.setRange(start: selRange.end(), end: view->doc()->documentEnd());
1308 }
1309 KateMatch match2(view->doc(), enabledOptions);
1310 match2.searchText(range: inputRange, pattern);
1311 if (match2.isValid()) {
1312 selectRange(view, range: match2.range());
1313 // addSecondarySelection(view, match2.range());
1314 }
1315 }
1316 }
1317}
1318
1319void KateSearchBar::enterPowerMode()
1320{
1321 QString initialPattern;
1322 bool selectionOnly = false;
1323
1324 // Guess settings from context: init pattern with current selection
1325 const bool selected = m_view->selection();
1326 if (selected) {
1327 const Range &selection = m_view->selectionRange();
1328 if (selection.onSingleLine()) {
1329 // ... with current selection
1330 initialPattern = m_view->selectionText();
1331 } else {
1332 // Enable selection only
1333 selectionOnly = true;
1334 }
1335 }
1336
1337 // If there's no new selection, we'll use the existing pattern
1338 if (initialPattern.isNull()) {
1339 // Coming from power search?
1340 const bool fromReplace = (m_powerUi != nullptr) && (m_widget->isVisible());
1341 if (fromReplace) {
1342 QLineEdit *const patternLineEdit = m_powerUi->pattern->lineEdit();
1343 Q_ASSERT(patternLineEdit != nullptr);
1344 patternLineEdit->selectAll();
1345 m_powerUi->pattern->setFocus(Qt::MouseFocusReason);
1346 return;
1347 }
1348
1349 // Coming from incremental search?
1350 const bool fromIncremental = (m_incUi != nullptr) && (m_widget->isVisible());
1351 if (fromIncremental) {
1352 initialPattern = m_incUi->pattern->currentText();
1353 } else {
1354 // Search bar probably newly opened. Reset initial replacement text to empty
1355 m_replacement.clear();
1356 }
1357 }
1358
1359 // Create dialog
1360 const bool create = (m_powerUi == nullptr);
1361 if (create) {
1362 // Kill incremental widget
1363 if (m_incUi != nullptr) {
1364 // Backup current settings
1365 const bool OF_INCREMENTAL = false;
1366 backupConfig(ofPower: OF_INCREMENTAL);
1367
1368 // Kill widget
1369 delete m_incUi;
1370 m_incUi = nullptr;
1371 m_layout->removeWidget(w: m_widget);
1372 m_widget->deleteLater(); // I didn't get a crash here but for symmetrie to the other mutate slot^
1373 }
1374
1375 // Add power widget
1376 m_widget = new QWidget(this);
1377 m_powerUi = new Ui::PowerSearchBar;
1378 m_powerUi->setupUi(m_widget);
1379 m_layout->addWidget(m_widget);
1380
1381 // Bind to shared history models
1382 m_powerUi->pattern->setDuplicatesEnabled(false);
1383 m_powerUi->pattern->setInsertPolicy(QComboBox::InsertAtTop);
1384 m_powerUi->pattern->setMaxCount(m_config->maxHistorySize());
1385 m_powerUi->pattern->setModel(KTextEditor::EditorPrivate::self()->searchHistoryModel());
1386 m_powerUi->pattern->lineEdit()->setClearButtonEnabled(true);
1387 m_powerUi->pattern->setCompleter(nullptr);
1388 m_powerUi->replacement->setDuplicatesEnabled(false);
1389 m_powerUi->replacement->setInsertPolicy(QComboBox::InsertAtTop);
1390 m_powerUi->replacement->setMaxCount(m_config->maxHistorySize());
1391 m_powerUi->replacement->setModel(KTextEditor::EditorPrivate::self()->replaceHistoryModel());
1392 m_powerUi->replacement->lineEdit()->setClearButtonEnabled(true);
1393 m_powerUi->replacement->setCompleter(nullptr);
1394
1395 // Filter Up/Down arrow key inputs to save unfinished search/replace text
1396 m_powerUi->pattern->installEventFilter(filterObj: this);
1397 m_powerUi->replacement->installEventFilter(filterObj: this);
1398
1399 // Icons
1400 // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing.
1401 QIcon mutateIcon = QIcon::fromTheme(QStringLiteral("games-config-options"), fallback: QIcon::fromTheme(QStringLiteral("preferences-system")));
1402 QIcon matchCaseIcon = QIcon::fromTheme(QStringLiteral("format-text-superscript"), fallback: QIcon::fromTheme(QStringLiteral("format-text-bold")));
1403 m_powerUi->mutate->setIcon(mutateIcon);
1404 m_powerUi->mutate->setChecked(true);
1405 m_powerUi->findNext->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search")));
1406 m_powerUi->findPrev->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search")));
1407 m_powerUi->findAll->setIcon(QIcon::fromTheme(QStringLiteral("edit-find")));
1408 m_powerUi->matchCase->setIcon(matchCaseIcon);
1409 m_powerUi->selectionOnly->setIcon(QIcon::fromTheme(QStringLiteral("edit-select-all")));
1410
1411 // Focus proxy
1412 centralWidget()->setFocusProxy(m_powerUi->pattern);
1413 }
1414
1415 m_powerUi->selectionOnly->setChecked(selectionOnly);
1416
1417 // Restore previous settings
1418 if (create) {
1419 m_powerUi->matchCase->setChecked(m_powerMatchCase);
1420 m_powerUi->searchMode->setCurrentIndex(m_powerMode);
1421 }
1422
1423 // force current index of -1 --> <cursor down> shows 1st completion entry instead of 2nd
1424 m_powerUi->pattern->setCurrentIndex(-1);
1425 m_powerUi->replacement->setCurrentIndex(-1);
1426
1427 // Set initial search pattern
1428 QLineEdit *const patternLineEdit = m_powerUi->pattern->lineEdit();
1429 Q_ASSERT(patternLineEdit != nullptr);
1430 patternLineEdit->setText(initialPattern);
1431 patternLineEdit->selectAll();
1432
1433 // Set initial replacement text
1434 QLineEdit *const replacementLineEdit = m_powerUi->replacement->lineEdit();
1435 Q_ASSERT(replacementLineEdit != nullptr);
1436 replacementLineEdit->setText(m_replacement);
1437
1438 // Propagate settings (slots are still inactive on purpose)
1439 onPowerPatternChanged(initialPattern);
1440 givePatternFeedback();
1441
1442 if (create) {
1443 // Slots
1444 connect(sender: m_powerUi->mutate, signal: &QToolButton::clicked, context: this, slot: &KateSearchBar::enterIncrementalMode);
1445 connect(sender: patternLineEdit, signal: &QLineEdit::textChanged, context: this, slot: &KateSearchBar::onPowerPatternChanged);
1446 connect(sender: m_powerUi->findNext, signal: &QToolButton::clicked, context: this, slot: &KateSearchBar::findNext);
1447 connect(sender: m_powerUi->findPrev, signal: &QToolButton::clicked, context: this, slot: &KateSearchBar::findPrevious);
1448 connect(sender: m_powerUi->replaceNext, signal: &QPushButton::clicked, context: this, slot: &KateSearchBar::replaceNext);
1449 connect(sender: m_powerUi->replaceAll, signal: &QPushButton::clicked, context: this, slot: &KateSearchBar::replaceAll);
1450 connect(sender: m_powerUi->searchMode, signal: &QComboBox::currentIndexChanged, context: this, slot: &KateSearchBar::onPowerModeChanged);
1451 connect(sender: m_powerUi->matchCase, signal: &QToolButton::toggled, context: this, slot: &KateSearchBar::onMatchCaseToggled);
1452 connect(sender: m_powerUi->findAll, signal: &QPushButton::clicked, context: this, slot: &KateSearchBar::findAll);
1453 connect(sender: m_powerUi->cancel, signal: &QPushButton::clicked, context: this, slot: &KateSearchBar::onPowerCancelFindOrReplace);
1454
1455 // Make [return] in pattern line edit trigger <find next> action
1456 connect(sender: patternLineEdit, signal: &QLineEdit::returnPressed, context: this, slot: &KateSearchBar::onReturnPressed);
1457 connect(sender: replacementLineEdit, signal: &QLineEdit::returnPressed, context: this, slot: &KateSearchBar::replaceNext);
1458
1459 // Hook into line edit context menus
1460 m_powerUi->pattern->setContextMenuPolicy(Qt::CustomContextMenu);
1461
1462 connect(sender: m_powerUi->pattern, signal: &QComboBox::customContextMenuRequested, context: this, slot: qOverload<const QPoint &>(&KateSearchBar::onPowerPatternContextMenuRequest));
1463 m_powerUi->replacement->setContextMenuPolicy(Qt::CustomContextMenu);
1464 connect(sender: m_powerUi->replacement,
1465 signal: &QComboBox::customContextMenuRequested,
1466 context: this,
1467 slot: qOverload<const QPoint &>(&KateSearchBar::onPowerReplacmentContextMenuRequest));
1468 }
1469
1470 // Focus
1471 if (m_widget->isVisible()) {
1472 m_powerUi->pattern->setFocus(Qt::MouseFocusReason);
1473 }
1474
1475 // move close button to right layout, ensures properly at top for both incremental + advanced mode
1476 m_powerUi->gridLayout->addWidget(closeButton(), row: 0, column: 2, rowSpan: 1, columnSpan: 1);
1477}
1478
1479void KateSearchBar::enterIncrementalMode()
1480{
1481 QString initialPattern;
1482
1483 // Guess settings from context: init pattern with current selection
1484 const bool selected = m_view->selection();
1485 if (selected) {
1486 const Range &selection = m_view->selectionRange();
1487 if (selection.onSingleLine()) {
1488 // ... with current selection
1489 initialPattern = m_view->selectionText();
1490 }
1491 }
1492
1493 // If there's no new selection, we'll use the existing pattern
1494 if (initialPattern.isNull()) {
1495 // Coming from incremental search?
1496 const bool fromIncremental = (m_incUi != nullptr) && (m_widget->isVisible());
1497 if (fromIncremental) {
1498 m_incUi->pattern->lineEdit()->selectAll();
1499 m_incUi->pattern->setFocus(Qt::MouseFocusReason);
1500 return;
1501 }
1502
1503 // Coming from power search?
1504 const bool fromReplace = (m_powerUi != nullptr) && (m_widget->isVisible());
1505 if (fromReplace) {
1506 initialPattern = m_powerUi->pattern->currentText();
1507 // current text will be used as initial replacement text later
1508 m_replacement = m_powerUi->replacement->currentText();
1509 }
1510 }
1511
1512 // Still no search pattern? Use the word under the cursor
1513 if (initialPattern.isNull()) {
1514 const KTextEditor::Cursor cursorPosition = m_view->cursorPosition();
1515 initialPattern = m_view->doc()->wordAt(cursor: cursorPosition);
1516 }
1517
1518 // Create dialog
1519 const bool create = (m_incUi == nullptr);
1520 if (create) {
1521 // Kill power widget
1522 if (m_powerUi != nullptr) {
1523 // Backup current settings
1524 const bool OF_POWER = true;
1525 backupConfig(ofPower: OF_POWER);
1526
1527 // Kill widget
1528 delete m_powerUi;
1529 m_powerUi = nullptr;
1530 m_layout->removeWidget(w: m_widget);
1531 m_widget->deleteLater(); // deleteLater, because it's not a good idea too delete the widget and there for the button triggering this slot
1532 }
1533
1534 // Add incremental widget
1535 m_widget = new QWidget(this);
1536 m_incUi = new Ui::IncrementalSearchBar;
1537 m_incUi->setupUi(m_widget);
1538 m_layout->addWidget(m_widget);
1539
1540 // Add margin to combo box label
1541 m_incUi->label->setContentsMargins(left: style()->pixelMetric(metric: QStyle::PM_LayoutLeftMargin), top: 0, right: 0, bottom: 0);
1542
1543 // Filter Up/Down arrow key inputs to save unfinished search text
1544 m_incUi->pattern->installEventFilter(filterObj: this);
1545
1546 // new QShortcut(KStandardShortcut::paste().primary(), m_incUi->pattern, SLOT(paste()), 0, Qt::WidgetWithChildrenShortcut);
1547 // if (!KStandardShortcut::paste().alternate().isEmpty())
1548 // new QShortcut(KStandardShortcut::paste().alternate(), m_incUi->pattern, SLOT(paste()), 0, Qt::WidgetWithChildrenShortcut);
1549
1550 // Icons
1551 // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing.
1552 QIcon mutateIcon = QIcon::fromTheme(QStringLiteral("games-config-options"), fallback: QIcon::fromTheme(QStringLiteral("preferences-system")));
1553 QIcon matchCaseIcon = QIcon::fromTheme(QStringLiteral("format-text-superscript"), fallback: QIcon::fromTheme(QStringLiteral("format-text-bold")));
1554 m_incUi->mutate->setIcon(mutateIcon);
1555 m_incUi->next->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search")));
1556 m_incUi->prev->setIcon(QIcon::fromTheme(QStringLiteral("go-up-search")));
1557 m_incUi->matchCase->setIcon(matchCaseIcon);
1558
1559 // Ensure minimum size
1560 m_incUi->pattern->setMinimumWidth(12 * m_incUi->pattern->fontMetrics().height());
1561
1562 // Customize status area
1563 m_incUi->status->setTextElideMode(Qt::ElideLeft);
1564
1565 // Focus proxy
1566 centralWidget()->setFocusProxy(m_incUi->pattern);
1567
1568 m_incUi->pattern->setDuplicatesEnabled(false);
1569 m_incUi->pattern->setInsertPolicy(QComboBox::InsertAtTop);
1570 m_incUi->pattern->setMaxCount(m_config->maxHistorySize());
1571 m_incUi->pattern->setModel(KTextEditor::EditorPrivate::self()->searchHistoryModel());
1572 m_incUi->pattern->lineEdit()->setClearButtonEnabled(true);
1573 m_incUi->pattern->setCompleter(nullptr);
1574 }
1575
1576 // Restore previous settings
1577 if (create) {
1578 m_incUi->matchCase->setChecked(m_incMatchCase);
1579 }
1580
1581 // force current index of -1 --> <cursor down> shows 1st completion entry instead of 2nd
1582 m_incUi->pattern->setCurrentIndex(-1);
1583
1584 // Set initial search pattern
1585 if (!create) {
1586 disconnect(sender: m_incUi->pattern, signal: &QComboBox::editTextChanged, receiver: this, slot: &KateSearchBar::onIncPatternChanged);
1587 }
1588 m_incUi->pattern->setEditText(initialPattern);
1589 connect(sender: m_incUi->pattern, signal: &QComboBox::editTextChanged, context: this, slot: &KateSearchBar::onIncPatternChanged);
1590 m_incUi->pattern->lineEdit()->selectAll();
1591
1592 // Propagate settings (slots are still inactive on purpose)
1593 if (initialPattern.isEmpty()) {
1594 // Reset edit color
1595 indicateMatch(matchResult: MatchNothing);
1596 }
1597
1598 // Enable/disable next/prev
1599 m_incUi->next->setDisabled(initialPattern.isEmpty());
1600 m_incUi->prev->setDisabled(initialPattern.isEmpty());
1601
1602 if (create) {
1603 // Slots
1604 connect(sender: m_incUi->mutate, signal: &QToolButton::clicked, context: this, slot: &KateSearchBar::enterPowerMode);
1605 connect(sender: m_incUi->pattern->lineEdit(), signal: &QLineEdit::returnPressed, context: this, slot: &KateSearchBar::onReturnPressed);
1606 connect(sender: m_incUi->next, signal: &QToolButton::clicked, context: this, slot: &KateSearchBar::findNext);
1607 connect(sender: m_incUi->prev, signal: &QToolButton::clicked, context: this, slot: &KateSearchBar::findPrevious);
1608 connect(sender: m_incUi->matchCase, signal: &QToolButton::toggled, context: this, slot: &KateSearchBar::onMatchCaseToggled);
1609 }
1610
1611 // Focus
1612 if (m_widget->isVisible()) {
1613 m_incUi->pattern->setFocus(Qt::MouseFocusReason);
1614 }
1615
1616 // move close button to right layout, ensures properly at top for both incremental + advanced mode
1617 m_incUi->hboxLayout->addWidget(closeButton());
1618}
1619
1620bool KateSearchBar::clearHighlights()
1621{
1622 // Remove ScrollBarMarks
1623 const QHash<int, KTextEditor::Mark *> &marks = m_view->document()->marks();
1624 QHashIterator<int, KTextEditor::Mark *> i(marks);
1625 while (i.hasNext()) {
1626 i.next();
1627 if (i.value()->type & KTextEditor::Document::SearchMatch) {
1628 m_view->document()->removeMark(line: i.value()->line, markType: KTextEditor::Document::SearchMatch);
1629 }
1630 }
1631
1632 if (m_infoMessage) {
1633 delete m_infoMessage;
1634 }
1635
1636 if (m_hlRanges.isEmpty()) {
1637 return false;
1638 }
1639 qDeleteAll(c: m_hlRanges);
1640 m_hlRanges.clear();
1641 return true;
1642}
1643
1644void KateSearchBar::updateHighlightColors()
1645{
1646 const QColor foregroundColor = m_view->defaultStyleAttribute(defaultStyle: KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color();
1647 const QColor &searchColor = m_view->rendererConfig()->searchHighlightColor();
1648 const QColor &replaceColor = m_view->rendererConfig()->replaceHighlightColor();
1649
1650 // init match attribute
1651 highlightMatchAttribute->setForeground(foregroundColor);
1652 highlightMatchAttribute->setBackground(searchColor);
1653 highlightMatchAttribute->dynamicAttribute(type: Attribute::ActivateMouseIn)->setBackground(searchColor);
1654 highlightMatchAttribute->dynamicAttribute(type: Attribute::ActivateMouseIn)->setForeground(foregroundColor);
1655 highlightMatchAttribute->dynamicAttribute(type: Attribute::ActivateCaretIn)->setBackground(searchColor);
1656 highlightMatchAttribute->dynamicAttribute(type: Attribute::ActivateCaretIn)->setForeground(foregroundColor);
1657
1658 // init replacement attribute
1659 highlightReplacementAttribute->setBackground(replaceColor);
1660 highlightReplacementAttribute->setForeground(foregroundColor);
1661}
1662
1663void KateSearchBar::showEvent(QShowEvent *event)
1664{
1665 // Update init cursor
1666 if (m_incUi != nullptr) {
1667 m_incInitCursor = m_view->cursorPosition();
1668 }
1669
1670 // We don't want to update if a "findOrReplaceAll" is running
1671 // other we end up deleting our working range and crash
1672 if (m_cancelFindOrReplace) {
1673 updateSelectionOnly();
1674 }
1675
1676 KateViewBarWidget::showEvent(event);
1677}
1678
1679bool KateSearchBar::eventFilter(QObject *obj, QEvent *event)
1680{
1681 QComboBox *combo = qobject_cast<QComboBox *>(object: obj);
1682 if (combo && event->type() == QEvent::KeyPress) {
1683 const int key = static_cast<QKeyEvent *>(event)->key();
1684 const int currentIndex = combo->currentIndex();
1685 const QString currentText = combo->currentText();
1686 QString &unfinishedText = (m_powerUi && combo == m_powerUi->replacement) ? m_replacement : m_unfinishedSearchText;
1687 if (key == Qt::Key_Up && currentIndex <= 0 && unfinishedText != currentText) {
1688 // Only restore unfinished text if we are already in the latest entry
1689 combo->setCurrentIndex(-1);
1690 combo->setCurrentText(unfinishedText);
1691 } else if (key == Qt::Key_Down || key == Qt::Key_Up) {
1692 // Only save unfinished text if it is not empty and it is modified
1693 const bool isUnfinishedSearch = (!currentText.trimmed().isEmpty() && (currentIndex == -1 || combo->itemText(index: currentIndex) != currentText));
1694 if (isUnfinishedSearch && unfinishedText != currentText) {
1695 unfinishedText = currentText;
1696 }
1697 }
1698 }
1699
1700 return QWidget::eventFilter(watched: obj, event);
1701}
1702
1703void KateSearchBar::updateSelectionOnly()
1704{
1705 // Make sure the previous selection-only search range is not used anymore
1706 if (m_workingRange && !m_selectionChangedByUndoRedo) {
1707 delete m_workingRange;
1708 m_workingRange = nullptr;
1709 }
1710
1711 if (m_powerUi == nullptr) {
1712 return;
1713 }
1714
1715 // Re-init "Selection only" checkbox if power search bar open
1716 const bool selected = m_view->selection();
1717 bool selectionOnly = selected;
1718 if (selected) {
1719 Range const &selection = m_view->selectionRange();
1720 selectionOnly = !selection.onSingleLine();
1721 }
1722 m_powerUi->selectionOnly->setChecked(selectionOnly);
1723}
1724
1725void KateSearchBar::updateIncInitCursor()
1726{
1727 if (m_incUi == nullptr) {
1728 return;
1729 }
1730
1731 // Update init cursor
1732 m_incInitCursor = m_view->cursorPosition();
1733}
1734
1735void KateSearchBar::onPowerPatternContextMenuRequest(const QPoint &pos)
1736{
1737 const bool FOR_PATTERN = true;
1738 showExtendedContextMenu(forPattern: FOR_PATTERN, pos);
1739}
1740
1741void KateSearchBar::onPowerPatternContextMenuRequest()
1742{
1743 onPowerPatternContextMenuRequest(pos: m_powerUi->pattern->mapFromGlobal(QCursor::pos()));
1744}
1745
1746void KateSearchBar::onPowerReplacmentContextMenuRequest(const QPoint &pos)
1747{
1748 const bool FOR_REPLACEMENT = false;
1749 showExtendedContextMenu(forPattern: FOR_REPLACEMENT, pos);
1750}
1751
1752void KateSearchBar::onPowerReplacmentContextMenuRequest()
1753{
1754 onPowerReplacmentContextMenuRequest(pos: m_powerUi->replacement->mapFromGlobal(QCursor::pos()));
1755}
1756
1757void KateSearchBar::onPowerCancelFindOrReplace()
1758{
1759 m_cancelFindOrReplace = true;
1760}
1761
1762bool KateSearchBar::isPower() const
1763{
1764 return m_powerUi != nullptr;
1765}
1766
1767void KateSearchBar::slotReadWriteChanged()
1768{
1769 if (!KateSearchBar::isPower()) {
1770 return;
1771 }
1772
1773 // perhaps disable/enable
1774 m_powerUi->replaceNext->setEnabled(m_view->doc()->isReadWrite() && isPatternValid());
1775 m_powerUi->replaceAll->setEnabled(m_view->doc()->isReadWrite() && isPatternValid());
1776}
1777
1778#include "moc_katesearchbar.cpp"
1779

source code of ktexteditor/src/search/katesearchbar.cpp