File indexing completed on 2024-05-12 09:55:24

0001 /*
0002     SPDX-FileCopyrightText: 2011-21 Kåre Särs <kare.sars@iki.fi>
0003     SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "SearchPlugin.h"
0009 #include "KateSearchCommand.h"
0010 #include "MatchExportDialog.h"
0011 #include "MatchProxyModel.h"
0012 #include "Results.h"
0013 
0014 #include <ktexteditor/document.h>
0015 #include <ktexteditor/editor.h>
0016 #include <ktexteditor/movingrange.h>
0017 #include <ktexteditor/view.h>
0018 
0019 #include <KAcceleratorManager>
0020 #include <KActionCollection>
0021 #include <KColorScheme>
0022 #include <KLocalizedString>
0023 #include <KPluginFactory>
0024 #include <KUrlCompletion>
0025 
0026 #include <KConfigGroup>
0027 #include <KXMLGUIFactory>
0028 
0029 #include <QClipboard>
0030 #include <QComboBox>
0031 #include <QCompleter>
0032 #include <QFileInfo>
0033 #include <QKeyEvent>
0034 #include <QMenu>
0035 #include <QPoint>
0036 
0037 #include <ktexteditor_utils.h>
0038 
0039 static QUrl localFileDirUp(const QUrl &url)
0040 {
0041     if (!url.isLocalFile()) {
0042         return url;
0043     }
0044 
0045     // else go up
0046     return QUrl::fromLocalFile(QFileInfo(url.toLocalFile()).dir().absolutePath());
0047 }
0048 
0049 static QAction *
0050 menuEntry(QMenu *menu, const QString &before, const QString &after, const QString &desc, QString menuBefore = QString(), QString menuAfter = QString());
0051 
0052 /**
0053  * When the action is triggered the cursor will be placed between @p before and @p after.
0054  */
0055 static QAction *menuEntry(QMenu *menu, const QString &before, const QString &after, const QString &desc, QString menuBefore, QString menuAfter)
0056 {
0057     if (menuBefore.isEmpty()) {
0058         menuBefore = before;
0059     }
0060     if (menuAfter.isEmpty()) {
0061         menuAfter = after;
0062     }
0063 
0064     QAction *const action = menu->addAction(menuBefore + menuAfter + QLatin1Char('\t') + desc);
0065     if (!action) {
0066         return nullptr;
0067     }
0068 
0069     action->setData(QString(before + QLatin1Char(' ') + after));
0070     return action;
0071 }
0072 
0073 /**
0074  * adds items and separators for special chars in "replace" field
0075  */
0076 static void addSpecialCharsHelperActionsForReplace(QSet<QAction *> *actionList, QMenu *menu)
0077 {
0078     QSet<QAction *> &actionPointers = *actionList;
0079     QString emptyQSTring;
0080 
0081     actionPointers << menuEntry(menu, QStringLiteral("\\n"), emptyQSTring, i18n("Line break"));
0082     actionPointers << menuEntry(menu, QStringLiteral("\\t"), emptyQSTring, i18n("Tab"));
0083 }
0084 
0085 /**
0086  * adds items and separators for regex in "search" field
0087  */
0088 static void addRegexHelperActionsForSearch(QSet<QAction *> *actionList, QMenu *menu)
0089 {
0090     QSet<QAction *> &actionPointers = *actionList;
0091     QString emptyQSTring;
0092 
0093     actionPointers << menuEntry(menu, QStringLiteral("^"), emptyQSTring, i18n("Beginning of line"));
0094     actionPointers << menuEntry(menu, QStringLiteral("$"), emptyQSTring, i18n("End of line"));
0095     menu->addSeparator();
0096     actionPointers << menuEntry(menu, QStringLiteral("."), emptyQSTring, i18n("Any single character (excluding line breaks)"));
0097     actionPointers << menuEntry(menu, QStringLiteral("[.]"), emptyQSTring, i18n("Literal dot"));
0098     menu->addSeparator();
0099     actionPointers << menuEntry(menu, QStringLiteral("+"), emptyQSTring, i18n("One or more occurrences"));
0100     actionPointers << menuEntry(menu, QStringLiteral("*"), emptyQSTring, i18n("Zero or more occurrences"));
0101     actionPointers << menuEntry(menu, QStringLiteral("?"), emptyQSTring, i18n("Zero or one occurrences"));
0102     actionPointers
0103         << menuEntry(menu, QStringLiteral("{"), QStringLiteral(",}"), i18n("<a> through <b> occurrences"), QStringLiteral("{a"), QStringLiteral(",b}"));
0104     menu->addSeparator();
0105     actionPointers << menuEntry(menu, QStringLiteral("("), QStringLiteral(")"), i18n("Group, capturing"));
0106     actionPointers << menuEntry(menu, QStringLiteral("|"), emptyQSTring, i18n("Or"));
0107     actionPointers << menuEntry(menu, QStringLiteral("["), QStringLiteral("]"), i18n("Set of characters"));
0108     actionPointers << menuEntry(menu, QStringLiteral("[^"), QStringLiteral("]"), i18n("Negative set of characters"));
0109     actionPointers << menuEntry(menu, QStringLiteral("(?:"), QStringLiteral(")"), i18n("Group, non-capturing"), QStringLiteral("(?:E"));
0110     actionPointers << menuEntry(menu, QStringLiteral("(?="), QStringLiteral(")"), i18n("Lookahead"), QStringLiteral("(?=E"));
0111     actionPointers << menuEntry(menu, QStringLiteral("(?!"), QStringLiteral(")"), i18n("Negative lookahead"), QStringLiteral("(?!E"));
0112 
0113     menu->addSeparator();
0114     actionPointers << menuEntry(menu, QStringLiteral("\\n"), emptyQSTring, i18n("Line break"));
0115     actionPointers << menuEntry(menu, QStringLiteral("\\t"), emptyQSTring, i18n("Tab"));
0116     actionPointers << menuEntry(menu, QStringLiteral("\\b"), emptyQSTring, i18n("Word boundary"));
0117     actionPointers << menuEntry(menu, QStringLiteral("\\B"), emptyQSTring, i18n("Not word boundary"));
0118     actionPointers << menuEntry(menu, QStringLiteral("\\d"), emptyQSTring, i18n("Digit"));
0119     actionPointers << menuEntry(menu, QStringLiteral("\\D"), emptyQSTring, i18n("Non-digit"));
0120     actionPointers << menuEntry(menu, QStringLiteral("\\s"), emptyQSTring, i18n("Whitespace (excluding line breaks)"));
0121     actionPointers << menuEntry(menu, QStringLiteral("\\S"), emptyQSTring, i18n("Non-whitespace (excluding line breaks)"));
0122     actionPointers << menuEntry(menu, QStringLiteral("\\w"), emptyQSTring, i18n("Word character (alphanumerics plus '_')"));
0123     actionPointers << menuEntry(menu, QStringLiteral("\\W"), emptyQSTring, i18n("Non-word character"));
0124 }
0125 
0126 /**
0127  * adds items and separators for regex in "replace" field
0128  */
0129 void KatePluginSearchView::addRegexHelperActionsForReplace(QSet<QAction *> *actionList, QMenu *menu)
0130 {
0131     QSet<QAction *> &actionPointers = *actionList;
0132     QString emptyQSTring;
0133 
0134     menu->addSeparator();
0135     actionPointers << menuEntry(menu, QStringLiteral("\\0"), emptyQSTring, i18n("Regular expression capture 0 (whole match)"));
0136     actionPointers << menuEntry(menu, QStringLiteral("\\"), emptyQSTring, i18n("Regular expression capture 1-9"), QStringLiteral("\\#"));
0137     actionPointers << menuEntry(menu, QStringLiteral("\\{"), QStringLiteral("}"), i18n("Regular expression capture 0-999"), QStringLiteral("\\{#"));
0138     menu->addSeparator();
0139     actionPointers << menuEntry(menu, QStringLiteral("\\U\\"), emptyQSTring, i18n("Upper-cased capture 0-9"), QStringLiteral("\\U\\#"));
0140     actionPointers << menuEntry(menu, QStringLiteral("\\U\\{"), QStringLiteral("}"), i18n("Upper-cased capture 0-999"), QStringLiteral("\\U\\{###"));
0141     actionPointers << menuEntry(menu, QStringLiteral("\\L\\"), emptyQSTring, i18n("Lower-cased capture 0-9"), QStringLiteral("\\L\\#"));
0142     actionPointers << menuEntry(menu, QStringLiteral("\\L\\{"), QStringLiteral("}"), i18n("Lower-cased capture 0-999"), QStringLiteral("\\L\\{###"));
0143 }
0144 
0145 /**
0146  * inserts text and sets cursor position
0147  */
0148 void KatePluginSearchView::regexHelperActOnAction(QAction *resultAction, const QSet<QAction *> &actionList, QLineEdit *lineEdit)
0149 {
0150     if (resultAction && actionList.contains(resultAction)) {
0151         const int cursorPos = lineEdit->cursorPosition();
0152         QStringList beforeAfter = resultAction->data().toString().split(QLatin1Char(' '));
0153         if (beforeAfter.size() != 2) {
0154             return;
0155         }
0156         lineEdit->insert(beforeAfter[0] + beforeAfter[1]);
0157         lineEdit->setCursorPosition(cursorPos + beforeAfter[0].count());
0158         lineEdit->setFocus();
0159     }
0160 }
0161 
0162 K_PLUGIN_FACTORY_WITH_JSON(KatePluginSearchFactory, "katesearch.json", registerPlugin<KatePluginSearch>();)
0163 
0164 KatePluginSearch::KatePluginSearch(QObject *parent, const QVariantList &)
0165     : KTextEditor::Plugin(parent)
0166 {
0167     // ensure we can send over vector of matches via queued connection
0168     qRegisterMetaType<QList<KateSearchMatch>>();
0169 
0170     m_searchCommand = new KateSearchCommand(this);
0171 }
0172 
0173 KatePluginSearch::~KatePluginSearch()
0174 {
0175     delete m_searchCommand;
0176 }
0177 
0178 QObject *KatePluginSearch::createView(KTextEditor::MainWindow *mainWindow)
0179 {
0180     KatePluginSearchView *view = new KatePluginSearchView(this, mainWindow, KTextEditor::Editor::instance()->application());
0181     connect(m_searchCommand, &KateSearchCommand::setSearchPlace, view, &KatePluginSearchView::setSearchPlace);
0182     connect(m_searchCommand, &KateSearchCommand::setCurrentFolder, view, &KatePluginSearchView::setCurrentFolder);
0183     connect(m_searchCommand, &KateSearchCommand::setSearchString, view, &KatePluginSearchView::setSearchString);
0184     connect(m_searchCommand, &KateSearchCommand::startSearch, view, &KatePluginSearchView::startSearch);
0185     connect(m_searchCommand, &KateSearchCommand::setRegexMode, view, &KatePluginSearchView::setRegexMode);
0186     connect(m_searchCommand, &KateSearchCommand::setCaseInsensitive, view, &KatePluginSearchView::setCaseInsensitive);
0187     connect(m_searchCommand, &KateSearchCommand::setExpandResults, view, &KatePluginSearchView::setExpandResults);
0188     connect(m_searchCommand, SIGNAL(newTab()), view, SLOT(addTab()));
0189 
0190     connect(view, &KatePluginSearchView::searchBusy, m_searchCommand, &KateSearchCommand::setBusy);
0191 
0192     return view;
0193 }
0194 
0195 bool ContainerWidget::focusNextPrevChild(bool next)
0196 {
0197     QWidget *fw = focusWidget();
0198     bool found = false;
0199     Q_EMIT nextFocus(fw, &found, next);
0200 
0201     if (found) {
0202         return true;
0203     }
0204     return QWidget::focusNextPrevChild(next);
0205 }
0206 
0207 void KatePluginSearchView::nextFocus(QWidget *currentWidget, bool *found, bool next)
0208 {
0209     *found = false;
0210 
0211     if (!currentWidget) {
0212         return;
0213     }
0214 
0215     // we use the object names here because there can be multiple trees (on multiple result tabs)
0216     if (next) {
0217         if (currentWidget->objectName() == QLatin1String("treeView") || currentWidget == m_ui.binaryCheckBox) {
0218             m_ui.searchCombo->setFocus();
0219             *found = true;
0220             return;
0221         }
0222         if (currentWidget == m_ui.excludeCombo && m_ui.searchPlaceCombo->currentIndex() > MatchModel::Folder) {
0223             m_ui.searchCombo->setFocus();
0224             *found = true;
0225             return;
0226         }
0227         if (currentWidget == m_ui.displayOptions) {
0228             if (m_ui.displayOptions->isChecked()) {
0229                 if (m_ui.searchPlaceCombo->currentIndex() < MatchModel::Folder) {
0230                     m_ui.searchCombo->setFocus();
0231                     *found = true;
0232                     return;
0233                 } else if (m_ui.searchPlaceCombo->currentIndex() == MatchModel::Folder) {
0234                     m_ui.folderRequester->setFocus();
0235                     *found = true;
0236                     return;
0237                 } else {
0238                     m_ui.filterCombo->setFocus();
0239                     *found = true;
0240                     return;
0241                 }
0242             } else {
0243                 Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
0244                 if (!res) {
0245                     return;
0246                 }
0247                 res->treeView->setFocus();
0248                 *found = true;
0249                 return;
0250             }
0251         }
0252     } else {
0253         if (currentWidget == m_ui.searchCombo) {
0254             if (m_ui.displayOptions->isChecked()) {
0255                 if (m_ui.searchPlaceCombo->currentIndex() < MatchModel::Folder) {
0256                     m_ui.displayOptions->setFocus();
0257                     *found = true;
0258                     return;
0259                 } else if (m_ui.searchPlaceCombo->currentIndex() == MatchModel::Folder) {
0260                     m_ui.binaryCheckBox->setFocus();
0261                     *found = true;
0262                     return;
0263                 } else {
0264                     m_ui.excludeCombo->setFocus();
0265                     *found = true;
0266                     return;
0267                 }
0268             } else {
0269                 Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
0270                 if (!res) {
0271                     return;
0272                 }
0273                 res->treeView->setFocus();
0274             }
0275             *found = true;
0276             return;
0277         } else {
0278             if (currentWidget->objectName() == QLatin1String("treeView")) {
0279                 m_ui.displayOptions->setFocus();
0280                 *found = true;
0281                 return;
0282             }
0283         }
0284     }
0285 }
0286 
0287 KatePluginSearchView::KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mainWin, KTextEditor::Application *application)
0288     : QObject(mainWin)
0289     , m_kateApp(application)
0290     , m_mainWindow(mainWin)
0291 {
0292     KXMLGUIClient::setComponentName(QStringLiteral("katesearch"), i18n("Search & Replace"));
0293     setXMLFile(QStringLiteral("ui.rc"));
0294 
0295     m_toolView = mainWin->createToolView(plugin,
0296                                          QStringLiteral("kate_plugin_katesearch"),
0297                                          KTextEditor::MainWindow::Bottom,
0298                                          QIcon::fromTheme(QStringLiteral("edit-find")),
0299                                          i18n("Search"));
0300 
0301     ContainerWidget *container = new ContainerWidget(m_toolView);
0302     QWidget *searchUi = new QWidget(container);
0303     m_ui.setupUi(searchUi);
0304 
0305     m_tabBar = new QTabBar(container);
0306     m_tabBar->setAutoHide(true);
0307     m_tabBar->setSelectionBehaviorOnRemove(QTabBar::SelectLeftTab);
0308     connect(m_tabBar, &QTabBar::currentChanged, m_ui.resultWidget, &QStackedWidget::setCurrentIndex);
0309     m_tabBar->setElideMode(Qt::ElideMiddle);
0310     m_tabBar->setTabsClosable(true);
0311     m_tabBar->setMovable(true);
0312     m_tabBar->setAutoHide(true);
0313     m_tabBar->setExpanding(false);
0314     m_tabBar->setSelectionBehaviorOnRemove(QTabBar::SelectLeftTab);
0315     KAcceleratorManager::setNoAccel(m_tabBar);
0316     connect(m_tabBar, &QTabBar::tabMoved, this, [this](int from, int to) {
0317         QWidget *fromWidget = m_ui.resultWidget->widget(from);
0318         m_ui.resultWidget->removeWidget(fromWidget);
0319         m_ui.resultWidget->insertWidget(to, fromWidget);
0320     });
0321 
0322     QVBoxLayout *layout = new QVBoxLayout(container);
0323     layout->addWidget(m_tabBar);
0324     layout->addWidget(searchUi);
0325     layout->setContentsMargins(0, 0, 0, 0);
0326     layout->setSpacing(0);
0327 
0328     container->setFocusProxy(m_ui.searchCombo);
0329     connect(container, &ContainerWidget::nextFocus, this, &KatePluginSearchView::nextFocus);
0330 
0331     QAction *a = actionCollection()->addAction(QStringLiteral("search_in_files"));
0332     actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_F));
0333     a->setText(i18n("Find in Files"));
0334     a->setIcon(QIcon::fromTheme(QStringLiteral("edit-find")));
0335     a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0336     connect(a, &QAction::triggered, this, &KatePluginSearchView::openSearchView);
0337 
0338     a = actionCollection()->addAction(QStringLiteral("search_in_files_new_tab"));
0339     a->setText(i18n("Find in Files (in a New Tab)"));
0340     // first add tab, then open search view, since open search view switches to show the search options
0341     connect(a, &QAction::triggered, this, &KatePluginSearchView::addTab);
0342     connect(a, &QAction::triggered, this, &KatePluginSearchView::openSearchView);
0343 
0344     a = actionCollection()->addAction(QStringLiteral("go_to_next_match"));
0345     a->setText(i18n("Go to Next Match"));
0346     actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::Key_F6));
0347     a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0348     connect(a, &QAction::triggered, this, &KatePluginSearchView::goToNextMatch);
0349 
0350     a = actionCollection()->addAction(QStringLiteral("go_to_prev_match"));
0351     a->setText(i18n("Go to Previous Match"));
0352     actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::SHIFT | Qt::Key_F6));
0353     a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0354     connect(a, &QAction::triggered, this, &KatePluginSearchView::goToPreviousMatch);
0355 
0356     a = actionCollection()->addAction(QStringLiteral("cut_searched_lines"));
0357     a->setText(i18n("Cut Matching Lines"));
0358     a->setIcon(QIcon::fromTheme(QStringLiteral("edit-cut")));
0359     a->setWhatsThis(i18n("This will cut all highlighted search match lines from the current document to the clipboard"));
0360     a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0361     connect(a, &QAction::triggered, this, &KatePluginSearchView::cutSearchedLines);
0362 
0363     a = actionCollection()->addAction(QStringLiteral("copy_searched_lines"));
0364     a->setText(i18n("Copy Matching Lines"));
0365     a->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
0366     a->setWhatsThis(i18n("This will copy all highlighted search match lines in the current document to the clipboard"));
0367     a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0368     connect(a, &QAction::triggered, this, &KatePluginSearchView::copySearchedLines);
0369 
0370     // Only show the tab bar when there is more than one tab
0371     KAcceleratorManager::setNoAccel(m_ui.resultWidget);
0372 
0373     // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing.
0374     QIcon dispOptIcon = QIcon::fromTheme(QStringLiteral("games-config-options"), QIcon::fromTheme(QStringLiteral("preferences-system")));
0375     QIcon matchCaseIcon = QIcon::fromTheme(QStringLiteral("format-text-superscript"), QIcon::fromTheme(QStringLiteral("format-text-bold")));
0376     QIcon useRegExpIcon = QIcon::fromTheme(QStringLiteral("code-context"), QIcon::fromTheme(QStringLiteral("edit-find-replace")));
0377     QIcon expandResultsIcon = QIcon::fromTheme(QStringLiteral("view-list-tree"), QIcon::fromTheme(QStringLiteral("format-indent-more")));
0378 
0379     m_ui.gridLayout->setSpacing(searchUi->style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing));
0380     m_ui.gridLayout->setContentsMargins(searchUi->style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
0381                                         searchUi->style()->pixelMetric(QStyle::PM_LayoutTopMargin),
0382                                         searchUi->style()->pixelMetric(QStyle::PM_LayoutRightMargin),
0383                                         searchUi->style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
0384     m_ui.displayOptions->setIcon(dispOptIcon);
0385     m_ui.searchButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-find")));
0386     m_ui.nextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search")));
0387     m_ui.stopButton->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
0388     m_ui.matchCase->setIcon(matchCaseIcon);
0389     m_ui.useRegExp->setIcon(useRegExpIcon);
0390     m_ui.expandResults->setIcon(expandResultsIcon);
0391     m_ui.filterBtn->setIcon(QIcon::fromTheme(QStringLiteral("view-filter")));
0392     m_ui.searchPlaceCombo->setItemIcon(MatchModel::CurrentFile, QIcon::fromTheme(QStringLiteral("text-plain")));
0393     m_ui.searchPlaceCombo->setItemIcon(MatchModel::OpenFiles, QIcon::fromTheme(QStringLiteral("text-plain")));
0394     m_ui.searchPlaceCombo->setItemIcon(MatchModel::Folder, QIcon::fromTheme(QStringLiteral("folder")));
0395     m_ui.folderUpButton->setIcon(QIcon::fromTheme(QStringLiteral("go-up")));
0396     m_ui.currentFolderButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
0397     m_ui.newTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-new")));
0398 
0399     m_ui.filterCombo->setToolTip(i18n("Comma separated list of file types to search in. Example: \"*.cpp,*.h\"\n"));
0400     m_ui.excludeCombo->setToolTip(i18n("Comma separated list of files and directories to exclude from the search. Example: \"build*\""));
0401 
0402     m_ui.filterBtn->setToolTip(i18n("Click to filter through results"));
0403     m_ui.filterBtn->setDisabled(true);
0404     connect(m_ui.filterBtn, &QToolButton::toggled, this, [this](bool on) {
0405         if (Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget())) {
0406             res->setFilterLineVisible(on);
0407         }
0408     });
0409 
0410     addTab();
0411 
0412     // get url-requester's combo box and sanely initialize
0413     KComboBox *cmbUrl = m_ui.folderRequester->comboBox();
0414     cmbUrl->setDuplicatesEnabled(false);
0415     cmbUrl->setEditable(true);
0416     m_ui.folderRequester->setMode(KFile::Directory | KFile::LocalOnly);
0417     KUrlCompletion *cmpl = new KUrlCompletion(KUrlCompletion::DirCompletion);
0418     cmbUrl->setCompletionObject(cmpl);
0419     cmbUrl->setAutoDeleteCompletionObject(true);
0420 
0421     connect(m_ui.newTabButton, &QToolButton::clicked, this, &KatePluginSearchView::addTab);
0422     connect(m_tabBar, &QTabBar::tabCloseRequested, this, &KatePluginSearchView::tabCloseRequested);
0423     connect(m_tabBar, &QTabBar::currentChanged, this, &KatePluginSearchView::resultTabChanged);
0424 
0425     connect(m_ui.folderUpButton, &QToolButton::clicked, this, &KatePluginSearchView::navigateFolderUp);
0426     connect(m_ui.currentFolderButton, &QToolButton::clicked, this, &KatePluginSearchView::setCurrentFolder);
0427     connect(m_ui.expandResults, &QToolButton::clicked, this, &KatePluginSearchView::expandResults);
0428 
0429     connect(m_ui.searchCombo, &QComboBox::editTextChanged, &m_changeTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
0430     connect(m_ui.matchCase, &QToolButton::toggled, &m_changeTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
0431     connect(m_ui.matchCase, &QToolButton::toggled, this, [this] {
0432         Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
0433         if (res) {
0434             res->matchCase = m_ui.matchCase->isChecked();
0435         }
0436     });
0437 
0438     connect(m_ui.searchCombo->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::startSearch);
0439     // connecting to returnPressed() of the folderRequester doesn't work, I haven't found out why yet. But connecting to the linedit works:
0440     connect(m_ui.folderRequester->comboBox()->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::startSearch);
0441     connect(m_ui.filterCombo, static_cast<void (KComboBox::*)(const QString &)>(&KComboBox::returnPressed), this, &KatePluginSearchView::startSearch);
0442     connect(m_ui.excludeCombo, static_cast<void (KComboBox::*)(const QString &)>(&KComboBox::returnPressed), this, &KatePluginSearchView::startSearch);
0443     connect(m_ui.searchButton, &QPushButton::clicked, this, &KatePluginSearchView::startSearch);
0444 
0445     connect(m_ui.displayOptions, &QToolButton::toggled, this, &KatePluginSearchView::toggleOptions);
0446     connect(m_ui.searchPlaceCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &KatePluginSearchView::searchPlaceChanged);
0447 
0448     connect(m_ui.stopButton, &QPushButton::clicked, this, &KatePluginSearchView::stopClicked);
0449 
0450     connect(m_ui.nextButton, &QToolButton::clicked, this, &KatePluginSearchView::goToNextMatch);
0451 
0452     connect(m_ui.replaceButton, &QPushButton::clicked, this, &KatePluginSearchView::replaceSingleMatch);
0453     connect(m_ui.replaceCheckedBtn, &QPushButton::clicked, this, &KatePluginSearchView::replaceChecked);
0454     connect(m_ui.replaceCombo->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::replaceChecked);
0455 
0456     m_ui.displayOptions->setChecked(true);
0457 
0458     connect(&m_searchOpenFiles, &SearchOpenFiles::matchesFound, this, &KatePluginSearchView::matchesFound);
0459     connect(&m_searchOpenFiles, &SearchOpenFiles::searchDone, this, &KatePluginSearchView::searchDone);
0460 
0461     m_diskSearchDoneTimer.setSingleShot(true);
0462     m_diskSearchDoneTimer.setInterval(10);
0463     connect(&m_diskSearchDoneTimer, &QTimer::timeout, this, &KatePluginSearchView::searchDone);
0464 
0465     m_updateCheckedStateTimer.setSingleShot(true);
0466     m_updateCheckedStateTimer.setInterval(10);
0467     connect(&m_updateCheckedStateTimer, &QTimer::timeout, this, &KatePluginSearchView::updateMatchMarks);
0468 
0469     // queued connect to signals emitted outside of background thread
0470     connect(&m_folderFilesList, &FolderFilesList::fileListReady, this, &KatePluginSearchView::folderFileListChanged, Qt::QueuedConnection);
0471     connect(
0472         &m_folderFilesList,
0473         &FolderFilesList::searching,
0474         this,
0475         [this](const QString &path) {
0476             Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
0477             if (res) {
0478                 res->matchModel.setFileListUpdate(path);
0479             }
0480         },
0481         Qt::QueuedConnection);
0482 
0483     connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, this, &KatePluginSearchView::clearDocMarksAndRanges);
0484     connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, &m_searchOpenFiles, &SearchOpenFiles::cancelSearch);
0485     connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, this, [this]() {
0486         Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
0487         if (res) {
0488             res->matchModel.cancelReplace();
0489         }
0490     });
0491 
0492     m_ui.searchCombo->lineEdit()->setPlaceholderText(i18n("Find"));
0493     // Hook into line edit context menus
0494     m_ui.searchCombo->setContextMenuPolicy(Qt::CustomContextMenu);
0495     connect(m_ui.searchCombo, &QComboBox::customContextMenuRequested, this, &KatePluginSearchView::searchContextMenu);
0496     m_ui.searchCombo->completer()->setCompletionMode(QCompleter::PopupCompletion);
0497     m_ui.searchCombo->completer()->setCaseSensitivity(Qt::CaseSensitive);
0498     m_ui.searchCombo->setInsertPolicy(QComboBox::NoInsert);
0499     m_ui.searchCombo->lineEdit()->setClearButtonEnabled(true);
0500     m_ui.searchCombo->setMaxCount(25);
0501     QAction *searchComboActionForInsertRegexButton =
0502         m_ui.searchCombo->lineEdit()->addAction(QIcon::fromTheme(QStringLiteral("code-context"), QIcon::fromTheme(QStringLiteral("edit-find-replace"))),
0503                                                 QLineEdit::TrailingPosition);
0504     connect(searchComboActionForInsertRegexButton, &QAction::triggered, this, [this]() {
0505         QMenu menu;
0506         QSet<QAction *> actionList;
0507         addRegexHelperActionsForSearch(&actionList, &menu);
0508         auto &&action = menu.exec(QCursor::pos());
0509         regexHelperActOnAction(action, actionList, m_ui.searchCombo->lineEdit());
0510     });
0511 
0512     m_ui.replaceCombo->lineEdit()->setPlaceholderText(i18n("Replace"));
0513     // Hook into line edit context menus
0514     m_ui.replaceCombo->setContextMenuPolicy(Qt::CustomContextMenu);
0515     connect(m_ui.replaceCombo, &QComboBox::customContextMenuRequested, this, &KatePluginSearchView::replaceContextMenu);
0516     m_ui.replaceCombo->completer()->setCompletionMode(QCompleter::PopupCompletion);
0517     m_ui.replaceCombo->completer()->setCaseSensitivity(Qt::CaseSensitive);
0518     m_ui.replaceCombo->setInsertPolicy(QComboBox::NoInsert);
0519     m_ui.replaceCombo->lineEdit()->setClearButtonEnabled(true);
0520     m_ui.replaceCombo->setMaxCount(25);
0521     QAction *replaceComboActionForInsertRegexButton =
0522         m_ui.replaceCombo->lineEdit()->addAction(QIcon::fromTheme(QStringLiteral("code-context")), QLineEdit::TrailingPosition);
0523     connect(replaceComboActionForInsertRegexButton, &QAction::triggered, this, [this]() {
0524         QMenu menu;
0525         QSet<QAction *> actionList;
0526         addRegexHelperActionsForReplace(&actionList, &menu);
0527         auto &&action = menu.exec(QCursor::pos());
0528         regexHelperActOnAction(action, actionList, m_ui.replaceCombo->lineEdit());
0529     });
0530     QAction *replaceComboActionForInsertSpecialButton = m_ui.replaceCombo->lineEdit()->addAction(QIcon::fromTheme(QStringLiteral("insert-text")), //
0531                                                                                                  QLineEdit::TrailingPosition);
0532     connect(replaceComboActionForInsertSpecialButton, &QAction::triggered, this, [this]() {
0533         QMenu menu;
0534         QSet<QAction *> actionList;
0535         addSpecialCharsHelperActionsForReplace(&actionList, &menu);
0536         auto &&action = menu.exec(QCursor::pos());
0537         regexHelperActOnAction(action, actionList, m_ui.replaceCombo->lineEdit());
0538     });
0539 
0540     connect(m_ui.useRegExp, &QToolButton::toggled, &m_changeTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
0541     auto onRegexToggleChanged = [this, searchComboActionForInsertRegexButton, replaceComboActionForInsertRegexButton] {
0542         Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
0543         if (res) {
0544             bool useRegExp = m_ui.useRegExp->isChecked();
0545             res->useRegExp = useRegExp;
0546             searchComboActionForInsertRegexButton->setVisible(useRegExp);
0547             replaceComboActionForInsertRegexButton->setVisible(useRegExp);
0548         }
0549     };
0550     connect(m_ui.useRegExp, &QToolButton::toggled, this, onRegexToggleChanged);
0551     onRegexToggleChanged(); // invoke initially
0552     m_changeTimer.setInterval(300);
0553     m_changeTimer.setSingleShot(true);
0554     connect(&m_changeTimer, &QTimer::timeout, this, &KatePluginSearchView::startSearchWhileTyping);
0555 
0556     m_toolView->setMinimumHeight(container->sizeHint().height());
0557 
0558     connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KatePluginSearchView::handleEsc);
0559 
0560     // watch for project plugin view creation/deletion
0561     connect(m_mainWindow, &KTextEditor::MainWindow::pluginViewCreated, this, &KatePluginSearchView::slotPluginViewCreated);
0562 
0563     connect(m_mainWindow, &KTextEditor::MainWindow::pluginViewDeleted, this, &KatePluginSearchView::slotPluginViewDeleted);
0564 
0565     connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KatePluginSearchView::updateMatchMarks);
0566 
0567     // Connect signals from project plugin to our slots
0568     m_projectPluginView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin"));
0569     slotPluginViewCreated(QStringLiteral("kateprojectplugin"), m_projectPluginView);
0570 
0571     searchPlaceChanged();
0572 
0573     m_toolView->installEventFilter(this);
0574 
0575     m_mainWindow->guiFactory()->addClient(this);
0576 
0577     // We dont want our shortcuts available in the whole mainwindow
0578     // It can conflict with Konsole shortcuts for e.g.,
0579     actionCollection()->removeAssociatedWidget(m_mainWindow->window());
0580     if (auto vm = m_mainWindow->window()->findChild<QWidget *>(QStringLiteral("KateViewManager"))) {
0581         actionCollection()->addAssociatedWidget(vm);
0582     }
0583     actionCollection()->addAssociatedWidget(container);
0584 
0585     auto e = KTextEditor::Editor::instance();
0586     connect(e, &KTextEditor::Editor::configChanged, this, &KatePluginSearchView::updateViewColors);
0587     updateViewColors();
0588 }
0589 
0590 KatePluginSearchView::~KatePluginSearchView()
0591 {
0592     cancelDiskFileSearch();
0593     clearMarksAndRanges();
0594     m_mainWindow->guiFactory()->removeClient(this);
0595     delete m_toolView;
0596 }
0597 
0598 QList<int> KatePluginSearchView::getDocumentSearchMarkedLines(KTextEditor::Document *currentDocument)
0599 {
0600     QList<int> result;
0601     if (!currentDocument) {
0602         return result;
0603     }
0604     QHash<int, KTextEditor::Mark *> documentMarksHash = currentDocument->marks();
0605     auto searchMarkType = KTextEditor::Document::SearchMatch;
0606     for (const int markedLineNumber : documentMarksHash.keys()) {
0607         auto documentMarkTypeMask = documentMarksHash.value(markedLineNumber)->type;
0608         if ((searchMarkType & documentMarkTypeMask) != searchMarkType)
0609             continue;
0610         result.push_back(markedLineNumber);
0611     }
0612     std::sort(result.begin(), result.end());
0613     return result;
0614 }
0615 
0616 void KatePluginSearchView::setClipboardFromDocumentLines(const KTextEditor::Document *currentDocument, const QList<int> lineNumbers)
0617 {
0618     QClipboard *clipboard = QGuiApplication::clipboard();
0619     QString text;
0620     for (int lineNumber : lineNumbers) {
0621         text += currentDocument->line(lineNumber);
0622         text += QLatin1String("\n");
0623     }
0624     clipboard->setText(text);
0625 }
0626 
0627 void KatePluginSearchView::cutSearchedLines()
0628 {
0629     if (!m_mainWindow->activeView()) {
0630         return;
0631     }
0632 
0633     KTextEditor::Document *currentDocument = m_mainWindow->activeView()->document();
0634     if (!currentDocument) {
0635         return;
0636     }
0637 
0638     QList<int> lineNumbers = getDocumentSearchMarkedLines(currentDocument);
0639     setClipboardFromDocumentLines(currentDocument, lineNumbers);
0640 
0641     // Iterate in descending line number order to remove the search matched lines to complete
0642     // the "cut" action.
0643     // Make one transaction for the whole remove to get all removes in one "undo"
0644     KTextEditor::Document::EditingTransaction transaction(currentDocument);
0645     for (auto iter = lineNumbers.rbegin(); iter != lineNumbers.rend(); ++iter) {
0646         int lineNumber = *iter;
0647         currentDocument->removeLine(lineNumber);
0648     }
0649 }
0650 
0651 void KatePluginSearchView::copySearchedLines()
0652 {
0653     if (!m_mainWindow->activeView()) {
0654         return;
0655     }
0656 
0657     KTextEditor::Document *currentDocument = m_mainWindow->activeView()->document();
0658     if (!currentDocument) {
0659         return;
0660     }
0661 
0662     QList<int> lineNumbers = getDocumentSearchMarkedLines(currentDocument);
0663     setClipboardFromDocumentLines(currentDocument, lineNumbers);
0664 }
0665 
0666 void KatePluginSearchView::navigateFolderUp()
0667 {
0668     // navigate one folder up
0669     m_ui.folderRequester->setUrl(localFileDirUp(m_ui.folderRequester->url()));
0670 }
0671 
0672 void KatePluginSearchView::setCurrentFolder()
0673 {
0674     if (!m_mainWindow) {
0675         return;
0676     }
0677     KTextEditor::View *editView = m_mainWindow->activeView();
0678     if (editView && editView->document()) {
0679         // upUrl as we want the folder not the file
0680         m_ui.folderRequester->setUrl(localFileDirUp(editView->document()->url()));
0681     }
0682     m_ui.displayOptions->setChecked(true);
0683 }
0684 
0685 void KatePluginSearchView::openSearchView()
0686 {
0687     if (!m_mainWindow) {
0688         return;
0689     }
0690     if (!m_toolView->isVisible()) {
0691         m_mainWindow->showToolView(m_toolView);
0692     }
0693     m_ui.searchCombo->setFocus(Qt::OtherFocusReason);
0694     if (m_ui.searchPlaceCombo->currentIndex() == MatchModel::Folder) {
0695         m_ui.displayOptions->setChecked(true);
0696     }
0697 
0698     KTextEditor::View *editView = m_mainWindow->activeView();
0699     if (editView && editView->document()) {
0700         if (m_ui.folderRequester->text().isEmpty()) {
0701             // upUrl as we want the folder not the file
0702             m_ui.folderRequester->setUrl(localFileDirUp(editView->document()->url()));
0703         }
0704         QString selection;
0705         if (editView->selection()) {
0706             selection = editView->selectionText();
0707             // remove possible trailing '\n'
0708             if (selection.endsWith(QLatin1Char('\n'))) {
0709                 selection = selection.left(selection.size() - 1);
0710             }
0711         }
0712         if (selection.isEmpty()) {
0713             selection = editView->document()->wordAt(editView->cursorPosition());
0714         }
0715 
0716         if (!selection.isEmpty() && !selection.contains(QLatin1Char('\n'))) {
0717             m_ui.searchCombo->blockSignals(true);
0718             m_ui.searchCombo->lineEdit()->setText(selection);
0719             m_ui.searchCombo->blockSignals(false);
0720         }
0721 
0722         m_ui.searchCombo->lineEdit()->selectAll();
0723         m_searchJustOpened = true;
0724         startSearchWhileTyping();
0725     }
0726 }
0727 
0728 void KatePluginSearchView::handleEsc(QEvent *e)
0729 {
0730     if (!m_mainWindow) {
0731         return;
0732     }
0733 
0734     QKeyEvent *k = static_cast<QKeyEvent *>(e);
0735     if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) {
0736         static ulong lastTimeStamp;
0737         if (lastTimeStamp == k->timestamp()) {
0738             // Same as previous... This looks like a bug somewhere...
0739             return;
0740         }
0741         lastTimeStamp = k->timestamp();
0742         if (!m_matchRanges.isEmpty()) {
0743             clearMarksAndRanges();
0744         } else if (m_toolView->isVisible()) {
0745             m_mainWindow->hideToolView(m_toolView);
0746         }
0747         // uncheck all so no new marks are added again when switching views
0748         Results *curResults = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
0749         if (curResults) {
0750             curResults->matchModel.uncheckAll();
0751         }
0752     }
0753 }
0754 
0755 void KatePluginSearchView::setSearchString(const QString &pattern)
0756 {
0757     m_ui.searchCombo->lineEdit()->setText(pattern);
0758 }
0759 
0760 void KatePluginSearchView::toggleOptions(bool show)
0761 {
0762     m_ui.stackedWidget->setCurrentIndex((show) ? 1 : 0);
0763     Results *curResults = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
0764     if (curResults) {
0765         curResults->displayFolderOptions = show;
0766     }
0767 }
0768 
0769 void KatePluginSearchView::setSearchPlace(int place)
0770 {
0771     if (place >= m_ui.searchPlaceCombo->count()) {
0772         // This probably means the project plugin is not active or no project loaded
0773         // fallback to search in folder
0774         qDebug() << place << "is not a valid search place index";
0775         place = MatchModel::Folder;
0776     }
0777     m_ui.searchPlaceCombo->setCurrentIndex(place);
0778 }
0779 
0780 QStringList KatePluginSearchView::filterFiles(const QStringList &files) const
0781 {
0782     QString types = m_ui.filterCombo->currentText();
0783     QString excludes = m_ui.excludeCombo->currentText();
0784     if (((types.isEmpty() || types == QLatin1String("*"))) && (excludes.isEmpty())) {
0785         // shortcut for use all files
0786         return files;
0787     }
0788 
0789     if (types.isEmpty()) {
0790         types = QStringLiteral("*");
0791     }
0792 
0793     const QStringList tmpTypes = types.split(QLatin1Char(','), Qt::SkipEmptyParts);
0794     QList<QRegularExpression> typeList;
0795     for (const auto &type : tmpTypes) {
0796         typeList << QRegularExpression(QRegularExpression::wildcardToRegularExpression(type.trimmed()));
0797     }
0798 
0799     const QStringList tmpExcludes = excludes.split(QLatin1Char(','), Qt::SkipEmptyParts);
0800     QList<QRegularExpression> excludeList;
0801     for (const auto &exclude : tmpExcludes) {
0802         excludeList << QRegularExpression(QRegularExpression::wildcardToRegularExpression(exclude.trimmed()));
0803     }
0804 
0805     QStringList filteredFiles;
0806     for (const QString &filePath : files) {
0807         bool isInSubDir = filePath.startsWith(m_resultBaseDir);
0808         QString nameToCheck = filePath;
0809         if (isInSubDir) {
0810             nameToCheck = filePath.mid(m_resultBaseDir.size());
0811         }
0812 
0813         bool skip = false;
0814         const QStringList pathSplit = nameToCheck.split(QLatin1Char('/'), Qt::SkipEmptyParts);
0815         for (const auto &regex : qAsConst(excludeList)) {
0816             for (const auto &part : pathSplit) {
0817                 QRegularExpressionMatch match = regex.match(part);
0818                 if (match.hasMatch()) {
0819                     skip = true;
0820                     break;
0821                 }
0822             }
0823         }
0824         if (skip) {
0825             continue;
0826         }
0827 
0828         QFileInfo fileInfo(filePath);
0829         QString fileName = fileInfo.fileName();
0830 
0831         for (const auto &regex : qAsConst(typeList)) {
0832             QRegularExpressionMatch match = regex.match(fileName);
0833             if (match.hasMatch()) {
0834                 filteredFiles << filePath;
0835                 break;
0836             }
0837         }
0838     }
0839     return filteredFiles;
0840 }
0841 
0842 void KatePluginSearchView::folderFileListChanged()
0843 {
0844     if (!m_curResults) {
0845         qWarning() << "This is a bug";
0846         searchDone();
0847         return;
0848     }
0849     QStringList fileList = m_folderFilesList.fileList();
0850 
0851     if (fileList.isEmpty()) {
0852         searchDone();
0853         return;
0854     }
0855 
0856     QList<KTextEditor::Document *> openList;
0857     const auto documents = m_kateApp->documents();
0858     for (int i = 0; i < documents.size(); i++) {
0859         int index = fileList.indexOf(documents[i]->url().toLocalFile());
0860         if (index != -1) {
0861             openList << documents[i];
0862             fileList.removeAt(index);
0863         }
0864     }
0865 
0866     // search order is important: Open files starts immediately and should finish
0867     // earliest after first event loop.
0868     // The DiskFile might finish immediately
0869     if (!openList.empty()) {
0870         m_searchOpenFiles.startSearch(openList, m_curResults->regExp);
0871     }
0872 
0873     startDiskFileSearch(fileList, m_curResults->regExp, m_ui.binaryCheckBox->isChecked());
0874 }
0875 
0876 void KatePluginSearchView::startDiskFileSearch(const QStringList &fileList, const QRegularExpression &reg, bool includeBinaryFiles)
0877 {
0878     if (fileList.isEmpty()) {
0879         searchDone();
0880         return;
0881     }
0882 
0883     // spread work to X threads => default to ideal thread count
0884     const int threadCount = m_searchDiskFilePool.maxThreadCount();
0885 
0886     // init worklist for these number of threads
0887     m_worklistForDiskFiles.init(fileList, threadCount);
0888 
0889     // spawn enough runnables, they will pull the files themself from our worklist
0890     // this must exactly match the count we used to init the worklist above, as this is used to finalize stuff!
0891     for (int i = 0; i < threadCount; ++i) {
0892         // new runnable, will pull work from the worklist itself!
0893         // worklist is used to drive if we need to stop the work, too!
0894         SearchDiskFiles *runner = new SearchDiskFiles(m_worklistForDiskFiles, reg, includeBinaryFiles);
0895 
0896         // queued connection for the results, this is emitted by a different thread than the runnable object and this one!
0897         connect(runner, &SearchDiskFiles::matchesFound, this, &KatePluginSearchView::matchesFound, Qt::QueuedConnection);
0898 
0899         // queued connection for the results, this is emitted by a different thread than the runnable object and this one!
0900         connect(
0901             runner,
0902             &SearchDiskFiles::destroyed,
0903             this,
0904             [this]() {
0905                 // signal the worklist one runnable more is done
0906                 m_worklistForDiskFiles.markOnRunnableAsDone();
0907 
0908                 // if no longer anything running, signal finished!
0909                 if (!m_worklistForDiskFiles.isRunning()) {
0910                     if (!m_diskSearchDoneTimer.isActive()) {
0911                         m_diskSearchDoneTimer.start();
0912                     }
0913                 }
0914             },
0915             Qt::QueuedConnection);
0916 
0917         // launch the runnable
0918         m_searchDiskFilePool.start(runner);
0919     }
0920 }
0921 
0922 void KatePluginSearchView::cancelDiskFileSearch()
0923 {
0924     // signal canceling to runnables
0925     m_worklistForDiskFiles.cancel();
0926 
0927     // wait for finalization
0928     m_searchDiskFilePool.clear();
0929     m_searchDiskFilePool.waitForDone();
0930 }
0931 
0932 bool KatePluginSearchView::searchingDiskFiles()
0933 {
0934     return m_worklistForDiskFiles.isRunning() || m_folderFilesList.isRunning();
0935 }
0936 
0937 void KatePluginSearchView::searchPlaceChanged()
0938 {
0939     int searchPlace = m_ui.searchPlaceCombo->currentIndex();
0940     const bool inFolder = (searchPlace == MatchModel::Folder);
0941 
0942     if (searchPlace < MatchModel::Folder) {
0943         m_ui.displayOptions->setChecked(false);
0944         m_ui.displayOptions->setEnabled(false);
0945     } else {
0946         m_ui.displayOptions->setEnabled(true);
0947         if (qobject_cast<QComboBox *>(sender())) {
0948             // Only display the options if the change was due to a direct
0949             // index change in the combo-box triggered by the user
0950             m_ui.displayOptions->setChecked(true);
0951         }
0952     }
0953 
0954     m_ui.filterCombo->setEnabled(searchPlace >= MatchModel::Folder);
0955     m_ui.excludeCombo->setEnabled(searchPlace >= MatchModel::Folder);
0956     m_ui.folderRequester->setEnabled(inFolder);
0957     m_ui.folderUpButton->setEnabled(inFolder);
0958     m_ui.currentFolderButton->setEnabled(inFolder);
0959     m_ui.recursiveCheckBox->setEnabled(inFolder);
0960     m_ui.hiddenCheckBox->setEnabled(inFolder);
0961     m_ui.symLinkCheckBox->setEnabled(inFolder);
0962     m_ui.binaryCheckBox->setEnabled(inFolder);
0963 
0964     if (inFolder && sender() == m_ui.searchPlaceCombo) {
0965         setCurrentFolder();
0966     }
0967 
0968     // ... and the labels:
0969     m_ui.folderLabel->setEnabled(m_ui.folderRequester->isEnabled());
0970     m_ui.filterLabel->setEnabled(m_ui.filterCombo->isEnabled());
0971     m_ui.excludeLabel->setEnabled(m_ui.excludeCombo->isEnabled());
0972 
0973     Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
0974     if (res) {
0975         res->searchPlaceIndex = searchPlace;
0976     }
0977 }
0978 
0979 void KatePluginSearchView::matchesFound(const QUrl &url, const QList<KateSearchMatch> &searchMatches, KTextEditor::Document *doc)
0980 {
0981     if (!m_curResults) {
0982         return;
0983     }
0984 
0985     m_curResults->matchModel.addMatches(url, searchMatches, doc);
0986     m_curResults->matches += searchMatches.size();
0987 }
0988 
0989 void KatePluginSearchView::stopClicked()
0990 {
0991     m_folderFilesList.terminateSearch();
0992     m_searchOpenFiles.cancelSearch();
0993     cancelDiskFileSearch();
0994     Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
0995     if (res) {
0996         res->matchModel.cancelReplace();
0997     }
0998 }
0999 
1000 /**
1001  * update the search widget colors and font. This is done on start of every
1002  * search so that if the user changes the theme, he can see the new colors
1003  * on the next search
1004  */
1005 void KatePluginSearchView::updateViewColors()
1006 {
1007     auto *e = KTextEditor::Editor::instance();
1008     const auto theme = e->theme();
1009 
1010     auto search = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::SearchHighlight));
1011     auto replace = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::ReplaceHighlight));
1012     auto fg = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Normal));
1013 
1014     if (!m_resultAttr) {
1015         m_resultAttr = new KTextEditor::Attribute();
1016     }
1017     m_resultAttr->clear();
1018     m_resultAttr->setBackground(search);
1019     m_resultAttr->setForeground(fg);
1020 
1021     m_replaceHighlightColor = replace;
1022 }
1023 
1024 // static QElapsedTimer s_timer;
1025 void KatePluginSearchView::startSearch()
1026 {
1027     // s_timer.start();
1028 
1029     // Forcefully stop any ongoing search or replace
1030     m_folderFilesList.terminateSearch();
1031     m_searchOpenFiles.terminateSearch();
1032     cancelDiskFileSearch();
1033 
1034     Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
1035     if (res) {
1036         res->matchModel.cancelReplace();
1037     }
1038 
1039     m_changeTimer.stop(); // make sure not to start a "while you type" search now
1040     m_mainWindow->showToolView(m_toolView); // in case we are invoked from the command interface
1041     m_projectSearchPlaceIndex = 0; // now that we started, don't switch back automatically
1042 
1043     if (m_ui.searchCombo->currentText().isEmpty()) {
1044         // return pressed in the folder combo or filter combo
1045         clearMarksAndRanges();
1046         return;
1047     }
1048 
1049     KTextEditor::View *activeView = m_mainWindow->activeView();
1050     QList<KTextEditor::Document *> documents;
1051     if ((m_ui.searchPlaceCombo->currentIndex() == MatchModel::CurrentFile && !activeView)
1052         || (m_ui.searchPlaceCombo->currentIndex() == MatchModel::OpenFiles && m_kateApp->documents().isEmpty())) {
1053         return;
1054     }
1055 
1056     m_isSearchAsYouType = false;
1057 
1058     QString currentSearchText = m_ui.searchCombo->currentText();
1059     m_ui.searchCombo->setItemText(0, QString()); // remove the text from index 0 on enter/search
1060     int index = m_ui.searchCombo->findText(currentSearchText);
1061     if (index > 0) {
1062         m_ui.searchCombo->removeItem(index);
1063     }
1064     m_ui.searchCombo->insertItem(1, currentSearchText);
1065     m_ui.searchCombo->setCurrentIndex(1);
1066 
1067     if (m_ui.filterCombo->findText(m_ui.filterCombo->currentText()) == -1) {
1068         m_ui.filterCombo->insertItem(0, m_ui.filterCombo->currentText());
1069         m_ui.filterCombo->setCurrentIndex(0);
1070     }
1071     if (m_ui.excludeCombo->findText(m_ui.excludeCombo->currentText()) == -1) {
1072         m_ui.excludeCombo->insertItem(0, m_ui.excludeCombo->currentText());
1073         m_ui.excludeCombo->setCurrentIndex(0);
1074     }
1075     if (m_ui.folderRequester->comboBox()->findText(m_ui.folderRequester->comboBox()->currentText()) == -1) {
1076         m_ui.folderRequester->comboBox()->insertItem(0, m_ui.folderRequester->comboBox()->currentText());
1077         m_ui.folderRequester->comboBox()->setCurrentIndex(0);
1078     }
1079     m_curResults = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
1080     if (!m_curResults) {
1081         qWarning() << "This is a bug";
1082         return;
1083     }
1084 
1085     QString pattern = (m_ui.useRegExp->isChecked() ? currentSearchText : QRegularExpression::escape(currentSearchText));
1086     QRegularExpression::PatternOptions patternOptions = QRegularExpression::UseUnicodePropertiesOption;
1087     if (!m_ui.matchCase->isChecked()) {
1088         patternOptions |= QRegularExpression::CaseInsensitiveOption;
1089     }
1090 
1091     if (m_ui.useRegExp->isChecked() && pattern.contains(QLatin1String("\\n"))) {
1092         patternOptions |= QRegularExpression::MultilineOption;
1093     }
1094     QRegularExpression reg(pattern, patternOptions);
1095 
1096     if (!reg.isValid()) {
1097         m_ui.searchCombo->setToolTip(reg.errorString());
1098         indicateMatch(MatchType::InvalidRegExp);
1099         return;
1100     }
1101     m_ui.searchCombo->setToolTip(QString());
1102 
1103     Q_EMIT searchBusy(true);
1104 
1105     m_curResults->searchStr = currentSearchText;
1106     m_curResults->regExp = reg;
1107     m_curResults->useRegExp = m_ui.useRegExp->isChecked();
1108     m_curResults->matchCase = m_ui.matchCase->isChecked();
1109     m_curResults->searchPlaceIndex = m_ui.searchPlaceCombo->currentIndex();
1110 
1111     m_ui.newTabButton->setDisabled(true);
1112     m_ui.searchCombo->setDisabled(true);
1113     m_ui.searchButton->setDisabled(true);
1114     m_ui.displayOptions->setChecked(false);
1115     m_ui.displayOptions->setDisabled(true);
1116     m_ui.replaceCheckedBtn->setDisabled(true);
1117     m_ui.replaceButton->setDisabled(true);
1118     m_ui.stopAndNext->setCurrentWidget(m_ui.stopButton);
1119     m_ui.replaceCombo->setDisabled(true);
1120     m_ui.searchPlaceCombo->setDisabled(true);
1121     m_ui.useRegExp->setDisabled(true);
1122     m_ui.matchCase->setDisabled(true);
1123     m_ui.expandResults->setDisabled(true);
1124     m_ui.currentFolderButton->setDisabled(true);
1125 
1126     clearMarksAndRanges();
1127     m_curResults->matches = 0;
1128 
1129     // BUG: 441340 We need to escape the & because it is used for accelerators/shortcut mnemonic by default
1130     QString tabName = m_ui.searchCombo->currentText();
1131     tabName.replace(QLatin1Char('&'), QLatin1String("&&"));
1132     m_tabBar->setTabText(m_ui.resultWidget->currentIndex(), tabName);
1133 
1134     m_toolView->setCursor(Qt::WaitCursor);
1135 
1136     const bool inCurrentProject = m_ui.searchPlaceCombo->currentIndex() == MatchModel::Project;
1137     const bool inAllOpenProjects = m_ui.searchPlaceCombo->currentIndex() == MatchModel::AllProjects;
1138 
1139     m_curResults->matchModel.clear();
1140     m_curResults->matchModel.setSearchPlace(static_cast<MatchModel::SearchPlaces>(m_curResults->searchPlaceIndex));
1141     m_curResults->matchModel.setSearchState(MatchModel::Searching);
1142     m_curResults->expandRoot();
1143 
1144     if (m_ui.searchPlaceCombo->currentIndex() == MatchModel::CurrentFile) {
1145         m_resultBaseDir.clear();
1146         QList<KTextEditor::Document *> documents;
1147         if (activeView) {
1148             documents << activeView->document();
1149         }
1150 
1151         m_searchOpenFiles.startSearch(documents, reg);
1152     } else if (m_ui.searchPlaceCombo->currentIndex() == MatchModel::OpenFiles) {
1153         m_resultBaseDir.clear();
1154         const QList<KTextEditor::Document *> documents = m_kateApp->documents();
1155         m_searchOpenFiles.startSearch(documents, reg);
1156     } else if (m_ui.searchPlaceCombo->currentIndex() == MatchModel::Folder) {
1157         m_resultBaseDir = m_ui.folderRequester->url().path();
1158         if (!m_resultBaseDir.isEmpty() && !m_resultBaseDir.endsWith(QLatin1Char('/'))) {
1159             m_resultBaseDir += QLatin1Char('/');
1160         }
1161         m_curResults->matchModel.setBaseSearchPath(m_resultBaseDir);
1162         m_folderFilesList.generateList(m_ui.folderRequester->text(),
1163                                        m_ui.recursiveCheckBox->isChecked(),
1164                                        m_ui.hiddenCheckBox->isChecked(),
1165                                        m_ui.symLinkCheckBox->isChecked(),
1166                                        m_ui.filterCombo->currentText(),
1167                                        m_ui.excludeCombo->currentText());
1168         // the file list will be ready when the thread returns (connected to folderFileListChanged)
1169     } else if (inCurrentProject || inAllOpenProjects) {
1170         /**
1171          * init search with file list from current project, if any
1172          */
1173         m_resultBaseDir.clear();
1174         QStringList files;
1175         if (m_projectPluginView) {
1176             if (inCurrentProject) {
1177                 m_resultBaseDir = m_projectPluginView->property("projectBaseDir").toString();
1178                 m_curResults->matchModel.setProjectName(m_projectPluginView->property("projectName").toString());
1179             } else {
1180                 m_resultBaseDir = m_projectPluginView->property("allProjectsCommonBaseDir").toString();
1181                 m_curResults->matchModel.setProjectName(m_projectPluginView->property("projectName").toString());
1182             }
1183 
1184             if (!m_resultBaseDir.endsWith(QLatin1Char('/'))) {
1185                 m_resultBaseDir += QLatin1Char('/');
1186             }
1187 
1188             QStringList projectFiles;
1189             if (inCurrentProject) {
1190                 projectFiles = m_projectPluginView->property("projectFiles").toStringList();
1191             } else {
1192                 projectFiles = m_projectPluginView->property("allProjectsFiles").toStringList();
1193             }
1194 
1195             files = filterFiles(projectFiles);
1196         }
1197         m_curResults->matchModel.setBaseSearchPath(m_resultBaseDir);
1198 
1199         QList<KTextEditor::Document *> openList;
1200         const auto docs = m_kateApp->documents();
1201         for (const auto doc : docs) {
1202             // match project file's list toLocalFile()
1203             int index = files.indexOf(doc->url().toLocalFile());
1204             if (index != -1) {
1205                 openList << doc;
1206                 files.removeAt(index);
1207             }
1208         }
1209         // search order is important: Open files starts immediately and should finish
1210         // earliest after first event loop.
1211         // The DiskFile might finish immediately
1212         if (!openList.empty()) {
1213             m_searchOpenFiles.startSearch(openList, m_curResults->regExp);
1214         }
1215         // We don't want to search for binary files in the project, so false is used instead of the checkbox
1216         // which is disabled in this case
1217         startDiskFileSearch(files, m_curResults->regExp, false);
1218     } else {
1219         qDebug() << "Case not handled:" << m_ui.searchPlaceCombo->currentIndex();
1220         Q_ASSERT_X(false, "KatePluginSearchView::startSearch", "case not handled");
1221     }
1222 }
1223 
1224 void KatePluginSearchView::startSearchWhileTyping()
1225 {
1226     if (searchingDiskFiles() || m_searchOpenFiles.searching()) {
1227         return;
1228     }
1229 
1230     // If the user has selected to disable search as you type, stop here
1231     int searchPlace = m_ui.searchPlaceCombo->currentIndex();
1232     if (!m_searchAsYouType.value(static_cast<MatchModel::SearchPlaces>(searchPlace), true)) {
1233         return;
1234     }
1235 
1236     QString currentSearchText = m_ui.searchCombo->currentText();
1237 
1238     m_ui.searchButton->setDisabled(currentSearchText.isEmpty());
1239 
1240     if (!m_mainWindow->activeView()) {
1241         return;
1242     }
1243 
1244     KTextEditor::Document *doc = m_mainWindow->activeView()->document();
1245     if (!doc) {
1246         return;
1247     }
1248 
1249     m_curResults = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
1250     if (!m_curResults) {
1251         qWarning() << "This is a bug";
1252         return;
1253     }
1254 
1255     // check if we typed something or just changed combobox index
1256     // changing index should not trigger a search-as-you-type
1257     if (m_ui.searchCombo->currentIndex() > 0 && currentSearchText == m_ui.searchCombo->itemText(m_ui.searchCombo->currentIndex())) {
1258         return;
1259     }
1260 
1261     // Now we should have a true typed text change
1262     m_isSearchAsYouType = true;
1263     clearMarksAndRanges();
1264 
1265     QString pattern = (m_ui.useRegExp->isChecked() ? currentSearchText : QRegularExpression::escape(currentSearchText));
1266     QRegularExpression::PatternOptions patternOptions = QRegularExpression::UseUnicodePropertiesOption;
1267     if (!m_ui.matchCase->isChecked()) {
1268         patternOptions |= QRegularExpression::CaseInsensitiveOption;
1269     }
1270     if (m_curResults->useRegExp && pattern.contains(QLatin1String("\\n"))) {
1271         patternOptions |= QRegularExpression::MultilineOption;
1272     }
1273     QRegularExpression reg(pattern, patternOptions);
1274 
1275     // If we have an invalid regex it could be caused by the user not having finished the query,
1276     // consequently we should ignore it skip the search but don't display an error message'
1277     if (!reg.isValid()) {
1278         indicateMatch(MatchType::InvalidRegExp);
1279         return;
1280     }
1281     m_ui.searchCombo->setToolTip(QString());
1282 
1283     Q_EMIT searchBusy(true);
1284 
1285     m_curResults->regExp = reg;
1286     m_curResults->useRegExp = m_ui.useRegExp->isChecked();
1287 
1288     m_ui.replaceCheckedBtn->setDisabled(true);
1289     m_ui.replaceButton->setDisabled(true);
1290     m_ui.nextButton->setDisabled(true);
1291 
1292     int cursorPosition = m_ui.searchCombo->lineEdit()->cursorPosition();
1293     bool hasSelected = m_ui.searchCombo->lineEdit()->hasSelectedText();
1294     m_ui.searchCombo->blockSignals(true);
1295     if (m_ui.searchCombo->count() == 0) {
1296         m_ui.searchCombo->insertItem(0, currentSearchText);
1297     } else {
1298         m_ui.searchCombo->setItemText(0, currentSearchText);
1299     }
1300     m_ui.searchCombo->setCurrentIndex(0);
1301     m_ui.searchCombo->lineEdit()->setCursorPosition(cursorPosition);
1302     if (hasSelected) {
1303         // This restores the select all from invoking openSearchView
1304         // This selects too much if we have a partial selection and toggle match-case/regexp
1305         m_ui.searchCombo->lineEdit()->selectAll();
1306     }
1307     m_ui.searchCombo->blockSignals(false);
1308 
1309     // Prepare for the new search content
1310     m_resultBaseDir.clear();
1311     m_curResults->matches = 0;
1312 
1313     m_curResults->matchModel.clear();
1314     m_curResults->matchModel.setSearchPlace(MatchModel::CurrentFile);
1315     m_curResults->matchModel.setSearchState(MatchModel::Searching);
1316     m_curResults->expandRoot();
1317 
1318     // Do the search
1319     int searchStoppedAt = m_searchOpenFiles.searchOpenFile(doc, reg, 0);
1320     searchWhileTypingDone();
1321 
1322     if (searchStoppedAt != 0) {
1323         delete m_infoMessage;
1324         const QString msg = i18n("Searching while you type was interrupted. It would have taken too long.");
1325         m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Warning);
1326         m_infoMessage->setPosition(KTextEditor::Message::TopInView);
1327         m_infoMessage->setAutoHide(3000);
1328         m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
1329         m_infoMessage->setView(m_mainWindow->activeView());
1330         m_mainWindow->activeView()->document()->postMessage(m_infoMessage);
1331     }
1332 
1333     // We need to escape the & because it is used for accelerators/shortcut mnemonic by default
1334     QString tabName = m_ui.searchCombo->currentText();
1335     tabName.replace(QLatin1Char('&'), QLatin1String("&&"));
1336     m_tabBar->setTabText(m_ui.resultWidget->currentIndex(), tabName);
1337 }
1338 
1339 void KatePluginSearchView::searchDone()
1340 {
1341     m_changeTimer.stop(); // avoid "while you type" search directly after
1342 
1343     if (searchingDiskFiles() || m_searchOpenFiles.searching()) {
1344         return;
1345     }
1346 
1347     QWidget *fw = QApplication::focusWidget();
1348     // NOTE: we take the focus widget here before the enabling/disabling
1349     // moves the focus around.
1350     m_ui.newTabButton->setDisabled(false);
1351     m_ui.searchCombo->setDisabled(false);
1352     m_ui.searchButton->setDisabled(false);
1353     m_ui.stopAndNext->setCurrentWidget(m_ui.nextButton);
1354     m_ui.displayOptions->setDisabled(false);
1355     m_ui.replaceCombo->setDisabled(false);
1356     m_ui.searchPlaceCombo->setDisabled(false);
1357     m_ui.useRegExp->setDisabled(false);
1358     m_ui.matchCase->setDisabled(false);
1359     m_ui.expandResults->setDisabled(false);
1360     m_ui.currentFolderButton->setDisabled(m_ui.searchPlaceCombo->currentIndex() != MatchModel::Folder);
1361 
1362     Q_EMIT searchBusy(false);
1363 
1364     if (!m_curResults) {
1365         return;
1366     }
1367 
1368     m_ui.replaceCheckedBtn->setDisabled(m_curResults->matches < 1);
1369     m_ui.replaceButton->setDisabled(m_curResults->matches < 1);
1370     m_ui.nextButton->setDisabled(m_curResults->matches < 1);
1371     m_ui.filterBtn->setDisabled(m_curResults->matches <= 1);
1372 
1373     // Set search to done. This sorts the model and collapses all items in the view
1374     m_curResults->matchModel.setSearchState(MatchModel::SearchDone);
1375 
1376     // expand the "header item " to display all files and all results if configured
1377     expandResults();
1378 
1379     m_curResults->treeView->resizeColumnToContents(0);
1380 
1381     indicateMatch(m_curResults->matches > 0 ? MatchType::HasMatch : MatchType::NoMatch);
1382 
1383     m_toolView->unsetCursor();
1384 
1385     if (fw == m_ui.stopButton) {
1386         m_ui.searchCombo->setFocus();
1387     }
1388 
1389     m_searchJustOpened = false;
1390     m_curResults->searchStr = m_ui.searchCombo->currentText();
1391     m_curResults = nullptr;
1392     updateMatchMarks();
1393 
1394     // qDebug() << "done:" << s_timer.elapsed();
1395 }
1396 
1397 void KatePluginSearchView::searchWhileTypingDone()
1398 {
1399     Q_EMIT searchBusy(false);
1400 
1401     if (!m_curResults) {
1402         return;
1403     }
1404 
1405     bool popupVisible = m_ui.searchCombo->lineEdit()->completer()->popup()->isVisible();
1406 
1407     m_ui.replaceCheckedBtn->setDisabled(m_curResults->matches < 1);
1408     m_ui.replaceButton->setDisabled(m_curResults->matches < 1);
1409     m_ui.nextButton->setDisabled(m_curResults->matches < 1);
1410     m_ui.filterBtn->setDisabled(m_curResults->matches <= 1);
1411 
1412     m_curResults->treeView->expandAll();
1413     m_curResults->treeView->resizeColumnToContents(0);
1414     if (m_curResults->treeView->columnWidth(0) < m_curResults->treeView->width() - 30) {
1415         m_curResults->treeView->setColumnWidth(0, m_curResults->treeView->width() - 30);
1416     }
1417 
1418     // Set search to done. This sorts the model and collapses all items in the view
1419     m_curResults->matchModel.setSearchState(MatchModel::SearchDone);
1420 
1421     // expand the "header item " to display all files and all results if configured
1422     expandResults();
1423 
1424     indicateMatch(m_curResults->matches > 0 ? MatchType::HasMatch : MatchType::NoMatch);
1425 
1426     if (popupVisible) {
1427         m_ui.searchCombo->lineEdit()->completer()->complete();
1428     }
1429     if (!m_searchJustOpened && m_ui.displayOptions->isEnabled()) {
1430         m_ui.displayOptions->setChecked(false);
1431     }
1432 
1433     m_searchJustOpened = false;
1434     m_curResults->searchStr = m_ui.searchCombo->currentText();
1435 
1436     m_curResults = nullptr;
1437 
1438     updateMatchMarks();
1439 }
1440 
1441 void KatePluginSearchView::indicateMatch(MatchType matchType)
1442 {
1443     QLineEdit *const lineEdit = m_ui.searchCombo->lineEdit();
1444     QPalette background(lineEdit->palette());
1445 
1446     if (matchType == MatchType::HasMatch) {
1447         // Green background for line edit
1448         KColorScheme::adjustBackground(background, KColorScheme::PositiveBackground);
1449     } else if (matchType == MatchType::InvalidRegExp) {
1450         // Red background in case an error occured
1451         KColorScheme::adjustBackground(background, KColorScheme::NegativeBackground);
1452     } else {
1453         // Reset background of line edit
1454         background = QPalette();
1455     }
1456     // Red background for line edit
1457     // KColorScheme::adjustBackground(background, KColorScheme::NegativeBackground);
1458     // Neutral background
1459     // KColorScheme::adjustBackground(background, KColorScheme::NeutralBackground);
1460 
1461     lineEdit->setPalette(background);
1462 }
1463 
1464 void KatePluginSearchView::replaceSingleMatch()
1465 {
1466     // Save the search text
1467     if (m_ui.searchCombo->findText(m_ui.searchCombo->currentText()) == -1) {
1468         m_ui.searchCombo->insertItem(1, m_ui.searchCombo->currentText());
1469         m_ui.searchCombo->setCurrentIndex(1);
1470     }
1471 
1472     // Save the replace text
1473     if (m_ui.replaceCombo->findText(m_ui.replaceCombo->currentText()) == -1) {
1474         m_ui.replaceCombo->insertItem(1, m_ui.replaceCombo->currentText());
1475         m_ui.replaceCombo->setCurrentIndex(1);
1476     }
1477 
1478     // Check if the cursor is at the current item if not jump there
1479     Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
1480     if (!res) {
1481         return; // Security measure
1482     }
1483 
1484     QModelIndex itemIndex = res->treeView->currentIndex();
1485     if (!itemIndex.isValid() || !res->isMatch(itemIndex)) {
1486         goToNextMatch();
1487         // If no item was selected "Replace" is similar to just pressing "Next"
1488         // We do not want to replace a string we do not see with plain "Replace"
1489         return;
1490     }
1491 
1492     if (!m_mainWindow->activeView() || !m_mainWindow->activeView()->cursorPosition().isValid()) {
1493         itemSelected(itemIndex); // Correct any bad cursor positions
1494         return;
1495     }
1496 
1497     KTextEditor::Range matchRange = res->matchRange(itemIndex);
1498 
1499     if (m_mainWindow->activeView()->cursorPosition() != matchRange.start()) {
1500         itemSelected(itemIndex);
1501         return;
1502     }
1503 
1504     Q_EMIT searchBusy(true);
1505 
1506     KTextEditor::Document *doc = m_mainWindow->activeView()->document();
1507 
1508     // FIXME The document might have been edited after the search.
1509     // Fix the ranges before attempting the replace
1510     res->replaceSingleMatch(doc, itemIndex, res->regExp, m_ui.replaceCombo->currentText());
1511 
1512     goToNextMatch();
1513 }
1514 
1515 void KatePluginSearchView::replaceChecked()
1516 {
1517     // Sync the current documents ranges with the model in case it has been edited
1518     syncModelRanges();
1519 
1520     // Clear match marks and ranges
1521     // we MUST do this because after we are done replacing, our current moving ranges
1522     // destroy the replace ranges and we don't get the highlights for replace for the
1523     // current open doc
1524     clearMarksAndRanges();
1525 
1526     if (m_ui.searchCombo->findText(m_ui.searchCombo->currentText()) == -1) {
1527         m_ui.searchCombo->insertItem(1, m_ui.searchCombo->currentText());
1528         m_ui.searchCombo->setCurrentIndex(1);
1529     }
1530 
1531     if (m_ui.replaceCombo->findText(m_ui.replaceCombo->currentText()) == -1) {
1532         m_ui.replaceCombo->insertItem(1, m_ui.replaceCombo->currentText());
1533         m_ui.replaceCombo->setCurrentIndex(1);
1534     }
1535 
1536     m_curResults = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
1537     if (!m_curResults) {
1538         qWarning() << "Results not found";
1539         return;
1540     }
1541 
1542     Q_EMIT searchBusy(true);
1543 
1544     m_ui.stopAndNext->setCurrentWidget(m_ui.stopButton);
1545     m_ui.displayOptions->setChecked(false);
1546     m_ui.displayOptions->setDisabled(true);
1547     m_ui.newTabButton->setDisabled(true);
1548     m_ui.searchCombo->setDisabled(true);
1549     m_ui.searchButton->setDisabled(true);
1550     m_ui.replaceCheckedBtn->setDisabled(true);
1551     m_ui.replaceButton->setDisabled(true);
1552     m_ui.replaceCombo->setDisabled(true);
1553     m_ui.searchPlaceCombo->setDisabled(true);
1554     m_ui.useRegExp->setDisabled(true);
1555     m_ui.matchCase->setDisabled(true);
1556     m_ui.expandResults->setDisabled(true);
1557     m_ui.currentFolderButton->setDisabled(true);
1558 
1559     m_curResults->replaceStr = m_ui.replaceCombo->currentText();
1560 
1561     m_curResults->matchModel.replaceChecked(m_curResults->regExp, m_curResults->replaceStr);
1562 }
1563 
1564 void KatePluginSearchView::replaceDone()
1565 {
1566     m_ui.stopAndNext->setCurrentWidget(m_ui.nextButton);
1567     m_ui.replaceCombo->setDisabled(false);
1568     m_ui.newTabButton->setDisabled(false);
1569     m_ui.searchCombo->setDisabled(false);
1570     m_ui.searchButton->setDisabled(false);
1571     m_ui.replaceCheckedBtn->setDisabled(false);
1572     m_ui.replaceButton->setDisabled(false);
1573     m_ui.displayOptions->setDisabled(false);
1574     m_ui.searchPlaceCombo->setDisabled(false);
1575     m_ui.useRegExp->setDisabled(false);
1576     m_ui.matchCase->setDisabled(false);
1577     m_ui.expandResults->setDisabled(false);
1578     m_ui.currentFolderButton->setDisabled(false);
1579     updateMatchMarks();
1580 
1581     Q_EMIT searchBusy(false);
1582 }
1583 
1584 /** Remove all moving ranges and document marks belonging to Search & Replace */
1585 void KatePluginSearchView::clearMarksAndRanges()
1586 {
1587     // If we have a MovingRange we have a corresponding MatchMark
1588     while (!m_matchRanges.isEmpty()) {
1589         clearDocMarksAndRanges(m_matchRanges.first()->document());
1590     }
1591 }
1592 
1593 void KatePluginSearchView::clearDocMarksAndRanges(KTextEditor::Document *doc)
1594 {
1595     // Before removing the ranges try to update the ranges in the model in case we have document changes.
1596     syncModelRanges();
1597 
1598     if (doc) {
1599         const QHash<int, KTextEditor::Mark *> marks = doc->marks();
1600         QHashIterator<int, KTextEditor::Mark *> i(marks);
1601         while (i.hasNext()) {
1602             i.next();
1603             if (i.value()->type & KTextEditor::Document::markType32) {
1604                 doc->removeMark(i.value()->line, KTextEditor::Document::markType32);
1605             }
1606         }
1607     }
1608 
1609     m_matchRanges.erase(std::remove_if(m_matchRanges.begin(),
1610                                        m_matchRanges.end(),
1611                                        [doc](KTextEditor::MovingRange *r) {
1612                                            if (r->document() == doc) {
1613                                                delete r;
1614                                                return true;
1615                                            }
1616                                            return false;
1617                                        }),
1618                         m_matchRanges.end());
1619 }
1620 
1621 void KatePluginSearchView::addRangeAndMark(KTextEditor::Document *doc, const KateSearchMatch &match, KTextEditor::Attribute::Ptr attr)
1622 {
1623     if (!doc || !match.checked) {
1624         return;
1625     }
1626 
1627     bool isReplaced = !match.replaceText.isEmpty();
1628 
1629     // Check that the match still matches
1630     if (m_curResults) {
1631         if (!isReplaced) {
1632             auto regMatch = MatchModel::rangeTextMatches(doc->text(match.range), m_curResults->regExp);
1633             if (regMatch.capturedStart() != 0) {
1634                 // qDebug() << doc->text(range) << "Does not match" << m_curResults->regExp.pattern();
1635                 return;
1636             }
1637         } else {
1638             if (doc->text(match.range) != match.replaceText) {
1639                 // qDebug() << doc->text(match.range) << "Does not match" << match.replaceText;
1640                 return;
1641             }
1642         }
1643     }
1644 
1645     // Highlight the match
1646     if (isReplaced) {
1647         attr->setBackground(m_replaceHighlightColor);
1648     }
1649 
1650     KTextEditor::MovingRange *mr = doc->newMovingRange(match.range);
1651     mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection
1652     mr->setAttribute(attr);
1653     mr->setAttributeOnlyForViews(true);
1654     m_matchRanges.append(mr);
1655 
1656     // Add a match mark
1657     static const auto description = i18n("Search Match");
1658     doc->setMarkDescription(KTextEditor::Document::markType32, description);
1659     doc->setMarkIcon(KTextEditor::Document::markType32, QIcon());
1660     doc->addMark(match.range.start().line(), KTextEditor::Document::markType32);
1661 }
1662 
1663 void KatePluginSearchView::updateCheckState(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles)
1664 {
1665     Q_UNUSED(topLeft);
1666     Q_UNUSED(bottomRight);
1667 
1668     // check tailored to the way signal is raised by the model
1669     // keep the check simple in case each one is one of many
1670     if (roles.size() == 0 || roles.size() > 1 || roles[0] != Qt::CheckStateRole) {
1671         return;
1672     }
1673     // more updates might follow, let's batch those
1674     if (!m_updateCheckedStateTimer.isActive()) {
1675         m_updateCheckedStateTimer.start();
1676     }
1677 }
1678 
1679 void KatePluginSearchView::updateMatchMarks()
1680 {
1681     // We only keep marks & ranges for one document at a time so clear the rest
1682     // This will also update the model ranges corresponding to the cleared ranges.
1683     clearMarksAndRanges();
1684 
1685     if (!m_mainWindow->activeView()) {
1686         return;
1687     }
1688 
1689     Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
1690     if (!res || res->isEmpty()) {
1691         return;
1692     }
1693     m_curResults = res;
1694 
1695     // add the marks if it is not already open
1696     KTextEditor::Document *doc = m_mainWindow->activeView()->document();
1697     if (!doc) {
1698         return;
1699     }
1700 
1701     connect(doc, &KTextEditor::Document::aboutToInvalidateMovingInterfaceContent, this, &KatePluginSearchView::clearMarksAndRanges, Qt::UniqueConnection);
1702     // Re-add the highlighting on document reload
1703     connect(doc, &KTextEditor::Document::reloaded, this, &KatePluginSearchView::updateMatchMarks, Qt::UniqueConnection);
1704     // Re-do highlight upon check mark update
1705     connect(&res->matchModel, &QAbstractItemModel::dataChanged, this, &KatePluginSearchView::updateCheckState, Qt::UniqueConnection);
1706 
1707     // Add match marks for all matches in the file
1708     const QList<KateSearchMatch> &fileMatches = res->matchModel.fileMatches(doc);
1709     for (const KateSearchMatch &match : fileMatches) {
1710         addRangeAndMark(doc, match, m_resultAttr);
1711     }
1712 }
1713 
1714 void KatePluginSearchView::syncModelRanges()
1715 {
1716     if (!m_curResults) {
1717         return;
1718     }
1719     // NOTE: We assume there are only ranges for one document in the ranges at a time...
1720     m_curResults->matchModel.updateMatchRanges(m_matchRanges);
1721 }
1722 
1723 void KatePluginSearchView::expandResults()
1724 {
1725     m_curResults = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
1726     if (!m_curResults) {
1727         qWarning() << "Results not found";
1728         return;
1729     }
1730 
1731     // we expand recursively if we either are told so or we have just one toplevel match item
1732     auto *model = m_curResults->treeView->model();
1733     QModelIndex rootItem = model->index(0, 0);
1734     if ((m_ui.expandResults->isChecked() && model->rowCount(rootItem) < 200) || model->rowCount(rootItem) == 1) {
1735         m_curResults->treeView->expandAll();
1736     } else {
1737         // first collapse all and the expand the root, much faster than collapsing all children manually
1738         m_curResults->treeView->collapseAll();
1739         m_curResults->treeView->expand(rootItem);
1740     }
1741 }
1742 
1743 void KatePluginSearchView::itemSelected(const QModelIndex &item)
1744 {
1745     m_curResults = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
1746     if (!m_curResults) {
1747         qDebug() << "No result widget available";
1748         return;
1749     }
1750 
1751     // Sync the current document matches with the model before jumping
1752     // FIXME do we want to do this on every edit in stead?
1753     syncModelRanges();
1754 
1755     // open any children to go to the first match in the file
1756     QModelIndex matchItem = item;
1757     if (item.model() == m_curResults->model()) {
1758         while (m_curResults->model()->hasChildren(matchItem)) {
1759             matchItem = m_curResults->model()->index(0, 0, matchItem);
1760         }
1761         m_curResults->treeView->setCurrentIndex(matchItem);
1762     }
1763 
1764     // get stuff
1765     int toLine = matchItem.data(MatchModel::StartLineRole).toInt();
1766     int toColumn = matchItem.data(MatchModel::StartColumnRole).toInt();
1767     QUrl url = matchItem.data(MatchModel::FileUrlRole).toUrl();
1768 
1769     // If this url is invalid, it could be that we are searching an unsaved file
1770     // use doc ptr in that case.
1771     KTextEditor::Document *doc = nullptr;
1772     if (url.isValid()) {
1773         doc = m_kateApp->findUrl(url);
1774         // add the marks to the document if it is not already open
1775         if (!doc) {
1776             doc = m_kateApp->openUrl(url);
1777         }
1778     } else {
1779         doc = matchItem.data(MatchModel::DocumentRole).value<KTextEditor::Document *>();
1780         if (!doc) {
1781             // maybe the doc was closed
1782             return;
1783         }
1784     }
1785 
1786     if (!doc) {
1787         qWarning() << "Could not open" << url;
1788         Q_ASSERT(false); // If we get here we have a bug
1789         return;
1790     }
1791 
1792     // open the right view...
1793     m_mainWindow->activateView(doc);
1794 
1795     // any view active?
1796     if (!m_mainWindow->activeView()) {
1797         qDebug() << "Could not activate view for:" << url;
1798         Q_ASSERT(false);
1799         return;
1800     }
1801 
1802     // set the cursor to the correct position
1803     m_mainWindow->activeView()->setCursorPosition(KTextEditor::Cursor(toLine, toColumn));
1804     m_mainWindow->activeView()->setFocus();
1805 }
1806 
1807 void KatePluginSearchView::goToNextMatch()
1808 {
1809     Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
1810     if (!res) {
1811         return;
1812     }
1813     m_curResults = res;
1814 
1815     m_ui.displayOptions->setChecked(false);
1816 
1817     QModelIndex currentIndex = res->treeView->currentIndex();
1818     bool focusInView = m_mainWindow->activeView() && m_mainWindow->activeView()->hasFocus();
1819 
1820     if (!currentIndex.isValid() && focusInView) {
1821         // no item has been visited && focus is not in searchCombo (probably in the view) ->
1822         // jump to the closest match after current cursor position
1823         auto *doc = m_mainWindow->activeView()->document();
1824 
1825         // check if current file is in the file list
1826         currentIndex = res->firstFileMatch(doc);
1827         if (currentIndex.isValid()) {
1828             // We have the index of the first match in the file
1829             // expand the file item
1830             res->treeView->expand(currentIndex.parent());
1831 
1832             // check if we can get the next match after the
1833             currentIndex = res->closestMatchAfter(doc, m_mainWindow->activeView()->cursorPosition());
1834             if (currentIndex.isValid()) {
1835                 itemSelected(currentIndex);
1836                 delete m_infoMessage;
1837                 const QString msg = i18n("Next from cursor");
1838                 m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information);
1839                 m_infoMessage->setPosition(KTextEditor::Message::BottomInView);
1840                 m_infoMessage->setAutoHide(2000);
1841                 m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
1842                 m_infoMessage->setView(m_mainWindow->activeView());
1843                 m_mainWindow->activeView()->document()->postMessage(m_infoMessage);
1844                 return;
1845             }
1846         }
1847     }
1848 
1849     if (!currentIndex.isValid()) {
1850         currentIndex = res->firstMatch();
1851         if (currentIndex.isValid()) {
1852             itemSelected(currentIndex);
1853             delete m_infoMessage;
1854             const QString msg = i18n("Starting from first match");
1855             m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information);
1856             m_infoMessage->setPosition(KTextEditor::Message::TopInView);
1857             m_infoMessage->setAutoHide(2000);
1858             m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
1859             m_infoMessage->setView(m_mainWindow->activeView());
1860             m_mainWindow->activeView()->document()->postMessage(m_infoMessage);
1861             return;
1862         }
1863     }
1864     if (!currentIndex.isValid()) {
1865         // no matches to activate
1866         return;
1867     }
1868 
1869     // we had an active item go to next
1870     currentIndex = res->nextMatch(currentIndex);
1871     itemSelected(currentIndex);
1872     if (currentIndex == res->firstMatch()) {
1873         delete m_infoMessage;
1874         const QString msg = i18n("Continuing from first match");
1875         m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information);
1876         m_infoMessage->setPosition(KTextEditor::Message::TopInView);
1877         m_infoMessage->setAutoHide(2000);
1878         m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
1879         m_infoMessage->setView(m_mainWindow->activeView());
1880         m_mainWindow->activeView()->document()->postMessage(m_infoMessage);
1881     }
1882 }
1883 
1884 void KatePluginSearchView::goToPreviousMatch()
1885 {
1886     Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
1887     if (!res) {
1888         return;
1889     }
1890     m_curResults = res;
1891 
1892     m_ui.displayOptions->setChecked(false);
1893 
1894     QModelIndex currentIndex = res->treeView->currentIndex();
1895     bool focusInView = m_mainWindow->activeView() && m_mainWindow->activeView()->hasFocus();
1896 
1897     if (!currentIndex.isValid() && focusInView) {
1898         // no item has been visited && focus is not in the view ->
1899         // jump to the closest match before current cursor position
1900         auto *doc = m_mainWindow->activeView()->document();
1901 
1902         // check if current file is in the file list
1903         currentIndex = res->firstFileMatch(doc);
1904         if (currentIndex.isValid()) {
1905             // We have the index of the first match in the file
1906             // expand the file item
1907             res->treeView->expand(currentIndex.parent());
1908 
1909             // check if we can get the next match after the
1910             currentIndex = res->closestMatchBefore(doc, m_mainWindow->activeView()->cursorPosition());
1911             if (currentIndex.isValid()) {
1912                 itemSelected(currentIndex);
1913                 delete m_infoMessage;
1914                 const QString msg = i18n("Next from cursor");
1915                 m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information);
1916                 m_infoMessage->setPosition(KTextEditor::Message::BottomInView);
1917                 m_infoMessage->setAutoHide(2000);
1918                 m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
1919                 m_infoMessage->setView(m_mainWindow->activeView());
1920                 m_mainWindow->activeView()->document()->postMessage(m_infoMessage);
1921                 return;
1922             }
1923         }
1924     }
1925 
1926     if (!currentIndex.isValid()) {
1927         currentIndex = res->lastMatch();
1928         if (currentIndex.isValid()) {
1929             itemSelected(currentIndex);
1930             delete m_infoMessage;
1931             const QString msg = i18n("Starting from last match");
1932             m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information);
1933             m_infoMessage->setPosition(KTextEditor::Message::TopInView);
1934             m_infoMessage->setAutoHide(2000);
1935             m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
1936             m_infoMessage->setView(m_mainWindow->activeView());
1937             m_mainWindow->activeView()->document()->postMessage(m_infoMessage);
1938             return;
1939         }
1940     }
1941     if (!currentIndex.isValid()) {
1942         // no matches to activate
1943         return;
1944     }
1945 
1946     // we had an active item go to next
1947     currentIndex = res->prevMatch(currentIndex);
1948     itemSelected(currentIndex);
1949     if (currentIndex == res->lastMatch()) {
1950         delete m_infoMessage;
1951         const QString msg = i18n("Continuing from last match");
1952         m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information);
1953         m_infoMessage->setPosition(KTextEditor::Message::TopInView);
1954         m_infoMessage->setAutoHide(2000);
1955         m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
1956         m_infoMessage->setView(m_mainWindow->activeView());
1957         m_mainWindow->activeView()->document()->postMessage(m_infoMessage);
1958     }
1959 }
1960 
1961 void KatePluginSearchView::setRegexMode(bool enabled)
1962 {
1963     m_ui.useRegExp->setChecked(enabled);
1964 }
1965 
1966 void KatePluginSearchView::setCaseInsensitive(bool enabled)
1967 {
1968     m_ui.matchCase->setChecked(enabled);
1969 }
1970 
1971 void KatePluginSearchView::setExpandResults(bool enabled)
1972 {
1973     m_ui.expandResults->setChecked(enabled);
1974 }
1975 
1976 void KatePluginSearchView::readSessionConfig(const KConfigGroup &cg)
1977 {
1978     m_ui.searchCombo->clear();
1979     m_ui.searchCombo->addItem(QString()); // Add empty Item
1980     m_ui.searchCombo->addItems(cg.readEntry("Search", QStringList()));
1981     m_ui.replaceCombo->clear();
1982     m_ui.replaceCombo->addItem(QString()); // Add empty Item
1983     m_ui.replaceCombo->addItems(cg.readEntry("Replaces", QStringList()));
1984     m_ui.matchCase->setChecked(cg.readEntry("MatchCase", false));
1985     m_ui.useRegExp->setChecked(cg.readEntry("UseRegExp", false));
1986     m_ui.expandResults->setChecked(cg.readEntry("ExpandSearchResults", false));
1987 
1988     int searchPlaceIndex = cg.readEntry("Place", 1);
1989     if (searchPlaceIndex < 0) {
1990         searchPlaceIndex = MatchModel::Folder; // for the case we happen to read -1 as Place
1991     }
1992     if ((searchPlaceIndex >= MatchModel::Project) && (searchPlaceIndex >= m_ui.searchPlaceCombo->count())) {
1993         // handle the case that project mode was selected, but not yet available
1994         m_projectSearchPlaceIndex = searchPlaceIndex;
1995         searchPlaceIndex = MatchModel::Folder;
1996     }
1997     m_ui.searchPlaceCombo->setCurrentIndex(searchPlaceIndex);
1998 
1999     m_ui.recursiveCheckBox->setChecked(cg.readEntry("Recursive", true));
2000     m_ui.hiddenCheckBox->setChecked(cg.readEntry("HiddenFiles", false));
2001     m_ui.symLinkCheckBox->setChecked(cg.readEntry("FollowSymLink", false));
2002     m_ui.binaryCheckBox->setChecked(cg.readEntry("BinaryFiles", false));
2003     m_ui.folderRequester->comboBox()->clear();
2004     m_ui.folderRequester->comboBox()->addItems(cg.readEntry("SearchDiskFiless", QStringList()));
2005     m_ui.folderRequester->setText(cg.readEntry("SearchDiskFiles", QString()));
2006     m_ui.filterCombo->clear();
2007     m_ui.filterCombo->addItems(cg.readEntry("Filters", QStringList()));
2008     m_ui.filterCombo->setCurrentIndex(cg.readEntry("CurrentFilter", -1));
2009     m_ui.excludeCombo->clear();
2010     m_ui.excludeCombo->addItems(cg.readEntry("ExcludeFilters", QStringList()));
2011     m_ui.excludeCombo->setCurrentIndex(cg.readEntry("CurrentExcludeFilter", -1));
2012     m_ui.displayOptions->setChecked(searchPlaceIndex == MatchModel::Folder);
2013 
2014     // Search as you type
2015     m_searchAsYouType.insert(MatchModel::CurrentFile, cg.readEntry("SearchAsYouTypeCurrentFile", true));
2016     m_searchAsYouType.insert(MatchModel::OpenFiles, cg.readEntry("SearchAsYouTypeOpenFiles", true));
2017     m_searchAsYouType.insert(MatchModel::Folder, cg.readEntry("SearchAsYouTypeFolder", true));
2018     m_searchAsYouType.insert(MatchModel::Project, cg.readEntry("SearchAsYouTypeProject", true));
2019     m_searchAsYouType.insert(MatchModel::AllProjects, cg.readEntry("SearchAsYouTypeAllProjects", true));
2020 }
2021 
2022 void KatePluginSearchView::writeSessionConfig(KConfigGroup &cg)
2023 {
2024     QStringList searchHistoy;
2025     for (int i = 1; i < m_ui.searchCombo->count(); i++) {
2026         searchHistoy << m_ui.searchCombo->itemText(i);
2027     }
2028     cg.writeEntry("Search", searchHistoy);
2029     QStringList replaceHistoy;
2030     for (int i = 1; i < m_ui.replaceCombo->count(); i++) {
2031         replaceHistoy << m_ui.replaceCombo->itemText(i);
2032     }
2033     cg.writeEntry("Replaces", replaceHistoy);
2034 
2035     cg.writeEntry("MatchCase", m_ui.matchCase->isChecked());
2036     cg.writeEntry("UseRegExp", m_ui.useRegExp->isChecked());
2037     cg.writeEntry("ExpandSearchResults", m_ui.expandResults->isChecked());
2038 
2039     cg.writeEntry("Place", m_ui.searchPlaceCombo->currentIndex());
2040     cg.writeEntry("Recursive", m_ui.recursiveCheckBox->isChecked());
2041     cg.writeEntry("HiddenFiles", m_ui.hiddenCheckBox->isChecked());
2042     cg.writeEntry("FollowSymLink", m_ui.symLinkCheckBox->isChecked());
2043     cg.writeEntry("BinaryFiles", m_ui.binaryCheckBox->isChecked());
2044     QStringList folders;
2045     for (int i = 0; i < qMin(m_ui.folderRequester->comboBox()->count(), 10); i++) {
2046         folders << m_ui.folderRequester->comboBox()->itemText(i);
2047     }
2048     cg.writeEntry("SearchDiskFiless", folders);
2049     cg.writeEntry("SearchDiskFiles", m_ui.folderRequester->text());
2050     QStringList filterItems;
2051     for (int i = 0; i < qMin(m_ui.filterCombo->count(), 10); i++) {
2052         filterItems << m_ui.filterCombo->itemText(i);
2053     }
2054     cg.writeEntry("Filters", filterItems);
2055     cg.writeEntry("CurrentFilter", m_ui.filterCombo->findText(m_ui.filterCombo->currentText()));
2056 
2057     QStringList excludeFilterItems;
2058     for (int i = 0; i < qMin(m_ui.excludeCombo->count(), 10); i++) {
2059         excludeFilterItems << m_ui.excludeCombo->itemText(i);
2060     }
2061     cg.writeEntry("ExcludeFilters", excludeFilterItems);
2062     cg.writeEntry("CurrentExcludeFilter", m_ui.excludeCombo->findText(m_ui.excludeCombo->currentText()));
2063 
2064     // Search as you type
2065     cg.writeEntry("SearchAsYouTypeCurrentFile", m_searchAsYouType.value(MatchModel::CurrentFile, true));
2066     cg.writeEntry("SearchAsYouTypeOpenFiles", m_searchAsYouType.value(MatchModel::OpenFiles, true));
2067     cg.writeEntry("SearchAsYouTypeFolder", m_searchAsYouType.value(MatchModel::Folder, true));
2068     cg.writeEntry("SearchAsYouTypeProject", m_searchAsYouType.value(MatchModel::Project, true));
2069     cg.writeEntry("SearchAsYouTypeAllProjects", m_searchAsYouType.value(MatchModel::AllProjects, true));
2070 }
2071 
2072 void KatePluginSearchView::addTab()
2073 {
2074     Results *res = new Results();
2075 
2076     res->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
2077     res->treeView->setRootIsDecorated(false);
2078     connect(res->treeView, &QTreeView::doubleClicked, this, &KatePluginSearchView::itemSelected, Qt::UniqueConnection);
2079     connect(res->treeView, &QTreeView::customContextMenuRequested, this, &KatePluginSearchView::customResMenuRequested, Qt::UniqueConnection);
2080     connect(res, &Results::requestDetachToMainWindow, this, &KatePluginSearchView::detachTabToMainWindow, Qt::UniqueConnection);
2081     res->matchModel.setDocumentManager(m_kateApp);
2082     connect(&res->matchModel, &MatchModel::replaceDone, this, &KatePluginSearchView::replaceDone);
2083 
2084     res->searchPlaceIndex = m_ui.searchPlaceCombo->currentIndex();
2085     res->useRegExp = m_ui.useRegExp->isChecked();
2086     res->matchCase = m_ui.matchCase->isChecked();
2087     m_ui.resultWidget->addWidget(res);
2088     m_tabBar->addTab(QString());
2089     m_tabBar->setCurrentIndex(m_tabBar->count() - 1);
2090     m_ui.stackedWidget->setCurrentIndex(0);
2091 
2092     // Don't show folder options widget for every new tab
2093     if (m_tabBar->count() == 1) {
2094         const bool showFolderOpts = res->searchPlaceIndex < MatchModel::Folder;
2095         m_ui.displayOptions->setChecked(showFolderOpts);
2096         res->displayFolderOptions = showFolderOpts;
2097     }
2098 
2099     res->treeView->installEventFilter(this);
2100 }
2101 
2102 void KatePluginSearchView::detachTabToMainWindow(Results *res)
2103 {
2104     if (!res) {
2105         return;
2106     }
2107 
2108     int i = m_tabBar->currentIndex();
2109     res->setWindowIcon(QIcon::fromTheme(QStringLiteral("edit-find")));
2110     res->setWindowTitle(i18n("Search: %1", m_tabBar->tabText(i)));
2111     Utils::addWidget(res, m_mainWindow);
2112     if (res == m_curResults) {
2113         m_curResults = nullptr;
2114     }
2115     res->isDetachedToMainWindow = true;
2116     m_tabBar->removeTab(i);
2117     addTab();
2118 }
2119 
2120 void KatePluginSearchView::tabCloseRequested(int index)
2121 {
2122     Results *tmp = qobject_cast<Results *>(m_ui.resultWidget->widget(index));
2123     if (m_curResults == tmp) {
2124         m_searchOpenFiles.cancelSearch();
2125         cancelDiskFileSearch();
2126         m_folderFilesList.terminateSearch();
2127     }
2128 
2129     if (m_ui.resultWidget->count() > 1) {
2130         m_tabBar->blockSignals(true);
2131         m_tabBar->removeTab(index);
2132         if (m_curResults == tmp) {
2133             m_curResults = nullptr;
2134         }
2135         m_ui.resultWidget->removeWidget(tmp);
2136         m_tabBar->blockSignals(false);
2137         delete tmp;
2138     }
2139 
2140     // focus the tab after or the first one if it is the last
2141     if (index >= m_ui.resultWidget->count()) {
2142         index = m_ui.resultWidget->count() - 1;
2143     }
2144     m_tabBar->setCurrentIndex(index); // this will change also the resultWidget index
2145     resultTabChanged(index); // but the index might not change so make sure we update
2146 
2147     updateMatchMarks();
2148 }
2149 
2150 void KatePluginSearchView::resultTabChanged(int index)
2151 {
2152     if (index < 0) {
2153         return;
2154     }
2155 
2156     Results *res = qobject_cast<Results *>(m_ui.resultWidget->widget(index));
2157     if (!res) {
2158         // qDebug() << "No res found";
2159         return;
2160     }
2161 
2162     // Restore display folder option state
2163     m_ui.displayOptions->setChecked(res->displayFolderOptions);
2164 
2165     m_ui.searchCombo->blockSignals(true);
2166     m_ui.matchCase->blockSignals(true);
2167     m_ui.useRegExp->blockSignals(true);
2168     m_ui.searchPlaceCombo->blockSignals(true);
2169 
2170     m_ui.searchCombo->lineEdit()->setText(res->searchStr);
2171     m_ui.useRegExp->setChecked(res->useRegExp);
2172     m_ui.matchCase->setChecked(res->matchCase);
2173     m_ui.searchPlaceCombo->setCurrentIndex(res->searchPlaceIndex);
2174 
2175     m_ui.searchCombo->blockSignals(false);
2176     m_ui.matchCase->blockSignals(false);
2177     m_ui.useRegExp->blockSignals(false);
2178     m_ui.searchPlaceCombo->blockSignals(false);
2179     searchPlaceChanged();
2180     updateMatchMarks();
2181 }
2182 
2183 void KatePluginSearchView::onResize(const QSize &size)
2184 {
2185     bool vertical = size.width() < size.height();
2186 
2187     if (!m_isVerticalLayout && vertical) {
2188         // Change the layout to vertical (left/right)
2189         m_isVerticalLayout = true;
2190 
2191         // Search row 1
2192         m_ui.gridLayout->addWidget(m_ui.searchCombo, 0, 0, 1, 5);
2193         // Search row 2
2194         m_ui.gridLayout->addWidget(m_ui.searchButton, 1, 0);
2195         m_ui.gridLayout->addWidget(m_ui.stopAndNext, 1, 1);
2196         m_ui.gridLayout->addWidget(m_ui.searchPlaceLayoutW, 1, 2, 1, 3);
2197 
2198         // Replace row 1
2199         m_ui.gridLayout->addWidget(m_ui.replaceCombo, 2, 0, 1, 5);
2200 
2201         // Replace row 2
2202         m_ui.gridLayout->addWidget(m_ui.replaceButton, 3, 0);
2203         m_ui.gridLayout->addWidget(m_ui.replaceCheckedBtn, 3, 1);
2204         m_ui.gridLayout->addWidget(m_ui.searchOptionsLayoutW, 3, 2);
2205         m_ui.gridLayout->addWidget(m_ui.newTabButton, 3, 3);
2206         m_ui.gridLayout->addWidget(m_ui.displayOptions, 3, 4);
2207 
2208         m_ui.gridLayout->setColumnStretch(0, 0);
2209         m_ui.gridLayout->setColumnStretch(2, 4);
2210 
2211     } else if (m_isVerticalLayout && !vertical) {
2212         // Change layout to horizontal (top/bottom)
2213         m_isVerticalLayout = false;
2214         // Top row
2215         m_ui.gridLayout->addWidget(m_ui.searchCombo, 0, 0);
2216         m_ui.gridLayout->addWidget(m_ui.searchButton, 0, 1);
2217         m_ui.gridLayout->addWidget(m_ui.stopAndNext, 0, 2);
2218         m_ui.gridLayout->addWidget(m_ui.searchPlaceLayoutW, 0, 3, 1, 3);
2219 
2220         // Second row
2221         m_ui.gridLayout->addWidget(m_ui.replaceCombo, 1, 0);
2222         m_ui.gridLayout->addWidget(m_ui.replaceButton, 1, 1);
2223         m_ui.gridLayout->addWidget(m_ui.replaceCheckedBtn, 1, 2);
2224         m_ui.gridLayout->addWidget(m_ui.searchOptionsLayoutW, 1, 3);
2225         m_ui.gridLayout->addWidget(m_ui.newTabButton, 1, 4);
2226         m_ui.gridLayout->addWidget(m_ui.displayOptions, 1, 5);
2227 
2228         m_ui.gridLayout->setColumnStretch(0, 4);
2229         m_ui.gridLayout->setColumnStretch(2, 0);
2230     }
2231 }
2232 
2233 void KatePluginSearchView::customResMenuRequested(const QPoint &pos)
2234 {
2235     QTreeView *tree = qobject_cast<QTreeView *>(sender());
2236     if (tree == nullptr) {
2237         return;
2238     }
2239     QMenu *menu = new QMenu(tree);
2240 
2241     QAction *copyAll = new QAction(i18n("Copy all"), tree);
2242     copyAll->setShortcut(QKeySequence::Copy);
2243     copyAll->setShortcutVisibleInContextMenu(true);
2244     menu->addAction(copyAll);
2245 
2246     QAction *copyExpanded = new QAction(i18n("Copy expanded"), tree);
2247     menu->addAction(copyExpanded);
2248 
2249     QAction *exportMatches = new QAction(i18n("Export matches"), tree);
2250     if (m_curResults && m_curResults->useRegExp) {
2251         menu->addAction(exportMatches);
2252     }
2253 
2254     if (m_curResults) {
2255         QAction *openAsEditorTab = new QAction(i18n("Open as Editor Tab"), tree);
2256         connect(openAsEditorTab, &QAction::triggered, this, [this] {
2257             detachTabToMainWindow(m_curResults);
2258         });
2259         menu->addAction(openAsEditorTab);
2260     }
2261 
2262     QAction *clear = menu->addAction(i18n("Clear"));
2263 
2264     menu->popup(tree->viewport()->mapToGlobal(pos));
2265 
2266     connect(copyAll, &QAction::triggered, this, [this](bool) {
2267         copySearchToClipboard(All);
2268     });
2269     connect(copyExpanded, &QAction::triggered, this, [this](bool) {
2270         copySearchToClipboard(AllExpanded);
2271     });
2272     connect(exportMatches, &QAction::triggered, this, [this](bool) {
2273         showExportMatchesDialog();
2274     });
2275     connect(clear, &QAction::triggered, this, [this] {
2276         if (Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget())) {
2277             res->matchModel.clear();
2278         }
2279         clearMarksAndRanges();
2280     });
2281 }
2282 
2283 void KatePluginSearchView::copySearchToClipboard(CopyResultType copyType)
2284 {
2285     Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
2286     if (!res) {
2287         return;
2288     }
2289     if (res->model()->rowCount() == 0) {
2290         return;
2291     }
2292 
2293     QString clipboard;
2294     auto *model = res->model();
2295 
2296     QModelIndex rootIndex = model->index(0, 0);
2297 
2298     clipboard = rootIndex.data(MatchModel::PlainTextRole).toString();
2299 
2300     int fileCount = model->rowCount(rootIndex);
2301     for (int i = 0; i < fileCount; ++i) {
2302         QModelIndex fileIndex = model->index(i, 0, rootIndex);
2303         if (res->treeView->isExpanded(fileIndex) || copyType == All) {
2304             clipboard += QLatin1String("\n") + fileIndex.data(MatchModel::PlainTextRole).toString();
2305             int matchCount = model->rowCount(fileIndex);
2306             for (int j = 0; j < matchCount; ++j) {
2307                 QModelIndex matchIndex = model->index(j, 0, fileIndex);
2308                 clipboard += QLatin1String("\n") + matchIndex.data(MatchModel::PlainTextRole).toString();
2309             }
2310         }
2311     }
2312     clipboard += QLatin1String("\n");
2313     QApplication::clipboard()->setText(clipboard);
2314 }
2315 
2316 void KatePluginSearchView::showExportMatchesDialog()
2317 {
2318     Results *res = qobject_cast<Results *>(m_ui.resultWidget->currentWidget());
2319     if (!res) {
2320         return;
2321     }
2322     MatchExportDialog matchExportDialog(m_mainWindow->window(), m_curResults->model(), &m_curResults->regExp);
2323     matchExportDialog.exec();
2324 }
2325 
2326 bool KatePluginSearchView::eventFilter(QObject *obj, QEvent *event)
2327 {
2328     if (event->type() == QEvent::ShortcutOverride) {
2329         // Ignore copy in ShortcutOverride and handle it in the KeyPress event
2330         QKeyEvent *ke = static_cast<QKeyEvent *>(event);
2331         if (ke->matches(QKeySequence::Copy)) {
2332             event->accept();
2333             return true;
2334         }
2335     } else if (event->type() == QEvent::KeyPress) {
2336         QKeyEvent *ke = static_cast<QKeyEvent *>(event);
2337         QTreeView *treeView = qobject_cast<QTreeView *>(obj);
2338         if (treeView) {
2339             if (ke->matches(QKeySequence::Copy)) {
2340                 copySearchToClipboard(All);
2341                 event->accept();
2342                 return true;
2343             }
2344             if (ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return) {
2345                 if (treeView->currentIndex().isValid()) {
2346                     itemSelected(treeView->currentIndex());
2347                     event->accept();
2348                     return true;
2349                 }
2350             }
2351         }
2352         // NOTE: Qt::Key_Escape is handled by handleEsc
2353     } else if (event->type() == QEvent::Resize) {
2354         QResizeEvent *re = static_cast<QResizeEvent *>(event);
2355         if (obj == m_toolView) {
2356             onResize(re->size());
2357         }
2358     }
2359     return QObject::eventFilter(obj, event);
2360 }
2361 
2362 void KatePluginSearchView::searchContextMenu(const QPoint &pos)
2363 {
2364     QSet<QAction *> actionPointers;
2365 
2366     QMenu *const contextMenu = m_ui.searchCombo->lineEdit()->createStandardContextMenu();
2367     if (!contextMenu) {
2368         return;
2369     }
2370 
2371     if (m_ui.useRegExp->isChecked()) {
2372         QMenu *menu = contextMenu->addMenu(i18n("Add..."));
2373         if (!menu) {
2374             return;
2375         }
2376 
2377         menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
2378 
2379         addRegexHelperActionsForSearch(&actionPointers, menu);
2380     }
2381 
2382     // add option to disable/enable search as you type
2383     QAction *a = contextMenu->addAction(QStringLiteral("search_as_you_type"));
2384     a->setText(i18n("Search As You Type"));
2385     a->setCheckable(true);
2386     int searchPlace = m_ui.searchPlaceCombo->currentIndex();
2387     bool enabled = m_searchAsYouType.value(static_cast<MatchModel::SearchPlaces>(searchPlace), true);
2388     a->setChecked(enabled);
2389     connect(a, &QAction::triggered, this, [this](bool checked) {
2390         int searchPlace = m_ui.searchPlaceCombo->currentIndex();
2391         m_searchAsYouType[static_cast<MatchModel::SearchPlaces>(searchPlace)] = checked;
2392     });
2393 
2394     // Show menu and act
2395     QAction *const result = contextMenu->exec(m_ui.searchCombo->mapToGlobal(pos));
2396     regexHelperActOnAction(result, actionPointers, m_ui.searchCombo->lineEdit());
2397 }
2398 
2399 void KatePluginSearchView::replaceContextMenu(const QPoint &pos)
2400 {
2401     QMenu *const contextMenu = m_ui.replaceCombo->lineEdit()->createStandardContextMenu();
2402     if (!contextMenu) {
2403         return;
2404     }
2405 
2406     QMenu *menu = contextMenu->addMenu(i18n("Add..."));
2407     if (!menu) {
2408         return;
2409     }
2410     menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
2411 
2412     QSet<QAction *> actionPointers;
2413     addSpecialCharsHelperActionsForReplace(&actionPointers, menu);
2414 
2415     if (m_ui.useRegExp->isChecked()) {
2416         addRegexHelperActionsForReplace(&actionPointers, menu);
2417     }
2418 
2419     // Show menu and act
2420     QAction *const result = contextMenu->exec(m_ui.replaceCombo->mapToGlobal(pos));
2421     regexHelperActOnAction(result, actionPointers, m_ui.replaceCombo->lineEdit());
2422 }
2423 
2424 void KatePluginSearchView::slotPluginViewCreated(const QString &name, QObject *pluginView)
2425 {
2426     // add view
2427     if (pluginView && name == QLatin1String("kateprojectplugin")) {
2428         m_projectPluginView = pluginView;
2429         slotProjectFileNameChanged();
2430         connect(pluginView, SIGNAL(projectFileNameChanged()), this, SLOT(slotProjectFileNameChanged()));
2431     }
2432 }
2433 
2434 void KatePluginSearchView::slotPluginViewDeleted(const QString &name, QObject *)
2435 {
2436     // remove view
2437     if (name == QLatin1String("kateprojectplugin")) {
2438         m_projectPluginView = nullptr;
2439         slotProjectFileNameChanged();
2440     }
2441 }
2442 
2443 void KatePluginSearchView::slotProjectFileNameChanged()
2444 {
2445     // query new project file name
2446     QString projectName;
2447     if (m_projectPluginView) {
2448         projectName = m_projectPluginView->property("projectName").toString();
2449     }
2450 
2451     // have project, enable gui for it
2452     if (!projectName.isEmpty()) {
2453         if (m_ui.searchPlaceCombo->count() <= MatchModel::Project) {
2454             // add "in Project"
2455             m_ui.searchPlaceCombo->addItem(QIcon::fromTheme(QStringLiteral("project-open")), i18n("In Current Project"));
2456             // add "in Open Projects"
2457             m_ui.searchPlaceCombo->addItem(QIcon::fromTheme(QStringLiteral("project-open")), i18n("In All Open Projects"));
2458             if (m_projectSearchPlaceIndex >= MatchModel::Project) {
2459                 // switch to search "in (all) Project"
2460                 setSearchPlace(m_projectSearchPlaceIndex);
2461                 m_projectSearchPlaceIndex = 0;
2462             }
2463         }
2464     }
2465 
2466     // else: disable gui for it
2467     else {
2468         if (m_ui.searchPlaceCombo->count() >= MatchModel::Project) {
2469             // switch to search "in Open files", if "in Project" is active
2470             int searchPlaceIndex = m_ui.searchPlaceCombo->currentIndex();
2471             if (searchPlaceIndex >= MatchModel::Project) {
2472                 m_projectSearchPlaceIndex = searchPlaceIndex;
2473                 setSearchPlace(MatchModel::OpenFiles);
2474             }
2475 
2476             // remove "in Project" and "in all projects"
2477             while (m_ui.searchPlaceCombo->count() > MatchModel::Project) {
2478                 m_ui.searchPlaceCombo->removeItem(m_ui.searchPlaceCombo->count() - 1);
2479             }
2480         }
2481     }
2482 }
2483 
2484 #include "SearchPlugin.moc"
2485 #include "moc_SearchPlugin.cpp"
2486 
2487 // kate: space-indent on; indent-width 4; replace-tabs on;