Commit 08d1155a authored by Kåre Särs's avatar Kåre Särs
Browse files

Add MatchModel to handle the matches

parent a4136d7a
......@@ -21,14 +21,15 @@ target_sources(katesearchplugin PRIVATE ${UI_SOURCES})
target_sources(
katesearchplugin
PRIVATE
plugin_search.cpp
search_open_files.cpp
SearchDiskFiles.cpp
FolderFilesList.cpp
replace_matches.cpp
htmldelegate.cpp
KateSearchCommand.cpp
MatchModel.cpp
SearchDiskFiles.cpp
htmldelegate.cpp
plugin.qrc
plugin_search.cpp
replace_matches.cpp
search_open_files.cpp
)
kcoreaddons_desktop_to_json(katesearchplugin katesearch.desktop)
......
/***************************************************************************
* This file is part of Kate build plugin
* SPDX-FileCopyrightText: 2014 Kåre Särs <kare.sars@iki.fi>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
***************************************************************************/
#include "MatchModel.h"
#include <KLocalizedString>
#include <QDebug>
#include <QTimer>
#include <QRegularExpression>
#include <QFileInfo>
#include <QDir>
#include <algorithm> // std::count_if
static const quintptr InvalidIndex = 0xFFFFFFFF;
static QUrl localFileDirUp(const QUrl &url)
{
if (!url.isLocalFile())
return url;
// else go up
return QUrl::fromLocalFile(QFileInfo(url.toLocalFile()).dir().absolutePath());
}
MatchModel::MatchModel(QObject *parent)
: QAbstractItemModel(parent)
{
}
MatchModel::~MatchModel()
{
}
void MatchModel::clear()
{
beginResetModel();
m_matchFiles.clear();
m_matchFileIndexHash.clear();
endResetModel();
}
/** This function returns the row index of the specified file.
* If the file does not exist in the model, the file will be added to the model. */
int MatchModel::matchFileRow(const QUrl& fileUrl)
{
return m_matchFileIndexHash.value(fileUrl, -1);
}
static const int totalContectLen = 150;
/** This function is used to add a match to a new file */
void MatchModel::addMatch(const QUrl &fileUrl, const QString &lineContent, int matchLen, int line, int column, int endLine, int endColumn)
{
int fileIndex = matchFileRow(fileUrl);
if (fileIndex == -1) {
fileIndex = m_matchFiles.size();
m_matchFileIndexHash.insert(fileUrl, fileIndex);
beginInsertRows(QModelIndex(), fileIndex, fileIndex);
// We are always starting the insert at the end, so we could optimize by delaying/grouping the signaling of the updates
m_matchFiles.append(MatchFile());
m_matchFiles[fileIndex].fileUrl = fileUrl;
endInsertRows();
}
MatchModel::Match match;
match.matchLen = matchLen;
match.startLine = line;
match.startColumn = column;
match.endLine = endLine;
match.endColumn = endColumn;
int contextLen = totalContectLen - matchLen;
int preLen = qMin(contextLen/3, column);
int postLen = contextLen - preLen;
match.preMatchStr = lineContent.mid(column-preLen, preLen);
if (column > preLen) match.preMatchStr.prepend(QLatin1String("..."));
match.postMatchStr = lineContent.mid(endColumn, postLen);
if (endColumn+preLen < lineContent.size()) match.postMatchStr.append(QLatin1String("..."));
match.matchStr = lineContent.mid(column, matchLen);
int matchIndex = m_matchFiles[fileIndex].matches.size();
beginInsertRows(index(fileIndex, 0), matchIndex, matchIndex);
// We are always starting the insert at the end, so we could optimize by delaying/grouping the signaling of the updates
m_matchFiles[fileIndex].matches.append(match);
endInsertRows();
}
void MatchModel::setMatchColors(const QColor &foreground, const QColor &background, const QColor &replaseBackground)
{
m_foregroundColor = foreground;
m_searchBackgroundColor = background;
m_replaceHighlightColor = replaseBackground;
}
// /** This function is used to modify a match */
// void MatchModel::replaceMatch(const QModelIndex &matchIndex, const QRegularExpression &regexp, const QString &replaceText)
// {
// if (!matchIndex.isValid()) return;
// }
//
// /** Replace all matches that have been checked */
// void MatchModel::replaceChecked(const QRegularExpression &regexp, const QString &replace)
// {
// }
QString MatchModel::matchToHtmlString(const Match &match) const
{
QString pre =match.preMatchStr.toHtmlEscaped();
QString matchStr = match.matchStr;
matchStr.replace(QLatin1Char('\n'), QStringLiteral("\\n"));
matchStr = matchStr.toHtmlEscaped();
matchStr = QStringLiteral("<span style=\"background-color:%1; color:%2;\">%3</span>")
.arg(m_searchBackgroundColor.name(), m_foregroundColor.name(), matchStr);
QString post = match.postMatchStr.toHtmlEscaped();
// (line:col)[space][space] ...Line text pre [highlighted match] Line text post....
QString displayText = QStringLiteral("(<b>%1:%2</b>) &nbsp;").arg(match.startLine + 1).arg(match.startColumn + 1) + pre + matchStr + post;
return displayText;
}
QString MatchModel::fileItemToHtmlString(const MatchFile &matchFile) const
{
QString path = matchFile.fileUrl.isLocalFile() ? localFileDirUp(matchFile.fileUrl).path() : matchFile.fileUrl.url();
if (!path.isEmpty() && !path.endsWith(QLatin1Char('/'))) {
path += QLatin1Char('/');
}
QString tmpStr = QStringLiteral("%1<b>%2: %3</b>").arg(path, matchFile.fileUrl.fileName()).arg(matchFile.matches.size());
return tmpStr;
}
QVariant MatchModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.column() < 0 || index.column() > 1) {
return QVariant();
}
if (role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::CheckStateRole) {
return QVariant();
}
// FIXME add one more level
int fileRow = index.internalId() == InvalidIndex ? index.row() : (int)index.internalId();
int matchRow = index.internalId() == InvalidIndex ? -1 : index.row();
if (fileRow < 0 || fileRow >= m_matchFiles.size()) {
qDebug() << "Should be a file (or the info item in the near future)" << fileRow;
return QVariant();
}
if (matchRow < 0) {
// File item
switch (role) {
case Qt::DisplayRole:
return fileItemToHtmlString(m_matchFiles[fileRow]);
case Qt::CheckStateRole:
return m_matchFiles[fileRow].checkState;
}
}
else if (matchRow < m_matchFiles[fileRow].matches.size()) {
// Match
const Match &match = m_matchFiles[fileRow].matches[matchRow];
switch (role) {
case Qt::DisplayRole:
return matchToHtmlString(match);
case Qt::CheckStateRole:
return match.checked ? Qt::Checked : Qt::Unchecked;
}
}
else {
qDebug() << "bad index";
return QVariant();
}
return QVariant();
}
static bool isChecked(const MatchModel::Match &match) { return match.checked; }
bool MatchModel::setData(const QModelIndex &itemIndex, const QVariant &, int role)
{
// FIXME
if (role != Qt::CheckStateRole)
return false;
if (!itemIndex.isValid())
return false;
if (itemIndex.column() != 0)
return false;
// Check/un-check the root-item and it's children
if (itemIndex.internalId() == InvalidIndex) {
int row = itemIndex.row();
if (row < 0 || row >= m_matchFiles.size()) return false;
QVector<Match> &matches = m_matchFiles[row].matches;
bool checked = m_matchFiles[row].checkState != Qt::Checked; // we toggle the current value
for (int i = 0; i < matches.size(); ++i) {
matches[i].checked = checked;
}
m_matchFiles[row].checkState = checked ? Qt::Checked : Qt::Unchecked;
QModelIndex rootFileIndex = index(row, 0);
dataChanged(rootFileIndex, rootFileIndex, QVector<int>(Qt::CheckStateRole));
dataChanged(index(0, 0, rootFileIndex), index(matches.count()-1, 0, rootFileIndex), QVector<int>(Qt::CheckStateRole));
return true;
}
int rootRow = itemIndex.internalId();
if (rootRow < 0 || rootRow >= m_matchFiles.size())
return false;
int row = itemIndex.row();
QVector<Match> &matches = m_matchFiles[rootRow].matches;
if (row < 0 || row >= matches.size())
return false;
// we toggle the current value
matches[row].checked = !matches[row].checked;
int checkedCount = std::count_if(matches.begin(), matches.end(), isChecked);
if (checkedCount == matches.size()) {
m_matchFiles[rootRow].checkState = Qt::Checked;
}
else if (checkedCount == 0) {
m_matchFiles[rootRow].checkState = Qt::Unchecked;
}
else {
m_matchFiles[rootRow].checkState = Qt::PartiallyChecked;
}
QModelIndex rootFileIndex = index(rootRow, 0);
dataChanged(rootFileIndex, rootFileIndex, QVector<int>(Qt::CheckStateRole));
dataChanged(index(row, 0, rootFileIndex), index(row, 0, rootFileIndex), QVector<int>(Qt::CheckStateRole));
return true;
}
Qt::ItemFlags MatchModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
if (index.column() == 0) {
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
int MatchModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid()) {
return m_matchFiles.size();
}
if (parent.internalId() != InvalidIndex) {
return 0;
}
int row = parent.row();
if (row < 0 || row >= m_matchFiles.size()) {
return 0;
}
return m_matchFiles[row].matches.size();
}
int MatchModel::columnCount(const QModelIndex &) const
{
return 1;
}
QModelIndex MatchModel::index(int row, int column, const QModelIndex &parent) const
{
quint32 rootIndex = InvalidIndex;
if (parent.isValid() && parent.internalId() == InvalidIndex) {
rootIndex = parent.row();
}
return createIndex(row, column, rootIndex);
}
QModelIndex MatchModel::parent(const QModelIndex &child) const
{
if (child.internalId() == InvalidIndex) {
return QModelIndex();
}
return createIndex(child.internalId(), 0, InvalidIndex);
}
/***************************************************************************
* This file is part of Kate search plugin
* SPDX-FileCopyrightText: 2020 Kåre Särs <kare.sars@iki.fi>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
***************************************************************************/
#ifndef MatchModel_h
#define MatchModel_h
#include <QAbstractItemModel>
#include <QString>
#include <QUrl>
#include <QBrush>
class MatchModel : public QAbstractItemModel
{
Q_OBJECT
public:
enum MatchDataRoles {
FileUrlRole = Qt::UserRole,
FileNameRole,
StartLineRole,
StartColumnRole,
EndLineRole,
EndColumnRole,
MatchLenRole,
PreMatchRole,
MatchRole,
PostMatchRole,
ReplacedRole,
ReplaceTextRole,
CheckedRole,
};
Q_ENUM(MatchDataRoles)
struct Match {
int startLine = 0;
int startColumn = 0;
int endLine = 0;
int endColumn = 0;
int matchLen = 0;
QString preMatchStr;
QString matchStr;
QString postMatchStr;
QString replaceText;
bool checked = true;
};
struct MatchFile {
QUrl fileUrl;
QVector<Match> matches;
Qt::CheckState checkState = Qt::Checked;
};
MatchModel(QObject *parent = nullptr);
~MatchModel() override;
void setMatchColors(const QColor &foreground, const QColor &background, const QColor &replaseBackground);
public Q_SLOTS:
/** This function clears all matches in all files */
void clear();
/** This function returns the row index of the specified file.
* If the file does not exist in the model, the file will be added to the model. */
int matchFileRow(const QUrl& fileUrl);
/** This function is used to add a new file */
void addMatch(const QUrl &fileUrl, const QString &lineContent, int matchLen, int line, int column, int endLine, int endColumn);
// /** This function is used to modify a match */
// void replaceMatch(const QModelIndex &matchIndex, const QRegularExpression &regexp, const QString &replaceText);
//
// /** Replace all matches that have been checked */
// void replaceChecked(const QRegularExpression &regexp, const QString &replace);
Q_SIGNALS:
public:
// 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;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
private:
QString matchToHtmlString(const Match &match) const;
QString fileItemToHtmlString(const MatchFile &matchFile) const;
QVector<MatchFile> m_matchFiles;
QHash<QUrl, int> m_matchFileIndexHash;
QColor m_searchBackgroundColor;
QColor m_foregroundColor;
QColor m_replaceHighlightColor;
};
#endif
......@@ -140,7 +140,7 @@ void SearchDiskFiles::searchSingleLineRegExp(const QString &fileName)
// emit all matches batched
if (!matches.isEmpty()) {
const QUrl fileUrl = QUrl::fromUserInput(fileName);
emit matchesFound(fileUrl.toString(), fileUrl.fileName(), matches);
emit matchesFound(fileUrl, matches);
}
}
......@@ -211,6 +211,6 @@ void SearchDiskFiles::searchMultiLineRegExp(const QString &fileName)
// emit all matches batched
if (!matches.isEmpty()) {
const QUrl fileUrl = QUrl::fromUserInput(fileName);
emit matchesFound(fileUrl.toString(), fileUrl.fileName(), matches);
emit matchesFound(fileUrl, matches);
}
}
......@@ -64,7 +64,7 @@ public Q_SLOTS:
void cancelSearch();
Q_SIGNALS:
void matchesFound(const QString &url, const QString &docName, const QVector<KateSearchMatch> &searchMatches);
void matchesFound(const QUrl &url, const QVector<KateSearchMatch> &searchMatches);
void searchDone();
void searching(const QString &file);
......
......@@ -216,6 +216,8 @@ Results::Results(QWidget *parent)
setupUi(this);
tree->setItemDelegate(new SPHtmlDelegate(tree));
treeView->setItemDelegate(new SPHtmlDelegate(tree));
treeView->setModel(&matchModel);
}
K_PLUGIN_FACTORY_WITH_JSON(KatePluginSearchFactory, "katesearch.json", registerPlugin<KatePluginSearch>();)
......@@ -465,6 +467,8 @@ KatePluginSearchView::KatePluginSearchView(KTextEditor::Plugin *plugin, KTextEdi
connect(&m_searchDiskFiles, &SearchDiskFiles::searchDone, this, &KatePluginSearchView::searchDone);
connect(&m_searchDiskFiles, static_cast<void (SearchDiskFiles::*)(const QString &)>(&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);
......@@ -815,7 +819,7 @@ void KatePluginSearchView::addHeaderItem()
m_curResults->tree->expandItem(item);
}
void KatePluginSearchView::addMatchesToRootFileItem(const QString &url, const QString &fName, const QList<QTreeWidgetItem *> &matchItems)
void KatePluginSearchView::addMatchesToRootFileItem(const QUrl &url, const QList<QTreeWidgetItem *> &matchItems)
{
if (!m_curResults) {
return;
......@@ -847,22 +851,18 @@ void KatePluginSearchView::addMatchesToRootFileItem(const QString &url, const QS
return;
}
QUrl fullUrl = QUrl::fromUserInput(url);
QString path = fullUrl.isLocalFile() ? localFileDirUp(fullUrl).path() : fullUrl.url();
QString path = url.isLocalFile() ? localFileDirUp(url).path() : url.url();
if (!path.isEmpty() && !path.endsWith(QLatin1Char('/'))) {
path += QLatin1Char('/');
}
path.remove(m_resultBaseDir);
QString name = fullUrl.fileName();
if (url.isEmpty()) {
name = fName;
}
QString fileName = url.fileName();
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)) {
if (root->child(i)->data(0, ReplaceMatches::FileUrlRole).toUrl() == url) {
int matches = root->child(i)->data(0, ReplaceMatches::StartLineRole).toInt() + matchItems.size();
QString tmpUrl = QStringLiteral("%1<b>%2</b>: <b>%3</b>").arg(path, name).arg(matches);
QString tmpUrl = QStringLiteral("%1<b>%2</b>: <b>%3</b>").arg(path, fileName).arg(matches);
root->child(i)->setData(0, Qt::DisplayRole, tmpUrl);
root->child(i)->setData(0, ReplaceMatches::StartLineRole, matches);
root->child(i)->addChildren(matchItems);
......@@ -871,12 +871,13 @@ void KatePluginSearchView::addMatchesToRootFileItem(const QString &url, const QS
}
// file item not found create a new one
QString tmpUrl = QStringLiteral("%1<b>%2</b>: <b>%3</b>").arg(path, name).arg(matchItems.size());
QString tmpUrl = QStringLiteral("%1<b>%2</b>: <b>%3</b>").arg(path, fileName).arg(matchItems.size());
TreeWidgetItem *item = new TreeWidgetItem(root, QStringList(tmpUrl));
item->setData(0, ReplaceMatches::FileUrlRole, url);
item->setData(0, ReplaceMatches::FileNameRole, fName);
item->setData(0, ReplaceMatches::FileNameRole, fileName);
item->setData(0, ReplaceMatches::StartLineRole, matchItems.size());
item->setCheckState(0, Qt::Checked);
item->setFlags(item->flags() | Qt::ItemIsAutoTristate);
item->addChildren(matchItems);
......@@ -954,7 +955,7 @@ void KatePluginSearchView::addMatchMark(KTextEditor::Document *doc, KTextEditor:
iface->addMark(line, KTextEditor::MarkInterface::markType32);
}
void KatePluginSearchView::matchesFound(const QString &url, const QString &fName, const QVector<KateSearchMatch> &searchMatches)
void KatePluginSearchView::matchesFound(const QUrl &url, const QVector<KateSearchMatch> &searchMatches)
{
static constexpr int contextLen = 70;
......@@ -996,10 +997,9 @@ void KatePluginSearchView::matchesFound(const QString &url, const QString &fName
displayText = displayText + pre + matchHighlighted + post;
TreeWidgetItem *item = new TreeWidgetItem(static_cast<TreeWidgetItem*>(nullptr), QStringList{displayText});
item->setData(0, ReplaceMatches::FileUrlRole, url);
item->setData(0, Qt::ToolTipRole, url);
item->setData(0, ReplaceMatches::FileNameRole, fName);
item->setData(0, ReplaceMatches::FileNameRole, url.fileName());
item->setData(0, ReplaceMatches::StartLineRole, searchMatch.matchRange.start().line());
item->setData(0, ReplaceMatches::StartColumnRole, searchMatch.matchRange.start().column());
item->setData(0, ReplaceMatches::MatchLenRole, searchMatch.matchLen);
......@@ -1011,7 +1011,7 @@ void KatePluginSearchView::matchesFound(const QString &url, const QString &fName
item->setCheckState(0, Qt::Checked);
items.push_back(item);
}
addMatchesToRootFileItem(url, fName, items);
addMatchesToRootFileItem(url, items);
m_curResults->matches += items.size();
}
......@@ -1092,6 +1092,11 @@ void KatePluginSearchView::updateSearchColors()
if (delegate) {
delegate->setDisplayFont(ciface->configValue(QStringLiteral("font")).value<QFont>());
}
auto* delegate2 = qobject_cast<SPHtmlDelegate*>(m_curResults->treeView->itemDelegate());
if (delegate2) {
delegate2->setDisplayFont(ciface->configValue(QStringLiteral("font")).value<QFont>());
}
m_curResults->matchModel.setMatchColors(m_foregroundColor.color(), m_searchBackgroundColor.color(), m_replaceHighlightColor.color());
}
}
}
......@@ -1103,7 +1108,7 @@ void KatePluginSearchView::startSearch()
m_folderFilesList.terminateSearch();
m_searchOpenFiles.terminateSearch();
m_searchDiskFiles.terminateSearch();
// Re-enable the handling of fisk-file-matches after one event loop
// Re-enable the handling of disk-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.
......@@ -1119,6 +1124,8 @@ void KatePluginSearchView::startSearch()
return;
}
m_matchModel.clear();
m_isSearchAsYouType = false;
QString currentSearchText = m_ui.searchCombo->currentText();
......@@ -1192,6 +1199,8 @@ void KatePluginSearchView::startSearch()
m_searchDiskFilesDone = false;
m_searchOpenFilesDone = false;
m_curResults->matchModel.clear();
const bool inCurrentProject = m_ui.searchPlaceCombo->currentIndex() == Project;
const bool inAllOpenProjects = m_ui.searchPlaceCombo->currentIndex() == AllProjects;
......@@ -1351,6 +1360,8 @@ void KatePluginSearchView::startSearchWhileTyping()
m_curResults->tree->setCurrentItem(nullptr);
m_curResults->matches = 0;
m_curResults->matchModel.clear();
// Add the search-as-you-type header item
TreeWidgetItem *item = new TreeWidgetItem(m_curResults->tree, QStringList());
item->setData(0, ReplaceMatches::FileUrlRole, doc->url().toString());
......
......@@ -34,6 +34,7 @@
#include "ui_results.h"
#include "ui_search.h"
#include "MatchModel.h"
#include "FolderFilesList.h"