diff --git a/outputview/outputmodel.cpp b/outputview/outputmodel.cpp index fe70067f74..dfad5d501e 100644 --- a/outputview/outputmodel.cpp +++ b/outputview/outputmodel.cpp @@ -1,414 +1,428 @@ /*************************************************************************** * This file is part of KDevelop * * Copyright 2007 Andreas Pakulat * * Copyright 2010 Aleix Pol Gonzalez * * Copyright (C) 2012 Morten Danielsen Volden mvolden2@gmail.com * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "outputmodel.h" #include "filtereditem.h" #include "outputfilteringstrategies.h" #include "debug.h" #include #include +#include #include #include #include #include #include #include +#include #include Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(KDevelop::IFilterStrategy*) namespace KDevelop { /** * Number of lines that are processed in one go before we notify the GUI thread * about the result. It is generally faster to add multiple items to a model * in one go compared to adding each item independently. */ static const int BATCH_SIZE = 50; /** * Time in ms that we wait in the parse worker for new incoming lines before * actually processing them. If we already have enough for one batch though * we process immediately. */ static const int BATCH_AGGREGATE_TIME_DELAY = 50; class ParseWorker : public QObject { Q_OBJECT public: ParseWorker() : QObject(0) - , m_filter( new NoFilterStrategy ) + , m_filter(new NoFilterStrategy) + , m_preFilterFunctions { + &KDevelop::stripAnsiSequences + } , m_timer(new QTimer(this)) { m_timer->setInterval(BATCH_AGGREGATE_TIME_DELAY); m_timer->setSingleShot(true); connect(m_timer, &QTimer::timeout, this, &ParseWorker::process); } public slots: void changeFilterStrategy( KDevelop::IFilterStrategy* newFilterStrategy ) { m_filter = QSharedPointer( newFilterStrategy ); } void addLines( const QStringList& lines ) { m_cachedLines << lines; if (m_cachedLines.size() >= BATCH_SIZE) { // if enough lines were added, process immediately m_timer->stop(); process(); } else if (!m_timer->isActive()) { m_timer->start(); } } void flushBuffers() { m_timer->stop(); process(); emit allDone(); } signals: void parsedBatch(const QVector& filteredItems); void allDone(); private slots: /** * Process *all* cached lines, emit parsedBatch for each batch */ void process() { QVector filteredItems; filteredItems.reserve(qMin(BATCH_SIZE, m_cachedLines.size())); + // apply pre-filtering functions + foreach (const auto filterFunction, m_preFilterFunctions) { + std::for_each(m_cachedLines.begin(), m_cachedLines.end(), [filterFunction](QString& in) { + in = filterFunction(in); + }); + } + + // apply filtering strategy foreach(const QString& line, m_cachedLines) { FilteredItem item = m_filter->errorInLine(line); if( item.type == FilteredItem::InvalidItem ) { item = m_filter->actionInLine(line); } filteredItems << item; if( filteredItems.size() == BATCH_SIZE ) { emit parsedBatch(filteredItems); filteredItems.clear(); filteredItems.reserve(qMin(BATCH_SIZE, m_cachedLines.size())); } } // Make sure to emit the rest as well if( !filteredItems.isEmpty() ) { emit parsedBatch(filteredItems); } m_cachedLines.clear(); } private: QSharedPointer m_filter; + QVector> m_preFilterFunctions; QStringList m_cachedLines; QTimer* m_timer; }; class ParsingThread { public: ParsingThread() { m_thread.setObjectName("OutputFilterThread"); } virtual ~ParsingThread() { if (m_thread.isRunning()) { m_thread.quit(); m_thread.wait(); } } void addWorker(ParseWorker* worker) { if (!m_thread.isRunning()) { m_thread.start(); } worker->moveToThread(&m_thread); } private: QThread m_thread; }; Q_GLOBAL_STATIC(ParsingThread, s_parsingThread); struct OutputModelPrivate { OutputModelPrivate( OutputModel* model, const QUrl& builddir = QUrl() ); ~OutputModelPrivate(); bool isValidIndex( const QModelIndex&, int currentRowCount ) const; OutputModel* model; ParseWorker* worker; QVector m_filteredItems; // We use std::set because that is ordered std::set m_errorItems; // Indices of all items that we want to move to using previous and next QUrl m_buildDir; void linesParsed(const QVector& items) { model->beginInsertRows( QModelIndex(), model->rowCount(), model->rowCount() + items.size() - 1); foreach( const FilteredItem& item, items ) { if( item.type == FilteredItem::ErrorItem ) { m_errorItems.insert(m_filteredItems.size()); } m_filteredItems << item; } model->endInsertRows(); } }; OutputModelPrivate::OutputModelPrivate( OutputModel* model_, const QUrl& builddir) : model(model_) , worker(new ParseWorker ) , m_buildDir( builddir ) { qRegisterMetaType >(); qRegisterMetaType(); s_parsingThread->addWorker(worker); model->connect(worker, &ParseWorker::parsedBatch, model, [=] (const QVector& items) { linesParsed(items); }); model->connect(worker, &ParseWorker::allDone, model, &OutputModel::allDone); } bool OutputModelPrivate::isValidIndex( const QModelIndex& idx, int currentRowCount ) const { return ( idx.isValid() && idx.row() >= 0 && idx.row() < currentRowCount && idx.column() == 0 ); } OutputModelPrivate::~OutputModelPrivate() { worker->deleteLater(); } OutputModel::OutputModel( const QUrl& builddir, QObject* parent ) : QAbstractListModel(parent) , d( new OutputModelPrivate( this, builddir ) ) { } OutputModel::OutputModel( QObject* parent ) : QAbstractListModel(parent) , d( new OutputModelPrivate( this ) ) { } OutputModel::~OutputModel() { delete d; } QVariant OutputModel::data(const QModelIndex& idx , int role ) const { if( d->isValidIndex(idx, rowCount()) ) { switch( role ) { case Qt::DisplayRole: return d->m_filteredItems.at( idx.row() ).shortenedText; break; case OutputModel::OutputItemTypeRole: return static_cast(d->m_filteredItems.at( idx.row() ).type); break; case Qt::FontRole: return QFontDatabase::systemFont(QFontDatabase::FixedFont); break; default: break; } } return QVariant(); } int OutputModel::rowCount( const QModelIndex& parent ) const { if( !parent.isValid() ) return d->m_filteredItems.count(); return 0; } QVariant OutputModel::headerData( int, Qt::Orientation, int ) const { return QVariant(); } void OutputModel::activate( const QModelIndex& index ) { if( index.model() != this || !d->isValidIndex(index, rowCount()) ) { return; } qCDebug(OUTPUTVIEW) << "Model activated" << index.row(); FilteredItem item = d->m_filteredItems.at( index.row() ); if( item.isActivatable ) { qCDebug(OUTPUTVIEW) << "activating:" << item.lineNo << item.url; KTextEditor::Cursor range( item.lineNo, item.columnNo ); KDevelop::IDocumentController *docCtrl = KDevelop::ICore::self()->documentController(); QUrl url = item.url; if (item.url.isEmpty()) { qWarning() << "trying to open empty url"; return; } if(url.isRelative()) { url = d->m_buildDir.resolved(url); } Q_ASSERT(!url.isRelative()); docCtrl->openDocument( url, range ); } else { qCDebug(OUTPUTVIEW) << "not an activateable item"; } } QModelIndex OutputModel::nextHighlightIndex( const QModelIndex ¤tIdx ) { int startrow = d->isValidIndex(currentIdx, rowCount()) ? currentIdx.row() + 1 : 0; if( !d->m_errorItems.empty() ) { qCDebug(OUTPUTVIEW) << "searching next error"; // Jump to the next error item std::set< int >::const_iterator next = d->m_errorItems.lower_bound( startrow ); if( next == d->m_errorItems.end() ) next = d->m_errorItems.begin(); return index( *next, 0, QModelIndex() ); } for( int row = 0; row < rowCount(); ++row ) { int currow = (startrow + row) % rowCount(); if( d->m_filteredItems.at( currow ).isActivatable ) { return index( currow, 0, QModelIndex() ); } } return QModelIndex(); } QModelIndex OutputModel::previousHighlightIndex( const QModelIndex ¤tIdx ) { //We have to ensure that startrow is >= rowCount - 1 to get a positive value from the % operation. int startrow = rowCount() + (d->isValidIndex(currentIdx, rowCount()) ? currentIdx.row() : rowCount()) - 1; if(!d->m_errorItems.empty()) { qCDebug(OUTPUTVIEW) << "searching previous error"; // Jump to the previous error item std::set< int >::const_iterator previous = d->m_errorItems.lower_bound( currentIdx.row() ); if( previous == d->m_errorItems.begin() ) previous = d->m_errorItems.end(); --previous; return index( *previous, 0, QModelIndex() ); } for ( int row = 0; row < rowCount(); ++row ) { int currow = (startrow - row) % rowCount(); if( d->m_filteredItems.at( currow ).isActivatable ) { return index( currow, 0, QModelIndex() ); } } return QModelIndex(); } void OutputModel::setFilteringStrategy(const OutputFilterStrategy& currentStrategy) { // TODO: Turn into factory, decouple from OutputModel IFilterStrategy* filter = 0; switch( currentStrategy ) { case NoFilter: filter = new NoFilterStrategy; break; case CompilerFilter: filter = new CompilerFilterStrategy( d->m_buildDir ); break; case ScriptErrorFilter: filter = new ScriptErrorFilterStrategy; break; case NativeAppErrorFilter: filter = new NativeAppErrorFilterStrategy; break; case StaticAnalysisFilter: filter = new StaticAnalysisFilterStrategy; break; default: // assert(false); filter = new NoFilterStrategy; break; } Q_ASSERT(filter); QMetaObject::invokeMethod(d->worker, "changeFilterStrategy", Q_ARG(KDevelop::IFilterStrategy*, filter)); } void OutputModel::appendLines( const QStringList& lines ) { if( lines.isEmpty() ) return; QMetaObject::invokeMethod(d->worker, "addLines", Q_ARG(QStringList, lines)); } void OutputModel::appendLine( const QString& l ) { appendLines( QStringList() << l ); } void OutputModel::ensureAllDone() { QMetaObject::invokeMethod(d->worker, "flushBuffers"); } } #include "outputmodel.moc" #include "moc_outputmodel.cpp" diff --git a/util/kdevstringhandler.cpp b/util/kdevstringhandler.cpp index 59d671fd3f..fbb5f64015 100644 --- a/util/kdevstringhandler.cpp +++ b/util/kdevstringhandler.cpp @@ -1,168 +1,231 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. This file mostly code takes from Qt's QSettings class, the copyright header from that file follows: **************************************************************************** ** ** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the QtCore module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial Usage ** Licensees holding valid Qt Commercial licenses may use this file in ** accordance with the Qt Commercial License Agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and Nokia. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain ** additional rights. These rights are described in the Nokia Qt LGPL ** Exception version 1.0, included in the file LGPL_EXCEPTION.txt in this ** package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3.0 as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU General Public License version 3.0 requirements will be ** met: http://www.gnu.org/copyleft/gpl.html. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at http://www.qtsoftware.com/contact. ** $QT_END_LICENSE$ ** **************************************************************************** */ #include "kdevstringhandler.h" #include #include #include #include #include #include #include namespace KDevelop { QString joinWithEscaping( const QStringList& input, const QChar& joinchar, const QChar& escapechar ) { QStringList tmp = input; return tmp.replaceInStrings( joinchar, QString( joinchar ) + QString( escapechar ) ).join( joinchar ); } QStringList splitWithEscaping( const QString& input, const QChar& splitchar, const QChar& escapechar ) { enum State { Normal, SeenEscape } state; state = Normal; QStringList result; QString currentstring; for( int i = 0; i < input.size(); i++ ) { switch( state ) { case Normal: if( input[i] == escapechar ) { state = SeenEscape; } else if( input[i] == splitchar ) { result << currentstring; currentstring = ""; } else { currentstring += input[i]; } break; case SeenEscape: currentstring += input[i]; state = Normal; break; } } if( !currentstring.isEmpty() ) { result << currentstring; } return result; } QVariant stringToQVariant(const QString& s) { // Taken from qsettings.cpp, stringToVariant() if (s.startsWith(QLatin1Char('@'))) { if (s.endsWith(QLatin1Char(')'))) { if (s.startsWith(QLatin1String("@Variant("))) { QByteArray a(s.toLatin1().mid(9)); QDataStream stream(&a, QIODevice::ReadOnly); stream.setVersion(QDataStream::Qt_4_4); QVariant result; stream >> result; return result; } } } return QVariant(); } QString qvariantToString(const QVariant& variant) { // Taken from qsettings.cpp, variantToString() QByteArray a; { QDataStream s(&a, QIODevice::WriteOnly); s.setVersion(QDataStream::Qt_4_4); s << variant; } QString result = QLatin1String("@Variant("); result += QString::fromLatin1(a.constData(), a.size()); result += QLatin1Char(')'); return result; } QString htmlToPlainText(const QString& s, HtmlToPlainTextMode mode) { switch (mode) { case FastMode: { QString result(s); result.remove(QRegExp("<[^>]+>")); return result; } case CompleteMode: { QTextDocument doc; doc.setHtml(s); return doc.toPlainText(); } } return QString(); // never reached } } +QString KDevelop::stripAnsiSequences(const QString& str) +{ + if (str.isEmpty()) { + return QString(); // fast path + } + + enum { + PLAIN, + ANSI_START, + ANSI_CSI, + ANSI_SEQUENCE, + ANSI_WAITING_FOR_ST, + ANSI_ST_STARTED + } state = PLAIN; + + QString result; + result.reserve(str.count()); + + foreach (const QChar c, str) { + const auto val = c.unicode(); + switch (state) { + case PLAIN: + if (val == 27) // 'ESC' + state = ANSI_START; + else if (val == 155) // equivalent to 'ESC'-'[' + state = ANSI_CSI; + else + result.append(c); + break; + case ANSI_START: + if (val == 91) // [ + state = ANSI_CSI; + else if (val == 80 || val == 93 || val == 94 || val == 95) // 'P', ']', '^' and '_' + state = ANSI_WAITING_FOR_ST; + else if (val >= 64 && val <= 95) + state = PLAIN; + else + state = ANSI_SEQUENCE; + break; + case ANSI_CSI: + if (val >= 64 && val <= 126) // Anything between '@' and '~' + state = PLAIN; + break; + case ANSI_SEQUENCE: + if (val >= 64 && val <= 95) // Anything between '@' and '_' + state = PLAIN; + break; + case ANSI_WAITING_FOR_ST: + if (val == 7) // 'BEL' + state = PLAIN; + else if (val == 27) // 'ESC' + state = ANSI_ST_STARTED; + break; + case ANSI_ST_STARTED: + if (val == 92) // '\' + state = PLAIN; + else + state = ANSI_WAITING_FOR_ST; + break; + } + } + return result; +} diff --git a/util/kdevstringhandler.h b/util/kdevstringhandler.h index 684798b27a..3dcce4592c 100644 --- a/util/kdevstringhandler.h +++ b/util/kdevstringhandler.h @@ -1,65 +1,71 @@ /* This file is part of KDevelop Copyright 2009 Andreas Pakulat This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_KDEVSTRINGHANDLER_H #define KDEVPLATFORM_KDEVSTRINGHANDLER_H #include "utilexport.h" class QString; class QChar; class QStringList; class QVariant; namespace KDevelop { KDEVPLATFORMUTIL_EXPORT QStringList splitWithEscaping( const QString& input, const QChar& splitChar, const QChar& escapeChar ); KDEVPLATFORMUTIL_EXPORT QString joinWithEscaping( const QStringList& input, const QChar& joinChar, const QChar& escapeChar ); /** * convert the @p variant into a string which can then be stored * easily in a KConfig entry. This supports any QVariant type (including custom types) * for which there is a QDataStream operator defined * @returns a QString containing the data from the QVariant. */ KDEVPLATFORMUTIL_EXPORT QString qvariantToString( const QVariant& variant ); /** * convert the @p s into a QVariant, usually the string is read from KConfig. * This supports any QVariant type (including custom types) * for which there is a QDataStream operator defined * @returns a QVariant created from the bytearray */ KDEVPLATFORMUTIL_EXPORT QVariant stringToQVariant( const QString& s ); enum HtmlToPlainTextMode { FastMode, /**< Fast (conversion via regular expression) */ CompleteMode, /**< Slower, but with expected behavior (conversion via QTextDocument::toPlainText). This also replaces
with newline chars, for example. */ }; /** * Strip HTML tags from string @p s * * @return String no longer containing any HTML tags */ KDEVPLATFORMUTIL_EXPORT QString htmlToPlainText(const QString& s, HtmlToPlainTextMode mode = FastMode); + + /** + * Strip ANSI sequences from string @p str + */ + KDEVPLATFORMUTIL_EXPORT QString stripAnsiSequences(const QString& str); + } #endif // KDEVPLATFORM_KDEVSTRINGHANDLER_H diff --git a/util/tests/test_stringhandler.cpp b/util/tests/test_stringhandler.cpp index a7854c78a4..e7c791d6f5 100644 --- a/util/tests/test_stringhandler.cpp +++ b/util/tests/test_stringhandler.cpp @@ -1,56 +1,75 @@ /* * Copyright 2014 Kevin Funk * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "test_stringhandler.h" #include "kdevstringhandler.h" #include QTEST_MAIN(TestStringHandler); using namespace KDevelop; Q_DECLARE_METATYPE(HtmlToPlainTextMode) void TestStringHandler::testHtmlToPlainText() { QFETCH(QString, html); QFETCH(HtmlToPlainTextMode, mode); QFETCH(QString, expectedPlainText); QString plainText = htmlToPlainText(html, mode); QCOMPARE(plainText, expectedPlainText); } void TestStringHandler::testHtmlToPlainText_data() { QTest::addColumn("html"); QTest::addColumn("mode"); QTest::addColumn("expectedPlainText"); QTest::newRow("simple-fast") << "

bar()

a
foo
" << KDevelop::FastMode << "bar() a foo"; QTest::newRow("simple-complete") << "

bar()

a
foo
" << KDevelop::CompleteMode << "bar() \na\nfoo"; } + +void TestStringHandler::testStripAnsiSequences() +{ + QFETCH(QString, input); + QFETCH(QString, expectedOutput); + + const auto output = stripAnsiSequences(input); + QCOMPARE(output, expectedOutput); +} + +void TestStringHandler::testStripAnsiSequences_data() +{ + QTest::addColumn("input"); + QTest::addColumn("expectedOutput"); + + QTest::newRow("simple") + << QStringLiteral("foo bar:") + << "foo bar:"; +} \ No newline at end of file diff --git a/util/tests/test_stringhandler.h b/util/tests/test_stringhandler.h index 5c1a07858b..4635fbf38a 100644 --- a/util/tests/test_stringhandler.h +++ b/util/tests/test_stringhandler.h @@ -1,37 +1,39 @@ /* * Copyright 2014 Kevin Funk * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #ifndef TESTSTRINGHANDLER_H #define TESTSTRINGHANDLER_H #include class TestStringHandler : public QObject { Q_OBJECT private Q_SLOTS: void testHtmlToPlainText(); void testHtmlToPlainText_data(); + void testStripAnsiSequences(); + void testStripAnsiSequences_data(); }; #endif // TESTSTRINGHANDLER_H