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 ®ex : 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 ®ex : 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 ®, 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;