File indexing completed on 2024-04-28 04:38:53
0001 /* 0002 SPDX-FileCopyrightText: 1999-2001 Bernd Gehrmann <bernd@kdevelop.org> 0003 SPDX-FileCopyrightText: 1999-2001 the KDevelop Team 0004 SPDX-FileCopyrightText: 2007 Dukju Ahn <dukjuahn@gmail.com> 0005 SPDX-FileCopyrightText: 2010 Silvère Lestang <silvere.lestang@gmail.com> 0006 SPDX-FileCopyrightText: 2010 Julien Desgats <julien.desgats@gmail.com> 0007 0008 SPDX-License-Identifier: GPL-2.0-or-later 0009 */ 0010 0011 #include "grepoutputmodel.h" 0012 #include "grepviewplugin.h" 0013 #include "greputil.h" 0014 0015 #include <interfaces/icore.h> 0016 #include <interfaces/idocumentcontroller.h> 0017 #include <interfaces/iprojectcontroller.h> 0018 0019 #include <KTextEditor/Cursor> 0020 #include <KTextEditor/Document> 0021 #include <KLocalizedString> 0022 0023 #include <QModelIndex> 0024 #include <QFontDatabase> 0025 0026 0027 using namespace KDevelop; 0028 0029 GrepOutputItem::GrepOutputItem(const DocumentChangePointer& change, const QString &text, bool checkable) 0030 : QStandardItem(), m_change(change) 0031 { 0032 setText(text); 0033 setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); 0034 0035 setCheckable(checkable); 0036 if(checkable) 0037 setCheckState(Qt::Checked); 0038 } 0039 0040 GrepOutputItem::GrepOutputItem(const QString& filename, const QString& text, bool checkable) 0041 : QStandardItem(), m_change(new DocumentChange(IndexedString(filename), KTextEditor::Range::invalid(), QString(), QString())) 0042 { 0043 setText(text); 0044 setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); 0045 setCheckable(checkable); 0046 if(checkable) 0047 { 0048 setAutoTristate(true); 0049 setCheckState(Qt::Checked); 0050 } 0051 } 0052 0053 int GrepOutputItem::lineNumber() const 0054 { 0055 // line starts at 0 for cursor but we want to start at 1 0056 return m_change->m_range.start().line() + 1; 0057 } 0058 0059 QString GrepOutputItem::filename() const 0060 { 0061 return m_change->m_document.str(); 0062 } 0063 0064 DocumentChangePointer GrepOutputItem::change() const 0065 { 0066 return m_change; 0067 } 0068 0069 bool GrepOutputItem::isText() const 0070 { 0071 return m_change->m_range.isValid(); 0072 } 0073 0074 void GrepOutputItem::propagateState() 0075 { 0076 for(int i = 0; i < rowCount(); i++) 0077 { 0078 auto *item = static_cast<GrepOutputItem *>(child(i)); 0079 if(item->isEnabled()) 0080 { 0081 item->setCheckState(checkState()); 0082 item->propagateState(); 0083 } 0084 } 0085 } 0086 0087 void GrepOutputItem::refreshState() 0088 { 0089 if(rowCount() > 0) 0090 { 0091 int checked = 0; 0092 int unchecked = 0; 0093 int enabled = 0; //only enabled items are relevants 0094 0095 for(int i = 0; i < rowCount(); i++) 0096 { 0097 QStandardItem *item = child(i); 0098 if(item->isEnabled()) 0099 { 0100 enabled += 1; 0101 switch(child(i)->checkState()) 0102 { 0103 case Qt::Checked: 0104 checked += 1; 0105 break; 0106 case Qt::Unchecked: 0107 unchecked += 1; 0108 break; 0109 default: break; 0110 } 0111 } 0112 } 0113 0114 if(enabled == 0) 0115 { 0116 setCheckState(Qt::Unchecked); 0117 setEnabled(false); 0118 } 0119 else if(checked == enabled) 0120 { 0121 setCheckState(Qt::Checked); 0122 } 0123 else if (unchecked == enabled) 0124 { 0125 setCheckState(Qt::Unchecked); 0126 } 0127 else 0128 { 0129 setCheckState(Qt::PartiallyChecked); 0130 } 0131 } 0132 0133 if(auto *p = static_cast<GrepOutputItem *>(parent())) 0134 { 0135 p->refreshState(); 0136 } 0137 } 0138 0139 QVariant GrepOutputItem::data ( int role ) const { 0140 auto *grepModel = static_cast<GrepOutputModel *>(model()); 0141 if(role == Qt::ToolTipRole && grepModel && isText()) 0142 { 0143 QString start = text().left(m_change->m_range.start().column()).toHtmlEscaped(); 0144 // show replaced version in tooltip if we are in replace mode 0145 const QString match = isCheckable() ? grepModel->replacementFor(m_change->m_oldText) : m_change->m_oldText; 0146 const QString repl = QLatin1String("<b>") + match.toHtmlEscaped() + QLatin1String("</b>"); 0147 QString end = text().mid(m_change->m_range.end().column()).toHtmlEscaped(); 0148 const QString toolTip = QLatin1String("<span style=\"white-space:nowrap\">") + QString(start + repl + end).trimmed() + QLatin1String("</span>"); 0149 return toolTip; 0150 } else if (role == Qt::FontRole) { 0151 return QFontDatabase::systemFont(QFontDatabase::FixedFont); 0152 } else { 0153 return QStandardItem::data(role); 0154 } 0155 } 0156 0157 GrepOutputItem::~GrepOutputItem() 0158 {} 0159 0160 /////////////////////////////////////////////////////////////// 0161 0162 GrepOutputModel::GrepOutputModel( QObject *parent ) 0163 : QStandardItemModel( parent ) 0164 { 0165 connect(this, &GrepOutputModel::itemChanged, 0166 this, &GrepOutputModel::updateCheckState); 0167 } 0168 0169 GrepOutputModel::~GrepOutputModel() 0170 {} 0171 0172 void GrepOutputModel::clear() 0173 { 0174 QStandardItemModel::clear(); 0175 // the above clear() also destroys the root item, so invalidate the pointer 0176 m_rootItem = nullptr; 0177 0178 m_fileCount = 0; 0179 m_matchCount = 0; 0180 } 0181 0182 void GrepOutputModel::setRegExp(const QRegExp& re) 0183 { 0184 m_regExp = re; 0185 m_finalUpToDate = false; 0186 } 0187 0188 void GrepOutputModel::setReplacement(const QString& repl) 0189 { 0190 m_replacement = repl; 0191 m_finalUpToDate = false; 0192 } 0193 0194 void GrepOutputModel::setReplacementTemplate(const QString& tmpl) 0195 { 0196 m_replacementTemplate = tmpl; 0197 m_finalUpToDate = false; 0198 } 0199 0200 QString GrepOutputModel::replacementFor(const QString &text) 0201 { 0202 if(!m_finalUpToDate) 0203 { 0204 m_finalReplacement = substitudePattern(m_replacementTemplate, m_replacement); 0205 m_finalUpToDate = true; 0206 } 0207 return QString(text).replace(m_regExp, m_finalReplacement); 0208 } 0209 0210 void GrepOutputModel::activate( const QModelIndex &idx ) 0211 { 0212 QStandardItem *stditem = itemFromIndex(idx); 0213 auto *grepitem = dynamic_cast<GrepOutputItem*>(stditem); 0214 if( !grepitem || !grepitem->isText() ) 0215 return; 0216 0217 QUrl url = QUrl::fromLocalFile(grepitem->filename()); 0218 0219 int line = grepitem->lineNumber() - 1; 0220 KTextEditor::Range range( line, 0, line+1, 0); 0221 0222 // Try to find the actual text range we found during the grep 0223 IDocument* doc = ICore::self()->documentController()->documentForUrl( url ); 0224 if(!doc) 0225 doc = ICore::self()->documentController()->openDocument( url, range ); 0226 if(!doc) 0227 return; 0228 if (KTextEditor::Document* tdoc = doc->textDocument()) { 0229 KTextEditor::Range matchRange = grepitem->change()->m_range; 0230 QString actualText = tdoc->text(matchRange); 0231 QString expectedText = grepitem->change()->m_oldText; 0232 if (actualText == expectedText) { 0233 range = matchRange; 0234 } 0235 } 0236 0237 ICore::self()->documentController()->activateDocument( doc, range ); 0238 } 0239 0240 QModelIndex GrepOutputModel::previousItemIndex(const QModelIndex ¤tIdx) const 0241 { 0242 GrepOutputItem* current_item = nullptr; 0243 0244 if (!currentIdx.isValid()) { 0245 // no item selected, search recursively for the last item in search results 0246 QStandardItem *it = item(0,0); 0247 while (it) { 0248 QStandardItem *child = it->child( it->rowCount() - 1 ); 0249 if (!child) return it->index(); 0250 it = child; 0251 } 0252 return QModelIndex(); 0253 } 0254 else 0255 current_item = static_cast<GrepOutputItem*>(itemFromIndex(currentIdx)); 0256 0257 if (current_item->parent() != nullptr) { 0258 int row = currentIdx.row(); 0259 0260 if(!current_item->isText()) // the item is a file 0261 { 0262 int item_row = current_item->row(); 0263 if(item_row > 0) 0264 { 0265 int idx_last_item = current_item->parent()->child(item_row - 1)->rowCount() - 1; 0266 return current_item->parent()->child(item_row - 1)->child(idx_last_item)->index(); 0267 } 0268 } 0269 else // the item is a match 0270 { 0271 if(row > 0) 0272 return current_item->parent()->child(row - 1)->index(); 0273 else // we return the index of the last item of the previous file 0274 { 0275 int parrent_row = current_item->parent()->row(); 0276 if(parrent_row > 0) 0277 { 0278 int idx_last_item = current_item->parent()->parent()->child(parrent_row - 1)->rowCount() - 1; 0279 return current_item->parent()->parent()->child(parrent_row - 1)->child(idx_last_item)->index(); 0280 } 0281 } 0282 } 0283 } 0284 return currentIdx; 0285 } 0286 0287 QModelIndex GrepOutputModel::nextItemIndex(const QModelIndex ¤tIdx) const 0288 { 0289 GrepOutputItem* current_item = nullptr; 0290 0291 if (!currentIdx.isValid()) { 0292 QStandardItem *it = item(0,0); 0293 if (!it) return QModelIndex(); 0294 current_item = static_cast<GrepOutputItem*>(it); 0295 } 0296 else 0297 current_item = static_cast<GrepOutputItem*>(itemFromIndex(currentIdx)); 0298 0299 if (current_item->parent() == nullptr) { 0300 // root item with overview of search results 0301 if (current_item->rowCount() > 0) 0302 return nextItemIndex(current_item->child(0)->index()); 0303 else 0304 return QModelIndex(); 0305 } else { 0306 int row = currentIdx.row(); 0307 if(!current_item->isText()) // the item is a file 0308 { 0309 int item_row = current_item->row(); 0310 if(item_row < current_item->parent()->rowCount()) 0311 { 0312 return current_item->parent()->child(item_row)->child(0)->index(); 0313 } 0314 } 0315 else // the item is a match 0316 { 0317 if(row < current_item->parent()->rowCount() - 1) 0318 return current_item->parent()->child(row + 1)->index(); 0319 else // we return the index of the first item of the next file 0320 { 0321 int parrent_row = current_item->parent()->row(); 0322 if(parrent_row < current_item->parent()->parent()->rowCount() - 1) 0323 { 0324 return current_item->parent()->parent()->child(parrent_row + 1)->child(0)->index(); 0325 } 0326 } 0327 } 0328 } 0329 return currentIdx; 0330 } 0331 0332 const GrepOutputItem *GrepOutputModel::getRootItem() const { 0333 return m_rootItem; 0334 } 0335 0336 bool GrepOutputModel::itemsCheckable() const 0337 { 0338 return m_itemsCheckable; 0339 } 0340 0341 void GrepOutputModel::makeItemsCheckable(bool checkable) 0342 { 0343 if(m_itemsCheckable == checkable) 0344 return; 0345 if(m_rootItem) 0346 makeItemsCheckable(checkable, m_rootItem); 0347 m_itemsCheckable = checkable; 0348 } 0349 0350 void GrepOutputModel::makeItemsCheckable(bool checkable, GrepOutputItem* item) 0351 { 0352 item->setCheckable(checkable); 0353 if(checkable) 0354 { 0355 item->setCheckState(Qt::Checked); 0356 if(item->rowCount() && checkable) 0357 item->setAutoTristate(true); 0358 } 0359 for(int row = 0; row < item->rowCount(); ++row) 0360 makeItemsCheckable(checkable, static_cast<GrepOutputItem*>(item->child(row, 0))); 0361 } 0362 0363 void GrepOutputModel::appendOutputs( const QString &filename, const GrepOutputItem::List &items ) 0364 { 0365 if(items.isEmpty()) 0366 return; 0367 0368 if(rowCount() == 0) 0369 { 0370 m_rootItem = new GrepOutputItem(QString(), QString(), m_itemsCheckable); 0371 appendRow(m_rootItem); 0372 } 0373 0374 m_fileCount += 1; 0375 m_matchCount += items.length(); 0376 0377 const QString matchText = i18np("<b>1</b> match", "<b>%1</b> matches", m_matchCount); 0378 const QString fileText = i18np("<b>1</b> file", "<b>%1</b> files", m_fileCount); 0379 0380 m_rootItem->setText(i18nc("%1 is e.g. '4 matches', %2 is e.g. '1 file'", "<b>%1 in %2</b>", matchText, fileText)); 0381 0382 QString fnString = i18np("%2: 1 match", "%2: %1 matches", 0383 items.length(), ICore::self()->projectController()->prettyFileName(QUrl::fromLocalFile(filename))); 0384 0385 auto *fileItem = new GrepOutputItem(filename, fnString, m_itemsCheckable); 0386 m_rootItem->appendRow(fileItem); 0387 for (const GrepOutputItem& item : items) { 0388 auto* copy = new GrepOutputItem(item); 0389 copy->setCheckable(m_itemsCheckable); 0390 if(m_itemsCheckable) 0391 { 0392 copy->setCheckState(Qt::Checked); 0393 if(copy->rowCount()) 0394 copy->setAutoTristate(true); 0395 } 0396 0397 fileItem->appendRow(copy); 0398 } 0399 } 0400 0401 void GrepOutputModel::updateCheckState(QStandardItem* item) 0402 { 0403 // if we don't disconnect the SIGNAL, the setCheckState will call it in loop 0404 disconnect(this, &GrepOutputModel::itemChanged, nullptr, nullptr); 0405 0406 // try to update checkstate on non checkable items would make a checkbox appear 0407 if(item->isCheckable()) 0408 { 0409 auto *it = static_cast<GrepOutputItem *>(item); 0410 it->propagateState(); 0411 it->refreshState(); 0412 } 0413 0414 connect(this, &GrepOutputModel::itemChanged, 0415 this, &GrepOutputModel::updateCheckState); 0416 } 0417 0418 void GrepOutputModel::doReplacements() 0419 { 0420 Q_ASSERT(m_rootItem); 0421 if (!m_rootItem) 0422 return; // nothing to do, abort 0423 0424 DocumentChangeSet changeSet; 0425 changeSet.setFormatPolicy(DocumentChangeSet::NoAutoFormat); 0426 for(int fileRow = 0; fileRow < m_rootItem->rowCount(); fileRow++) 0427 { 0428 auto *file = static_cast<GrepOutputItem *>(m_rootItem->child(fileRow)); 0429 0430 for(int matchRow = 0; matchRow < file->rowCount(); matchRow++) 0431 { 0432 auto *match = static_cast<GrepOutputItem *>(file->child(matchRow)); 0433 if(match->checkState() == Qt::Checked) 0434 { 0435 DocumentChangePointer change = match->change(); 0436 // setting replacement text based on current replace value 0437 change->m_newText = replacementFor(change->m_oldText); 0438 changeSet.addChange(change); 0439 // this item cannot be checked anymore 0440 match->setCheckState(Qt::Unchecked); 0441 match->setEnabled(false); 0442 } 0443 } 0444 } 0445 0446 DocumentChangeSet::ChangeResult result = changeSet.applyAllChanges(); 0447 if(!result.m_success) 0448 { 0449 DocumentChangePointer ch = result.m_reasonChange; 0450 if(ch) 0451 emit showErrorMessage(i18nc("%1 is the old text, %2 is the new text, %3 is the file path, %4 and %5 are its row and column", 0452 "Failed to replace <b>%1</b> by <b>%2</b> in %3:%4:%5", 0453 ch->m_oldText.toHtmlEscaped(), ch->m_newText.toHtmlEscaped(), ch->m_document.toUrl().toLocalFile(), 0454 ch->m_range.start().line() + 1, ch->m_range.start().column() + 1)); 0455 } 0456 } 0457 0458 void GrepOutputModel::showMessageSlot(IStatus* status, const QString& message) 0459 { 0460 m_savedMessage = message; 0461 m_savedIStatus = status; 0462 showMessageEmit(); 0463 } 0464 0465 void GrepOutputModel::showMessageEmit() 0466 { 0467 emit showMessage(m_savedIStatus, m_savedMessage); 0468 } 0469 0470 bool GrepOutputModel::hasResults() 0471 { 0472 return(m_matchCount > 0); 0473 } 0474 0475 #include "moc_grepoutputmodel.cpp"