File indexing completed on 2024-04-28 05:49:12

0001 /*
0002     SPDX-FileCopyrightText: 2021 Kåre Särs <kare.sars@iki.fi>
0003     SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "MatchModel.h"
0009 #include <KLocalizedString>
0010 #include <QDebug>
0011 #include <QDir>
0012 #include <QFileInfo>
0013 #include <algorithm> // std::count_if
0014 
0015 #include <KTextEditor/Document>
0016 #include <ktexteditor/movingrange.h>
0017 
0018 static const quintptr InfoItemId = 0xFFFFFFFF;
0019 static const quintptr FileItemId = 0x7FFFFFFF;
0020 
0021 // Model indexes
0022 // - (0, 0, InfoItemId) (row, column, internalId)
0023 //   | - (0, 0, FileItemId)
0024 //   |    | - (0, 0, 0)
0025 //   |    | - (1, 0, 0)
0026 //   | - (1, 0, FileItemId)
0027 //   |    | - (0, 0, 1)
0028 //   |    | - (1, 0, 1)
0029 
0030 static QUrl localFileDirUp(const QUrl &url)
0031 {
0032     if (!url.isLocalFile()) {
0033         return url;
0034     }
0035 
0036     // else go up
0037     return QUrl::fromLocalFile(QFileInfo(url.toLocalFile()).dir().absolutePath());
0038 }
0039 
0040 MatchModel::MatchModel(QObject *parent)
0041     : QAbstractItemModel(parent)
0042 {
0043     m_infoUpdateTimer.setInterval(100); // FIXME why does this delay not work?
0044     m_infoUpdateTimer.setSingleShot(true);
0045     connect(&m_infoUpdateTimer, &QTimer::timeout, this, [this]() {
0046         dataChanged(createIndex(0, 0, InfoItemId), createIndex(0, 0, InfoItemId));
0047     });
0048 }
0049 
0050 MatchModel::~MatchModel()
0051 {
0052 }
0053 
0054 void MatchModel::setDocumentManager(KTextEditor::Application *manager)
0055 {
0056     m_docManager = manager;
0057     connect(m_docManager, &KTextEditor::Application::documentWillBeDeleted, this, &MatchModel::cancelReplace);
0058 }
0059 
0060 void MatchModel::setSearchPlace(MatchModel::SearchPlaces searchPlace)
0061 {
0062     m_searchPlace = searchPlace;
0063     if (!m_infoUpdateTimer.isActive()) {
0064         m_infoUpdateTimer.start();
0065     }
0066 }
0067 
0068 void MatchModel::setFileListUpdate(const QString &path)
0069 {
0070     m_lastSearchPath = path;
0071     m_searchState = Preparing;
0072     if (!m_infoUpdateTimer.isActive()) {
0073         m_infoUpdateTimer.start();
0074     }
0075 }
0076 
0077 void MatchModel::setSearchState(MatchModel::SearchState searchState)
0078 {
0079     m_searchState = searchState;
0080     if (!m_infoUpdateTimer.isActive()) {
0081         m_infoUpdateTimer.start();
0082     }
0083     if (m_searchState == SearchDone) {
0084         beginResetModel();
0085         std::sort(m_matchFiles.begin(), m_matchFiles.end(), [](const MatchFile &l, const MatchFile &r) {
0086             return l.fileUrl < r.fileUrl;
0087         });
0088         for (int i = 0; i < m_matchFiles.size(); ++i) {
0089             if (m_matchFiles.at(i).fileUrl.isValid()) {
0090                 m_matchFileIndexHash[m_matchFiles[i].fileUrl] = i;
0091             } else if (m_matchFiles.at(i).doc) {
0092                 m_matchUnsavedFileIndexHash[m_matchFiles.at(i).doc] = i;
0093             } else {
0094                 qWarning() << "Trying to setSearchState for invalid doc";
0095                 Q_UNREACHABLE();
0096             }
0097         }
0098         endResetModel();
0099     }
0100 }
0101 
0102 void MatchModel::setBaseSearchPath(const QString &baseSearchPath)
0103 {
0104     m_resultBaseDir = baseSearchPath;
0105     if (!m_infoUpdateTimer.isActive()) {
0106         m_infoUpdateTimer.start();
0107     }
0108 }
0109 
0110 void MatchModel::setProjectName(const QString &projectName)
0111 {
0112     m_projectName = projectName;
0113     if (!m_infoUpdateTimer.isActive()) {
0114         m_infoUpdateTimer.start();
0115     }
0116 }
0117 
0118 void MatchModel::clear()
0119 {
0120     beginResetModel();
0121     m_matchFiles.clear();
0122     m_matchFileIndexHash.clear();
0123     m_matchUnsavedFileIndexHash.clear();
0124     m_lastMatchUrl.clear();
0125     endResetModel();
0126 }
0127 
0128 /** This function returns the row index of the specified file.
0129  * If the file does not exist in the model, the file will be added to the model. */
0130 int MatchModel::matchFileRow(const QUrl &fileUrl, KTextEditor::Document *doc) const
0131 {
0132     const int ret = m_matchFileIndexHash.value(fileUrl, -1);
0133     if (ret != -1) {
0134         return ret;
0135     }
0136     return m_matchUnsavedFileIndexHash.value(doc, -1);
0137 }
0138 
0139 /** This function is used to add a match to a new file */
0140 void MatchModel::addMatches(const QUrl &fileUrl, const QList<KateSearchMatch> &searchMatches, KTextEditor::Document *doc)
0141 {
0142     m_lastMatchUrl = fileUrl;
0143     m_searchState = Searching;
0144     // update match/search info
0145     if (!m_infoUpdateTimer.isActive()) {
0146         m_infoUpdateTimer.start();
0147     }
0148 
0149     if (searchMatches.isEmpty()) {
0150         return;
0151     }
0152 
0153     if (m_matchFiles.isEmpty()) {
0154         beginInsertRows(QModelIndex(), 0, 0);
0155         endInsertRows();
0156     }
0157 
0158     int fileIndex = matchFileRow(fileUrl, doc);
0159     if (fileIndex == -1) {
0160         fileIndex = m_matchFiles.size();
0161 
0162         if (fileUrl.isValid()) {
0163             m_matchFileIndexHash.insert(fileUrl, fileIndex);
0164         } else if (doc) {
0165             m_matchUnsavedFileIndexHash.insert(doc, fileIndex);
0166         } else {
0167             qWarning() << "Trying to insert invalid match, url is invalid, doc is null";
0168             Q_UNREACHABLE();
0169         }
0170 
0171         beginInsertRows(createIndex(0, 0, InfoItemId), fileIndex, fileIndex);
0172         // We are always starting the insert at the end, so we could optimize by delaying/grouping the signaling of the updates
0173         m_matchFiles.append(MatchFile());
0174         m_matchFiles[fileIndex].fileUrl = fileUrl;
0175         m_matchFiles[fileIndex].doc = doc;
0176         endInsertRows();
0177     }
0178 
0179     int matchIndex = m_matchFiles[fileIndex].matches.size();
0180     beginInsertRows(createIndex(fileIndex, 0, FileItemId), matchIndex, matchIndex + searchMatches.size() - 1);
0181     m_matchFiles[fileIndex].matches += searchMatches;
0182     endInsertRows();
0183 }
0184 
0185 void MatchModel::setMatchColors(const QString &foreground, const QString &background, const QString &replaceBackground)
0186 {
0187     m_foregroundColor = foreground;
0188     m_searchBackgroundColor = background;
0189     m_replaceHighlightColor = replaceBackground;
0190 }
0191 
0192 KateSearchMatch *MatchModel::matchFromIndex(const QModelIndex &matchIndex)
0193 {
0194     if (!isMatch(matchIndex)) {
0195         qDebug() << "Not a valid match index";
0196         return nullptr;
0197     }
0198 
0199     int fileRow = matchIndex.internalId();
0200     int matchRow = matchIndex.row();
0201 
0202     return &m_matchFiles[fileRow].matches[matchRow];
0203 }
0204 
0205 KTextEditor::Range MatchModel::matchRange(const QModelIndex &matchIndex) const
0206 {
0207     if (!isMatch(matchIndex)) {
0208         qDebug() << "Not a valid match index";
0209         return KTextEditor::Range();
0210     }
0211     int fileRow = matchIndex.internalId();
0212     int matchRow = matchIndex.row();
0213     return m_matchFiles[fileRow].matches[matchRow].range;
0214 }
0215 
0216 const QList<KateSearchMatch> &MatchModel::fileMatches(KTextEditor::Document *doc) const
0217 {
0218     int row = matchFileRow(doc->url(), doc);
0219     if (row < 0 || row >= m_matchFiles.size()) {
0220         static const QList<KateSearchMatch> EmptyDummy;
0221         return EmptyDummy;
0222     }
0223     return m_matchFiles[row].matches;
0224 }
0225 
0226 void MatchModel::updateMatchRanges(const QList<KTextEditor::MovingRange *> &ranges)
0227 {
0228     if (ranges.isEmpty()) {
0229         return;
0230     }
0231 
0232     auto *doc = ranges.first()->document();
0233     const QUrl fileUrl = doc->url();
0234     // NOTE: we assume there are only ranges for one document in the provided ranges
0235     // NOTE: we also assume the document is not deleted as we clear the ranges when the document is deleted
0236 
0237     int fileRow = matchFileRow(fileUrl, doc);
0238     if (fileRow < 0 || fileRow >= m_matchFiles.size()) {
0239         // qDebug() << "No such results" << fileRow << fileUrl;
0240         return; // No such document in the results
0241     }
0242 
0243     QList<KateSearchMatch> &matches = m_matchFiles[fileRow].matches;
0244 
0245     if (ranges.size() != matches.size()) {
0246         // The sizes do not match so we cannot match the ranges easily.. abort
0247         qDebug() << __func__ << ranges.size() << "!=" << matches.size() << fileUrl << doc;
0248         return;
0249     }
0250 
0251     if (ranges.size() > 1000) {
0252         // if we have > 1000 matches in a file it could get slow to update it all the time
0253         return;
0254     }
0255 
0256     for (int i = 0; i < ranges.size(); ++i) {
0257         matches[i].range = ranges[i]->toRange();
0258     }
0259     QModelIndex rootFileIndex = index(fileRow, 0, createIndex(0, 0, InfoItemId));
0260     dataChanged(index(0, 0, rootFileIndex), index(matches.count() - 1, 0, rootFileIndex));
0261 }
0262 
0263 QRegularExpressionMatch MatchModel::rangeTextMatches(const QString &rangeText, QRegularExpression regExp)
0264 {
0265     // special handling for lookahead and lookbehind
0266     QString pattern = regExp.pattern();
0267 
0268     // NOTE: Negative look-ahead/behind are not a problem as they are not part of the range
0269     static const QRegularExpression lookaheadRegex(QStringLiteral("^.*(\\(\\?=[^\\)]+\\))$"));
0270     static const QRegularExpression lookbehindRegex(QStringLiteral("^(\\(\\?<=[^\\)]+\\)).*$"));
0271 
0272     // Remove possible lookahead as we do not have the tail to compare with
0273     auto lookMatch = lookaheadRegex.match(pattern);
0274     if (lookMatch.hasMatch()) {
0275         pattern.remove(lookMatch.capturedStart(1), lookMatch.capturedLength(1));
0276         regExp.setPattern(pattern);
0277     }
0278     // Remove possible lookbehind as we do not have the prefix
0279     lookMatch = lookbehindRegex.match(pattern);
0280     if (lookMatch.hasMatch()) {
0281         pattern.remove(lookMatch.capturedStart(1), lookMatch.capturedLength(1));
0282         regExp.setPattern(pattern);
0283     }
0284 
0285     return regExp.match(rangeText);
0286 }
0287 
0288 /** This function is used to replace a match */
0289 bool MatchModel::replaceMatch(KTextEditor::Document *doc, const QModelIndex &matchIndex, const QRegularExpression &regExp, const QString &replaceString)
0290 {
0291     if (!doc) {
0292         qDebug() << "No doc";
0293         return false;
0294     }
0295 
0296     Match *matchItem = matchFromIndex(matchIndex);
0297 
0298     if (!matchItem) {
0299         qDebug() << "Not a valid index";
0300         return false;
0301     }
0302 
0303     // don't replace an already replaced item
0304     if (!matchItem->replaceText.isEmpty()) {
0305         // qDebug() << "not replacing already replaced item";
0306         return false;
0307     }
0308 
0309     // Check that the text has not been modified and still matches + get captures for the replace
0310     QString matchLines = doc->text(matchItem->range);
0311     QRegularExpressionMatch match = rangeTextMatches(matchLines, regExp);
0312     if (match.capturedStart() != 0) {
0313         qDebug() << matchLines << "Does not match" << regExp.pattern();
0314         return false;
0315     }
0316 
0317     // Modify the replace string according to this match
0318     QString replaceText = MatchModel::generateReplaceString(match, replaceString);
0319 
0320     // Replace the string
0321     doc->replaceText(matchItem->range, replaceText);
0322 
0323     // update the range
0324     int newEndLine = matchItem->range.start().line() + replaceText.count(QLatin1Char('\n'));
0325     int lastNL = replaceText.lastIndexOf(QLatin1Char('\n'));
0326     int newEndColumn = lastNL == -1 ? matchItem->range.start().column() + replaceText.length() : replaceText.length() - lastNL - 1;
0327     matchItem->range.setEnd(KTextEditor::Cursor{newEndLine, newEndColumn});
0328 
0329     matchItem->replaceText = replaceText;
0330     return true;
0331 }
0332 
0333 /** This function is used to replace a match */
0334 bool MatchModel::replaceSingleMatch(KTextEditor::Document *doc, const QModelIndex &matchIndex, const QRegularExpression &regExp, const QString &replaceString)
0335 {
0336     if (!doc) {
0337         qDebug() << "No doc";
0338         return false;
0339     }
0340 
0341     if (!isMatch(matchIndex)) {
0342         qDebug() << "This should not be possible";
0343         return false;
0344     }
0345 
0346     if (matchIndex.internalId() == InfoItemId || matchIndex.internalId() == FileItemId) {
0347         qDebug() << "You cannot replace a file or the info item";
0348         return false;
0349     }
0350 
0351     // Create a vector of moving ranges for updating the tree-view after replace
0352     QList<KTextEditor::MovingRange *> matchRanges;
0353     // Only add items after "matchIndex"
0354     int fileRow = matchIndex.internalId();
0355     int matchRow = matchIndex.row();
0356 
0357     QList<Match> &matches = m_matchFiles[fileRow].matches;
0358 
0359     for (int i = matchRow + 1; i < matches.size(); ++i) {
0360         KTextEditor::MovingRange *mr = doc->newMovingRange(matches[i].range);
0361         matchRanges.append(mr);
0362     }
0363 
0364     // The first range in the vector is for this match
0365     if (!replaceMatch(doc, matchIndex, regExp, replaceString)) {
0366         return false;
0367     }
0368 
0369     // Update the items after the matchIndex
0370     for (int i = matchRow + 1; i < matches.size(); ++i) {
0371         Q_ASSERT(!matchRanges.isEmpty());
0372         KTextEditor::MovingRange *mr = matchRanges.takeFirst();
0373         matches[i].range = mr->toRange();
0374         delete mr;
0375     }
0376     Q_ASSERT(matchRanges.isEmpty());
0377 
0378     dataChanged(createIndex(matchRow, 0, fileRow), createIndex(matches.size() - 1, 0, fileRow));
0379 
0380     return true;
0381 }
0382 
0383 void MatchModel::doReplaceNextMatch()
0384 {
0385     Q_ASSERT(m_docManager);
0386 
0387     if (m_cancelReplace || m_replaceFile >= m_matchFiles.size()) {
0388         m_replaceFile = -1;
0389         Q_EMIT replaceDone();
0390         return;
0391     }
0392 
0393     // NOTE The document managers signal documentWillBeDeleted() must be connected to
0394     // cancelReplace(). A closed file could lead to a crash if it is not handled.
0395     // this is now done in setDocumentManager()
0396 
0397     MatchFile &matchFile = m_matchFiles[m_replaceFile];
0398 
0399     if (matchFile.checkState == Qt::Unchecked) {
0400         m_replaceFile++;
0401         QTimer::singleShot(0, this, &MatchModel::doReplaceNextMatch);
0402         return;
0403     }
0404 
0405     KTextEditor::Document *doc;
0406     if (matchFile.fileUrl.isValid()) {
0407         doc = m_docManager->findUrl(matchFile.fileUrl);
0408         if (!doc) {
0409             doc = m_docManager->openUrl(matchFile.fileUrl);
0410         }
0411     } else {
0412         doc = matchFile.doc;
0413     }
0414 
0415     if (!doc) {
0416         qDebug() << "Failed to open the document" << matchFile.fileUrl << doc;
0417         m_replaceFile++;
0418         QTimer::singleShot(0, this, &MatchModel::doReplaceNextMatch);
0419         return;
0420     }
0421 
0422     if (matchFile.fileUrl.isValid() && doc->url() != matchFile.fileUrl) {
0423         qDebug() << "url differences" << matchFile.fileUrl << doc->url();
0424         matchFile.fileUrl = doc->url();
0425     } else if (matchFile.doc != doc) {
0426         qDebug() << "doc differences" << matchFile.fileUrl << doc->url();
0427         matchFile.doc = doc;
0428     }
0429 
0430     auto &matches = matchFile.matches;
0431 
0432     // Create a vector of moving ranges for updating the matches after replace
0433     QList<KTextEditor::MovingRange *> matchRanges;
0434     matchRanges.reserve(matches.size());
0435     for (const auto &match : qAsConst(matches)) {
0436         matchRanges.append(doc->newMovingRange(match.range));
0437     }
0438 
0439     // Make one transaction for the whole replace to speed up things
0440     // and get all replacements in one "undo"
0441     KTextEditor::Document::EditingTransaction transaction(doc);
0442 
0443     for (int i = 0; i < matches.size(); ++i) {
0444         if (matches[i].checked && matches[i].matchesFilter) {
0445             replaceMatch(doc, createIndex(i, 0, m_replaceFile), m_regExp, m_replaceText);
0446         }
0447         // The document has been modified -> make sure the next match has the correct range
0448         if (i < matches.size() - 1) {
0449             matches[i + 1].range = matchRanges[i + 1]->toRange();
0450         }
0451     }
0452 
0453     dataChanged(createIndex(0, 0, m_replaceFile), createIndex(matches.size() - 1, 0, m_replaceFile));
0454 
0455     // free our moving ranges
0456     qDeleteAll(matchRanges);
0457 
0458     m_replaceFile++;
0459     QTimer::singleShot(0, this, &MatchModel::doReplaceNextMatch);
0460 }
0461 
0462 /** Initiate a replace of all matches that have been checked */
0463 void MatchModel::replaceChecked(const QRegularExpression &regExp, const QString &replaceString)
0464 {
0465     Q_ASSERT(m_docManager != nullptr);
0466     if (m_replaceFile != -1) {
0467         Q_ASSERT(m_replaceFile != -1);
0468         return; // already replacing
0469     }
0470 
0471     m_replaceFile = 0;
0472     m_regExp = regExp;
0473     m_replaceText = replaceString;
0474     m_cancelReplace = false;
0475     doReplaceNextMatch();
0476 }
0477 
0478 void MatchModel::cancelReplace()
0479 {
0480     m_replaceFile = -1;
0481     m_cancelReplace = true;
0482 }
0483 
0484 void MatchModel::setFilterText(const QString &text)
0485 {
0486     m_filterText = text;
0487 }
0488 
0489 bool MatchModel::matchesFilter(const QModelIndex &index)
0490 {
0491     bool matches = true;
0492     if (!m_filterText.isEmpty()) {
0493         const QString text = index.data(MatchModel::PlainTextRole).toString();
0494         matches = text.contains(m_filterText, Qt::CaseInsensitive);
0495     }
0496 
0497     int fileRow = index.internalId() == InfoItemId ? -1 : index.internalId() == FileItemId ? index.row() : (int)index.internalId();
0498     int matchRow = index.internalId() == InfoItemId || index.internalId() == FileItemId ? -1 : index.row();
0499 
0500     if ((fileRow >= 0) && (fileRow < m_matchFiles.size()) && (matchRow >= 0) && (matchRow < m_matchFiles[fileRow].matches.size())) {
0501         // also match by filename
0502         if (matches == false) {
0503             const QString fileStr = fileToPlainText(m_matchFiles[fileRow]);
0504             matches = fileStr.contains(m_filterText, Qt::CaseInsensitive);
0505         }
0506         m_matchFiles[fileRow].matches[matchRow].matchesFilter = matches;
0507     }
0508 
0509     return matches;
0510 }
0511 
0512 static QString nbsFormated(int number, int width)
0513 {
0514     QString str = QString::number(number);
0515     int strWidth = str.size();
0516     str.reserve(width);
0517     while (strWidth < width) {
0518         str = QStringLiteral("&nbsp;") + str;
0519         strWidth++;
0520     }
0521     return str;
0522 }
0523 
0524 QString MatchModel::infoHtmlString() const
0525 {
0526     if (m_matchFiles.isEmpty() && m_searchState == SearchDone && m_lastMatchUrl.isEmpty()) {
0527         return QString();
0528     }
0529 
0530     int matchesTotal = 0;
0531     int checkedTotal = 0;
0532     for (const auto &matchFile : qAsConst(m_matchFiles)) {
0533         for (const auto &match : qAsConst(matchFile.matches)) {
0534             if (match.matchesFilter) {
0535                 matchesTotal++;
0536                 if (match.checked) {
0537                     checkedTotal++;
0538                 }
0539             }
0540         }
0541     }
0542 
0543     if (m_searchState == Preparing) {
0544         if (m_lastSearchPath.size() >= 73) {
0545             return i18n("<b><i>Generating file list: ...%1</i></b>", m_lastSearchPath.right(70).toHtmlEscaped());
0546         } else {
0547             return i18n("<b><i>Generating file list: ...%1</i></b>", m_lastSearchPath.toHtmlEscaped());
0548         }
0549     }
0550 
0551     if (m_searchState == Searching) {
0552         QString searchUrl = m_lastMatchUrl.toDisplayString(QUrl::PreferLocalFile);
0553 
0554         if (searchUrl.size() > 73) {
0555             return i18np("<b><i>One match found, searching: ...%2</i></b>",
0556                          "<b><i>%1 matches found, searching: ...%2</i></b>",
0557                          matchesTotal,
0558                          searchUrl.right(70).toHtmlEscaped());
0559         } else {
0560             return i18np("<b><i>One match found, searching: %2</i></b>",
0561                          "<b><i>%1 matches found, searching: %2</i></b>",
0562                          matchesTotal,
0563                          searchUrl.toHtmlEscaped());
0564         }
0565     }
0566 
0567     QString checkedStr = i18np("One checked", "%1 checked", checkedTotal);
0568 
0569     switch (m_searchPlace) {
0570     case CurrentFile:
0571         return i18np("<b><i>One match (%2) found in file</i></b>", "<b><i>%1 matches (%2) found in current file</i></b>", matchesTotal, checkedStr);
0572     case MatchModel::OpenFiles:
0573         return i18np("<b><i>One match (%2) found in open files</i></b>", "<b><i>%1 matches (%2) found in open files</i></b>", matchesTotal, checkedStr);
0574         break;
0575     case MatchModel::Folder:
0576         return i18np("<b><i>One match (%3) found in folder %2</i></b>",
0577                      "<b><i>%1 matches (%3) found in folder %2</i></b>",
0578                      matchesTotal,
0579                      m_resultBaseDir.toHtmlEscaped(),
0580                      checkedStr);
0581         break;
0582     case MatchModel::Project: {
0583         return i18np("<b><i>One match (%4) found in project %2 (%3)</i></b>",
0584                      "<b><i>%1 matches (%4) found in project %2 (%3)</i></b>",
0585                      matchesTotal,
0586                      m_projectName.toHtmlEscaped(),
0587                      m_resultBaseDir.toHtmlEscaped(),
0588                      checkedStr);
0589         break;
0590     }
0591     case MatchModel::AllProjects: // "in Open Projects"
0592         return i18np("<b><i>One match (%3) found in all open projects (common parent: %2)</i></b>",
0593                      "<b><i>%1 matches (%3) found in all open projects (common parent: %2)</i></b>",
0594                      matchesTotal,
0595                      m_resultBaseDir,
0596                      checkedStr);
0597         break;
0598     }
0599 
0600     return QString();
0601 }
0602 
0603 QString MatchModel::matchPath(const MatchFile &matchFile) const
0604 {
0605     QString path = matchFile.fileUrl.isLocalFile() ? localFileDirUp(matchFile.fileUrl).path() : matchFile.fileUrl.url();
0606     // make sure only to remove the leading part and not subsequent occurrences
0607     // also, if the basedir is root /, then do not strip that, as that would be more confusing
0608 
0609     // Add the trailing '/' to the path, if needed
0610     if (!path.isEmpty() && !path.endsWith(QLatin1Char('/'))) {
0611         path += QLatin1Char('/');
0612     }
0613     if (m_resultBaseDir.length() > 1 && path.startsWith(m_resultBaseDir)) {
0614         path = path.mid(m_resultBaseDir.length());
0615     }
0616     return path;
0617 }
0618 
0619 QString MatchModel::fileToHtmlString(const MatchFile &matchFile) const
0620 {
0621     if (matchFile.fileUrl.isEmpty() && matchFile.doc) {
0622         return matchFile.doc->documentName();
0623     }
0624 
0625     QString path = matchPath(matchFile);
0626     path = path.toHtmlEscaped();
0627     // dim the path color slightly
0628     const auto fgColor = QColor(m_foregroundColor);
0629     QString fg;
0630     if (fgColor.lightness() < 127) {
0631         fg = fgColor.lighter(150).name();
0632     } else {
0633         fg = fgColor.darker(150).name();
0634     }
0635     int filteredMatches = std::count_if(matchFile.matches.begin(), matchFile.matches.end(), [](const KateSearchMatch &match) {
0636         return match.matchesFilter;
0637     });
0638     QString tmpStr = QStringLiteral("<span style=\"color:%1;\">%2</span><b>%3: %4</b>")
0639                          .arg(fg)
0640                          .arg(path)
0641                          .arg(matchFile.fileUrl.fileName().toHtmlEscaped())
0642                          .arg(filteredMatches);
0643 
0644     return tmpStr;
0645 }
0646 
0647 QString MatchModel::matchToHtmlString(const Match &match) const
0648 {
0649     QString pre = match.preMatchStr;
0650     if (match.preMatchStr.size() == PreContextLen) {
0651         pre.replace(0, 3, QLatin1String("..."));
0652     }
0653     pre = pre.toHtmlEscaped();
0654 
0655     QString matchStr = match.matchStr.toHtmlEscaped();
0656     ;
0657 
0658     QString replaceStr = match.replaceText.toHtmlEscaped();
0659 
0660     if (!replaceStr.isEmpty()) {
0661         matchStr = QLatin1String("<i><s>") + matchStr + QLatin1String("</s></i> ");
0662     }
0663     matchStr = QStringLiteral("<span style=\"background-color:%1; color:%2;\">%3</span>").arg(m_searchBackgroundColor, m_foregroundColor, matchStr);
0664 
0665     if (!replaceStr.isEmpty()) {
0666         matchStr += QStringLiteral("<span style=\"background-color:%1; color:%2;\">%3</span>").arg(m_replaceHighlightColor, m_foregroundColor, replaceStr);
0667     }
0668 
0669     matchStr.replace(QLatin1Char('\n'), QStringLiteral("\\n"));
0670     matchStr.replace(QLatin1Char('\t'), QStringLiteral("\\t"));
0671 
0672     QString post = match.postMatchStr;
0673     int nlIndex = post.indexOf(QLatin1Char('\n'));
0674     if (nlIndex != -1) {
0675         post = post.mid(0, nlIndex);
0676     }
0677     if (post.size() == PostContextLen) {
0678         post.replace(PostContextLen - 3, 3, QLatin1String("..."));
0679     }
0680     post = post.toHtmlEscaped();
0681 
0682     // (line:col)[space][space] ...Line text pre [highlighted match] Line text post....
0683     QString displayText = QStringLiteral("%1:%2").arg(nbsFormated(match.range.start().line() + 1, 3)).arg(nbsFormated(match.range.start().column() + 1, 3))
0684         + pre + matchStr + post;
0685 
0686     return displayText;
0687 }
0688 
0689 QString MatchModel::infoToPlainText() const
0690 {
0691     if (m_matchFiles.isEmpty() && m_searchState == SearchDone) {
0692         return QString();
0693     }
0694 
0695     int matchesTotal = 0;
0696     int checkedTotal = 0;
0697     for (const auto &matchFile : qAsConst(m_matchFiles)) {
0698         matchesTotal += matchFile.matches.size();
0699         checkedTotal += std::count_if(matchFile.matches.begin(), matchFile.matches.end(), [](const KateSearchMatch &match) {
0700             return match.checked;
0701         });
0702     }
0703 
0704     if (m_searchState == Preparing) {
0705         if (m_lastSearchPath.size() >= 73) {
0706             return i18n("Generating file list: ...%1", m_lastSearchPath.right(70));
0707         } else {
0708             return i18n("Generating file list: ...%1", m_lastSearchPath);
0709         }
0710     }
0711 
0712     if (m_searchState == Searching) {
0713         QString searchUrl = m_lastMatchUrl.toDisplayString(QUrl::PreferLocalFile);
0714 
0715         if (searchUrl.size() > 73) {
0716             return i18np("One match found, searching: ...%2", "%1 matches found, searching: ...%2", matchesTotal, searchUrl.right(70));
0717         } else {
0718             return i18np("One match found, searching: %2", "%1 matches found, searching: %2", matchesTotal, searchUrl);
0719         }
0720     }
0721 
0722     QString checkedStr = i18np("One checked", "%1 checked", checkedTotal);
0723 
0724     switch (m_searchPlace) {
0725     case CurrentFile:
0726         return i18np("One match (%2) found in file", "%1 matches (%2) found in current file", matchesTotal, checkedStr);
0727     case MatchModel::OpenFiles:
0728         return i18np("One match (%2) found in open files", "%1 matches (%2) found in open files", matchesTotal, checkedStr);
0729         break;
0730     case MatchModel::Folder:
0731         return i18np("One match (%3) found in folder %2", "%1 matches (%3) found in folder %2", matchesTotal, m_resultBaseDir, checkedStr);
0732         break;
0733     case MatchModel::Project: {
0734         return i18np("One match (%4) found in project %2 (%3)",
0735                      "%1 matches (%4) found in project %2 (%3)",
0736                      matchesTotal,
0737                      m_projectName,
0738                      m_resultBaseDir,
0739                      checkedStr);
0740         break;
0741     }
0742     case MatchModel::AllProjects: // "in Open Projects"
0743         return i18np("One match (%3) found in all open projects (common parent: %2)",
0744                      "%1 matches (%3) found in all open projects (common parent: %2)",
0745                      matchesTotal,
0746                      m_resultBaseDir,
0747                      checkedStr);
0748         break;
0749     }
0750 
0751     return QString();
0752 }
0753 
0754 QString MatchModel::fileToPlainText(const MatchFile &matchFile) const
0755 {
0756     QString path = matchPath(matchFile);
0757 
0758     QString tmpStr = QStringLiteral("%1%2: %3").arg(path, matchFile.fileUrl.fileName()).arg(matchFile.matches.size());
0759 
0760     return tmpStr;
0761 }
0762 
0763 QString MatchModel::matchToPlainText(const Match &match)
0764 {
0765     QString pre = match.preMatchStr;
0766 
0767     QString matchStr = match.matchStr;
0768     matchStr.replace(QLatin1Char('\n'), QStringLiteral("\\n"));
0769 
0770     QString replaceStr = match.replaceText;
0771     if (!replaceStr.isEmpty()) {
0772         matchStr = QLatin1String("----") + matchStr + QLatin1String("----");
0773         matchStr += QLatin1String("++++") + replaceStr + QLatin1String("++++");
0774     }
0775     QString post = match.postMatchStr;
0776 
0777     matchStr.replace(QLatin1Char('\n'), QStringLiteral("\\n"));
0778     matchStr.replace(QLatin1Char('\t'), QStringLiteral("\\t"));
0779     replaceStr.replace(QLatin1Char('\n'), QStringLiteral("\\n"));
0780     replaceStr.replace(QLatin1Char('\t'), QStringLiteral("\\t"));
0781 
0782     // (line:col)[space][space] ...Line text pre [highlighted match] Line text post....
0783     QString displayText = QStringLiteral("%1:%2: ").arg(match.range.start().line() + 1, 3).arg(match.range.start().column() + 1, 3) + pre + matchStr + post;
0784     return displayText;
0785 }
0786 
0787 bool MatchModel::isMatch(const QModelIndex &itemIndex)
0788 {
0789     if (!itemIndex.isValid()) {
0790         return false;
0791     }
0792     if (itemIndex.internalId() == InfoItemId) {
0793         return false;
0794     }
0795     if (itemIndex.internalId() == FileItemId) {
0796         return false;
0797     }
0798 
0799     return true;
0800 }
0801 
0802 QModelIndex MatchModel::fileIndex(const QUrl &url, KTextEditor::Document *doc) const
0803 {
0804     int row = matchFileRow(url, doc);
0805     if (row == -1) {
0806         return QModelIndex();
0807     }
0808     return createIndex(row, 0, FileItemId);
0809 }
0810 
0811 QModelIndex MatchModel::firstMatch() const
0812 {
0813     if (m_matchFiles.isEmpty()) {
0814         return QModelIndex();
0815     }
0816 
0817     return createIndex(0, 0, static_cast<quintptr>(0));
0818 }
0819 
0820 QModelIndex MatchModel::lastMatch() const
0821 {
0822     if (m_matchFiles.isEmpty()) {
0823         return QModelIndex();
0824     }
0825     const MatchFile &matchFile = m_matchFiles.constLast();
0826     return createIndex(matchFile.matches.size() - 1, 0, m_matchFiles.size() - 1);
0827 }
0828 
0829 QModelIndex MatchModel::firstFileMatch(KTextEditor::Document *doc) const
0830 {
0831     int row = matchFileRow(doc->url(), doc);
0832     if (row == -1) {
0833         return QModelIndex();
0834     }
0835 
0836     // if a file is in the vector it has a match
0837     return createIndex(0, 0, row);
0838 }
0839 
0840 QModelIndex MatchModel::closestMatchAfter(KTextEditor::Document *doc, const KTextEditor::Cursor &cursor) const
0841 {
0842     int row = matchFileRow(doc->url(), doc);
0843     if (row < 0) {
0844         return QModelIndex();
0845     }
0846     if (row >= m_matchFiles.size()) {
0847         return QModelIndex();
0848     }
0849     if (!cursor.isValid()) {
0850         return QModelIndex();
0851     }
0852 
0853     // if a file is in the vector it has a match
0854     const MatchFile &matchFile = m_matchFiles[row];
0855 
0856     int i = 0;
0857     for (; i < matchFile.matches.size() - 1; ++i) {
0858         if (matchFile.matches[i].range.end() >= cursor) {
0859             break;
0860         }
0861     }
0862 
0863     return createIndex(i, 0, row);
0864 }
0865 
0866 QModelIndex MatchModel::closestMatchBefore(KTextEditor::Document *doc, const KTextEditor::Cursor &cursor) const
0867 {
0868     int row = matchFileRow(doc->url(), doc);
0869     if (row < 0) {
0870         return QModelIndex();
0871     }
0872     if (row >= m_matchFiles.size()) {
0873         return QModelIndex();
0874     }
0875     if (!cursor.isValid()) {
0876         return QModelIndex();
0877     }
0878 
0879     // if a file is in the vector it has a match
0880     const MatchFile &matchFile = m_matchFiles[row];
0881 
0882     int i = matchFile.matches.size() - 1;
0883     for (; i >= 0; --i) {
0884         if (matchFile.matches[i].range.start() <= cursor) {
0885             break;
0886         }
0887     }
0888 
0889     return createIndex(i, 0, row);
0890 }
0891 
0892 QModelIndex MatchModel::nextMatch(const QModelIndex &itemIndex) const
0893 {
0894     if (!itemIndex.isValid()) {
0895         return firstMatch();
0896     }
0897 
0898     int fileRow = itemIndex.internalId() < FileItemId ? itemIndex.internalId() : itemIndex.row();
0899     if (fileRow < 0 || fileRow >= m_matchFiles.size()) {
0900         return QModelIndex();
0901     }
0902 
0903     int matchRow = itemIndex.internalId() < FileItemId ? itemIndex.row() : 0;
0904     matchRow++;
0905     if (matchRow >= m_matchFiles[fileRow].matches.size()) {
0906         fileRow++;
0907         matchRow = 0;
0908     }
0909 
0910     if (fileRow >= m_matchFiles.size()) {
0911         fileRow = 0;
0912     }
0913     return createIndex(matchRow, 0, fileRow);
0914 }
0915 
0916 QModelIndex MatchModel::prevMatch(const QModelIndex &itemIndex) const
0917 {
0918     if (!itemIndex.isValid()) {
0919         return lastMatch();
0920     }
0921 
0922     int fileRow = itemIndex.internalId() < FileItemId ? itemIndex.internalId() : itemIndex.row();
0923     if (fileRow < 0 || fileRow >= m_matchFiles.size()) {
0924         return QModelIndex();
0925     }
0926 
0927     int matchRow = itemIndex.internalId() < FileItemId ? itemIndex.row() : 0;
0928     matchRow--;
0929     if (matchRow < 0) {
0930         fileRow--;
0931     }
0932     if (fileRow < 0) {
0933         fileRow = m_matchFiles.size() - 1;
0934     }
0935     if (matchRow < 0) {
0936         matchRow = m_matchFiles[fileRow].matches.size() - 1;
0937     }
0938     return createIndex(matchRow, 0, fileRow);
0939 }
0940 
0941 QVariant MatchModel::data(const QModelIndex &index, int role) const
0942 {
0943     if (!index.isValid()) {
0944         return QVariant();
0945     }
0946 
0947     if (index.column() < 0 || index.column() > 1) {
0948         return QVariant();
0949     }
0950 
0951     int fileRow = index.internalId() == InfoItemId ? -1 : index.internalId() == FileItemId ? index.row() : (int)index.internalId();
0952     int matchRow = index.internalId() == InfoItemId || index.internalId() == FileItemId ? -1 : index.row();
0953 
0954     if (fileRow == -1) {
0955         // Info Item
0956         switch (role) {
0957         case Qt::DisplayRole:
0958             return infoHtmlString();
0959         case PlainTextRole:
0960             return infoToPlainText();
0961         case Qt::CheckStateRole:
0962             return m_infoCheckState;
0963         }
0964         return QVariant();
0965     }
0966 
0967     if (fileRow < 0 || fileRow >= m_matchFiles.size()) {
0968         qDebug() << "Should be a file (or the info item in the near future)" << fileRow;
0969         return QVariant();
0970     }
0971 
0972     if (matchRow < 0) {
0973         // File item
0974         switch (role) {
0975         case Qt::DisplayRole:
0976             return fileToHtmlString(m_matchFiles[fileRow]);
0977         case Qt::CheckStateRole:
0978             return m_matchFiles[fileRow].checkState;
0979         case FileUrlRole:
0980             return m_matchFiles[fileRow].fileUrl;
0981         case PlainTextRole:
0982             return fileToPlainText(m_matchFiles[fileRow]);
0983         case LastMatchedRangeInFileRole:
0984             if (m_matchFiles[fileRow].matches.isEmpty()) {
0985                 qWarning() << "Unexpected empty matches for file!";
0986                 return {};
0987             }
0988             return QVariant::fromValue(m_matchFiles[fileRow].matches.constLast().range);
0989         }
0990     } else if (matchRow < m_matchFiles[fileRow].matches.size()) {
0991         // Match
0992         const Match &match = m_matchFiles[fileRow].matches[matchRow];
0993         switch (role) {
0994         case Qt::DisplayRole:
0995             return matchToHtmlString(match);
0996         case Qt::CheckStateRole:
0997             return match.checked ? Qt::Checked : Qt::Unchecked;
0998         case FileUrlRole:
0999             return m_matchFiles[fileRow].fileUrl;
1000         case DocumentRole:
1001             return QVariant::fromValue(m_matchFiles[fileRow].doc.data());
1002         case StartLineRole:
1003             return match.range.start().line();
1004         case StartColumnRole:
1005             return match.range.start().column();
1006         case EndLineRole:
1007             return match.range.end().line();
1008         case EndColumnRole:
1009             return match.range.end().column();
1010         case PreMatchRole:
1011             return match.preMatchStr;
1012         case MatchRole:
1013             return match.matchStr;
1014         case PostMatchRole:
1015             return match.postMatchStr;
1016         case ReplacedRole:
1017             return !match.replaceText.isEmpty();
1018         case ReplaceTextRole:
1019             return match.replaceText;
1020         case PlainTextRole:
1021             return matchToPlainText(match);
1022         case MatchItemRole:
1023             return QVariant::fromValue(match);
1024         case LastMatchedRangeInFileRole:
1025             qWarning() << "Requested last matched line from a match item instead of file item1";
1026             return {};
1027         }
1028     } else {
1029         qDebug() << "bad index";
1030         return QVariant();
1031     }
1032 
1033     return QVariant();
1034 }
1035 
1036 bool MatchModel::setFileChecked(int fileRow, bool checked)
1037 {
1038     if (fileRow < 0 || fileRow >= m_matchFiles.size()) {
1039         return false;
1040     }
1041     QList<Match> &matches = m_matchFiles[fileRow].matches;
1042     for (int i = 0; i < matches.size(); ++i) {
1043         matches[i].checked = checked;
1044     }
1045     m_matchFiles[fileRow].checkState = checked ? Qt::Checked : Qt::Unchecked;
1046     QModelIndex rootFileIndex = index(fileRow, 0, createIndex(0, 0, InfoItemId));
1047     dataChanged(index(0, 0, rootFileIndex), index(matches.count() - 1, 0, rootFileIndex), QList<int>{Qt::CheckStateRole});
1048     dataChanged(rootFileIndex, rootFileIndex, QList<int>{Qt::CheckStateRole});
1049     return true;
1050 }
1051 
1052 bool MatchModel::setData(const QModelIndex &itemIndex, const QVariant &, int role)
1053 {
1054     if (role != Qt::CheckStateRole) {
1055         return false;
1056     }
1057     if (!itemIndex.isValid()) {
1058         return false;
1059     }
1060     if (itemIndex.column() != 0) {
1061         return false;
1062     }
1063 
1064     // Check/un-check the File Item and it's children
1065     if (itemIndex.internalId() == InfoItemId) {
1066         bool checked = m_infoCheckState != Qt::Checked;
1067         for (int i = 0; i < m_matchFiles.size(); ++i) {
1068             setFileChecked(i, checked);
1069         }
1070         m_infoCheckState = checked ? Qt::Checked : Qt::Unchecked;
1071         QModelIndex infoIndex = createIndex(0, 0, InfoItemId);
1072         dataChanged(infoIndex, infoIndex, QList<int>{Qt::CheckStateRole});
1073         return true;
1074     }
1075 
1076     if (itemIndex.internalId() == FileItemId) {
1077         int fileRow = itemIndex.row();
1078         if (fileRow < 0 || fileRow >= m_matchFiles.size()) {
1079             return false;
1080         }
1081         bool checked = m_matchFiles[fileRow].checkState != Qt::Checked; // we toggle the current value
1082         setFileChecked(fileRow, checked);
1083 
1084         // compare file items
1085         Qt::CheckState checkState = m_matchFiles[0].checkState;
1086         for (int i = 1; i < m_matchFiles.size(); ++i) {
1087             if (checkState != m_matchFiles[i].checkState) {
1088                 checkState = Qt::PartiallyChecked;
1089                 break;
1090             }
1091         }
1092         m_infoCheckState = checkState;
1093         QModelIndex infoIndex = createIndex(0, 0, InfoItemId);
1094         dataChanged(infoIndex, infoIndex, QList<int>{Qt::CheckStateRole});
1095         return true;
1096     }
1097 
1098     int rootRow = itemIndex.internalId();
1099     if (rootRow < 0 || rootRow >= m_matchFiles.size()) {
1100         return false;
1101     }
1102 
1103     int row = itemIndex.row();
1104     QList<Match> &matches = m_matchFiles[rootRow].matches;
1105     if (row < 0 || row >= matches.size()) {
1106         return false;
1107     }
1108 
1109     // we toggle the current value
1110     matches[row].checked = !matches[row].checked;
1111 
1112     int checkedCount = std::count_if(matches.begin(), matches.end(), [](const KateSearchMatch &match) {
1113         return match.checked;
1114     });
1115 
1116     if (checkedCount == matches.size()) {
1117         m_matchFiles[rootRow].checkState = Qt::Checked;
1118     } else if (checkedCount == 0) {
1119         m_matchFiles[rootRow].checkState = Qt::Unchecked;
1120     } else {
1121         m_matchFiles[rootRow].checkState = Qt::PartiallyChecked;
1122     }
1123 
1124     QModelIndex rootFileIndex = index(rootRow, 0);
1125     dataChanged(rootFileIndex, rootFileIndex, QList<int>{Qt::CheckStateRole});
1126     dataChanged(index(row, 0, rootFileIndex), index(row, 0, rootFileIndex), QList<int>{Qt::CheckStateRole});
1127     return true;
1128 }
1129 
1130 void MatchModel::uncheckAll()
1131 {
1132     for (int i = 0; i < m_matchFiles.size(); ++i) {
1133         setFileChecked(i, false);
1134     }
1135     m_infoCheckState = Qt::Unchecked;
1136 }
1137 
1138 QString MatchModel::generateReplaceString(const QRegularExpressionMatch &match, const QString &replaceString)
1139 {
1140     // Modify the replace string according to this match
1141     QString replaceText = replaceString;
1142     replaceText.replace(QLatin1String("\\\\"), QLatin1String("¤Search&Replace¤"));
1143 
1144     // allow captures \0 .. \9
1145     for (int j = qMin(9, match.lastCapturedIndex()); j >= 0; --j) {
1146         QString captureLX = QStringLiteral("\\L\\%1").arg(j);
1147         QString captureUX = QStringLiteral("\\U\\%1").arg(j);
1148         QString captureX = QStringLiteral("\\%1").arg(j);
1149         QString captured = match.captured(j);
1150         captured.replace(QLatin1String("\\"), QLatin1String("¤Search&Replace¤"));
1151         replaceText.replace(captureLX, captured.toLower());
1152         replaceText.replace(captureUX, captured.toUpper());
1153         replaceText.replace(captureX, captured);
1154     }
1155 
1156     // allow captures \{0} .. \{9999999}...
1157     for (int j = match.lastCapturedIndex(); j >= 0; --j) {
1158         QString captureLX = QStringLiteral("\\L\\{%1}").arg(j);
1159         QString captureUX = QStringLiteral("\\U\\{%1}").arg(j);
1160         QString captureX = QStringLiteral("\\{%1}").arg(j);
1161         QString captured = match.captured(j);
1162         captured.replace(QLatin1String("\\"), QLatin1String("¤Search&Replace¤"));
1163         replaceText.replace(captureLX, captured.toLower());
1164         replaceText.replace(captureUX, captured.toUpper());
1165         replaceText.replace(captureX, captured);
1166     }
1167 
1168     replaceText.replace(QLatin1String("\\n"), QLatin1String("\n"));
1169     replaceText.replace(QLatin1String("\\t"), QLatin1String("\t"));
1170     replaceText.replace(QLatin1String("¤Search&Replace¤"), QLatin1String("\\"));
1171 
1172     return replaceText;
1173 }
1174 
1175 Qt::ItemFlags MatchModel::flags(const QModelIndex &index) const
1176 {
1177     if (!index.isValid()) {
1178         return Qt::NoItemFlags;
1179     }
1180 
1181     if (index.column() == 0) {
1182         return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
1183     }
1184 
1185     return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
1186 }
1187 
1188 int MatchModel::rowCount(const QModelIndex &parent) const
1189 {
1190     if (!parent.isValid()) {
1191         return (m_matchFiles.isEmpty() && m_searchState == SearchDone && m_lastMatchUrl.isEmpty()) ? 0 : 1;
1192     }
1193 
1194     if (parent.internalId() == InfoItemId) {
1195         return m_matchFiles.size();
1196     }
1197 
1198     if (parent.internalId() != FileItemId) {
1199         // matches do not have children
1200         return 0;
1201     }
1202 
1203     // If we get here parent.internalId() == FileItemId
1204     int row = parent.row();
1205     if (row < 0 || row >= m_matchFiles.size()) {
1206         return 0;
1207     }
1208 
1209     return m_matchFiles[row].matches.size();
1210 }
1211 
1212 int MatchModel::columnCount(const QModelIndex &) const
1213 {
1214     return 1;
1215 }
1216 
1217 QModelIndex MatchModel::index(int row, int column, const QModelIndex &parent) const
1218 {
1219     // Create the Info Item
1220     if (!parent.isValid()) {
1221         return createIndex(0, 0, InfoItemId);
1222     }
1223 
1224     // File Item
1225     if (parent.internalId() == InfoItemId) {
1226         return createIndex(row, column, FileItemId);
1227     }
1228 
1229     // Match Item
1230     if (parent.internalId() == FileItemId) {
1231         return createIndex(row, column, parent.row());
1232     }
1233 
1234     // Parent is a match which does not have children
1235     return QModelIndex();
1236 }
1237 
1238 QModelIndex MatchModel::parent(const QModelIndex &child) const
1239 {
1240     if (child.internalId() == InfoItemId) {
1241         return QModelIndex();
1242     }
1243 
1244     if (child.internalId() == FileItemId) {
1245         return createIndex(0, 0, InfoItemId);
1246     }
1247 
1248     return createIndex(child.internalId(), 0, FileItemId);
1249 }
1250 
1251 #include "moc_MatchModel.cpp"