File indexing completed on 2024-04-28 04:37:01

0001 /*
0002     SPDX-FileCopyrightText: 2007 Andreas Pakulat <apaku@gmx.de>
0003     SPDX-FileCopyrightText: 2010 Aleix Pol Gonzalez <aleixpol@kde.org>
0004     SPDX-FileCopyrightText: 2012 Morten Danielsen Volden <mvolden2@gmail.com>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "outputmodel.h"
0010 #include "filtereditem.h"
0011 #include "outputfilteringstrategies.h"
0012 #include "debug.h"
0013 
0014 #include <interfaces/icore.h>
0015 #include <interfaces/idocumentcontroller.h>
0016 #include <util/kdevstringhandler.h>
0017 
0018 #include <QStringList>
0019 #include <QTimer>
0020 #include <QThread>
0021 #include <QFont>
0022 #include <QFontDatabase>
0023 
0024 #include <functional>
0025 #include <set>
0026 
0027 namespace KDevelop
0028 {
0029 
0030 /**
0031  * Number of lines that are processed in one go before we notify the GUI thread
0032  * about the result. It is generally faster to add multiple items to a model
0033  * in one go compared to adding each item independently.
0034  */
0035 static const int BATCH_SIZE = 50;
0036 
0037 /**
0038  * Time in ms that we wait in the parse worker for new incoming lines before
0039  * actually processing them. If we already have enough for one batch though
0040  * we process immediately.
0041  */
0042 static const int BATCH_AGGREGATE_TIME_DELAY = 50;
0043 
0044 class ParseWorker : public QObject
0045 {
0046     Q_OBJECT
0047 public:
0048     ParseWorker()
0049         : QObject(nullptr)
0050         , m_filter(new NoFilterStrategy)
0051         , m_timer(new QTimer(this))
0052     {
0053         m_timer->setInterval(BATCH_AGGREGATE_TIME_DELAY);
0054         m_timer->setSingleShot(true);
0055         connect(m_timer, &QTimer::timeout, this, &ParseWorker::process);
0056     }
0057 
0058 public Q_SLOTS:
0059     void changeFilterStrategy( KDevelop::IFilterStrategy* newFilterStrategy )
0060     {
0061         m_filter = QSharedPointer<IFilterStrategy>( newFilterStrategy );
0062     }
0063 
0064     void addLines( const QStringList& lines )
0065     {
0066         m_cachedLines << lines;
0067 
0068         if (m_cachedLines.size() >= BATCH_SIZE) {
0069             // if enough lines were added, process immediately
0070             m_timer->stop();
0071             process();
0072         } else if (!m_timer->isActive()) {
0073             m_timer->start();
0074         }
0075     }
0076 
0077     void flushBuffers()
0078     {
0079         m_timer->stop();
0080         process();
0081         emit allDone();
0082     }
0083 
0084 Q_SIGNALS:
0085     void parsedBatch(const QVector<KDevelop::FilteredItem>& filteredItems);
0086     void progress(const KDevelop::IFilterStrategy::Progress& progress);
0087     void allDone();
0088 
0089 private Q_SLOTS:
0090     /**
0091      * Process *all* cached lines, emit parsedBatch for each batch
0092      */
0093     void process()
0094     {
0095         QVector<KDevelop::FilteredItem> filteredItems;
0096         filteredItems.reserve(qMin(BATCH_SIZE, m_cachedLines.size()));
0097 
0098         // apply pre-filtering functions
0099         std::transform(m_cachedLines.constBegin(), m_cachedLines.constEnd(),
0100                        m_cachedLines.begin(), &KDevelop::stripAnsiSequences);
0101 
0102         // apply filtering strategy
0103         for (const QString& line : qAsConst(m_cachedLines)) {
0104             FilteredItem item = m_filter->errorInLine(line);
0105             if( item.type == FilteredItem::InvalidItem ) {
0106                 item = m_filter->actionInLine(line);
0107             }
0108 
0109             filteredItems << item;
0110 
0111             auto progress = m_filter->progressInLine(line);
0112             if (progress.percent >= 0 && m_progress.percent != progress.percent) {
0113                 m_progress = progress;
0114                 emit this->progress(m_progress);
0115             }
0116 
0117             if( filteredItems.size() == BATCH_SIZE ) {
0118                 emit parsedBatch(filteredItems);
0119                 filteredItems.clear();
0120                 filteredItems.reserve(qMin(BATCH_SIZE, m_cachedLines.size()));
0121             }
0122         }
0123 
0124         // Make sure to emit the rest as well
0125         if( !filteredItems.isEmpty() ) {
0126             emit parsedBatch(filteredItems);
0127         }
0128         m_cachedLines.clear();
0129     }
0130 
0131 private:
0132     QSharedPointer<IFilterStrategy> m_filter;
0133     QStringList m_cachedLines;
0134 
0135     QTimer* m_timer;
0136     IFilterStrategy::Progress m_progress;
0137 };
0138 
0139 class ParsingThread
0140 {
0141 public:
0142     ParsingThread()
0143     {
0144         m_thread.setObjectName(QStringLiteral("OutputFilterThread"));
0145     }
0146     virtual ~ParsingThread()
0147     {
0148         if (m_thread.isRunning()) {
0149             m_thread.quit();
0150             m_thread.wait();
0151         }
0152     }
0153     void addWorker(ParseWorker* worker)
0154     {
0155         if (!m_thread.isRunning()) {
0156             m_thread.start();
0157         }
0158         worker->moveToThread(&m_thread);
0159     }
0160 private:
0161     QThread m_thread;
0162 };
0163 
0164 Q_GLOBAL_STATIC(ParsingThread, s_parsingThread)
0165 
0166 class OutputModelPrivate
0167 {
0168 public:
0169     explicit OutputModelPrivate( OutputModel* model, const QUrl& builddir = QUrl() );
0170     ~OutputModelPrivate();
0171     bool isValidIndex( const QModelIndex&, int currentRowCount ) const;
0172 
0173     OutputModel* model;
0174     ParseWorker* worker;
0175 
0176     QVector<FilteredItem> m_filteredItems;
0177     // We use std::set because that is ordered
0178     std::set<int> m_errorItems; // Indices of all items that we want to move to using previous and next
0179     QUrl m_buildDir;
0180 
0181     void linesParsed(const QVector<KDevelop::FilteredItem>& items)
0182     {
0183         model->beginInsertRows( QModelIndex(), model->rowCount(), model->rowCount() + items.size() -  1);
0184 
0185         m_filteredItems.reserve(m_filteredItems.size() + items.size());
0186         for (const FilteredItem& item : items) {
0187             if( item.type == FilteredItem::ErrorItem ) {
0188                 m_errorItems.insert(m_filteredItems.size());
0189             }
0190             m_filteredItems << item;
0191         }
0192 
0193         model->endInsertRows();
0194     }
0195 };
0196 
0197 OutputModelPrivate::OutputModelPrivate( OutputModel* model_, const QUrl& builddir)
0198 : model(model_)
0199 , worker(new ParseWorker )
0200 , m_buildDir( builddir )
0201 {
0202     qRegisterMetaType<QVector<KDevelop::FilteredItem> >();
0203     qRegisterMetaType<KDevelop::IFilterStrategy*>();
0204     qRegisterMetaType<KDevelop::IFilterStrategy::Progress>();
0205 
0206     s_parsingThread->addWorker(worker);
0207     model->connect(worker, &ParseWorker::parsedBatch,
0208                    model, [=] (const QVector<KDevelop::FilteredItem>& items) { linesParsed(items); });
0209     model->connect(worker, &ParseWorker::allDone,
0210                    model, &OutputModel::allDone);
0211     model->connect(worker, &ParseWorker::progress,
0212                    model, &OutputModel::progress);
0213 }
0214 
0215 bool OutputModelPrivate::isValidIndex( const QModelIndex& idx, int currentRowCount ) const
0216 {
0217     return ( idx.isValid() && idx.row() >= 0 && idx.row() < currentRowCount && idx.column() == 0 );
0218 }
0219 
0220 OutputModelPrivate::~OutputModelPrivate()
0221 {
0222     worker->deleteLater();
0223 }
0224 
0225 OutputModel::OutputModel( const QUrl& builddir, QObject* parent )
0226 : QAbstractListModel(parent)
0227 , d_ptr(new OutputModelPrivate(this, builddir))
0228 {
0229 }
0230 
0231 OutputModel::OutputModel( QObject* parent )
0232     : QAbstractListModel(parent)
0233     , d_ptr(new OutputModelPrivate(this))
0234 {
0235 }
0236 
0237 OutputModel::~OutputModel() = default;
0238 
0239 QVariant OutputModel::data(const QModelIndex& idx , int role ) const
0240 {
0241     Q_D(const OutputModel);
0242 
0243     if( d->isValidIndex(idx, rowCount()) )
0244     {
0245         switch( role )
0246         {
0247             case Qt::DisplayRole:
0248                 return d->m_filteredItems.at( idx.row() ).originalLine;
0249             case OutputModel::OutputItemTypeRole:
0250                 return static_cast<int>(d->m_filteredItems.at( idx.row() ).type);
0251             case Qt::FontRole:
0252                 return QFontDatabase::systemFont(QFontDatabase::FixedFont);
0253         }
0254     }
0255     return QVariant();
0256 }
0257 
0258 int OutputModel::rowCount( const QModelIndex& parent ) const
0259 {
0260     Q_D(const OutputModel);
0261 
0262     if( !parent.isValid() )
0263         return d->m_filteredItems.count();
0264     return 0;
0265 }
0266 
0267 QVariant OutputModel::headerData( int, Qt::Orientation, int ) const
0268 {
0269     return QVariant();
0270 }
0271 
0272 void OutputModel::activate( const QModelIndex& index )
0273 {
0274     Q_D(OutputModel);
0275 
0276     if( index.model() != this || !d->isValidIndex(index, rowCount()) )
0277     {
0278         return;
0279     }
0280     qCDebug(OUTPUTVIEW) << "Model activated" << index.row();
0281 
0282 
0283     FilteredItem item = d->m_filteredItems.at( index.row() );
0284     if( item.isActivatable )
0285     {
0286         qCDebug(OUTPUTVIEW) << "activating:" << item.lineNo << item.url;
0287         KTextEditor::Cursor range( item.lineNo, item.columnNo );
0288         KDevelop::IDocumentController *docCtrl = KDevelop::ICore::self()->documentController();
0289         QUrl url = item.url;
0290         if (item.url.isEmpty()) {
0291             qCWarning(OUTPUTVIEW) << "trying to open empty url";
0292             return;
0293         }
0294         if(url.isRelative()) {
0295             url = d->m_buildDir.resolved(url);
0296         }
0297         Q_ASSERT(!url.isRelative());
0298         docCtrl->openDocument( url, range );
0299     } else {
0300         qCDebug(OUTPUTVIEW) << "not an activateable item";
0301     }
0302 }
0303 
0304 QModelIndex OutputModel::firstHighlightIndex()
0305 {
0306     Q_D(OutputModel);
0307 
0308     if( !d->m_errorItems.empty() ) {
0309         return index( *d->m_errorItems.begin(), 0, QModelIndex() );
0310     }
0311 
0312     for( int row = 0; row < rowCount(); ++row ) {
0313         if( d->m_filteredItems.at( row ).isActivatable ) {
0314             return index( row, 0, QModelIndex() );
0315         }
0316     }
0317 
0318     return QModelIndex();
0319 }
0320 
0321 QModelIndex OutputModel::nextHighlightIndex( const QModelIndex &currentIdx )
0322 {
0323     Q_D(OutputModel);
0324 
0325     int startrow = d->isValidIndex(currentIdx, rowCount()) ? currentIdx.row() + 1 : 0;
0326 
0327     if( !d->m_errorItems.empty() )
0328     {
0329         qCDebug(OUTPUTVIEW) << "searching next error";
0330         // Jump to the next error item
0331         auto next = d->m_errorItems.lower_bound( startrow );
0332         if( next == d->m_errorItems.end() )
0333             next = d->m_errorItems.begin();
0334 
0335         return index( *next, 0, QModelIndex() );
0336     }
0337 
0338     for( int row = 0; row < rowCount(); ++row )
0339     {
0340         int currow = (startrow + row) % rowCount();
0341         if( d->m_filteredItems.at( currow ).isActivatable )
0342         {
0343             return index( currow, 0, QModelIndex() );
0344         }
0345     }
0346     return QModelIndex();
0347 }
0348 
0349 QModelIndex OutputModel::previousHighlightIndex( const QModelIndex &currentIdx )
0350 {
0351     Q_D(OutputModel);
0352 
0353     //We have to ensure that startrow is >= rowCount - 1 to get a positive value from the % operation.
0354     int startrow = rowCount() + (d->isValidIndex(currentIdx, rowCount()) ? currentIdx.row() : rowCount()) - 1;
0355 
0356     if(!d->m_errorItems.empty())
0357     {
0358         qCDebug(OUTPUTVIEW) << "searching previous error";
0359 
0360         // Jump to the previous error item
0361         auto previous = d->m_errorItems.lower_bound( currentIdx.row() );
0362 
0363         if( previous == d->m_errorItems.begin() )
0364             previous = d->m_errorItems.end();
0365 
0366         --previous;
0367 
0368         return index( *previous, 0, QModelIndex() );
0369     }
0370 
0371     for ( int row = 0; row < rowCount(); ++row )
0372     {
0373         int currow = (startrow - row) % rowCount();
0374         if( d->m_filteredItems.at( currow ).isActivatable )
0375         {
0376             return index( currow, 0, QModelIndex() );
0377         }
0378     }
0379     return QModelIndex();
0380 }
0381 
0382 QModelIndex OutputModel::lastHighlightIndex()
0383 {
0384     Q_D(OutputModel);
0385 
0386     if( !d->m_errorItems.empty() ) {
0387         return index( *d->m_errorItems.rbegin(), 0, QModelIndex() );
0388     }
0389 
0390     for( int row = rowCount()-1; row >=0; --row ) {
0391         if( d->m_filteredItems.at( row ).isActivatable ) {
0392             return index( row, 0, QModelIndex() );
0393         }
0394     }
0395 
0396     return QModelIndex();
0397 }
0398 
0399 void OutputModel::setFilteringStrategy(const OutputFilterStrategy& currentStrategy)
0400 {
0401     Q_D(OutputModel);
0402 
0403     // TODO: Turn into factory, decouple from OutputModel
0404     IFilterStrategy* filter = nullptr;
0405     switch( currentStrategy )
0406     {
0407         case NoFilter:
0408             filter = new NoFilterStrategy;
0409             break;
0410         case CompilerFilter:
0411             filter = new CompilerFilterStrategy( d->m_buildDir );
0412             break;
0413         case ScriptErrorFilter:
0414             filter = new ScriptErrorFilterStrategy;
0415             break;
0416         case NativeAppErrorFilter:
0417             filter = new NativeAppErrorFilterStrategy;
0418             break;
0419         case StaticAnalysisFilter:
0420             filter = new StaticAnalysisFilterStrategy;
0421             break;
0422     }
0423     if (!filter) {
0424         filter = new NoFilterStrategy;
0425     }
0426 
0427     QMetaObject::invokeMethod(d->worker, "changeFilterStrategy",
0428                               Q_ARG(KDevelop::IFilterStrategy*, filter));
0429 }
0430 
0431 void OutputModel::setFilteringStrategy(IFilterStrategy* filterStrategy)
0432 {
0433     Q_D(OutputModel);
0434 
0435     QMetaObject::invokeMethod(d->worker, "changeFilterStrategy",
0436                               Q_ARG(KDevelop::IFilterStrategy*, filterStrategy));
0437 }
0438 
0439 void OutputModel::appendLines( const QStringList& lines )
0440 {
0441     Q_D(OutputModel);
0442 
0443     if( lines.isEmpty() )
0444         return;
0445 
0446     QMetaObject::invokeMethod(d->worker, "addLines",
0447                               Q_ARG(QStringList, lines));
0448 }
0449 
0450 void OutputModel::appendLine( const QString& line )
0451 {
0452     appendLines( QStringList() << line );
0453 }
0454 
0455 void OutputModel::ensureAllDone()
0456 {
0457     Q_D(OutputModel);
0458 
0459     QMetaObject::invokeMethod(d->worker, "flushBuffers");
0460 }
0461 
0462 void OutputModel::clear()
0463 {
0464     Q_D(OutputModel);
0465 
0466     ensureAllDone();
0467     beginResetModel();
0468     d->m_filteredItems.clear();
0469     endResetModel();
0470 }
0471 
0472 }
0473 
0474 #include "outputmodel.moc"
0475 #include "moc_outputmodel.cpp"