diff --git a/addons/search/FolderFilesList.cpp b/addons/search/FolderFilesList.cpp index 5cfd82bab..dafc57fa2 100644 --- a/addons/search/FolderFilesList.cpp +++ b/addons/search/FolderFilesList.cpp @@ -1,149 +1,159 @@ /* Kate search plugin * * Copyright (C) 2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #include "FolderFilesList.h" #include #include #include #include #include #include FolderFilesList::FolderFilesList(QObject *parent) : QThread(parent) { } FolderFilesList::~FolderFilesList() { m_cancelSearch = true; wait(); } void FolderFilesList::run() { m_files.clear(); QFileInfo folderInfo(m_folder); checkNextItem(folderInfo); - if (m_cancelSearch) + if (m_cancelSearch) { m_files.clear(); + } + else { + Q_EMIT fileListReady(); + } } void FolderFilesList::generateList(const QString &folder, bool recursive, bool hidden, bool symlinks, bool binary, const QString &types, const QString &excludes) { m_cancelSearch = false; m_folder = folder; if (!m_folder.endsWith(QLatin1Char('/'))) { m_folder += QLatin1Char('/'); } m_recursive = recursive; m_hidden = hidden; m_symlinks = symlinks; m_binary = binary; m_types.clear(); const auto typesList = types.split(QLatin1Char(','), QString::SkipEmptyParts); for (const QString &type : typesList) { m_types << type.trimmed(); } if (m_types.isEmpty()) { m_types << QStringLiteral("*"); } QStringList tmpExcludes = excludes.split(QLatin1Char(',')); m_excludeList.clear(); for (int i = 0; i < tmpExcludes.size(); i++) { QRegExp rx(tmpExcludes[i].trimmed()); rx.setPatternSyntax(QRegExp::Wildcard); m_excludeList << rx; } m_time.restart(); start(); } +void FolderFilesList::terminateSearch() +{ + m_cancelSearch = true; + wait(); +} + QStringList FolderFilesList::fileList() { return m_files; } void FolderFilesList::cancelSearch() { m_cancelSearch = true; } void FolderFilesList::checkNextItem(const QFileInfo &item) { if (m_cancelSearch) { return; } if (m_time.elapsed() > 100) { m_time.restart(); emit searching(item.absoluteFilePath()); } if (item.isFile()) { if (!m_binary) { QMimeType mimeType = QMimeDatabase().mimeTypeForFile(item); if (!mimeType.inherits(QStringLiteral("text/plain"))) { return; } } m_files << item.canonicalFilePath(); } else { QDir currentDir(item.absoluteFilePath()); if (!currentDir.isReadable()) { // qDebug() << currentDir.absolutePath() << "Not readable"; return; } QDir::Filters filter = QDir::Files | QDir::NoDotAndDotDot | QDir::Readable; if (m_hidden) filter |= QDir::Hidden; if (m_recursive) filter |= QDir::AllDirs; if (!m_symlinks) filter |= QDir::NoSymLinks; // sort the items to have an deterministic order! const QFileInfoList currentItems = currentDir.entryInfoList(m_types, filter, QDir::Name | QDir::LocaleAware); bool skip; for (const auto ¤tItem : currentItems) { skip = false; for (const auto ®ex : qAsConst(m_excludeList)) { QString matchString = currentItem.filePath(); if (currentItem.filePath().startsWith(m_folder)) { matchString = currentItem.filePath().mid(m_folder.size()); } if (regex.exactMatch(matchString)) { skip = true; break; } } if (!skip) { checkNextItem(currentItem); } } } } diff --git a/addons/search/FolderFilesList.h b/addons/search/FolderFilesList.h index fbf9fdb9d..ec329d76b 100644 --- a/addons/search/FolderFilesList.h +++ b/addons/search/FolderFilesList.h @@ -1,68 +1,71 @@ /* Kate search plugin * * Copyright (C) 2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #ifndef FolderFilesList_h #define FolderFilesList_h #include #include #include #include #include #include class FolderFilesList : public QThread { Q_OBJECT public: FolderFilesList(QObject *parent = nullptr); ~FolderFilesList() override; void run() override; void generateList(const QString &folder, bool recursive, bool hidden, bool symlinks, bool binary, const QString &types, const QString &excludes); + void terminateSearch(); + QStringList fileList(); public Q_SLOTS: void cancelSearch(); Q_SIGNALS: void searching(const QString &path); + void fileListReady(); private: void checkNextItem(const QFileInfo &item); private: QString m_folder; QStringList m_files; bool m_cancelSearch = false; bool m_recursive = false; bool m_hidden = false; bool m_symlinks = false; bool m_binary = false; QStringList m_types; QVector m_excludeList; QElapsedTimer m_time; }; #endif diff --git a/addons/search/SearchDiskFiles.cpp b/addons/search/SearchDiskFiles.cpp index 68e5fe58f..f4c3fd86e 100644 --- a/addons/search/SearchDiskFiles.cpp +++ b/addons/search/SearchDiskFiles.cpp @@ -1,184 +1,198 @@ /* Kate search plugin * * Copyright (C) 2011-2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #include "SearchDiskFiles.h" #include #include #include SearchDiskFiles::SearchDiskFiles(QObject *parent) : QThread(parent) { } SearchDiskFiles::~SearchDiskFiles() { m_cancelSearch = true; wait(); } void SearchDiskFiles::startSearch(const QStringList &files, const QRegularExpression ®exp) { if (files.empty()) { emit searchDone(); return; } m_cancelSearch = false; + m_terminateSearch = false; m_files = files; m_regExp = regexp; m_matchCount = 0; m_statusTime.restart(); start(); } void SearchDiskFiles::run() { for (const QString &fileName : qAsConst(m_files)) { if (m_cancelSearch) { break; } if (m_statusTime.elapsed() > 100) { m_statusTime.restart(); emit searching(fileName); } if (m_regExp.pattern().contains(QLatin1String("\\n"))) { searchMultiLineRegExp(fileName); } else { searchSingleLineRegExp(fileName); } } - emit searchDone(); + + if (!m_terminateSearch) { + emit searchDone(); + } m_cancelSearch = true; } void SearchDiskFiles::cancelSearch() { m_cancelSearch = true; } +void SearchDiskFiles::terminateSearch() +{ + m_cancelSearch = true; + m_terminateSearch = true; + wait(); +} + bool SearchDiskFiles::searching() { return !m_cancelSearch; } void SearchDiskFiles::searchSingleLineRegExp(const QString &fileName) { QFile file(fileName); + QUrl fileUrl = QUrl::fromUserInput(fileName); if (!file.open(QFile::ReadOnly)) { return; } QTextStream stream(&file); QString line; int i = 0; int column; QRegularExpressionMatch match; while (!(line = stream.readLine()).isNull()) { if (m_cancelSearch) break; match = m_regExp.match(line); column = match.capturedStart(); while (column != -1 && !match.captured().isEmpty()) { - // limit line length + if (m_cancelSearch) + break; + // limit line length in the treeview if (line.length() > 1024) line = line.left(1024); - QUrl fileUrl = QUrl::fromUserInput(fileName); + emit matchFound(fileUrl.toString(), fileUrl.fileName(), line, match.capturedLength(), i, column, i, column + match.capturedLength()); match = m_regExp.match(line, column + match.capturedLength()); column = match.capturedStart(); m_matchCount++; // NOTE: This sleep is here so that the main thread will get a chance to // handle any stop button clicks if there are a lot of matches if (m_matchCount % 50) msleep(1); } i++; } } void SearchDiskFiles::searchMultiLineRegExp(const QString &fileName) { QFile file(fileName); int column = 0; int line = 0; static QString fullDoc; static QVector lineStart; QRegularExpression tmpRegExp = m_regExp; if (!file.open(QFile::ReadOnly)) { return; } QTextStream stream(&file); fullDoc = stream.readAll(); fullDoc.remove(QLatin1Char('\r')); lineStart.clear(); lineStart << 0; for (int i = 0; i < fullDoc.size() - 1; i++) { if (fullDoc[i] == QLatin1Char('\n')) { lineStart << i + 1; } } if (tmpRegExp.pattern().endsWith(QLatin1Char('$'))) { fullDoc += QLatin1Char('\n'); QString newPatern = tmpRegExp.pattern(); newPatern.replace(QStringLiteral("$"), QStringLiteral("(?=\\n)")); tmpRegExp.setPattern(newPatern); } QRegularExpressionMatch match; match = tmpRegExp.match(fullDoc); column = match.capturedStart(); while (column != -1 && !match.captured().isEmpty()) { if (m_cancelSearch) break; // search for the line number of the match int i; line = -1; for (i = 1; i < lineStart.size(); i++) { if (lineStart[i] > column) { line = i - 1; break; } } if (line == -1) { break; } QUrl fileUrl = QUrl::fromUserInput(fileName); int startColumn = (column - lineStart[line]); int endLine = line + match.captured().count(QLatin1Char('\n')); int lastNL = match.captured().lastIndexOf(QLatin1Char('\n')); int endColumn = lastNL == -1 ? startColumn + match.captured().length() : match.captured().length() - lastNL - 1; emit matchFound(fileUrl.toString(), fileUrl.fileName(), fullDoc.mid(lineStart[line], column - lineStart[line]) + match.captured(), match.capturedLength(), line, startColumn, endLine, endColumn); match = tmpRegExp.match(fullDoc, column + match.capturedLength()); column = match.capturedStart(); m_matchCount++; // NOTE: This sleep is here so that the main thread will get a chance to // handle any stop button clicks if there are a lot of matches if (m_matchCount % 50) msleep(1); } } diff --git a/addons/search/SearchDiskFiles.h b/addons/search/SearchDiskFiles.h index 422bd2b1d..1a5797a0c 100644 --- a/addons/search/SearchDiskFiles.h +++ b/addons/search/SearchDiskFiles.h @@ -1,65 +1,67 @@ /* Kate search plugin * * Copyright (C) 2011-2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #ifndef SearchDiskFiles_h #define SearchDiskFiles_h #include #include #include #include #include #include #include class SearchDiskFiles : public QThread { Q_OBJECT public: SearchDiskFiles(QObject *parent = nullptr); ~SearchDiskFiles() override; void startSearch(const QStringList &iles, const QRegularExpression ®exp); void run() override; + void terminateSearch(); bool searching(); private: void searchSingleLineRegExp(const QString &fileName); void searchMultiLineRegExp(const QString &fileName); public Q_SLOTS: void cancelSearch(); Q_SIGNALS: void matchFound(const QString &url, const QString &docName, const QString &lineContent, int matchLen, int line, int column, int endLine, int endColumn); void searchDone(); void searching(const QString &file); private: QRegularExpression m_regExp; QStringList m_files; bool m_cancelSearch = true; + bool m_terminateSearch = false; int m_matchCount = 0; QElapsedTimer m_statusTime; }; #endif diff --git a/addons/search/plugin_search.cpp b/addons/search/plugin_search.cpp index 587d0ff24..a926fd23b 100644 --- a/addons/search/plugin_search.cpp +++ b/addons/search/plugin_search.cpp @@ -1,2344 +1,2357 @@ /* Kate search plugin * * Copyright (C) 2011-2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #include "plugin_search.h" #include "htmldelegate.h" #include #include #include #include #include #include #include #include #include #include "kacceleratormanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static QUrl localFileDirUp(const QUrl &url) { if (!url.isLocalFile()) return url; // else go up return QUrl::fromLocalFile(QFileInfo(url.toLocalFile()).dir().absolutePath()); } static QAction *menuEntry(QMenu *menu, const QString &before, const QString &after, const QString &desc, QString menuBefore = QString(), QString menuAfter = QString()); /** * When the action is triggered the cursor will be placed between @p before and @p after. */ static QAction *menuEntry(QMenu *menu, const QString &before, const QString &after, const QString &desc, QString menuBefore, QString menuAfter) { if (menuBefore.isEmpty()) menuBefore = before; if (menuAfter.isEmpty()) menuAfter = after; QAction *const action = menu->addAction(menuBefore + menuAfter + QLatin1Char('\t') + desc); if (!action) return nullptr; action->setData(QString(before + QLatin1Char(' ') + after)); return action; } /** * adds items and separators for special chars in "replace" field */ static void addSpecialCharsHelperActionsForReplace(QSet *actionList, QMenu *menu) { QSet &actionPointers = *actionList; QString emptyQSTring; actionPointers << menuEntry(menu, QStringLiteral("\\n"), emptyQSTring, i18n("Line break")); actionPointers << menuEntry(menu, QStringLiteral("\\t"), emptyQSTring, i18n("Tab")); } /** * adds items and separators for regex in "search" field */ static void addRegexHelperActionsForSearch(QSet *actionList, QMenu *menu) { QSet &actionPointers = *actionList; QString emptyQSTring; actionPointers << menuEntry(menu, QStringLiteral("^"), emptyQSTring, i18n("Beginning of line")); actionPointers << menuEntry(menu, QStringLiteral("$"), emptyQSTring, i18n("End of line")); menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("."), emptyQSTring, i18n("Any single character (excluding line breaks)")); actionPointers << menuEntry(menu, QStringLiteral("[.]"), emptyQSTring, i18n("Literal dot")); menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("+"), emptyQSTring, i18n("One or more occurrences")); actionPointers << menuEntry(menu, QStringLiteral("*"), emptyQSTring, i18n("Zero or more occurrences")); actionPointers << menuEntry(menu, QStringLiteral("?"), emptyQSTring, i18n("Zero or one occurrences")); actionPointers << menuEntry(menu, QStringLiteral("{"), QStringLiteral(",}"), i18n(" through occurrences"), QStringLiteral("{a"), QStringLiteral(",b}")); menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("("), QStringLiteral(")"), i18n("Group, capturing")); actionPointers << menuEntry(menu, QStringLiteral("|"), emptyQSTring, i18n("Or")); actionPointers << menuEntry(menu, QStringLiteral("["), QStringLiteral("]"), i18n("Set of characters")); actionPointers << menuEntry(menu, QStringLiteral("[^"), QStringLiteral("]"), i18n("Negative set of characters")); actionPointers << menuEntry(menu, QStringLiteral("(?:"), QStringLiteral(")"), i18n("Group, non-capturing"), QStringLiteral("(?:E")); actionPointers << menuEntry(menu, QStringLiteral("(?="), QStringLiteral(")"), i18n("Lookahead"), QStringLiteral("(?=E")); actionPointers << menuEntry(menu, QStringLiteral("(?!"), QStringLiteral(")"), i18n("Negative lookahead"), QStringLiteral("(?!E")); menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("\\n"), emptyQSTring, i18n("Line break")); actionPointers << menuEntry(menu, QStringLiteral("\\t"), emptyQSTring, i18n("Tab")); actionPointers << menuEntry(menu, QStringLiteral("\\b"), emptyQSTring, i18n("Word boundary")); actionPointers << menuEntry(menu, QStringLiteral("\\B"), emptyQSTring, i18n("Not word boundary")); actionPointers << menuEntry(menu, QStringLiteral("\\d"), emptyQSTring, i18n("Digit")); actionPointers << menuEntry(menu, QStringLiteral("\\D"), emptyQSTring, i18n("Non-digit")); actionPointers << menuEntry(menu, QStringLiteral("\\s"), emptyQSTring, i18n("Whitespace (excluding line breaks)")); actionPointers << menuEntry(menu, QStringLiteral("\\S"), emptyQSTring, i18n("Non-whitespace (excluding line breaks)")); actionPointers << menuEntry(menu, QStringLiteral("\\w"), emptyQSTring, i18n("Word character (alphanumerics plus '_')")); actionPointers << menuEntry(menu, QStringLiteral("\\W"), emptyQSTring, i18n("Non-word character")); } /** * adds items and separators for regex in "replace" field */ static void addRegexHelperActionsForReplace(QSet *actionList, QMenu *menu) { QSet &actionPointers = *actionList; QString emptyQSTring; menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("\\0"), emptyQSTring, i18n("Regular expression capture 0 (whole match)")); actionPointers << menuEntry(menu, QStringLiteral("\\"), emptyQSTring, i18n("Regular expression capture 1-9"), QStringLiteral("\\#")); actionPointers << menuEntry(menu, QStringLiteral("\\{"), QStringLiteral("}"), i18n("Regular expression capture 0-999"), QStringLiteral("\\{#")); menu->addSeparator(); actionPointers << menuEntry(menu, QStringLiteral("\\U\\"), emptyQSTring, i18n("Upper-cased capture 0-9"), QStringLiteral("\\U\\#")); actionPointers << menuEntry(menu, QStringLiteral("\\U\\{"), QStringLiteral("}"), i18n("Upper-cased capture 0-999"), QStringLiteral("\\U\\{###")); actionPointers << menuEntry(menu, QStringLiteral("\\L\\"), emptyQSTring, i18n("Lower-cased capture 0-9"), QStringLiteral("\\L\\#")); actionPointers << menuEntry(menu, QStringLiteral("\\L\\{"), QStringLiteral("}"), i18n("Lower-cased capture 0-999"), QStringLiteral("\\L\\{###")); } /** * inserts text and sets cursor position */ static void regexHelperActOnAction(QAction *resultAction, const QSet &actionList, QLineEdit *lineEdit) { if (resultAction && actionList.contains(resultAction)) { const int cursorPos = lineEdit->cursorPosition(); QStringList beforeAfter = resultAction->data().toString().split(QLatin1Char(' ')); if (beforeAfter.size() != 2) return; lineEdit->insert(beforeAfter[0] + beforeAfter[1]); lineEdit->setCursorPosition(cursorPos + beforeAfter[0].count()); lineEdit->setFocus(); } } class TreeWidgetItem : public QTreeWidgetItem { public: TreeWidgetItem(QTreeWidget *parent) : QTreeWidgetItem(parent) { } TreeWidgetItem(QTreeWidget *parent, const QStringList &list) : QTreeWidgetItem(parent, list) { } TreeWidgetItem(QTreeWidgetItem *parent, const QStringList &list) : QTreeWidgetItem(parent, list) { } private: bool operator<(const QTreeWidgetItem &other) const override { if (childCount() == 0) { int line = data(0, ReplaceMatches::StartLineRole).toInt(); int column = data(0, ReplaceMatches::StartColumnRole).toInt(); int oLine = other.data(0, ReplaceMatches::StartLineRole).toInt(); int oColumn = other.data(0, ReplaceMatches::StartColumnRole).toInt(); if (line < oLine) { return true; } if ((line == oLine) && (column < oColumn)) { return true; } return false; } int sepCount = data(0, ReplaceMatches::FileUrlRole).toString().count(QDir::separator()); int oSepCount = other.data(0, ReplaceMatches::FileUrlRole).toString().count(QDir::separator()); if (sepCount < oSepCount) return true; if (sepCount > oSepCount) return false; return data(0, ReplaceMatches::FileUrlRole).toString().toLower() < other.data(0, ReplaceMatches::FileUrlRole).toString().toLower(); } }; Results::Results(QWidget *parent) : QWidget(parent) { setupUi(this); tree->setItemDelegate(new SPHtmlDelegate(tree)); } K_PLUGIN_FACTORY_WITH_JSON(KatePluginSearchFactory, "katesearch.json", registerPlugin();) KatePluginSearch::KatePluginSearch(QObject *parent, const QList &) : KTextEditor::Plugin(parent) { m_searchCommand = new KateSearchCommand(this); } KatePluginSearch::~KatePluginSearch() { delete m_searchCommand; } QObject *KatePluginSearch::createView(KTextEditor::MainWindow *mainWindow) { KatePluginSearchView *view = new KatePluginSearchView(this, mainWindow, KTextEditor::Editor::instance()->application()); connect(m_searchCommand, &KateSearchCommand::setSearchPlace, view, &KatePluginSearchView::setSearchPlace); connect(m_searchCommand, &KateSearchCommand::setCurrentFolder, view, &KatePluginSearchView::setCurrentFolder); connect(m_searchCommand, &KateSearchCommand::setSearchString, view, &KatePluginSearchView::setSearchString); connect(m_searchCommand, &KateSearchCommand::startSearch, view, &KatePluginSearchView::startSearch); connect(m_searchCommand, SIGNAL(newTab()), view, SLOT(addTab())); return view; } bool ContainerWidget::focusNextPrevChild(bool next) { QWidget *fw = focusWidget(); bool found = false; emit nextFocus(fw, &found, next); if (found) { return true; } return QWidget::focusNextPrevChild(next); } void KatePluginSearchView::nextFocus(QWidget *currentWidget, bool *found, bool next) { *found = false; if (!currentWidget) { return; } // we use the object names here because there can be multiple replaceButtons (on multiple result tabs) if (next) { if (currentWidget->objectName() == QLatin1String("tree") || currentWidget == m_ui.binaryCheckBox) { m_ui.newTabButton->setFocus(); *found = true; return; } if (currentWidget == m_ui.displayOptions) { if (m_ui.displayOptions->isChecked()) { m_ui.folderRequester->setFocus(); *found = true; return; } else { Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { return; } res->tree->setFocus(); *found = true; return; } } } else { if (currentWidget == m_ui.newTabButton) { if (m_ui.displayOptions->isChecked()) { m_ui.binaryCheckBox->setFocus(); } else { Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { return; } res->tree->setFocus(); } *found = true; return; } else { if (currentWidget->objectName() == QLatin1String("tree")) { m_ui.displayOptions->setFocus(); *found = true; return; } } } } KatePluginSearchView::KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mainWin, KTextEditor::Application *application) : QObject(mainWin) , m_kateApp(application) - , m_curResults(nullptr) - , m_searchJustOpened(false) - , m_projectSearchPlaceIndex(0) - , m_searchDiskFilesDone(true) - , m_searchOpenFilesDone(true) - , m_isSearchAsYouType(false) - , m_isLeftRight(false) - , m_projectPluginView(nullptr) , m_mainWindow(mainWin) { KXMLGUIClient::setComponentName(QStringLiteral("katesearch"), i18n("Kate Search & Replace")); setXMLFile(QStringLiteral("ui.rc")); m_toolView = mainWin->createToolView(plugin, QStringLiteral("kate_plugin_katesearch"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Search and Replace")); ContainerWidget *container = new ContainerWidget(m_toolView); m_ui.setupUi(container); container->setFocusProxy(m_ui.searchCombo); connect(container, &ContainerWidget::nextFocus, this, &KatePluginSearchView::nextFocus); QAction *a = actionCollection()->addAction(QStringLiteral("search_in_files")); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_F)); a->setText(i18n("Search in Files")); connect(a, &QAction::triggered, this, &KatePluginSearchView::openSearchView); a = actionCollection()->addAction(QStringLiteral("search_in_files_new_tab")); a->setText(i18n("Search in Files (in new tab)")); // first add tab, then open search view, since open search view switches to show the search options connect(a, &QAction::triggered, this, &KatePluginSearchView::addTab); connect(a, &QAction::triggered, this, &KatePluginSearchView::openSearchView); a = actionCollection()->addAction(QStringLiteral("go_to_next_match")); a->setText(i18n("Go to Next Match")); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::Key_F6)); connect(a, &QAction::triggered, this, &KatePluginSearchView::goToNextMatch); a = actionCollection()->addAction(QStringLiteral("go_to_prev_match")); a->setText(i18n("Go to Previous Match")); actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::SHIFT + Qt::Key_F6)); connect(a, &QAction::triggered, this, &KatePluginSearchView::goToPreviousMatch); m_ui.resultTabWidget->tabBar()->setSelectionBehaviorOnRemove(QTabBar::SelectLeftTab); KAcceleratorManager::setNoAccel(m_ui.resultTabWidget); // Gnome does not seem to have all icons we want, so we use fall-back icons for those that are missing. QIcon dispOptIcon = QIcon::fromTheme(QStringLiteral("games-config-options"), QIcon::fromTheme(QStringLiteral("preferences-system"))); QIcon matchCaseIcon = QIcon::fromTheme(QStringLiteral("format-text-superscript"), QIcon::fromTheme(QStringLiteral("format-text-bold"))); QIcon useRegExpIcon = QIcon::fromTheme(QStringLiteral("code-context"), QIcon::fromTheme(QStringLiteral("edit-find-replace"))); QIcon expandResultsIcon = QIcon::fromTheme(QStringLiteral("view-list-tree"), QIcon::fromTheme(QStringLiteral("format-indent-more"))); m_ui.displayOptions->setIcon(dispOptIcon); m_ui.searchButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-find"))); m_ui.nextButton->setIcon(QIcon::fromTheme(QStringLiteral("go-down-search"))); m_ui.stopButton->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); m_ui.matchCase->setIcon(matchCaseIcon); m_ui.useRegExp->setIcon(useRegExpIcon); m_ui.expandResults->setIcon(expandResultsIcon); m_ui.searchPlaceCombo->setItemIcon(CurrentFile, QIcon::fromTheme(QStringLiteral("text-plain"))); m_ui.searchPlaceCombo->setItemIcon(OpenFiles, QIcon::fromTheme(QStringLiteral("text-plain"))); m_ui.searchPlaceCombo->setItemIcon(Folder, QIcon::fromTheme(QStringLiteral("folder"))); m_ui.folderUpButton->setIcon(QIcon::fromTheme(QStringLiteral("go-up"))); m_ui.currentFolderButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); m_ui.newTabButton->setIcon(QIcon::fromTheme(QStringLiteral("tab-new"))); m_ui.filterCombo->setToolTip(i18n("Comma separated list of file types to search in. Example: \"*.cpp,*.h\"\n")); m_ui.excludeCombo->setToolTip(i18n("Comma separated list of files and directories to exclude from the search. Example: \"build*\"")); // the order here is important to get the tabBar hidden for only one tab addTab(); m_ui.resultTabWidget->tabBar()->hide(); // get url-requester's combo box and sanely initialize KComboBox *cmbUrl = m_ui.folderRequester->comboBox(); cmbUrl->setDuplicatesEnabled(false); cmbUrl->setEditable(true); m_ui.folderRequester->setMode(KFile::Directory | KFile::LocalOnly); KUrlCompletion *cmpl = new KUrlCompletion(KUrlCompletion::DirCompletion); cmbUrl->setCompletionObject(cmpl); cmbUrl->setAutoDeleteCompletionObject(true); connect(m_ui.newTabButton, &QToolButton::clicked, this, &KatePluginSearchView::addTab); connect(m_ui.resultTabWidget, &QTabWidget::tabCloseRequested, this, &KatePluginSearchView::tabCloseRequested); connect(m_ui.resultTabWidget, &QTabWidget::currentChanged, this, &KatePluginSearchView::resultTabChanged); connect(m_ui.folderUpButton, &QToolButton::clicked, this, &KatePluginSearchView::navigateFolderUp); connect(m_ui.currentFolderButton, &QToolButton::clicked, this, &KatePluginSearchView::setCurrentFolder); connect(m_ui.expandResults, &QToolButton::clicked, this, &KatePluginSearchView::expandResults); connect(m_ui.searchCombo, &QComboBox::editTextChanged, &m_changeTimer, static_cast(&QTimer::start)); connect(m_ui.matchCase, &QToolButton::toggled, &m_changeTimer, static_cast(&QTimer::start)); connect(m_ui.matchCase, &QToolButton::toggled, this, [=] { Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (res) { res->matchCase = m_ui.matchCase->isChecked(); } }); connect(m_ui.searchCombo->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::startSearch); // connecting to returnPressed() of the folderRequester doesn't work, I haven't found out why yet. But connecting to the linedit works: connect(m_ui.folderRequester->comboBox()->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::startSearch); connect(m_ui.filterCombo, static_cast(&KComboBox::returnPressed), this, &KatePluginSearchView::startSearch); connect(m_ui.excludeCombo, static_cast(&KComboBox::returnPressed), this, &KatePluginSearchView::startSearch); connect(m_ui.searchButton, &QPushButton::clicked, this, &KatePluginSearchView::startSearch); connect(m_ui.displayOptions, &QToolButton::toggled, this, &KatePluginSearchView::toggleOptions); connect(m_ui.searchPlaceCombo, static_cast(&QComboBox::currentIndexChanged), this, &KatePluginSearchView::searchPlaceChanged); connect(m_ui.searchPlaceCombo, static_cast(&QComboBox::currentIndexChanged), this, [this](int) { if (m_ui.searchPlaceCombo->currentIndex() == Folder) { m_ui.displayOptions->setChecked(true); } }); - connect(m_ui.stopButton, &QPushButton::clicked, &m_searchOpenFiles, &SearchOpenFiles::cancelSearch); - connect(m_ui.stopButton, &QPushButton::clicked, &m_searchDiskFiles, &SearchDiskFiles::cancelSearch); - connect(m_ui.stopButton, &QPushButton::clicked, &m_folderFilesList, &FolderFilesList::cancelSearch); - connect(m_ui.stopButton, &QPushButton::clicked, &m_replacer, &ReplaceMatches::cancelReplace); + connect(m_ui.stopButton, &QPushButton::clicked, this, &KatePluginSearchView::stopClicked); connect(m_ui.nextButton, &QToolButton::clicked, this, &KatePluginSearchView::goToNextMatch); connect(m_ui.replaceButton, &QPushButton::clicked, this, &KatePluginSearchView::replaceSingleMatch); connect(m_ui.replaceCheckedBtn, &QPushButton::clicked, this, &KatePluginSearchView::replaceChecked); connect(m_ui.replaceCombo->lineEdit(), &QLineEdit::returnPressed, this, &KatePluginSearchView::replaceChecked); m_ui.displayOptions->setChecked(true); connect(&m_searchOpenFiles, &SearchOpenFiles::matchFound, this, &KatePluginSearchView::matchFound); connect(&m_searchOpenFiles, &SearchOpenFiles::searchDone, this, &KatePluginSearchView::searchDone); connect(&m_searchOpenFiles, static_cast(&SearchOpenFiles::searching), this, &KatePluginSearchView::searching); - connect(&m_folderFilesList, &FolderFilesList::finished, this, &KatePluginSearchView::folderFileListChanged); + connect(&m_folderFilesList, &FolderFilesList::fileListReady, this, &KatePluginSearchView::folderFileListChanged); connect(&m_folderFilesList, &FolderFilesList::searching, this, &KatePluginSearchView::searching); connect(&m_searchDiskFiles, &SearchDiskFiles::matchFound, this, &KatePluginSearchView::matchFound); connect(&m_searchDiskFiles, &SearchDiskFiles::searchDone, this, &KatePluginSearchView::searchDone); connect(&m_searchDiskFiles, static_cast(&SearchDiskFiles::searching), this, &KatePluginSearchView::searching); connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, &m_searchOpenFiles, &SearchOpenFiles::cancelSearch); connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, &m_replacer, &ReplaceMatches::cancelReplace); connect(m_kateApp, &KTextEditor::Application::documentWillBeDeleted, this, &KatePluginSearchView::clearDocMarks); connect(&m_replacer, &ReplaceMatches::replaceStatus, this, &KatePluginSearchView::replaceStatus); // Hook into line edit context menus m_ui.searchCombo->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_ui.searchCombo, &QComboBox::customContextMenuRequested, this, &KatePluginSearchView::searchContextMenu); m_ui.searchCombo->completer()->setCompletionMode(QCompleter::PopupCompletion); m_ui.searchCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); m_ui.searchCombo->setInsertPolicy(QComboBox::NoInsert); m_ui.searchCombo->lineEdit()->setClearButtonEnabled(true); m_ui.searchCombo->setMaxCount(25); QAction *searchComboActionForInsertRegexButton = m_ui.searchCombo->lineEdit()->addAction(QIcon::fromTheme(QStringLiteral("code-context"), QIcon::fromTheme(QStringLiteral("edit-find-replace"))), QLineEdit::TrailingPosition); connect(searchComboActionForInsertRegexButton, &QAction::triggered, this, [this]() { QMenu menu; QSet actionList; addRegexHelperActionsForSearch(&actionList, &menu); auto &&action = menu.exec(QCursor::pos()); regexHelperActOnAction(action, actionList, m_ui.searchCombo->lineEdit()); }); m_ui.replaceCombo->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_ui.replaceCombo, &QComboBox::customContextMenuRequested, this, &KatePluginSearchView::replaceContextMenu); m_ui.replaceCombo->completer()->setCompletionMode(QCompleter::PopupCompletion); m_ui.replaceCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); m_ui.replaceCombo->setInsertPolicy(QComboBox::NoInsert); m_ui.replaceCombo->lineEdit()->setClearButtonEnabled(true); m_ui.replaceCombo->setMaxCount(25); QAction *replaceComboActionForInsertRegexButton = m_ui.replaceCombo->lineEdit()->addAction(QIcon::fromTheme(QStringLiteral("code-context")), QLineEdit::TrailingPosition); connect(replaceComboActionForInsertRegexButton, &QAction::triggered, this, [this]() { QMenu menu; QSet actionList; addRegexHelperActionsForReplace(&actionList, &menu); auto &&action = menu.exec(QCursor::pos()); regexHelperActOnAction(action, actionList, m_ui.replaceCombo->lineEdit()); }); QAction *replaceComboActionForInsertSpecialButton = m_ui.replaceCombo->lineEdit()->addAction(QIcon::fromTheme(QStringLiteral("insert-text")), QLineEdit::TrailingPosition); connect(replaceComboActionForInsertSpecialButton, &QAction::triggered, this, [this]() { QMenu menu; QSet actionList; addSpecialCharsHelperActionsForReplace(&actionList, &menu); auto &&action = menu.exec(QCursor::pos()); regexHelperActOnAction(action, actionList, m_ui.replaceCombo->lineEdit()); }); connect(m_ui.useRegExp, &QToolButton::toggled, &m_changeTimer, static_cast(&QTimer::start)); auto onRegexToggleChanged = [=] { Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (res) { bool useRegExp = m_ui.useRegExp->isChecked(); res->useRegExp = useRegExp; searchComboActionForInsertRegexButton->setVisible(useRegExp); replaceComboActionForInsertRegexButton->setVisible(useRegExp); } }; connect(m_ui.useRegExp, &QToolButton::toggled, this, onRegexToggleChanged); onRegexToggleChanged(); // invoke initially m_changeTimer.setInterval(300); m_changeTimer.setSingleShot(true); connect(&m_changeTimer, &QTimer::timeout, this, &KatePluginSearchView::startSearchWhileTyping); m_toolView->setMinimumHeight(container->sizeHint().height()); connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KatePluginSearchView::handleEsc); // watch for project plugin view creation/deletion connect(m_mainWindow, &KTextEditor::MainWindow::pluginViewCreated, this, &KatePluginSearchView::slotPluginViewCreated); connect(m_mainWindow, &KTextEditor::MainWindow::pluginViewDeleted, this, &KatePluginSearchView::slotPluginViewDeleted); connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KatePluginSearchView::docViewChanged); // Connect signals from project plugin to our slots m_projectPluginView = m_mainWindow->pluginView(QStringLiteral("kateprojectplugin")); slotPluginViewCreated(QStringLiteral("kateprojectplugin"), m_projectPluginView); m_replacer.setDocumentManager(m_kateApp); connect(&m_replacer, &ReplaceMatches::replaceDone, this, &KatePluginSearchView::replaceDone); searchPlaceChanged(); m_toolView->installEventFilter(this); m_mainWindow->guiFactory()->addClient(this); m_updateSumaryTimer.setInterval(1); m_updateSumaryTimer.setSingleShot(true); connect(&m_updateSumaryTimer, &QTimer::timeout, this, &KatePluginSearchView::updateResultsRootItem); } KatePluginSearchView::~KatePluginSearchView() { clearMarks(); m_mainWindow->guiFactory()->removeClient(this); delete m_toolView; } void KatePluginSearchView::navigateFolderUp() { // navigate one folder up m_ui.folderRequester->setUrl(localFileDirUp(m_ui.folderRequester->url())); } void KatePluginSearchView::setCurrentFolder() { if (!m_mainWindow) { return; } KTextEditor::View *editView = m_mainWindow->activeView(); if (editView && editView->document()) { // upUrl as we want the folder not the file m_ui.folderRequester->setUrl(localFileDirUp(editView->document()->url())); } m_ui.displayOptions->setChecked(true); } void KatePluginSearchView::openSearchView() { if (!m_mainWindow) { return; } if (!m_toolView->isVisible()) { m_mainWindow->showToolView(m_toolView); } m_ui.searchCombo->setFocus(Qt::OtherFocusReason); if (m_ui.searchPlaceCombo->currentIndex() == Folder) { m_ui.displayOptions->setChecked(true); } KTextEditor::View *editView = m_mainWindow->activeView(); if (editView && editView->document()) { if (m_ui.folderRequester->text().isEmpty()) { // upUrl as we want the folder not the file m_ui.folderRequester->setUrl(localFileDirUp(editView->document()->url())); } QString selection; if (editView->selection()) { selection = editView->selectionText(); // remove possible trailing '\n' if (selection.endsWith(QLatin1Char('\n'))) { selection = selection.left(selection.size() - 1); } } if (selection.isEmpty()) { selection = editView->document()->wordAt(editView->cursorPosition()); } if (!selection.isEmpty() && !selection.contains(QLatin1Char('\n'))) { m_ui.searchCombo->blockSignals(true); m_ui.searchCombo->lineEdit()->setText(selection); m_ui.searchCombo->blockSignals(false); } m_ui.searchCombo->lineEdit()->selectAll(); m_searchJustOpened = true; startSearchWhileTyping(); } } void KatePluginSearchView::handleEsc(QEvent *e) { if (!m_mainWindow) return; QKeyEvent *k = static_cast(e); if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) { static ulong lastTimeStamp; if (lastTimeStamp == k->timestamp()) { // Same as previous... This looks like a bug somewhere... return; } lastTimeStamp = k->timestamp(); if (!m_matchRanges.isEmpty()) { clearMarks(); } else if (m_toolView->isVisible()) { m_mainWindow->hideToolView(m_toolView); } // Remove check marks Results *curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!curResults) { qWarning() << "This is a bug"; return; } QTreeWidgetItemIterator it(curResults->tree); while (*it) { (*it)->setCheckState(0, Qt::Unchecked); ++it; } } } void KatePluginSearchView::setSearchString(const QString &pattern) { m_ui.searchCombo->lineEdit()->setText(pattern); } void KatePluginSearchView::toggleOptions(bool show) { m_ui.stackedWidget->setCurrentIndex((show) ? 1 : 0); } void KatePluginSearchView::setSearchPlace(int place) { if (place >= m_ui.searchPlaceCombo->count()) { // This probably means the project plugin is not active or no project loaded // fallback to search in folder qDebug() << place << "is not a valid search place index"; place = Folder; } m_ui.searchPlaceCombo->setCurrentIndex(place); } QStringList KatePluginSearchView::filterFiles(const QStringList &files) const { QString types = m_ui.filterCombo->currentText(); QString excludes = m_ui.excludeCombo->currentText(); if (((types.isEmpty() || types == QLatin1String("*"))) && (excludes.isEmpty())) { // shortcut for use all files return files; } QStringList tmpTypes = types.split(QLatin1Char(',')); QVector typeList(tmpTypes.size()); for (int i = 0; i < tmpTypes.size(); i++) { QRegExp rx(tmpTypes[i].trimmed()); rx.setPatternSyntax(QRegExp::Wildcard); typeList << rx; } QStringList tmpExcludes = excludes.split(QLatin1Char(',')); QVector excludeList(tmpExcludes.size()); for (int i = 0; i < tmpExcludes.size(); i++) { QRegExp rx(tmpExcludes[i].trimmed()); rx.setPatternSyntax(QRegExp::Wildcard); excludeList << rx; } QStringList filteredFiles; for (const QString &fileName : files) { bool isInSubDir = fileName.startsWith(m_resultBaseDir); QString nameToCheck = fileName; if (isInSubDir) { nameToCheck = fileName.mid(m_resultBaseDir.size()); } bool skip = false; for (const auto ®ex : qAsConst(excludeList)) { if (regex.exactMatch(nameToCheck)) { skip = true; break; } } if (skip) { continue; } for (const auto ®ex : qAsConst(typeList)) { if (regex.exactMatch(nameToCheck)) { filteredFiles << fileName; break; } } } return filteredFiles; } void KatePluginSearchView::folderFileListChanged() { m_searchDiskFilesDone = false; m_searchOpenFilesDone = false; if (!m_curResults) { qWarning() << "This is a bug"; m_searchDiskFilesDone = true; m_searchOpenFilesDone = true; searchDone(); return; } QStringList fileList = m_folderFilesList.fileList(); QList openList; for (int i = 0; i < m_kateApp->documents().size(); i++) { int index = fileList.indexOf(m_kateApp->documents()[i]->url().toLocalFile()); if (index != -1) { openList << m_kateApp->documents()[i]; fileList.removeAt(index); } } // search order is important: Open files starts immediately and should finish // earliest after first event loop. // The DiskFile might finish immediately if (!openList.empty()) { m_searchOpenFiles.startSearch(openList, m_curResults->regExp); } else { m_searchOpenFilesDone = true; } m_searchDiskFiles.startSearch(fileList, m_curResults->regExp); } void KatePluginSearchView::searchPlaceChanged() { int searchPlace = m_ui.searchPlaceCombo->currentIndex(); const bool inFolder = (searchPlace == Folder); m_ui.filterCombo->setEnabled(searchPlace >= Folder); m_ui.excludeCombo->setEnabled(searchPlace >= Folder); m_ui.folderRequester->setEnabled(inFolder); m_ui.folderUpButton->setEnabled(inFolder); m_ui.currentFolderButton->setEnabled(inFolder); m_ui.recursiveCheckBox->setEnabled(inFolder); m_ui.hiddenCheckBox->setEnabled(inFolder); m_ui.symLinkCheckBox->setEnabled(inFolder); m_ui.binaryCheckBox->setEnabled(inFolder); if (inFolder && sender() == m_ui.searchPlaceCombo) { setCurrentFolder(); } // ... and the labels: m_ui.folderLabel->setEnabled(m_ui.folderRequester->isEnabled()); m_ui.filterLabel->setEnabled(m_ui.filterCombo->isEnabled()); m_ui.excludeLabel->setEnabled(m_ui.excludeCombo->isEnabled()); Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (res) { res->searchPlaceIndex = searchPlace; } } void KatePluginSearchView::addHeaderItem() { QTreeWidgetItem *item = new QTreeWidgetItem(m_curResults->tree, QStringList()); item->setCheckState(0, Qt::Checked); item->setFlags(item->flags() | Qt::ItemIsAutoTristate); m_curResults->tree->expandItem(item); } QTreeWidgetItem *KatePluginSearchView::rootFileItem(const QString &url, const QString &fName) { if (!m_curResults) { return nullptr; } QUrl fullUrl = QUrl::fromUserInput(url); QString path = fullUrl.isLocalFile() ? localFileDirUp(fullUrl).path() : fullUrl.url(); if (!path.isEmpty() && !path.endsWith(QLatin1Char('/'))) { path += QLatin1Char('/'); } path.remove(m_resultBaseDir); QString name = fullUrl.fileName(); if (url.isEmpty()) { name = fName; } // make sure we have a root item if (m_curResults->tree->topLevelItemCount() == 0) { addHeaderItem(); } QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (m_isSearchAsYouType) { return root; } for (int i = 0; i < root->childCount(); i++) { // qDebug() << root->child(i)->data(0, ReplaceMatches::FileNameRole).toString() << fName; if ((root->child(i)->data(0, ReplaceMatches::FileUrlRole).toString() == url) && (root->child(i)->data(0, ReplaceMatches::FileNameRole).toString() == fName)) { int matches = root->child(i)->data(0, ReplaceMatches::StartLineRole).toInt() + 1; QString tmpUrl = QStringLiteral("%1%2: %3").arg(path, name).arg(matches); root->child(i)->setData(0, Qt::DisplayRole, tmpUrl); root->child(i)->setData(0, ReplaceMatches::StartLineRole, matches); return root->child(i); } } // file item not found create a new one QString tmpUrl = QStringLiteral("%1%2: %3").arg(path, name).arg(1); TreeWidgetItem *item = new TreeWidgetItem(root, QStringList(tmpUrl)); item->setData(0, ReplaceMatches::FileUrlRole, url); item->setData(0, ReplaceMatches::FileNameRole, fName); item->setData(0, ReplaceMatches::StartLineRole, 1); item->setCheckState(0, Qt::Checked); item->setFlags(item->flags() | Qt::ItemIsAutoTristate); return item; } void KatePluginSearchView::addMatchMark(KTextEditor::Document *doc, QTreeWidgetItem *item) { if (!doc || !item) { return; } KTextEditor::View *activeView = m_mainWindow->activeView(); KTextEditor::MovingInterface *miface = qobject_cast(doc); KTextEditor::ConfigInterface *ciface = qobject_cast(activeView); KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); int line = item->data(0, ReplaceMatches::StartLineRole).toInt(); int column = item->data(0, ReplaceMatches::StartColumnRole).toInt(); int endLine = item->data(0, ReplaceMatches::EndLineRole).toInt(); int endColumn = item->data(0, ReplaceMatches::EndColumnRole).toInt(); bool isReplaced = item->data(0, ReplaceMatches::ReplacedRole).toBool(); if (isReplaced) { QColor replaceColor(Qt::green); if (ciface) replaceColor = ciface->configValue(QStringLiteral("replace-highlight-color")).value(); attr->setBackground(replaceColor); if (activeView) { attr->setForeground(activeView->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color()); } } else { QColor searchColor(Qt::yellow); if (ciface) searchColor = ciface->configValue(QStringLiteral("search-highlight-color")).value(); attr->setBackground(searchColor); if (activeView) { attr->setForeground(activeView->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color()); } } KTextEditor::Range range(line, column, endLine, endColumn); // Check that the match still matches if (m_curResults) { if (!isReplaced) { // special handling for "(?=\\n)" in multi-line search QRegularExpression tmpReg = m_curResults->regExp; if (m_curResults->regExp.pattern().endsWith(QLatin1String("(?=\\n)"))) { QString newPatern = tmpReg.pattern(); newPatern.replace(QStringLiteral("(?=\\n)"), QStringLiteral("$")); tmpReg.setPattern(newPatern); } // Check that the match still matches ;) if (tmpReg.match(doc->text(range)).capturedStart() != 0) { // qDebug() << doc->text(range) << "Does not match" << m_curResults->regExp.pattern(); return; } } else { if (doc->text(range) != item->data(0, ReplaceMatches::ReplacedTextRole).toString()) { // qDebug() << doc->text(range) << "Does not match" << item->data(0, ReplaceMatches::ReplacedTextRole).toString(); return; } } } // Highlight the match KTextEditor::MovingRange *mr = miface->newMovingRange(range); mr->setAttribute(attr); mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection mr->setAttributeOnlyForViews(true); m_matchRanges.append(mr); // Add a match mark #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,69,0) KTextEditor::MarkInterfaceV2 *iface = qobject_cast(doc); #else KTextEditor::MarkInterface *iface = qobject_cast(doc); #endif if (!iface) return; iface->setMarkDescription(KTextEditor::MarkInterface::markType32, i18n("SearchHighLight")); #if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,69,0) iface->setMarkIcon(KTextEditor::MarkInterface::markType32, QIcon()); #else iface->setMarkPixmap(KTextEditor::MarkInterface::markType32, QIcon().pixmap(0, 0)); #endif iface->addMark(line, KTextEditor::MarkInterface::markType32); connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(clearMarks()), Qt::UniqueConnection); } static const int contextLen = 70; void KatePluginSearchView::matchFound(const QString &url, const QString &fName, const QString &lineContent, int matchLen, int startLine, int startColumn, int endLine, int endColumn) { - if (!m_curResults) { + if (!m_curResults || (sender() == &m_searchDiskFiles && m_blockDiskMatchFound)) { return; } int preLen = contextLen; int preStart = startColumn - preLen; if (preStart < 0) { preLen += preStart; preStart = 0; } QString pre; if (preLen == contextLen) { pre = QStringLiteral("..."); } pre += lineContent.mid(preStart, preLen).toHtmlEscaped(); QString match = lineContent.mid(startColumn, matchLen).toHtmlEscaped(); match.replace(QLatin1Char('\n'), QStringLiteral("\\n")); QString post = lineContent.mid(startColumn + matchLen, contextLen); if (post.size() >= contextLen) { post += QStringLiteral("..."); } post = post.toHtmlEscaped(); QStringList row; row << i18n("Line: %1 Column: %2: %3", startLine + 1, startColumn + 1, pre + QStringLiteral("") + match + QStringLiteral("") + post); TreeWidgetItem *item = new TreeWidgetItem(rootFileItem(url, fName), row); item->setData(0, ReplaceMatches::FileUrlRole, url); item->setData(0, Qt::ToolTipRole, url); item->setData(0, ReplaceMatches::FileNameRole, fName); item->setData(0, ReplaceMatches::StartLineRole, startLine); item->setData(0, ReplaceMatches::StartColumnRole, startColumn); item->setData(0, ReplaceMatches::MatchLenRole, matchLen); item->setData(0, ReplaceMatches::PreMatchRole, pre); item->setData(0, ReplaceMatches::MatchRole, match); item->setData(0, ReplaceMatches::PostMatchRole, post); item->setData(0, ReplaceMatches::EndLineRole, endLine); item->setData(0, ReplaceMatches::EndColumnRole, endColumn); item->setCheckState(0, Qt::Checked); m_curResults->matches++; } void KatePluginSearchView::clearMarks() { const auto docs = m_kateApp->documents(); for (KTextEditor::Document *doc : docs) { clearDocMarks(doc); } qDeleteAll(m_matchRanges); m_matchRanges.clear(); } void KatePluginSearchView::clearDocMarks(KTextEditor::Document *doc) { KTextEditor::MarkInterface *iface; iface = qobject_cast(doc); if (iface) { const QHash marks = iface->marks(); QHashIterator i(marks); while (i.hasNext()) { i.next(); if (i.value()->type & KTextEditor::MarkInterface::markType32) { iface->removeMark(i.value()->line, KTextEditor::MarkInterface::markType32); } } } int i = 0; while (i < m_matchRanges.size()) { if (m_matchRanges.at(i)->document() == doc) { delete m_matchRanges.at(i); m_matchRanges.removeAt(i); } else { i++; } } m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { qWarning() << "This is a bug"; return; } } +void KatePluginSearchView::stopClicked() +{ + m_folderFilesList.cancelSearch(); + m_searchOpenFiles.cancelSearch(); + m_searchDiskFiles.cancelSearch(); + m_replacer.cancelReplace(); + m_searchDiskFilesDone = true; + m_searchOpenFilesDone = true; + searchDone(); // Just in case the folder list was being populated... +} + void KatePluginSearchView::startSearch() { + // Forcefully stop any ongoing search or replace + m_blockDiskMatchFound = true; // Do not allow leftover machFound:s from a previous search to be added + m_folderFilesList.terminateSearch(); + m_searchOpenFiles.terminateSearch(); + m_searchDiskFiles.terminateSearch(); + // Re-enable the handling of fisk-file-matches after one event loop + // For some reason blocking of signals or disconnect/connect does not prevent the slot from being called, + // so we use m_blockDiskMatchFound to skip any old matchFound signals during the first event loop. + // New matches from disk-files should not come before the first event loop has executed. + QTimer::singleShot(0, this, [this]() { m_blockDiskMatchFound = false; }); + m_replacer.terminateReplace(); + m_changeTimer.stop(); // make sure not to start a "while you type" search now m_mainWindow->showToolView(m_toolView); // in case we are invoked from the command interface m_projectSearchPlaceIndex = 0; // now that we started, don't switch back automatically if (m_ui.searchCombo->currentText().isEmpty()) { // return pressed in the folder combo or filter combo return; } m_isSearchAsYouType = false; QString currentSearchText = m_ui.searchCombo->currentText(); m_ui.searchCombo->setItemText(0, QString()); // remove the text from index 0 on enter/search int index = m_ui.searchCombo->findText(currentSearchText); if (index > 0) { m_ui.searchCombo->removeItem(index); } m_ui.searchCombo->insertItem(1, currentSearchText); m_ui.searchCombo->setCurrentIndex(1); if (m_ui.filterCombo->findText(m_ui.filterCombo->currentText()) == -1) { m_ui.filterCombo->insertItem(0, m_ui.filterCombo->currentText()); m_ui.filterCombo->setCurrentIndex(0); } if (m_ui.excludeCombo->findText(m_ui.excludeCombo->currentText()) == -1) { m_ui.excludeCombo->insertItem(0, m_ui.excludeCombo->currentText()); m_ui.excludeCombo->setCurrentIndex(0); } if (m_ui.folderRequester->comboBox()->findText(m_ui.folderRequester->comboBox()->currentText()) == -1) { m_ui.folderRequester->comboBox()->insertItem(0, m_ui.folderRequester->comboBox()->currentText()); m_ui.folderRequester->comboBox()->setCurrentIndex(0); } m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { qWarning() << "This is a bug"; return; } QRegularExpression::PatternOptions patternOptions = (m_ui.matchCase->isChecked() ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption); QString pattern = (m_ui.useRegExp->isChecked() ? currentSearchText : QRegularExpression::escape(currentSearchText)); QRegularExpression reg(pattern, patternOptions); if (!reg.isValid()) { // qDebug() << "invalid regexp"; indicateMatch(false); return; } m_curResults->regExp = reg; m_curResults->useRegExp = m_ui.useRegExp->isChecked(); m_curResults->matchCase = m_ui.matchCase->isChecked(); m_curResults->searchPlaceIndex = m_ui.searchPlaceCombo->currentIndex(); m_ui.newTabButton->setDisabled(true); m_ui.searchCombo->setDisabled(true); m_ui.searchButton->setDisabled(true); m_ui.displayOptions->setChecked(false); m_ui.displayOptions->setDisabled(true); m_ui.replaceCheckedBtn->setDisabled(true); m_ui.replaceButton->setDisabled(true); m_ui.stopAndNext->setCurrentWidget(m_ui.stopButton); m_ui.replaceCombo->setDisabled(true); m_ui.searchPlaceCombo->setDisabled(true); m_ui.useRegExp->setDisabled(true); m_ui.matchCase->setDisabled(true); m_ui.expandResults->setDisabled(true); m_ui.currentFolderButton->setDisabled(true); clearMarks(); m_curResults->tree->clear(); m_curResults->tree->setCurrentItem(nullptr); m_curResults->matches = 0; disconnect(m_curResults->tree, &QTreeWidget::itemChanged, &m_updateSumaryTimer, nullptr); m_ui.resultTabWidget->setTabText(m_ui.resultTabWidget->currentIndex(), m_ui.searchCombo->currentText()); m_toolView->setCursor(Qt::WaitCursor); m_searchDiskFilesDone = false; m_searchOpenFilesDone = false; const bool inCurrentProject = m_ui.searchPlaceCombo->currentIndex() == Project; const bool inAllOpenProjects = m_ui.searchPlaceCombo->currentIndex() == AllProjects; if (m_ui.searchPlaceCombo->currentIndex() == CurrentFile) { m_searchDiskFilesDone = true; m_resultBaseDir.clear(); QList documents; KTextEditor::View *activeView = m_mainWindow->activeView(); if (activeView) { documents << activeView->document(); } addHeaderItem(); m_searchOpenFiles.startSearch(documents, reg); } else if (m_ui.searchPlaceCombo->currentIndex() == OpenFiles) { m_searchDiskFilesDone = true; m_resultBaseDir.clear(); const QList documents = m_kateApp->documents(); addHeaderItem(); m_searchOpenFiles.startSearch(documents, reg); } else if (m_ui.searchPlaceCombo->currentIndex() == Folder) { m_resultBaseDir = m_ui.folderRequester->url().path(); if (!m_resultBaseDir.isEmpty() && !m_resultBaseDir.endsWith(QLatin1Char('/'))) m_resultBaseDir += QLatin1Char('/'); addHeaderItem(); m_folderFilesList.generateList(m_ui.folderRequester->text(), m_ui.recursiveCheckBox->isChecked(), m_ui.hiddenCheckBox->isChecked(), m_ui.symLinkCheckBox->isChecked(), m_ui.binaryCheckBox->isChecked(), m_ui.filterCombo->currentText(), m_ui.excludeCombo->currentText()); // the file list will be ready when the thread returns (connected to folderFileListChanged) } else if (inCurrentProject || inAllOpenProjects) { /** * init search with file list from current project, if any */ m_resultBaseDir.clear(); QStringList files; if (m_projectPluginView) { if (inCurrentProject) { m_resultBaseDir = m_projectPluginView->property("projectBaseDir").toString(); } else { m_resultBaseDir = m_projectPluginView->property("allProjectsCommonBaseDir").toString(); } if (!m_resultBaseDir.endsWith(QLatin1Char('/'))) m_resultBaseDir += QLatin1Char('/'); QStringList projectFiles; if (inCurrentProject) { projectFiles = m_projectPluginView->property("projectFiles").toStringList(); } else { projectFiles = m_projectPluginView->property("allProjectsFiles").toStringList(); } files = filterFiles(projectFiles); } addHeaderItem(); QList openList; const auto docs = m_kateApp->documents(); for (const auto doc : docs) { // match project file's list toLocalFile() int index = files.indexOf(doc->url().toLocalFile()); if (index != -1) { openList << doc; files.removeAt(index); } } // search order is important: Open files starts immediately and should finish // earliest after first event loop. // The DiskFile might finish immediately if (!openList.empty()) { m_searchOpenFiles.startSearch(openList, m_curResults->regExp); } else { m_searchOpenFilesDone = true; } m_searchDiskFiles.startSearch(files, reg); } else { qDebug() << "Case not handled:" << m_ui.searchPlaceCombo->currentIndex(); Q_ASSERT_X(false, "KatePluginSearchView::startSearch", "case not handled"); } } void KatePluginSearchView::startSearchWhileTyping() { if (!m_searchDiskFilesDone || !m_searchOpenFilesDone) { return; } m_isSearchAsYouType = true; QString currentSearchText = m_ui.searchCombo->currentText(); m_ui.searchButton->setDisabled(currentSearchText.isEmpty()); // Do not clear the search results if you press up by mistake if (currentSearchText.isEmpty()) return; if (!m_mainWindow->activeView()) return; KTextEditor::Document *doc = m_mainWindow->activeView()->document(); if (!doc) return; m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { qWarning() << "This is a bug"; return; } // check if we typed something or just changed combobox index // changing index should not trigger a search-as-you-type if (m_ui.searchCombo->currentIndex() > 0 && currentSearchText == m_ui.searchCombo->itemText(m_ui.searchCombo->currentIndex())) { return; } // Now we should have a true typed text change QRegularExpression::PatternOptions patternOptions = (m_ui.matchCase->isChecked() ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption); QString pattern = (m_ui.useRegExp->isChecked() ? currentSearchText : QRegularExpression::escape(currentSearchText)); QRegularExpression reg(pattern, patternOptions); if (!reg.isValid()) { // qDebug() << "invalid regexp"; indicateMatch(false); return; } disconnect(m_curResults->tree, &QTreeWidget::itemChanged, &m_updateSumaryTimer, nullptr); m_curResults->regExp = reg; m_curResults->useRegExp = m_ui.useRegExp->isChecked(); m_ui.replaceCheckedBtn->setDisabled(true); m_ui.replaceButton->setDisabled(true); m_ui.nextButton->setDisabled(true); int cursorPosition = m_ui.searchCombo->lineEdit()->cursorPosition(); bool hasSelected = m_ui.searchCombo->lineEdit()->hasSelectedText(); m_ui.searchCombo->blockSignals(true); m_ui.searchCombo->setItemText(0, currentSearchText); m_ui.searchCombo->setCurrentIndex(0); m_ui.searchCombo->lineEdit()->setCursorPosition(cursorPosition); if (hasSelected) { // This restores the select all from invoking openSearchView // This selects too much if we have a partial selection and toggle match-case/regexp m_ui.searchCombo->lineEdit()->selectAll(); } m_ui.searchCombo->blockSignals(false); // Prepare for the new search content clearMarks(); m_resultBaseDir.clear(); m_curResults->tree->clear(); m_curResults->tree->setCurrentItem(nullptr); m_curResults->matches = 0; // Add the search-as-you-type header item TreeWidgetItem *item = new TreeWidgetItem(m_curResults->tree, QStringList()); item->setData(0, ReplaceMatches::FileUrlRole, doc->url().toString()); item->setData(0, ReplaceMatches::FileNameRole, doc->documentName()); item->setData(0, ReplaceMatches::StartLineRole, 0); item->setCheckState(0, Qt::Checked); item->setFlags(item->flags() | Qt::ItemIsAutoTristate); // Do the search int searchStoppedAt = m_searchOpenFiles.searchOpenFile(doc, reg, 0); searchWhileTypingDone(); if (searchStoppedAt != 0) { delete m_infoMessage; const QString msg = i18n("Searching while you type was interrupted. It would have taken too long."); m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Warning); m_infoMessage->setPosition(KTextEditor::Message::TopInView); m_infoMessage->setAutoHide(3000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(m_mainWindow->activeView()); m_mainWindow->activeView()->document()->postMessage(m_infoMessage); } } void KatePluginSearchView::searchDone() { m_changeTimer.stop(); // avoid "while you type" search directly after if (sender() == &m_searchDiskFiles) { m_searchDiskFilesDone = true; } if (sender() == &m_searchOpenFiles) { m_searchOpenFilesDone = true; } if (!m_searchDiskFilesDone || !m_searchOpenFilesDone) { return; } QWidget *fw = QApplication::focusWidget(); // NOTE: we take the focus widget here before the enabling/disabling // moves the focus around. m_ui.newTabButton->setDisabled(false); m_ui.searchCombo->setDisabled(false); m_ui.searchButton->setDisabled(false); m_ui.stopAndNext->setCurrentWidget(m_ui.nextButton); m_ui.displayOptions->setDisabled(false); m_ui.replaceCombo->setDisabled(false); m_ui.searchPlaceCombo->setDisabled(false); m_ui.useRegExp->setDisabled(false); m_ui.matchCase->setDisabled(false); m_ui.expandResults->setDisabled(false); m_ui.currentFolderButton->setDisabled(false); if (!m_curResults) { return; } m_ui.replaceCheckedBtn->setDisabled(m_curResults->matches < 1); m_ui.replaceButton->setDisabled(m_curResults->matches < 1); m_ui.nextButton->setDisabled(m_curResults->matches < 1); m_curResults->tree->sortItems(0, Qt::AscendingOrder); m_curResults->tree->expandAll(); m_curResults->tree->resizeColumnToContents(0); if (m_curResults->tree->columnWidth(0) < m_curResults->tree->width() - 30) { m_curResults->tree->setColumnWidth(0, m_curResults->tree->width() - 30); } // expand the "header item " to display all files and all results if configured expandResults(); updateResultsRootItem(); connect(m_curResults->tree, &QTreeWidget::itemChanged, &m_updateSumaryTimer, static_cast(&QTimer::start)); indicateMatch(m_curResults->matches > 0); m_curResults = nullptr; m_toolView->unsetCursor(); if (fw == m_ui.stopButton) { m_ui.searchCombo->setFocus(); } m_searchJustOpened = false; } void KatePluginSearchView::searchWhileTypingDone() { if (!m_curResults) { return; } bool popupVisible = m_ui.searchCombo->lineEdit()->completer()->popup()->isVisible(); m_ui.replaceCheckedBtn->setDisabled(m_curResults->matches < 1); m_ui.replaceButton->setDisabled(m_curResults->matches < 1); m_ui.nextButton->setDisabled(m_curResults->matches < 1); m_curResults->tree->expandAll(); m_curResults->tree->resizeColumnToContents(0); if (m_curResults->tree->columnWidth(0) < m_curResults->tree->width() - 30) { m_curResults->tree->setColumnWidth(0, m_curResults->tree->width() - 30); } QWidget *focusObject = nullptr; QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (root) { QTreeWidgetItem *child = root->child(0); if (!m_searchJustOpened) { focusObject = qobject_cast(QGuiApplication::focusObject()); } indicateMatch(child); updateResultsRootItem(); connect(m_curResults->tree, &QTreeWidget::itemChanged, &m_updateSumaryTimer, static_cast(&QTimer::start)); } m_curResults = nullptr; if (focusObject) { focusObject->setFocus(); } if (popupVisible) { m_ui.searchCombo->lineEdit()->completer()->complete(); } m_searchJustOpened = false; } void KatePluginSearchView::searching(const QString &file) { if (!m_curResults) { return; } QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (root) { if (file.size() > 70) { root->setData(0, Qt::DisplayRole, i18n("Searching: ...%1", file.right(70))); } else { root->setData(0, Qt::DisplayRole, i18n("Searching: %1", file)); } } } void KatePluginSearchView::indicateMatch(bool hasMatch) { QLineEdit *const lineEdit = m_ui.searchCombo->lineEdit(); QPalette background(lineEdit->palette()); if (hasMatch) { // Green background for line edit KColorScheme::adjustBackground(background, KColorScheme::PositiveBackground); } else { // Reset background of line edit background = QPalette(); } // Red background for line edit // KColorScheme::adjustBackground(background, KColorScheme::NegativeBackground); // Neutral background // KColorScheme::adjustBackground(background, KColorScheme::NeutralBackground); lineEdit->setPalette(background); } void KatePluginSearchView::replaceSingleMatch() { // Save the search text if (m_ui.searchCombo->findText(m_ui.searchCombo->currentText()) == -1) { m_ui.searchCombo->insertItem(1, m_ui.searchCombo->currentText()); m_ui.searchCombo->setCurrentIndex(1); } // Save the replace text if (m_ui.replaceCombo->findText(m_ui.replaceCombo->currentText()) == -1) { m_ui.replaceCombo->insertItem(1, m_ui.replaceCombo->currentText()); m_ui.replaceCombo->setCurrentIndex(1); } // Check if the cursor is at the current item if not jump there Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { return; // Security measure } QTreeWidgetItem *item = res->tree->currentItem(); if (!item || !item->parent()) { // Nothing was selected goToNextMatch(); return; } if (!m_mainWindow->activeView() || !m_mainWindow->activeView()->cursorPosition().isValid()) { itemSelected(item); // Correct any bad cursor positions return; } int cursorLine = m_mainWindow->activeView()->cursorPosition().line(); int cursorColumn = m_mainWindow->activeView()->cursorPosition().column(); int startLine = item->data(0, ReplaceMatches::StartLineRole).toInt(); int startColumn = item->data(0, ReplaceMatches::StartColumnRole).toInt(); if ((cursorLine != startLine) || (cursorColumn != startColumn)) { itemSelected(item); return; } KTextEditor::Document *doc = m_mainWindow->activeView()->document(); // Find the corresponding range int i; for (i = 0; i < m_matchRanges.size(); i++) { if (m_matchRanges[i]->document() != doc) continue; if (m_matchRanges[i]->start().line() != startLine) continue; if (m_matchRanges[i]->start().column() != startColumn) continue; break; } if (i >= m_matchRanges.size()) { goToNextMatch(); return; } m_replacer.replaceSingleMatch(doc, item, res->regExp, m_ui.replaceCombo->currentText()); goToNextMatch(); } void KatePluginSearchView::replaceChecked() { if (m_ui.searchCombo->findText(m_ui.searchCombo->currentText()) == -1) { m_ui.searchCombo->insertItem(1, m_ui.searchCombo->currentText()); m_ui.searchCombo->setCurrentIndex(1); } if (m_ui.replaceCombo->findText(m_ui.replaceCombo->currentText()) == -1) { m_ui.replaceCombo->insertItem(1, m_ui.replaceCombo->currentText()); m_ui.replaceCombo->setCurrentIndex(1); } m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { qWarning() << "Results not found"; return; } m_ui.stopAndNext->setCurrentWidget(m_ui.stopButton); m_ui.displayOptions->setChecked(false); m_ui.displayOptions->setDisabled(true); m_ui.newTabButton->setDisabled(true); m_ui.searchCombo->setDisabled(true); m_ui.searchButton->setDisabled(true); m_ui.replaceCheckedBtn->setDisabled(true); m_ui.replaceButton->setDisabled(true); m_ui.replaceCombo->setDisabled(true); m_ui.searchPlaceCombo->setDisabled(true); m_ui.useRegExp->setDisabled(true); m_ui.matchCase->setDisabled(true); m_ui.expandResults->setDisabled(true); m_ui.currentFolderButton->setDisabled(true); m_curResults->replaceStr = m_ui.replaceCombo->currentText(); QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (root) { m_curResults->treeRootText = root->data(0, Qt::DisplayRole).toString(); } m_replacer.replaceChecked(m_curResults->tree, m_curResults->regExp, m_curResults->replaceStr); } void KatePluginSearchView::replaceStatus(const QUrl &url, int replacedInFile, int matchesInFile) { if (!m_curResults) { // qDebug() << "m_curResults == nullptr"; return; } QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (root) { QString file = url.toString(QUrl::PreferLocalFile); if (file.size() > 70) { root->setData(0, Qt::DisplayRole, i18n("Processed %1 of %2 matches in: ...%3", replacedInFile, matchesInFile, file.right(70))); } else { root->setData(0, Qt::DisplayRole, i18n("Processed %1 of %2 matches in: %3", replacedInFile, matchesInFile, file)); } } } void KatePluginSearchView::replaceDone() { m_ui.stopAndNext->setCurrentWidget(m_ui.nextButton); m_ui.replaceCombo->setDisabled(false); m_ui.newTabButton->setDisabled(false); m_ui.searchCombo->setDisabled(false); m_ui.searchButton->setDisabled(false); m_ui.replaceCheckedBtn->setDisabled(false); m_ui.replaceButton->setDisabled(false); m_ui.displayOptions->setDisabled(false); m_ui.searchPlaceCombo->setDisabled(false); m_ui.useRegExp->setDisabled(false); m_ui.matchCase->setDisabled(false); m_ui.expandResults->setDisabled(false); m_ui.currentFolderButton->setDisabled(false); if (!m_curResults) { // qDebug() << "m_curResults == nullptr"; return; } QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (root) { root->setData(0, Qt::DisplayRole, m_curResults->treeRootText); } } void KatePluginSearchView::docViewChanged() { if (!m_mainWindow->activeView()) { return; } Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { // qDebug() << "No res"; return; } m_curResults = res; // add the marks if it is not already open KTextEditor::Document *doc = m_mainWindow->activeView()->document(); if (doc && res->tree->topLevelItemCount() > 0) { // There is always one root item with match count // and X children with files or matches in case of search while typing QTreeWidgetItem *rootItem = res->tree->topLevelItem(0); QTreeWidgetItem *fileItem = nullptr; for (int i = 0; i < rootItem->childCount(); i++) { QString url = rootItem->child(i)->data(0, ReplaceMatches::FileUrlRole).toString(); QString fName = rootItem->child(i)->data(0, ReplaceMatches::FileNameRole).toString(); if (url == doc->url().toString() && fName == doc->documentName()) { fileItem = rootItem->child(i); break; } } if (fileItem) { clearDocMarks(doc); if (m_isSearchAsYouType) { fileItem = fileItem->parent(); } for (int i = 0; i < fileItem->childCount(); i++) { if (fileItem->child(i)->checkState(0) == Qt::Unchecked) { continue; } addMatchMark(doc, fileItem->child(i)); } } // Re-add the highlighting on document reload connect(doc, &KTextEditor::Document::reloaded, this, &KatePluginSearchView::docViewChanged, Qt::UniqueConnection); } } void KatePluginSearchView::expandResults() { m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { qWarning() << "Results not found"; return; } if (m_ui.expandResults->isChecked()) { m_curResults->tree->expandAll(); } else { QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); m_curResults->tree->expandItem(root); if (root && (root->childCount() > 1)) { for (int i = 0; i < root->childCount(); i++) { m_curResults->tree->collapseItem(root->child(i)); } } } } void KatePluginSearchView::updateResultsRootItem() { m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { return; } QTreeWidgetItem *root = m_curResults->tree->topLevelItem(0); if (!root) { // nothing to update return; } int checkedItemCount = 0; if (m_curResults->matches > 0) { for (QTreeWidgetItemIterator it(m_curResults->tree, QTreeWidgetItemIterator::Checked | QTreeWidgetItemIterator::NoChildren); *it; ++it) { checkedItemCount++; } } QString checkedStr = i18np("One checked", "%1 checked", checkedItemCount); int searchPlace = m_ui.searchPlaceCombo->currentIndex(); if (m_isSearchAsYouType) { searchPlace = CurrentFile; } switch (searchPlace) { case CurrentFile: root->setData(0, Qt::DisplayRole, i18np("One match (%2) found in file", "%1 matches (%2) found in file", m_curResults->matches, checkedStr)); break; case OpenFiles: root->setData(0, Qt::DisplayRole, i18np("One match (%2) found in open files", "%1 matches (%2) found in open files", m_curResults->matches, checkedStr)); break; case Folder: root->setData(0, Qt::DisplayRole, i18np("One match (%3) found in folder %2", "%1 matches (%3) found in folder %2", m_curResults->matches, m_resultBaseDir, checkedStr)); break; case Project: { QString projectName; if (m_projectPluginView) { projectName = m_projectPluginView->property("projectName").toString(); } root->setData(0, Qt::DisplayRole, i18np("One match (%4) found in project %2 (%3)", "%1 matches (%4) found in project %2 (%3)", m_curResults->matches, projectName, m_resultBaseDir, checkedStr)); break; } case AllProjects: // "in Open Projects" root->setData(0, Qt::DisplayRole, i18np("One match (%3) found in all open projects (common parent: %2)", "%1 matches (%3) found in all open projects (common parent: %2)", m_curResults->matches, m_resultBaseDir, checkedStr)); break; } docViewChanged(); } void KatePluginSearchView::itemSelected(QTreeWidgetItem *item) { if (!item) return; m_curResults = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!m_curResults) { return; } while (item->data(0, ReplaceMatches::StartColumnRole).toString().isEmpty()) { item->treeWidget()->expandItem(item); item = item->child(0); if (!item) return; } item->treeWidget()->setCurrentItem(item); // get stuff int toLine = item->data(0, ReplaceMatches::StartLineRole).toInt(); int toColumn = item->data(0, ReplaceMatches::StartColumnRole).toInt(); KTextEditor::Document *doc; QString url = item->data(0, ReplaceMatches::FileUrlRole).toString(); if (!url.isEmpty()) { doc = m_kateApp->findUrl(QUrl::fromUserInput(url)); } else { doc = m_replacer.findNamed(item->data(0, ReplaceMatches::FileNameRole).toString()); } // add the marks to the document if it is not already open if (!doc) { doc = m_kateApp->openUrl(QUrl::fromUserInput(url)); } if (!doc) return; // open the right view... m_mainWindow->activateView(doc); // any view active? if (!m_mainWindow->activeView()) { return; } // set the cursor to the correct position m_mainWindow->activeView()->setCursorPosition(KTextEditor::Cursor(toLine, toColumn)); m_mainWindow->activeView()->setFocus(); } void KatePluginSearchView::goToNextMatch() { bool wrapFromFirst = false; bool startFromFirst = false; bool startFromCursor = false; Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { return; } QTreeWidgetItem *curr = res->tree->currentItem(); bool focusInView = m_mainWindow->activeView() && m_mainWindow->activeView()->hasFocus(); if (!curr && focusInView) { // no item has been visited && focus is not in searchCombo (probably in the view) -> // jump to the closest match after current cursor position // check if current file is in the file list curr = res->tree->topLevelItem(0); while (curr && curr->data(0, ReplaceMatches::FileUrlRole).toString() != m_mainWindow->activeView()->document()->url().toString()) { curr = res->tree->itemBelow(curr); } // now we are either in this file or !curr if (curr) { QTreeWidgetItem *fileBefore = curr; res->tree->expandItem(curr); int lineNr = 0; int columnNr = 0; if (m_mainWindow->activeView()->cursorPosition().isValid()) { lineNr = m_mainWindow->activeView()->cursorPosition().line(); columnNr = m_mainWindow->activeView()->cursorPosition().column(); } if (!curr->data(0, ReplaceMatches::StartColumnRole).isValid()) { curr = res->tree->itemBelow(curr); }; while (curr && curr->data(0, ReplaceMatches::StartLineRole).toInt() <= lineNr && curr->data(0, ReplaceMatches::FileUrlRole).toString() == m_mainWindow->activeView()->document()->url().toString()) { if (curr->data(0, ReplaceMatches::StartLineRole).toInt() == lineNr && curr->data(0, ReplaceMatches::StartColumnRole).toInt() >= columnNr - curr->data(0, ReplaceMatches::MatchLenRole).toInt()) { break; } fileBefore = curr; curr = res->tree->itemBelow(curr); } curr = fileBefore; startFromCursor = true; } } if (!curr) { curr = res->tree->topLevelItem(0); startFromFirst = true; } if (!curr) return; if (!curr->data(0, ReplaceMatches::StartColumnRole).toString().isEmpty()) { curr = res->tree->itemBelow(curr); if (!curr) { wrapFromFirst = true; curr = res->tree->topLevelItem(0); } } itemSelected(curr); if (startFromFirst) { delete m_infoMessage; const QString msg = i18n("Starting from first match"); m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information); m_infoMessage->setPosition(KTextEditor::Message::TopInView); m_infoMessage->setAutoHide(2000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(m_mainWindow->activeView()); m_mainWindow->activeView()->document()->postMessage(m_infoMessage); } else if (startFromCursor) { delete m_infoMessage; const QString msg = i18n("Next from cursor"); m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information); m_infoMessage->setPosition(KTextEditor::Message::BottomInView); m_infoMessage->setAutoHide(2000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(m_mainWindow->activeView()); m_mainWindow->activeView()->document()->postMessage(m_infoMessage); } else if (wrapFromFirst) { delete m_infoMessage; const QString msg = i18n("Continuing from first match"); m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information); m_infoMessage->setPosition(KTextEditor::Message::TopInView); m_infoMessage->setAutoHide(2000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(m_mainWindow->activeView()); m_mainWindow->activeView()->document()->postMessage(m_infoMessage); } } void KatePluginSearchView::goToPreviousMatch() { bool fromLast = false; Results *res = qobject_cast(m_ui.resultTabWidget->currentWidget()); if (!res) { return; } if (res->tree->topLevelItemCount() == 0) { return; } QTreeWidgetItem *curr = res->tree->currentItem(); if (!curr) { // no item has been visited -> jump to the closest match before current cursor position // check if current file is in the file curr = res->tree->topLevelItem(0); while (curr && curr->data(0, ReplaceMatches::FileUrlRole).toString() != m_mainWindow->activeView()->document()->url().toString()) { curr = res->tree->itemBelow(curr); } // now we are either in this file or !curr if (curr) { res->tree->expandItem(curr); int lineNr = 0; int columnNr = 0; if (m_mainWindow->activeView()->cursorPosition().isValid()) { lineNr = m_mainWindow->activeView()->cursorPosition().line(); columnNr = m_mainWindow->activeView()->cursorPosition().column() - 1; } if (!curr->data(0, ReplaceMatches::StartColumnRole).isValid()) { curr = res->tree->itemBelow(curr); }; while (curr && curr->data(0, ReplaceMatches::StartLineRole).toInt() <= lineNr && curr->data(0, ReplaceMatches::FileUrlRole).toString() == m_mainWindow->activeView()->document()->url().toString()) { if (curr->data(0, ReplaceMatches::StartLineRole).toInt() == lineNr && curr->data(0, ReplaceMatches::StartColumnRole).toInt() > columnNr) { break; } curr = res->tree->itemBelow(curr); } } } QTreeWidgetItem *startChild = curr; // go to the item above. (curr == null is not a problem) curr = res->tree->itemAbove(curr); // expand the items above if needed if (curr && curr->data(0, ReplaceMatches::StartColumnRole).toString().isEmpty()) { res->tree->expandItem(curr); // probably this file item curr = res->tree->itemAbove(curr); if (curr && curr->data(0, ReplaceMatches::StartColumnRole).toString().isEmpty()) { res->tree->expandItem(curr); // probably file above if this is reached } curr = res->tree->itemAbove(startChild); } // skip file name items and the root item while (curr && curr->data(0, ReplaceMatches::StartColumnRole).toString().isEmpty()) { curr = res->tree->itemAbove(curr); } if (!curr) { // select the last child of the last next-to-top-level item QTreeWidgetItem *root = res->tree->topLevelItem(0); // select the last "root item" if (!root || (root->childCount() < 1)) return; root = root->child(root->childCount() - 1); // select the last match of the "root item" if (!root || (root->childCount() < 1)) return; curr = root->child(root->childCount() - 1); fromLast = true; } itemSelected(curr); if (fromLast) { delete m_infoMessage; const QString msg = i18n("Continuing from last match"); m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information); m_infoMessage->setPosition(KTextEditor::Message::BottomInView); m_infoMessage->setAutoHide(2000); m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate); m_infoMessage->setView(m_mainWindow->activeView()); m_mainWindow->activeView()->document()->postMessage(m_infoMessage); } } void KatePluginSearchView::readSessionConfig(const KConfigGroup &cg) { m_ui.searchCombo->clear(); m_ui.searchCombo->addItem(QString()); // Add empty Item m_ui.searchCombo->addItems(cg.readEntry("Search", QStringList())); m_ui.replaceCombo->clear(); m_ui.replaceCombo->addItem(QString()); // Add empty Item m_ui.replaceCombo->addItems(cg.readEntry("Replaces", QStringList())); m_ui.matchCase->setChecked(cg.readEntry("MatchCase", false)); m_ui.useRegExp->setChecked(cg.readEntry("UseRegExp", false)); m_ui.expandResults->setChecked(cg.readEntry("ExpandSearchResults", false)); int searchPlaceIndex = cg.readEntry("Place", 1); if (searchPlaceIndex < 0) { searchPlaceIndex = Folder; // for the case we happen to read -1 as Place } if ((searchPlaceIndex >= Project) && (searchPlaceIndex >= m_ui.searchPlaceCombo->count())) { // handle the case that project mode was selected, but not yet available m_projectSearchPlaceIndex = searchPlaceIndex; searchPlaceIndex = Folder; } m_ui.searchPlaceCombo->setCurrentIndex(searchPlaceIndex); m_ui.recursiveCheckBox->setChecked(cg.readEntry("Recursive", true)); m_ui.hiddenCheckBox->setChecked(cg.readEntry("HiddenFiles", false)); m_ui.symLinkCheckBox->setChecked(cg.readEntry("FollowSymLink", false)); m_ui.binaryCheckBox->setChecked(cg.readEntry("BinaryFiles", false)); m_ui.folderRequester->comboBox()->clear(); m_ui.folderRequester->comboBox()->addItems(cg.readEntry("SearchDiskFiless", QStringList())); m_ui.folderRequester->setText(cg.readEntry("SearchDiskFiles", QString())); m_ui.filterCombo->clear(); m_ui.filterCombo->addItems(cg.readEntry("Filters", QStringList())); m_ui.filterCombo->setCurrentIndex(cg.readEntry("CurrentFilter", -1)); m_ui.excludeCombo->clear(); m_ui.excludeCombo->addItems(cg.readEntry("ExcludeFilters", QStringList())); m_ui.excludeCombo->setCurrentIndex(cg.readEntry("CurrentExcludeFilter", -1)); m_ui.displayOptions->setChecked(searchPlaceIndex == Folder); } void KatePluginSearchView::writeSessionConfig(KConfigGroup &cg) { QStringList searchHistoy; for (int i = 1; i < m_ui.searchCombo->count(); i++) { searchHistoy << m_ui.searchCombo->itemText(i); } cg.writeEntry("Search", searchHistoy); QStringList replaceHistoy; for (int i = 1; i < m_ui.replaceCombo->count(); i++) { replaceHistoy << m_ui.replaceCombo->itemText(i); } cg.writeEntry("Replaces", replaceHistoy); cg.writeEntry("MatchCase", m_ui.matchCase->isChecked()); cg.writeEntry("UseRegExp", m_ui.useRegExp->isChecked()); cg.writeEntry("ExpandSearchResults", m_ui.expandResults->isChecked()); cg.writeEntry("Place", m_ui.searchPlaceCombo->currentIndex()); cg.writeEntry("Recursive", m_ui.recursiveCheckBox->isChecked()); cg.writeEntry("HiddenFiles", m_ui.hiddenCheckBox->isChecked()); cg.writeEntry("FollowSymLink", m_ui.symLinkCheckBox->isChecked()); cg.writeEntry("BinaryFiles", m_ui.binaryCheckBox->isChecked()); QStringList folders; for (int i = 0; i < qMin(m_ui.folderRequester->comboBox()->count(), 10); i++) { folders << m_ui.folderRequester->comboBox()->itemText(i); } cg.writeEntry("SearchDiskFiless", folders); cg.writeEntry("SearchDiskFiles", m_ui.folderRequester->text()); QStringList filterItems; for (int i = 0; i < qMin(m_ui.filterCombo->count(), 10); i++) { filterItems << m_ui.filterCombo->itemText(i); } cg.writeEntry("Filters", filterItems); cg.writeEntry("CurrentFilter", m_ui.filterCombo->findText(m_ui.filterCombo->currentText())); QStringList excludeFilterItems; for (int i = 0; i < qMin(m_ui.excludeCombo->count(), 10); i++) { excludeFilterItems << m_ui.excludeCombo->itemText(i); } cg.writeEntry("ExcludeFilters", excludeFilterItems); cg.writeEntry("CurrentExcludeFilter", m_ui.excludeCombo->findText(m_ui.excludeCombo->currentText())); } void KatePluginSearchView::addTab() { if ((sender() != m_ui.newTabButton) && (m_ui.resultTabWidget->count() > 0) && m_ui.resultTabWidget->tabText(m_ui.resultTabWidget->currentIndex()).isEmpty()) { return; } Results *res = new Results(); res->tree->setRootIsDecorated(false); connect(res->tree, &QTreeWidget::itemDoubleClicked, this, &KatePluginSearchView::itemSelected, Qt::UniqueConnection); res->searchPlaceIndex = m_ui.searchPlaceCombo->currentIndex(); res->useRegExp = m_ui.useRegExp->isChecked(); res->matchCase = m_ui.matchCase->isChecked(); m_ui.resultTabWidget->addTab(res, QString()); m_ui.resultTabWidget->setCurrentIndex(m_ui.resultTabWidget->count() - 1); m_ui.stackedWidget->setCurrentIndex(0); m_ui.resultTabWidget->tabBar()->show(); m_ui.displayOptions->setChecked(false); res->tree->installEventFilter(this); } void KatePluginSearchView::tabCloseRequested(int index) { Results *tmp = qobject_cast(m_ui.resultTabWidget->widget(index)); if (m_curResults == tmp) { m_searchOpenFiles.cancelSearch(); m_searchDiskFiles.cancelSearch(); + m_folderFilesList.cancelSearch(); } if (m_ui.resultTabWidget->count() > 1) { delete tmp; // remove the tab m_curResults = nullptr; } if (m_ui.resultTabWidget->count() == 1) { m_ui.resultTabWidget->tabBar()->hide(); } } void KatePluginSearchView::resultTabChanged(int index) { if (index < 0) { return; } Results *res = qobject_cast(m_ui.resultTabWidget->widget(index)); if (!res) { // qDebug() << "No res found"; return; } m_ui.searchCombo->blockSignals(true); m_ui.matchCase->blockSignals(true); m_ui.useRegExp->blockSignals(true); m_ui.searchPlaceCombo->blockSignals(true); m_ui.searchCombo->lineEdit()->setText(m_ui.resultTabWidget->tabText(index)); m_ui.useRegExp->setChecked(res->useRegExp); m_ui.matchCase->setChecked(res->matchCase); m_ui.searchPlaceCombo->setCurrentIndex(res->searchPlaceIndex); m_ui.searchCombo->blockSignals(false); m_ui.matchCase->blockSignals(false); m_ui.useRegExp->blockSignals(false); m_ui.searchPlaceCombo->blockSignals(false); searchPlaceChanged(); } void KatePluginSearchView::onResize(const QSize &size) { bool vertical = size.width() < size.height(); if (!m_isLeftRight && vertical) { m_isLeftRight = true; m_ui.gridLayout->addWidget(m_ui.searchCombo, 0, 1, 1, 8); m_ui.gridLayout->addWidget(m_ui.findLabel, 0, 0); m_ui.gridLayout->addWidget(m_ui.searchButton, 1, 0, 1, 2); m_ui.gridLayout->addWidget(m_ui.stopAndNext, 1, 2); m_ui.gridLayout->addWidget(m_ui.searchPlaceCombo, 1, 3, 1, 3); m_ui.gridLayout->addWidget(m_ui.displayOptions, 1, 6); m_ui.gridLayout->addWidget(m_ui.matchCase, 1, 7); m_ui.gridLayout->addWidget(m_ui.useRegExp, 1, 8); m_ui.gridLayout->addWidget(m_ui.replaceCombo, 2, 1, 1, 8); m_ui.gridLayout->addWidget(m_ui.replaceLabel, 2, 0); m_ui.gridLayout->addWidget(m_ui.replaceButton, 3, 0, 1, 2); m_ui.gridLayout->addWidget(m_ui.replaceCheckedBtn, 3, 2); m_ui.gridLayout->addWidget(m_ui.expandResults, 3, 7); m_ui.gridLayout->addWidget(m_ui.newTabButton, 3, 8); m_ui.gridLayout->setColumnStretch(4, 2); m_ui.gridLayout->setColumnStretch(2, 0); } else if (m_isLeftRight && !vertical) { m_isLeftRight = false; m_ui.gridLayout->addWidget(m_ui.searchCombo, 0, 2); m_ui.gridLayout->addWidget(m_ui.findLabel, 0, 1); m_ui.gridLayout->addWidget(m_ui.searchButton, 0, 3); m_ui.gridLayout->addWidget(m_ui.stopAndNext, 0, 4); m_ui.gridLayout->addWidget(m_ui.searchPlaceCombo, 0, 5, 1, 4); m_ui.gridLayout->addWidget(m_ui.matchCase, 1, 5); m_ui.gridLayout->addWidget(m_ui.useRegExp, 1, 6); m_ui.gridLayout->addWidget(m_ui.replaceCombo, 1, 2); m_ui.gridLayout->addWidget(m_ui.replaceLabel, 1, 1); m_ui.gridLayout->addWidget(m_ui.replaceButton, 1, 3); m_ui.gridLayout->addWidget(m_ui.replaceCheckedBtn, 1, 4); m_ui.gridLayout->addWidget(m_ui.expandResults, 1, 8); m_ui.gridLayout->addWidget(m_ui.newTabButton, 0, 0); m_ui.gridLayout->addWidget(m_ui.displayOptions, 1, 0); m_ui.gridLayout->setColumnStretch(4, 0); m_ui.gridLayout->setColumnStretch(2, 2); m_ui.findLabel->setAlignment(Qt::AlignRight); m_ui.replaceLabel->setAlignment(Qt::AlignRight); } } bool KatePluginSearchView::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *ke = static_cast(event); QTreeWidget *tree = qobject_cast(obj); if (tree) { if (ke->matches(QKeySequence::Copy)) { // user pressed ctrl+c -> copy full URL to the clipboard QVariant variant = tree->currentItem()->data(0, ReplaceMatches::FileUrlRole); QApplication::clipboard()->setText(variant.toString()); event->accept(); return true; } if (ke->key() == Qt::Key_Enter || ke->key() == Qt::Key_Return) { if (tree->currentItem()) { itemSelected(tree->currentItem()); event->accept(); return true; } } } // NOTE: Qt::Key_Escape is handled by handleEsc } if (event->type() == QEvent::Resize) { QResizeEvent *re = static_cast(event); if (obj == m_toolView) { onResize(re->size()); } } return QObject::eventFilter(obj, event); } void KatePluginSearchView::searchContextMenu(const QPoint &pos) { QSet actionPointers; QMenu *const contextMenu = m_ui.searchCombo->lineEdit()->createStandardContextMenu(); if (!contextMenu) return; if (m_ui.useRegExp->isChecked()) { QMenu *menu = contextMenu->addMenu(i18n("Add...")); if (!menu) return; menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); addRegexHelperActionsForSearch(&actionPointers, menu); } // Show menu and act QAction *const result = contextMenu->exec(m_ui.searchCombo->mapToGlobal(pos)); regexHelperActOnAction(result, actionPointers, m_ui.searchCombo->lineEdit()); } void KatePluginSearchView::replaceContextMenu(const QPoint &pos) { QMenu *const contextMenu = m_ui.replaceCombo->lineEdit()->createStandardContextMenu(); if (!contextMenu) return; QMenu *menu = contextMenu->addMenu(i18n("Add...")); if (!menu) return; menu->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); QSet actionPointers; addSpecialCharsHelperActionsForReplace(&actionPointers, menu); if (m_ui.useRegExp->isChecked()) { addRegexHelperActionsForReplace(&actionPointers, menu); } // Show menu and act QAction *const result = contextMenu->exec(m_ui.replaceCombo->mapToGlobal(pos)); regexHelperActOnAction(result, actionPointers, m_ui.replaceCombo->lineEdit()); } void KatePluginSearchView::slotPluginViewCreated(const QString &name, QObject *pluginView) { // add view if (pluginView && name == QLatin1String("kateprojectplugin")) { m_projectPluginView = pluginView; slotProjectFileNameChanged(); connect(pluginView, SIGNAL(projectFileNameChanged()), this, SLOT(slotProjectFileNameChanged())); } } void KatePluginSearchView::slotPluginViewDeleted(const QString &name, QObject *) { // remove view if (name == QLatin1String("kateprojectplugin")) { m_projectPluginView = nullptr; slotProjectFileNameChanged(); } } void KatePluginSearchView::slotProjectFileNameChanged() { // query new project file name QString projectFileName; if (m_projectPluginView) { projectFileName = m_projectPluginView->property("projectFileName").toString(); } // have project, enable gui for it if (!projectFileName.isEmpty()) { if (m_ui.searchPlaceCombo->count() <= Project) { // add "in Project" m_ui.searchPlaceCombo->addItem(QIcon::fromTheme(QStringLiteral("project-open")), i18n("In Current Project")); // add "in Open Projects" m_ui.searchPlaceCombo->addItem(QIcon::fromTheme(QStringLiteral("project-open")), i18n("In All Open Projects")); if (m_projectSearchPlaceIndex >= Project) { // switch to search "in (all) Project" setSearchPlace(m_projectSearchPlaceIndex); m_projectSearchPlaceIndex = 0; } } } // else: disable gui for it else { if (m_ui.searchPlaceCombo->count() >= Project) { // switch to search "in Open files", if "in Project" is active int searchPlaceIndex = m_ui.searchPlaceCombo->currentIndex(); if (searchPlaceIndex >= Project) { m_projectSearchPlaceIndex = searchPlaceIndex; setSearchPlace(OpenFiles); } // remove "in Project" and "in all projects" while (m_ui.searchPlaceCombo->count() > Project) { m_ui.searchPlaceCombo->removeItem(m_ui.searchPlaceCombo->count() - 1); } } } } KateSearchCommand::KateSearchCommand(QObject *parent) : KTextEditor::Command(QStringList() << QStringLiteral("grep") << QStringLiteral("newGrep") << QStringLiteral("search") << QStringLiteral("newSearch") << QStringLiteral("pgrep") << QStringLiteral("newPGrep"), parent) { } bool KateSearchCommand::exec(KTextEditor::View * /*view*/, const QString &cmd, QString & /*msg*/, const KTextEditor::Range &) { // create a list of args QStringList args(cmd.split(QLatin1Char(' '), QString::KeepEmptyParts)); QString command = args.takeFirst(); QString searchText = args.join(QLatin1Char(' ')); if (command == QLatin1String("grep") || command == QLatin1String("newGrep")) { emit setSearchPlace(KatePluginSearchView::Folder); emit setCurrentFolder(); if (command == QLatin1String("newGrep")) emit newTab(); } else if (command == QLatin1String("search") || command == QLatin1String("newSearch")) { emit setSearchPlace(KatePluginSearchView::OpenFiles); if (command == QLatin1String("newSearch")) emit newTab(); } else if (command == QLatin1String("pgrep") || command == QLatin1String("newPGrep")) { emit setSearchPlace(KatePluginSearchView::Project); if (command == QLatin1String("newPGrep")) emit newTab(); } emit setSearchString(searchText); emit startSearch(); return true; } bool KateSearchCommand::help(KTextEditor::View * /*view*/, const QString &cmd, QString &msg) { if (cmd.startsWith(QLatin1String("grep"))) { msg = i18n("Usage: grep [pattern to search for in folder]"); } else if (cmd.startsWith(QLatin1String("newGrep"))) { msg = i18n("Usage: newGrep [pattern to search for in folder]"); } else if (cmd.startsWith(QLatin1String("search"))) { msg = i18n("Usage: search [pattern to search for in open files]"); } else if (cmd.startsWith(QLatin1String("newSearch"))) { msg = i18n("Usage: search [pattern to search for in open files]"); } else if (cmd.startsWith(QLatin1String("pgrep"))) { msg = i18n("Usage: pgrep [pattern to search for in current project]"); } else if (cmd.startsWith(QLatin1String("newPGrep"))) { msg = i18n("Usage: newPGrep [pattern to search for in current project]"); } return true; } #include "plugin_search.moc" // kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/search/plugin_search.h b/addons/search/plugin_search.h index e44a9be6a..e35eef744 100644 --- a/addons/search/plugin_search.h +++ b/addons/search/plugin_search.h @@ -1,238 +1,240 @@ /* Kate search plugin * * Copyright (C) 2011-2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #ifndef _PLUGIN_SEARCH_H_ #define _PLUGIN_SEARCH_H_ #include #include #include #include #include #include #include #include #include #include #include "ui_results.h" #include "ui_search.h" #include "FolderFilesList.h" #include "SearchDiskFiles.h" #include "replace_matches.h" #include "search_open_files.h" class KateSearchCommand; namespace KTextEditor { class MovingRange; } class Results : public QWidget, public Ui::Results { Q_OBJECT public: Results(QWidget *parent = nullptr); int matches = 0; QRegularExpression regExp; bool useRegExp = false; bool matchCase = false; QString replaceStr; int searchPlaceIndex = 0; QString treeRootText; }; // This class keeps the focus inside the S&R plugin when pressing tab/shift+tab by overriding focusNextPrevChild() class ContainerWidget : public QWidget { Q_OBJECT public: ContainerWidget(QWidget *parent) : QWidget(parent) { } Q_SIGNALS: void nextFocus(QWidget *currentWidget, bool *found, bool next); protected: bool focusNextPrevChild(bool next) override; }; class KatePluginSearch : public KTextEditor::Plugin { Q_OBJECT public: explicit KatePluginSearch(QObject *parent = nullptr, const QList & = QList()); ~KatePluginSearch() override; QObject *createView(KTextEditor::MainWindow *mainWindow) override; private: KateSearchCommand *m_searchCommand = nullptr; }; class KatePluginSearchView : public QObject, public KXMLGUIClient, public KTextEditor::SessionConfigInterface { Q_OBJECT Q_INTERFACES(KTextEditor::SessionConfigInterface) public: enum SearchPlaces { CurrentFile, OpenFiles, Folder, Project, AllProjects }; KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mainWindow, KTextEditor::Application *application); ~KatePluginSearchView() override; void readSessionConfig(const KConfigGroup &config) override; void writeSessionConfig(KConfigGroup &config) override; public Q_SLOTS: + void stopClicked(); void startSearch(); void setSearchString(const QString &pattern); void navigateFolderUp(); void setCurrentFolder(); void setSearchPlace(int place); void goToNextMatch(); void goToPreviousMatch(); private Q_SLOTS: void openSearchView(); void handleEsc(QEvent *e); void nextFocus(QWidget *currentWidget, bool *found, bool next); void addTab(); void tabCloseRequested(int index); void toggleOptions(bool show); void searchContextMenu(const QPoint &pos); void replaceContextMenu(const QPoint &pos); void searchPlaceChanged(); void startSearchWhileTyping(); void folderFileListChanged(); void matchFound(const QString &url, const QString &fileName, const QString &lineContent, int matchLen, int startLine, int startColumn, int endLine, int endColumn); void addMatchMark(KTextEditor::Document *doc, QTreeWidgetItem *item); void searchDone(); void searchWhileTypingDone(); void indicateMatch(bool hasMatch); void searching(const QString &file); void itemSelected(QTreeWidgetItem *item); void clearMarks(); void clearDocMarks(KTextEditor::Document *doc); void replaceSingleMatch(); void replaceChecked(); void replaceStatus(const QUrl &url, int replacedInFile, int matchesInFile); void replaceDone(); void docViewChanged(); void resultTabChanged(int index); void expandResults(); void updateResultsRootItem(); /** * keep track if the project plugin is alive and if the project file did change */ void slotPluginViewCreated(const QString &name, QObject *pluginView); void slotPluginViewDeleted(const QString &name, QObject *pluginView); void slotProjectFileNameChanged(); protected: bool eventFilter(QObject *obj, QEvent *ev) override; void addHeaderItem(); private: QTreeWidgetItem *rootFileItem(const QString &url, const QString &fName); QStringList filterFiles(const QStringList &files) const; void onResize(const QSize &size); Ui::SearchDialog m_ui{}; QWidget *m_toolView; KTextEditor::Application *m_kateApp; SearchOpenFiles m_searchOpenFiles; FolderFilesList m_folderFilesList; SearchDiskFiles m_searchDiskFiles; ReplaceMatches m_replacer; QAction *m_matchCase = nullptr; QAction *m_useRegExp = nullptr; - Results *m_curResults; - bool m_searchJustOpened; - int m_projectSearchPlaceIndex; - bool m_searchDiskFilesDone; - bool m_searchOpenFilesDone; - bool m_isSearchAsYouType; - bool m_isLeftRight; + Results *m_curResults = nullptr; + bool m_searchJustOpened = false; + int m_projectSearchPlaceIndex = 0; + bool m_searchDiskFilesDone = true; + bool m_searchOpenFilesDone = true; + bool m_isSearchAsYouType = false; + bool m_isLeftRight = false; + bool m_blockDiskMatchFound = false; QString m_resultBaseDir; QList m_matchRanges; QTimer m_changeTimer; QTimer m_updateSumaryTimer; QPointer m_infoMessage; /** * current project plugin view, if any */ - QObject *m_projectPluginView; + QObject *m_projectPluginView = nullptr; /** * our main window */ KTextEditor::MainWindow *m_mainWindow; }; class KateSearchCommand : public KTextEditor::Command { Q_OBJECT public: KateSearchCommand(QObject *parent); Q_SIGNALS: void setSearchPlace(int place); void setCurrentFolder(); void setSearchString(const QString &pattern); void startSearch(); void newTab(); // // KTextEditor::Command // public: bool exec(KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &range = KTextEditor::Range::invalid()) override; bool help(KTextEditor::View *view, const QString &cmd, QString &msg) override; }; #endif // kate: space-indent on; indent-width 4; replace-tabs on; diff --git a/addons/search/replace_matches.cpp b/addons/search/replace_matches.cpp index ef884a474..7394d0579 100644 --- a/addons/search/replace_matches.cpp +++ b/addons/search/replace_matches.cpp @@ -1,339 +1,350 @@ /* Kate search plugin * * Copyright (C) 2011-2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #include "replace_matches.h" #include #include #include ReplaceMatches::ReplaceMatches(QObject *parent) : QObject(parent) { } void ReplaceMatches::replaceChecked(QTreeWidget *tree, const QRegularExpression ®exp, const QString &replace) { if (m_manager == nullptr) return; if (m_rootIndex != -1) return; // already replacing m_tree = tree; m_rootIndex = 0; m_childStartIndex = 0; m_regExp = regexp; m_replaceText = replace; m_cancelReplace = false; + m_terminateReplace = false; m_progressTime.restart(); doReplaceNextMatch(); } void ReplaceMatches::setDocumentManager(KTextEditor::Application *manager) { m_manager = manager; } void ReplaceMatches::cancelReplace() { m_cancelReplace = true; } +void ReplaceMatches::terminateReplace() +{ + m_cancelReplace = true; + m_terminateReplace = true; +} + KTextEditor::Document *ReplaceMatches::findNamed(const QString &name) { const QList docs = m_manager->documents(); for (KTextEditor::Document *it : docs) { if (it->documentName() == name) { return it; } } return nullptr; } bool ReplaceMatches::replaceMatch(KTextEditor::Document *doc, QTreeWidgetItem *item, const KTextEditor::Range &range, const QRegularExpression ®Exp, const QString &replaceTxt) { if (!doc || !item) { return false; } // don't replace an already replaced item if (item->data(0, ReplaceMatches::ReplacedRole).toBool()) { // qDebug() << "not replacing already replaced item"; return false; } // Check that the text has not been modified and still matches + get captures for the replace QString matchLines = doc->text(range); QRegularExpressionMatch match = regExp.match(matchLines); if (match.capturedStart() != 0) { // qDebug() << matchLines << "Does not match" << regExp.pattern(); return false; } // Modify the replace string according to this match QString replaceText = replaceTxt; replaceText.replace(QLatin1String("\\\\"), QLatin1String("¤Search&Replace¤")); // allow captures \0 .. \9 for (int j = qMin(9, match.lastCapturedIndex()); j >= 0; --j) { QString captureLX = QStringLiteral("\\L\\%1").arg(j); QString captureUX = QStringLiteral("\\U\\%1").arg(j); QString captureX = QStringLiteral("\\%1").arg(j); replaceText.replace(captureLX, match.captured(j).toLower()); replaceText.replace(captureUX, match.captured(j).toUpper()); replaceText.replace(captureX, match.captured(j)); } // allow captures \{0} .. \{9999999}... for (int j = match.lastCapturedIndex(); j >= 0; --j) { QString captureLX = QStringLiteral("\\L\\{%1}").arg(j); QString captureUX = QStringLiteral("\\U\\{%1}").arg(j); QString captureX = QStringLiteral("\\{%1}").arg(j); replaceText.replace(captureLX, match.captured(j).toLower()); replaceText.replace(captureUX, match.captured(j).toUpper()); replaceText.replace(captureX, match.captured(j)); } replaceText.replace(QLatin1String("\\n"), QLatin1String("\n")); replaceText.replace(QLatin1String("\\t"), QLatin1String("\t")); replaceText.replace(QLatin1String("¤Search&Replace¤"), QLatin1String("\\")); doc->replaceText(range, replaceText); int newEndLine = range.start().line() + replaceText.count(QLatin1Char('\n')); int lastNL = replaceText.lastIndexOf(QLatin1Char('\n')); int newEndColumn = lastNL == -1 ? range.start().column() + replaceText.length() : replaceText.length() - lastNL - 1; item->setData(0, ReplaceMatches::ReplacedRole, true); item->setData(0, ReplaceMatches::StartLineRole, range.start().line()); item->setData(0, ReplaceMatches::StartColumnRole, range.start().column()); item->setData(0, ReplaceMatches::EndLineRole, newEndLine); item->setData(0, ReplaceMatches::EndColumnRole, newEndColumn); item->setData(0, ReplaceMatches::ReplacedTextRole, replaceText); // Convert replace text back to "html" replaceText.replace(QLatin1Char('\n'), QStringLiteral("\\n")); replaceText.replace(QLatin1Char('\t'), QStringLiteral("\\t")); QString html = item->data(0, ReplaceMatches::PreMatchRole).toString(); html += QLatin1String("") + item->data(0, ReplaceMatches::MatchRole).toString() + QLatin1String(" "); html += QLatin1String("") + replaceText + QLatin1String(""); html += item->data(0, ReplaceMatches::PostMatchRole).toString(); item->setData(0, Qt::DisplayRole, i18n("Line: %1: %2", range.start().line() + 1, html)); return true; } bool ReplaceMatches::replaceSingleMatch(KTextEditor::Document *doc, QTreeWidgetItem *item, const QRegularExpression ®Exp, const QString &replaceTxt) { if (!doc || !item) { return false; } QTreeWidgetItem *rootItem = item->parent(); if (!rootItem) { return false; } // Create a vector of moving ranges for updating the tree-view after replace QVector matches; QVector replaced; KTextEditor::MovingInterface *miface = qobject_cast(doc); int i = 0; // Only add items after "item" for (; i < rootItem->childCount(); i++) { if (item == rootItem->child(i)) break; } for (int j = i; j < rootItem->childCount(); j++) { QTreeWidgetItem *tmp = rootItem->child(j); int startLine = tmp->data(0, ReplaceMatches::StartLineRole).toInt(); int startColumn = tmp->data(0, ReplaceMatches::StartColumnRole).toInt(); int endLine = tmp->data(0, ReplaceMatches::EndLineRole).toInt(); int endColumn = tmp->data(0, ReplaceMatches::EndColumnRole).toInt(); KTextEditor::Range range(startLine, startColumn, endLine, endColumn); KTextEditor::MovingRange *mr = miface->newMovingRange(range); matches.append(mr); } if (matches.isEmpty()) { return false; } // The first range in the vector is for this match if (!replaceMatch(doc, item, matches[0]->toRange(), regExp, replaceTxt)) { return false; } delete matches.takeFirst(); // Update the remaining tree-view-items for (int j = i + 1; j < rootItem->childCount() && !matches.isEmpty(); j++) { QTreeWidgetItem *tmp = rootItem->child(j); tmp->setData(0, ReplaceMatches::StartLineRole, matches.first()->start().line()); tmp->setData(0, ReplaceMatches::StartColumnRole, matches.first()->start().column()); tmp->setData(0, ReplaceMatches::EndLineRole, matches.first()->end().line()); tmp->setData(0, ReplaceMatches::EndColumnRole, matches.first()->end().column()); delete matches.takeFirst(); } qDeleteAll(matches); return true; } void ReplaceMatches::doReplaceNextMatch() { + if (m_terminateReplace) { + return; + } + if (!m_manager || m_tree->topLevelItemCount() != 1) { updateTreeViewItems(nullptr); m_rootIndex = -1; emit replaceDone(); return; } // NOTE The document managers signal documentWillBeDeleted() must be connected to // cancelReplace(). A closed file could lead to a crash if it is not handled. // Open the file QTreeWidgetItem *fileItem = m_tree->topLevelItem(0)->child(m_rootIndex); if (!fileItem) { updateTreeViewItems(nullptr); m_rootIndex = -1; emit replaceDone(); return; } bool isSearchAsYouType = false; if (!fileItem->data(0, StartColumnRole).toString().isEmpty()) { // this is a search as you type replace fileItem = m_tree->topLevelItem(0); isSearchAsYouType = true; } if (m_cancelReplace) { updateTreeViewItems(fileItem); m_rootIndex = -1; emit replaceDone(); return; } if (fileItem->checkState(0) == Qt::Unchecked) { updateTreeViewItems(fileItem); QTimer::singleShot(0, this, &ReplaceMatches::doReplaceNextMatch); return; } KTextEditor::Document *doc; QString docUrl = fileItem->data(0, FileUrlRole).toString(); if (docUrl.isEmpty()) { doc = findNamed(fileItem->data(0, FileNameRole).toString()); } else { doc = m_manager->findUrl(QUrl::fromUserInput(docUrl)); if (!doc) { doc = m_manager->openUrl(QUrl::fromUserInput(fileItem->data(0, FileUrlRole).toString())); } } if (!doc) { updateTreeViewItems(fileItem); QTimer::singleShot(0, this, &ReplaceMatches::doReplaceNextMatch); return; } if (m_progressTime.elapsed() > 100) { m_progressTime.restart(); if (m_currentMatches.isEmpty()) { emit replaceStatus(doc->url(), 0, 0); } else { emit replaceStatus(doc->url(), m_childStartIndex, m_currentMatches.count()); } } if (m_childStartIndex == 0) { // Create a vector of moving ranges for updating the tree-view after replace QVector replaced; KTextEditor::MovingInterface *miface = qobject_cast(doc); for (int j = 0; j < fileItem->childCount(); ++j) { QTreeWidgetItem *item = fileItem->child(j); int startLine = item->data(0, ReplaceMatches::StartLineRole).toInt(); int startColumn = item->data(0, ReplaceMatches::StartColumnRole).toInt(); int endLine = item->data(0, ReplaceMatches::EndLineRole).toInt(); int endColumn = item->data(0, ReplaceMatches::EndColumnRole).toInt(); KTextEditor::Range range(startLine, startColumn, endLine, endColumn); KTextEditor::MovingRange *mr = miface->newMovingRange(range); m_currentMatches.append(mr); m_currentReplaced << false; } } // Make one transaction for the whole replace to speed up things // and get all replacements in one "undo" KTextEditor::Document::EditingTransaction transaction(doc); // now do the replaces int i = m_childStartIndex; for (; i < fileItem->childCount(); ++i) { if (m_progressTime.elapsed() > 100) { break; } QTreeWidgetItem *item = fileItem->child(i); if (item->checkState(0) == Qt::Checked) { m_currentReplaced[i] = replaceMatch(doc, item, m_currentMatches[i]->toRange(), m_regExp, m_replaceText); item->setCheckState(0, Qt::PartiallyChecked); } } if (i == fileItem->childCount()) { updateTreeViewItems(fileItem); if (isSearchAsYouType) { m_rootIndex = -1; emit replaceDone(); return; } } else { m_childStartIndex = i; } QTimer::singleShot(0, this, &ReplaceMatches::doReplaceNextMatch); } void ReplaceMatches::updateTreeViewItems(QTreeWidgetItem *fileItem) { if (fileItem && m_currentReplaced.size() == m_currentMatches.size() && m_currentReplaced.size() == fileItem->childCount()) { for (int j = 0; j < m_currentReplaced.size() && j < m_currentMatches.size(); ++j) { QTreeWidgetItem *item = fileItem->child(j); if (!m_currentReplaced[j] && item) { item->setData(0, ReplaceMatches::StartLineRole, m_currentMatches[j]->start().line()); item->setData(0, ReplaceMatches::StartColumnRole, m_currentMatches[j]->start().column()); item->setData(0, ReplaceMatches::EndLineRole, m_currentMatches[j]->end().line()); item->setData(0, ReplaceMatches::EndColumnRole, m_currentMatches[j]->end().column()); } } } qDeleteAll(m_currentMatches); m_rootIndex++; m_childStartIndex = 0; m_currentMatches.clear(); m_currentReplaced.clear(); } diff --git a/addons/search/replace_matches.h b/addons/search/replace_matches.h index 3a34c0bf3..ba8806bf5 100644 --- a/addons/search/replace_matches.h +++ b/addons/search/replace_matches.h @@ -1,88 +1,90 @@ /* Kate search plugin * * Copyright (C) 2011 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #ifndef _REPLACE_MATCHES_H_ #define _REPLACE_MATCHES_H_ #include #include #include #include #include #include #include #include class ReplaceMatches : public QObject { Q_OBJECT public: enum MatchData { FileUrlRole = Qt::UserRole, FileNameRole, StartLineRole, StartColumnRole, EndLineRole, EndColumnRole, MatchLenRole, PreMatchRole, MatchRole, PostMatchRole, ReplacedRole, ReplacedTextRole, }; ReplaceMatches(QObject *parent = nullptr); void setDocumentManager(KTextEditor::Application *manager); bool replaceMatch(KTextEditor::Document *doc, QTreeWidgetItem *item, const KTextEditor::Range &range, const QRegularExpression ®Exp, const QString &replaceTxt); bool replaceSingleMatch(KTextEditor::Document *doc, QTreeWidgetItem *item, const QRegularExpression ®Exp, const QString &replaceTxt); void replaceChecked(QTreeWidget *tree, const QRegularExpression ®exp, const QString &replace); KTextEditor::Document *findNamed(const QString &name); public Q_SLOTS: void cancelReplace(); + void terminateReplace(); private Q_SLOTS: void doReplaceNextMatch(); Q_SIGNALS: void replaceStatus(const QUrl &url, int replacedInFile, int matchesInFile); void replaceDone(); private: void updateTreeViewItems(QTreeWidgetItem *fileItem); KTextEditor::Application *m_manager = nullptr; QTreeWidget *m_tree = nullptr; int m_rootIndex = -1; int m_childStartIndex = -1; QVector m_currentMatches; QVector m_currentReplaced; QRegularExpression m_regExp; QString m_replaceText; bool m_cancelReplace = false; + bool m_terminateReplace = false; QElapsedTimer m_progressTime; }; #endif diff --git a/addons/search/search_open_files.cpp b/addons/search/search_open_files.cpp index 01d3d2e05..a36a5586a 100644 --- a/addons/search/search_open_files.cpp +++ b/addons/search/search_open_files.cpp @@ -1,188 +1,202 @@ /* Kate search plugin * * Copyright (C) 2011-2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #include "search_open_files.h" #include SearchOpenFiles::SearchOpenFiles(QObject *parent) : QObject(parent) { - connect(this, &SearchOpenFiles::searchNextFile, this, &SearchOpenFiles::doSearchNextFile, Qt::QueuedConnection); + m_nextRunTimer.setInterval(0); + m_nextRunTimer.setSingleShot(true); + connect(&m_nextRunTimer, &QTimer::timeout, this, [this](){ doSearchNextFile(m_nextLine); }); } bool SearchOpenFiles::searching() { return !m_cancelSearch; } void SearchOpenFiles::startSearch(const QList &list, const QRegularExpression ®exp) { - if (m_nextIndex != -1) + if (m_nextFileIndex != -1) return; m_docList = list; - m_nextIndex = 0; + m_nextFileIndex = 0; m_regExp = regexp; m_cancelSearch = false; + m_terminateSearch = false; m_statusTime.restart(); - emit searchNextFile(0); + m_nextLine = 0; + m_nextRunTimer.start(0); +} + +void SearchOpenFiles::terminateSearch() +{ + m_cancelSearch = true; + m_terminateSearch = true; + m_nextFileIndex = -1; + m_nextLine = -1; + m_nextRunTimer.stop(); } void SearchOpenFiles::cancelSearch() { m_cancelSearch = true; } void SearchOpenFiles::doSearchNextFile(int startLine) { - if (m_cancelSearch || m_nextIndex >= m_docList.size()) { - m_nextIndex = -1; + if (m_cancelSearch || m_nextFileIndex >= m_docList.size()) { + m_nextFileIndex = -1; m_cancelSearch = true; - emit searchDone(); + m_nextLine = -1; return; } // NOTE The document managers signal documentWillBeDeleted() must be connected to // cancelSearch(). A closed file could lead to a crash if it is not handled. - int line = searchOpenFile(m_docList[m_nextIndex], m_regExp, startLine); + int line = searchOpenFile(m_docList[m_nextFileIndex], m_regExp, startLine); if (line == 0) { // file searched go to next - m_nextIndex++; - if (m_nextIndex == m_docList.size()) { - m_nextIndex = -1; + m_nextFileIndex++; + if (m_nextFileIndex == m_docList.size()) { + m_nextFileIndex = -1; m_cancelSearch = true; emit searchDone(); } else { - emit searchNextFile(0); + m_nextLine = 0; } } else { - emit searchNextFile(line); + m_nextLine = line; } + m_nextRunTimer.start(); } int SearchOpenFiles::searchOpenFile(KTextEditor::Document *doc, const QRegularExpression ®Exp, int startLine) { if (m_statusTime.elapsed() > 100) { m_statusTime.restart(); emit searching(doc->url().toString()); } if (regExp.pattern().contains(QLatin1String("\\n"))) { return searchMultiLineRegExp(doc, regExp, startLine); } return searchSingleLineRegExp(doc, regExp, startLine); } int SearchOpenFiles::searchSingleLineRegExp(KTextEditor::Document *doc, const QRegularExpression ®Exp, int startLine) { int column; QElapsedTimer time; time.start(); for (int line = startLine; line < doc->lines(); line++) { if (time.elapsed() > 100) { // qDebug() << "Search time exceeded" << time.elapsed() << line; return line; } QRegularExpressionMatch match; match = regExp.match(doc->line(line)); column = match.capturedStart(); while (column != -1 && !match.captured().isEmpty()) { emit matchFound(doc->url().toString(), doc->documentName(), doc->line(line), match.capturedLength(), line, column, line, column + match.capturedLength()); match = regExp.match(doc->line(line), column + match.capturedLength()); column = match.capturedStart(); } } return 0; } int SearchOpenFiles::searchMultiLineRegExp(KTextEditor::Document *doc, const QRegularExpression ®Exp, int inStartLine) { int column = 0; int startLine = 0; QElapsedTimer time; time.start(); QRegularExpression tmpRegExp = regExp; if (inStartLine == 0) { // Copy the whole file to a temporary buffer to be able to search newlines m_fullDoc.clear(); m_lineStart.clear(); m_lineStart << 0; for (int i = 0; i < doc->lines(); i++) { m_fullDoc += doc->line(i) + QLatin1Char('\n'); m_lineStart << m_fullDoc.size(); } if (!regExp.pattern().endsWith(QLatin1Char('$'))) { // if regExp ends with '$' leave the extra newline at the end as // '$' will be replaced with (?=\\n), which needs the extra newline m_fullDoc.remove(m_fullDoc.size() - 1, 1); } } else { if (inStartLine > 0 && inStartLine < m_lineStart.size()) { column = m_lineStart[inStartLine]; startLine = inStartLine; } else { return 0; } } if (regExp.pattern().endsWith(QLatin1Char('$'))) { QString newPatern = tmpRegExp.pattern(); newPatern.replace(QStringLiteral("$"), QStringLiteral("(?=\\n)")); tmpRegExp.setPattern(newPatern); } QRegularExpressionMatch match; match = tmpRegExp.match(m_fullDoc, column); column = match.capturedStart(); while (column != -1 && !match.captured().isEmpty()) { // search for the line number of the match int i; startLine = -1; for (i = 1; i < m_lineStart.size(); i++) { if (m_lineStart[i] > column) { startLine = i - 1; break; } } if (startLine == -1) { break; } int startColumn = (column - m_lineStart[startLine]); int endLine = startLine + match.captured().count(QLatin1Char('\n')); int lastNL = match.captured().lastIndexOf(QLatin1Char('\n')); int endColumn = lastNL == -1 ? startColumn + match.captured().length() : match.captured().length() - lastNL - 1; emit matchFound(doc->url().toString(), doc->documentName(), doc->line(startLine).left(column - m_lineStart[startLine]) + match.captured(), match.capturedLength(), startLine, startColumn, endLine, endColumn); match = tmpRegExp.match(m_fullDoc, column + match.capturedLength()); column = match.capturedStart(); if (time.elapsed() > 100) { // qDebug() << "Search time exceeded" << time.elapsed() << line; return startLine; } } return 0; } diff --git a/addons/search/search_open_files.h b/addons/search/search_open_files.h index 865eaf443..f27ac9115 100644 --- a/addons/search/search_open_files.h +++ b/addons/search/search_open_files.h @@ -1,68 +1,72 @@ /* Kate search plugin * * Copyright (C) 2011-2013 by Kåre Särs * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program in a file called COPYING; if not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ #ifndef _SEARCH_OPEN_FILES_H_ #define _SEARCH_OPEN_FILES_H_ #include #include #include #include +#include class SearchOpenFiles : public QObject { Q_OBJECT public: SearchOpenFiles(QObject *parent = nullptr); void startSearch(const QList &list, const QRegularExpression ®exp); bool searching(); + void terminateSearch(); public Q_SLOTS: void cancelSearch(); /// return 0 on success or a line number where we stopped. int searchOpenFile(KTextEditor::Document *doc, const QRegularExpression ®Exp, int startLine); private Q_SLOTS: void doSearchNextFile(int startLine); private: int searchSingleLineRegExp(KTextEditor::Document *doc, const QRegularExpression ®Exp, int startLine); int searchMultiLineRegExp(KTextEditor::Document *doc, const QRegularExpression ®Exp, int startLine); Q_SIGNALS: - void searchNextFile(int startLine); - void matchFound(const QString &url, const QString &fileName, const QString &lineContent, int matchLen, int line, int column, int endLine, int endColumn); + void matchFound(const QString &url, const QString &fileName, const QString &lineContent, int matchLen, int line, int column, int endLine, int endColumn); void searchDone(); void searching(const QString &file); private: QList m_docList; - int m_nextIndex = -1; + int m_nextFileIndex = -1; + QTimer m_nextRunTimer; + int m_nextLine = -1; QRegularExpression m_regExp; bool m_cancelSearch = true; + bool m_terminateSearch = false; QString m_fullDoc; QVector m_lineStart; QElapsedTimer m_statusTime; }; #endif