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 &currentIdx) 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 &currentIdx) 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"