File indexing completed on 2024-05-12 16:10:30
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 ¤tIdx ) 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 ¤tIdx ) 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"