Commit db468760 authored by Waqar Ahmed's avatar Waqar Ahmed
Browse files

search: Allow filtering search results

Currently you can use search plugin to search for something in your
files/project. Often times there are a lot of results and you need to
filter in on the results. With this change you can filter on the results
quickly.

To do so, click the "Filter" tool button beside the "new tab" button. A
line edit should appear at the bottom of the search results. Results
will get filtered based on whatever you type in there.
parent be7ede3a
Pipeline #116455 passed with stage
in 3 minutes and 5 seconds
......@@ -27,10 +27,12 @@ target_sources(
KateSearchCommand.cpp
MatchExportDialog.cpp
MatchModel.cpp
MatchProxyModel.cpp
SearchDiskFiles.cpp
htmldelegate.cpp
plugin.qrc
plugin_search.cpp
search_open_files.cpp
Results.cpp
)
......@@ -11,7 +11,7 @@
#include <QMenu>
#include <QRegularExpression>
MatchExportDialog::MatchExportDialog(QWidget *parent, MatchModel *matchModel, QRegularExpression *regExp)
MatchExportDialog::MatchExportDialog(QWidget *parent, QAbstractItemModel *matchModel, QRegularExpression *regExp)
: QDialog(parent)
, m_matchModel(matchModel)
, m_regExp(regExp)
......
......@@ -16,7 +16,7 @@ class MatchExportDialog : public QDialog, public Ui::MatchExportDialog
Q_OBJECT
public:
MatchExportDialog(QWidget *parent, MatchModel *matchModel, QRegularExpression *regExp);
MatchExportDialog(QWidget *parent, QAbstractItemModel *matchModel, QRegularExpression *regExp);
virtual ~MatchExportDialog();
......@@ -25,6 +25,6 @@ protected Q_SLOTS:
void generateMatchExport();
private:
MatchModel *m_matchModel;
QAbstractItemModel *m_matchModel;
QRegularExpression *m_regExp;
};
......@@ -91,8 +91,6 @@ public:
/** This function clears all matches in all files */
void clear();
KTextEditor::Range matchRange(const QModelIndex &matchIndex) const;
const QVector<KateSearchMatch> &fileMatches(const QUrl &fileUrl) const;
void updateMatchRanges(const QVector<KTextEditor::MovingRange *> &ranges);
......@@ -114,9 +112,6 @@ public Q_SLOTS:
* This is done to update the match tree when we generate the search file list. */
void setFileListUpdate(const QString &path);
/** This function is used to replace a single match */
bool replaceSingleMatch(KTextEditor::Document *doc, const QModelIndex &matchIndex, const QRegularExpression &regExp, const QString &replaceString);
/** Initiate a replace of all matches that have been checked.
* The actual replacing is split up into slot calls that are added to the event loop */
void replaceChecked(const QRegularExpression &regExp, const QString &replaceString);
......@@ -127,6 +122,7 @@ public Q_SLOTS:
Q_SIGNALS:
void replaceDone();
// QModelIndex api. Use with care if you are accessing it directly or access through 'Results' instead
public:
bool isMatch(const QModelIndex &itemIndex) const;
QModelIndex fileIndex(const QUrl &url) const;
......@@ -138,6 +134,11 @@ public:
QModelIndex nextMatch(const QModelIndex &itemIndex) const;
QModelIndex prevMatch(const QModelIndex &itemIndex) const;
KTextEditor::Range matchRange(const QModelIndex &matchIndex) const;
/** This function is used to replace a single match */
bool replaceSingleMatch(KTextEditor::Document *doc, const QModelIndex &matchIndex, const QRegularExpression &regExp, const QString &replaceString);
// Model-View model functions
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
......@@ -186,6 +187,8 @@ private:
QRegularExpression m_regExp;
QString m_replaceText;
bool m_cancelReplace = true;
friend class Results;
};
Q_DECLARE_METATYPE(KateSearchMatch)
......
#include "MatchProxyModel.h"
#include "MatchModel.h"
bool MatchProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &parent) const
{
// root item always visible
if (!parent.isValid()) {
return true;
}
// nothing to filter
if (m_text.isEmpty()) {
return true;
}
const auto index = sourceModel()->index(sourceRow, 0, parent);
if (!index.isValid()) {
return false;
}
const QString text = index.data(MatchModel::PlainTextRole).toString();
// match text;
if (text.contains(m_text, Qt::CaseInsensitive)) {
return true;
}
// text didn't match. Check if this is a match item & its parent is accepted
if (isMatchItem(index) && parentAcceptsRow(parent)) {
return true;
}
// filter it out
return false;
}
bool MatchProxyModel::isMatchItem(const QModelIndex &index) const
{
return index.parent().isValid() && index.parent().parent().isValid();
}
bool MatchProxyModel::parentAcceptsRow(const QModelIndex &source_parent) const
{
if (source_parent.isValid()) {
const QModelIndex index = source_parent.parent();
if (filterAcceptsRow(source_parent.row(), index)) {
return true;
}
// we don't want to recurse because our root item is always accepted
}
return false;
}
#ifndef KATE_SEARCH_MATCH_PROXY_MODEL_H
#define KATE_SEARCH_MATCH_PROXY_MODEL_H
#include <QSortFilterProxyModel>
class MatchProxyModel final : public QSortFilterProxyModel
{
Q_OBJECT
public:
using QSortFilterProxyModel::QSortFilterProxyModel;
bool filterAcceptsRow(int sourceRow, const QModelIndex &parent) const override;
Q_SLOT void setFilterText(const QString &text)
{
beginResetModel();
m_text = text;
endResetModel();
}
private:
bool isMatchItem(const QModelIndex &index) const;
bool parentAcceptsRow(const QModelIndex &source_parent) const;
QString m_text;
};
#endif
#include "Results.h"
#include "MatchProxyModel.h"
#include "htmldelegate.h"
#include <KSyntaxHighlighting/Theme>
#include <KTextEditor/Editor>
Results::Results(QWidget *parent)
: QWidget(parent)
{
setupUi(this);
treeView->setItemDelegate(new SPHtmlDelegate(treeView));
MatchProxyModel *proxy = new MatchProxyModel(this);
proxy->setSourceModel(&matchModel);
proxy->setRecursiveFilteringEnabled(true);
treeView->setModel(proxy);
filterLineEdit->setVisible(false);
filterLineEdit->setPlaceholderText(i18n("Type to filter through results..."));
connect(filterLineEdit, &QLineEdit::textChanged, this, [this, proxy](const QString &text) {
proxy->setFilterText(text);
QTimer::singleShot(10, treeView, &QTreeView::expandAll);
});
auto updateColors = [this](KTextEditor::Editor *e) {
if (!e) {
return;
}
const auto theme = e->theme();
auto bg = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor));
auto hl = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::TextSelection));
auto search = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::SearchHighlight));
auto replace = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::ReplaceHighlight));
auto fg = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Normal));
auto pal = treeView->palette();
pal.setColor(QPalette::Base, bg);
pal.setColor(QPalette::Highlight, hl);
pal.setColor(QPalette::Text, fg);
matchModel.setMatchColors(fg.name(QColor::HexArgb), search.name(QColor::HexArgb), replace.name(QColor::HexArgb));
treeView->setPalette(pal);
Q_EMIT colorsChanged();
};
auto e = KTextEditor::Editor::instance();
connect(e, &KTextEditor::Editor::configChanged, this, updateColors);
updateColors(e);
}
void Results::setFilterLineVisible(bool visible)
{
filterLineEdit->setVisible(visible);
if (!visible) {
filterLineEdit->clear();
} else {
filterLineEdit->setFocus();
}
}
void Results::expandRoot()
{
treeView->expand(treeView->model()->index(0, 0));
}
MatchProxyModel *Results::model() const
{
return static_cast<MatchProxyModel *>(treeView->model());
}
bool Results::isMatch(const QModelIndex &index) const
{
Q_ASSERT(index.model() == model());
return matchModel.isMatch(model()->mapToSource(index));
}
QModelIndex Results::firstFileMatch(const QUrl &url) const
{
return model()->mapFromSource(matchModel.firstFileMatch(url));
}
QModelIndex Results::closestMatchAfter(const QUrl &url, const KTextEditor::Cursor &cursor) const
{
return model()->mapFromSource(matchModel.closestMatchAfter(url, cursor));
}
QModelIndex Results::firstMatch() const
{
return model()->mapFromSource(matchModel.firstMatch());
}
QModelIndex Results::nextMatch(const QModelIndex &itemIndex) const
{
Q_ASSERT(itemIndex.model() == model());
return model()->mapFromSource(matchModel.nextMatch(model()->mapToSource(itemIndex)));
}
QModelIndex Results::prevMatch(const QModelIndex &itemIndex) const
{
Q_ASSERT(itemIndex.model() == model());
return model()->mapFromSource(matchModel.prevMatch(model()->mapToSource(itemIndex)));
}
QModelIndex Results::closestMatchBefore(const QUrl &url, const KTextEditor::Cursor &cursor) const
{
return model()->mapFromSource(matchModel.closestMatchBefore(url, cursor));
}
QModelIndex Results::lastMatch() const
{
return model()->mapFromSource(matchModel.lastMatch());
}
KTextEditor::Range Results::matchRange(const QModelIndex &matchIndex) const
{
Q_ASSERT(matchIndex.model() == model());
return matchModel.matchRange(model()->mapToSource(matchIndex));
}
bool Results::replaceSingleMatch(KTextEditor::Document *doc, const QModelIndex &matchIndex, const QRegularExpression &regExp, const QString &replaceString)
{
Q_ASSERT(matchIndex.model() == model());
const auto sourceIndex = model()->mapToSource(matchIndex);
return matchModel.replaceSingleMatch(doc, sourceIndex, regExp, replaceString);
}
void Results::setDisplayFont(const QFont &f)
{
if (treeView->itemDelegate()) {
auto *delegate = static_cast<SPHtmlDelegate *>(treeView->itemDelegate());
delegate->setDisplayFont(f);
}
}
#ifndef KATE_SEARCH_RESULTS_H
#define KATE_SEARCH_RESULTS_H
#include "MatchModel.h"
#include "ui_results.h"
#include <QWidget>
class Results final : 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;
MatchModel matchModel;
void setFilterLineVisible(bool visible);
void expandRoot();
bool isMatch(const QModelIndex &index) const;
class MatchProxyModel *model() const;
QModelIndex firstFileMatch(const QUrl &url) const;
QModelIndex closestMatchAfter(const QUrl &url, const KTextEditor::Cursor &cursor) const;
QModelIndex firstMatch() const;
QModelIndex nextMatch(const QModelIndex &itemIndex) const;
QModelIndex prevMatch(const QModelIndex &itemIndex) const;
QModelIndex closestMatchBefore(const QUrl &url, const KTextEditor::Cursor &cursor) const;
QModelIndex lastMatch() const;
KTextEditor::Range matchRange(const QModelIndex &matchIndex) const;
bool replaceSingleMatch(KTextEditor::Document *doc, const QModelIndex &matchIndex, const QRegularExpression &regExp, const QString &replaceString);
void setDisplayFont(const QFont &);
Q_SIGNALS:
void colorsChanged();
};
#endif
......@@ -7,7 +7,8 @@
#include "plugin_search.h"
#include "KateSearchCommand.h"
#include "MatchExportDialog.h"
#include "htmldelegate.h"
#include "MatchProxyModel.h"
#include "Results.h"
#include <ktexteditor/configinterface.h>
#include <ktexteditor/document.h>
......@@ -17,11 +18,9 @@
#include <ktexteditor/movingrange.h>
#include <ktexteditor/view.h>
#include "kacceleratormanager.h"
#include <KAboutData>
#include <KAcceleratorManager>
#include <KActionCollection>
#include <KColorScheme>
#include <KLineEdit>
#include <KLocalizedString>
#include <KPluginFactory>
#include <KUrlCompletion>
......@@ -32,14 +31,11 @@
#include <QClipboard>
#include <QComboBox>
#include <QCompleter>
#include <QDir>
#include <QFileInfo>
#include <QKeyEvent>
#include <QMenu>
#include <QMetaObject>
#include <QPoint>
#include <QScrollBar>
#include <QTextDocument>
static QUrl localFileDirUp(const QUrl &url)
{
......@@ -164,41 +160,6 @@ void KatePluginSearchView::regexHelperActOnAction(QAction *resultAction, const Q
}
}
Results::Results(QWidget *parent)
: QWidget(parent)
{
setupUi(this);
treeView->setItemDelegate(new SPHtmlDelegate(treeView));
treeView->setModel(&matchModel);
auto updateColors = [this](KTextEditor::Editor *e) {
if (!e) {
return;
}
const auto theme = e->theme();
auto bg = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::BackgroundColor));
auto hl = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::TextSelection));
auto search = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::SearchHighlight));
auto replace = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::ReplaceHighlight));
auto fg = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::Normal));
auto pal = treeView->palette();
pal.setColor(QPalette::Base, bg);
pal.setColor(QPalette::Highlight, hl);
pal.setColor(QPalette::Text, fg);
matchModel.setMatchColors(fg.name(QColor::HexArgb), search.name(QColor::HexArgb), replace.name(QColor::HexArgb));
treeView->setPalette(pal);
Q_EMIT colorsChanged();
};
auto e = KTextEditor::Editor::instance();
connect(e, &KTextEditor::Editor::configChanged, this, updateColors);
updateColors(e);
}
K_PLUGIN_FACTORY_WITH_JSON(KatePluginSearchFactory, "katesearch.json", registerPlugin<KatePluginSearch>();)
KatePluginSearch::KatePluginSearch(QObject *parent, const QList<QVariant> &)
......@@ -382,6 +343,7 @@ KatePluginSearchView::KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEdi
m_ui.matchCase->setIcon(matchCaseIcon);
m_ui.useRegExp->setIcon(useRegExpIcon);
m_ui.expandResults->setIcon(expandResultsIcon);
m_ui.filterBtn->setIcon(QIcon::fromTheme(QStringLiteral("view-filter")));
m_ui.searchPlaceCombo->setItemIcon(MatchModel::CurrentFile, QIcon::fromTheme(QStringLiteral("text-plain")));
m_ui.searchPlaceCombo->setItemIcon(MatchModel::OpenFiles, QIcon::fromTheme(QStringLiteral("text-plain")));
m_ui.searchPlaceCombo->setItemIcon(MatchModel::Folder, QIcon::fromTheme(QStringLiteral("folder")));
......@@ -392,6 +354,14 @@ KatePluginSearchView::KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEdi
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*\""));
m_ui.filterBtn->setToolTip(i18n("Click to filter through results"));
m_ui.filterBtn->setDisabled(true);
connect(m_ui.filterBtn, &QToolButton::toggled, this, [this](bool on) {
if (Results *res = qobject_cast<Results *>(m_ui.resultTabWidget->currentWidget())) {
res->setFilterLineVisible(on);
}
});
addTab();
// get url-requester's combo box and sanely initialize
......@@ -921,10 +891,7 @@ void KatePluginSearchView::updateViewColors()
m_resultAttr->setForeground(fg);
if (m_curResults) {
auto *delegate = qobject_cast<SPHtmlDelegate *>(m_curResults->treeView->itemDelegate());
if (delegate) {
delegate->setDisplayFont(ciface->configValue(QStringLiteral("font")).value<QFont>());
}
m_curResults->setDisplayFont(ciface->configValue(QStringLiteral("font")).value<QFont>());
}
}
}
......@@ -1040,7 +1007,7 @@ void KatePluginSearchView::startSearch()
m_curResults->matchModel.clear();
m_curResults->matchModel.setSearchPlace(static_cast<MatchModel::SearchPlaces>(m_curResults->searchPlaceIndex));
m_curResults->matchModel.setSearchState(MatchModel::Searching);
m_curResults->treeView->expand(m_curResults->matchModel.index(0, 0));
m_curResults->expandRoot();
if (m_ui.searchPlaceCombo->currentIndex() == MatchModel::CurrentFile) {
m_resultBaseDir.clear();
......@@ -1215,7 +1182,7 @@ void KatePluginSearchView::startSearchWhileTyping()
m_curResults->matchModel.clear();
m_curResults->matchModel.setSearchPlace(MatchModel::CurrentFile);
m_curResults->matchModel.setSearchState(MatchModel::Searching);
m_curResults->treeView->expand(m_curResults->matchModel.index(0, 0));
m_curResults->expandRoot();
// Do the search
int searchStoppedAt = m_searchOpenFiles.searchOpenFile(doc, reg, 0);
......@@ -1265,6 +1232,7 @@ void KatePluginSearchView::searchDone()
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_ui.filterBtn->setDisabled(m_curResults->matches <= 1);
// Set search to done. This sorts the model and collapses all items in the view
m_curResults->matchModel.setSearchState(MatchModel::SearchDone);
......@@ -1302,6 +1270,7 @@ void KatePluginSearchView::searchWhileTypingDone()
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_ui.filterBtn->setDisabled(m_curResults->matches <= 1);
m_curResults->treeView->expandAll();
m_curResults->treeView->resizeColumnToContents(0);
......@@ -1370,7 +1339,7 @@ void KatePluginSearchView::replaceSingleMatch()
}
QModelIndex itemIndex = res->treeView->currentIndex();
if (!res->matchModel.isMatch(itemIndex)) {
if (!res->isMatch(itemIndex)) {
goToNextMatch();
}
......@@ -1379,7 +1348,7 @@ void KatePluginSearchView::replaceSingleMatch()
return;
}
KTextEditor::Range matchRange = res->matchModel.matchRange(itemIndex);
KTextEditor::Range matchRange = res->matchRange(itemIndex);
if (m_mainWindow->activeView()->cursorPosition() != matchRange.start()) {
itemSelected(itemIndex);
......@@ -1392,7 +1361,7 @@ void KatePluginSearchView::replaceSingleMatch()
// FIXME The document might have been edited after the search.
// Fix the ranges before attempting the replace
res->matchModel.replaceSingleMatch(doc, itemIndex, res->regExp, m_ui.replaceCombo->currentText());
res->replaceSingleMatch(doc, itemIndex, res->regExp, m_ui.replaceCombo->currentText());
goToNextMatch();
}
......@@ -1628,8 +1597,9 @@ void KatePluginSearchView::expandResults()
}
// we expand recursively if we either are told so or we have just one toplevel match item
QModelIndex rootItem = m_curResults->matchModel.index(0, 0);
if ((m_ui.expandResults->isChecked() && m_curResults->matchModel.rowCount(rootItem) < 200) || m_curResults->matchModel.rowCount(rootItem) == 1) {
auto *model = m_curResults->treeView->model();
QModelIndex rootItem = model->index(0, 0);
if ((m_ui.expandResults->isChecked() && model->rowCount(rootItem) < 200) || model->rowCount(rootItem) == 1) {
m_curResults->treeView->expandAll();
} else {
// first collapse all and the expand the root, much faster than collapsing all children manually
......@@ -1652,8 +1622,8 @@ void KatePluginSearchView::itemSelected(const QModelIndex &item)
// open any children to go to the first match in the file
QModelIndex matchItem = item;
while (m_curResults->matchModel.hasChildren(matchItem)) {
matchItem = m_curResults->matchModel.index(0, 0, matchItem);
while (m_curResults->model()->hasChildren(matchItem)) {
matchItem = m_curResults->model()->index(0, 0, matchItem);
}
m_curResults->treeView->setCurrentIndex(matchItem);
......@@ -1708,14 +1678,14 @@ void KatePluginSearchView::goToNextMatch()
QUrl docUrl = m_mainWindow->activeView()->document()->url();
// check if current file is in the file list
currentIndex = res->matchModel.firstFileMatch(docUrl);
currentIndex = res->firstFileMatch(docUrl);
if (currentIndex.isValid()) {
// We have the index of the first match in the file
// expand the file item
res->treeView->expand(currentIndex.parent());
// check if we can get the next match after the
currentIndex = res->matchModel.closestMatchAfter(docUrl, m_mainWindow->activeView()->cursorPosition());
currentIndex = res->closestMatchAfter(docUrl, m_mainWindow->activeView()->cursorPosition());
if (currentIndex.isValid()) {
itemSelected(currentIndex);
delete m_infoMessage;
......@@ -1732,7 +1702,7 @@ void KatePluginSearchView::goToNextMatch()
}
if (!currentIndex.isValid()) {
currentIndex = res->matchModel.firstMatch();
currentIndex = res->firstMatch();
if (currentIndex.isValid()) {
itemSelected(currentIndex);
delete m_infoMessage;
......@@ -1752,9 +1722,9 @@ void KatePluginSearchView::goToNextMatch()
}
// we had an active item go to next
currentIndex = res->matchModel.nextMatch(currentIndex);
currentIndex = res->nextMatch(currentIndex);
itemSelected(currentIndex);
if (currentIndex == res->matchModel.firstMatch()) {
if (currentIndex == res->firstMatch()) {
delete m_infoMessage;
const QString msg = i18n("Continuing from first match");
m_infoMessage = new KTextEditor::Message(msg, KTextEditor::Message::Information);
......@@ -1785,14 +1755,14 @@ void KatePluginSearchView::goToPreviousMatch()
QUrl docUrl = m_mainWindow->activeView()->document()->url();
// check if current file is in the file list
currentIndex = res->matchModel.firstFileMatch(docUrl);
currentIndex = res->firstFileMatch(docUrl);
if (currentIndex.isValid()) {
// We have the index of the first match in the file
// expand the file item
res->treeView->expand(currentIndex.parent());
// check if we can get the next match after the
currentIndex = res->matchModel.closestMatchBefore(docUrl, m_mainWindow->activeView()->cursorPosition());
currentIndex = res->closestMatchBefore(docUrl, m_mainWindow->activeView()->cursorPosition());
if (currentIndex.isValid()) {
itemSelected(currentIndex);
delete m_infoMessage;
......@@ -1809,7 +1779,7 @@ void KatePluginSearchView::goToPreviousMatch()
}
if (!currentIndex.isValid()) {
currentIndex = res->matchModel.lastMatch();
currentIndex = res->lastMatch();
if (currentIndex.isValid()) {
itemSelected(currentIndex);