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

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