File indexing completed on 2024-05-12 09:55:21
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 ®Exp, 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 ®Exp, 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 ®Exp, 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(" ") + 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"