diff options
Diffstat (limited to 'src/plugins/coreplugin/outputwindow.cpp')
-rw-r--r-- | src/plugins/coreplugin/outputwindow.cpp | 335 |
1 files changed, 198 insertions, 137 deletions
diff --git a/src/plugins/coreplugin/outputwindow.cpp b/src/plugins/coreplugin/outputwindow.cpp index 149c7189fd..7c05ee6630 100644 --- a/src/plugins/coreplugin/outputwindow.cpp +++ b/src/plugins/coreplugin/outputwindow.cpp @@ -26,19 +26,31 @@ #include "outputwindow.h" #include "actionmanager/actionmanager.h" +#include "editormanager/editormanager.h" #include "coreconstants.h" +#include "coreplugin.h" #include "icore.h" #include <utils/outputformatter.h> -#include <utils/synchronousprocess.h> +#include <utils/qtcassert.h> #include <QAction> #include <QCursor> +#include <QElapsedTimer> #include <QMimeData> #include <QPointer> #include <QRegularExpression> #include <QScrollBar> #include <QTextBlock> +#include <QTimer> + +#ifdef WITH_TESTS +#include <QtTest> +#endif + +#include <numeric> + +const int chunkSize = 10000; using namespace Utils; @@ -54,18 +66,12 @@ public: { } - ~OutputWindowPrivate() - { - ICore::removeContextObject(outputWindowContext); - delete outputWindowContext; - } - - IContext *outputWindowContext = nullptr; - QPointer<Utils::OutputFormatter> formatter; QString settingsKey; + OutputFormatter formatter; + QList<QPair<QString, OutputFormat>> queuedOutput; + QTimer queueTimer; - bool enforceNewline = false; - bool prependCarriageReturn = false; + bool flushRequested = false; bool scrollToBottom = true; bool linksActive = true; bool zoomEnabled = false; @@ -78,6 +84,8 @@ public: int lastFilteredBlockNumber = -1; QPalette originalPalette; OutputWindow::FilterModeFlags filterMode = OutputWindow::FilterModeFlag::Default; + QTimer scrollTimer; + QElapsedTimer lastMessage; }; } // namespace Internal @@ -93,13 +101,18 @@ OutputWindow::OutputWindow(Context context, const QString &settingsKey, QWidget setFrameShape(QFrame::NoFrame); setMouseTracking(true); setUndoRedoEnabled(false); + d->formatter.setPlainTextEdit(this); + + d->queueTimer.setSingleShot(true); + d->queueTimer.setInterval(10); + connect(&d->queueTimer, &QTimer::timeout, this, &OutputWindow::handleNextOutputChunk); d->settingsKey = settingsKey; - d->outputWindowContext = new IContext; - d->outputWindowContext->setContext(context); - d->outputWindowContext->setWidget(this); - ICore::addContextObject(d->outputWindowContext); + auto outputWindowContext = new IContext(this); + outputWindowContext->setContext(context); + outputWindowContext->setWidget(this); + ICore::addContextObject(outputWindowContext); auto undoAction = new QAction(this); auto redoAction = new QAction(this); @@ -135,16 +148,21 @@ OutputWindow::OutputWindow(Context context, const QString &settingsKey, QWidget Core::ICore::settings()->setValue(d->settingsKey, fontZoom()); }); + connect(outputFormatter(), &OutputFormatter::openInEditorRequested, this, + [](const Utils::FilePath &fp, int line, int column) { + EditorManager::openEditorAt(fp.toString(), line, column); + }); + undoAction->setEnabled(false); redoAction->setEnabled(false); cutAction->setEnabled(false); copyAction->setEnabled(false); - m_scrollTimer.setInterval(10); - m_scrollTimer.setSingleShot(true); - connect(&m_scrollTimer, &QTimer::timeout, + d->scrollTimer.setInterval(10); + d->scrollTimer.setSingleShot(true); + connect(&d->scrollTimer, &QTimer::timeout, this, &OutputWindow::scrollToBottom); - m_lastMessage.start(); + d->lastMessage.start(); d->originalFontSize = font().pointSizeF(); @@ -165,13 +183,17 @@ void OutputWindow::mousePressEvent(QMouseEvent *e) QPlainTextEdit::mousePressEvent(e); } +void OutputWindow::handleLink(const QPoint &pos) +{ + const QString href = anchorAt(pos); + if (!href.isEmpty()) + d->formatter.handleLink(href); +} + void OutputWindow::mouseReleaseEvent(QMouseEvent *e) { - if (d->linksActive && d->mouseButtonPressed == Qt::LeftButton) { - const QString href = anchorAt(e->pos()); - if (d->formatter) - d->formatter->handleLink(href); - } + if (d->linksActive && d->mouseButtonPressed == Qt::LeftButton) + handleLink(e->pos()); // Mouse was released, activate links again d->linksActive = true; @@ -214,16 +236,15 @@ void OutputWindow::keyPressEvent(QKeyEvent *ev) verticalScrollBar()->triggerAction(QAbstractSlider::SliderToMaximum); } -OutputFormatter *OutputWindow::formatter() const +void OutputWindow::setLineParsers(const QList<OutputLineParser *> &parsers) { - return d->formatter; + reset(); + d->formatter.setLineParsers(parsers); } -void OutputWindow::setFormatter(OutputFormatter *formatter) +OutputFormatter *OutputWindow::outputFormatter() const { - d->formatter = formatter; - if (d->formatter) - d->formatter->setPlainTextEdit(this); + return &d->formatter; } void OutputWindow::showEvent(QShowEvent *e) @@ -364,47 +385,28 @@ void OutputWindow::filterNewContent() scrollToBottom(); } -QString OutputWindow::doNewlineEnforcement(const QString &out) +void OutputWindow::handleNextOutputChunk() { - d->scrollToBottom = true; - QString s = out; - if (d->enforceNewline) { - s.prepend('\n'); - d->enforceNewline = false; + QTC_ASSERT(!d->queuedOutput.isEmpty(), return); + auto &chunk = d->queuedOutput.first(); + if (chunk.first.size() <= chunkSize) { + handleOutputChunk(chunk.first, chunk.second); + d->queuedOutput.removeFirst(); + } else { + handleOutputChunk(chunk.first.left(chunkSize), chunk.second); + chunk.first.remove(0, chunkSize); } - - if (s.endsWith('\n')) { - d->enforceNewline = true; // make appendOutputInline put in a newline next time - s.chop(1); + if (!d->queuedOutput.isEmpty()) + d->queueTimer.start(); + else if (d->flushRequested) { + d->formatter.flush(); + d->flushRequested = false; } - - return s; -} - -void OutputWindow::setMaxCharCount(int count) -{ - d->maxCharCount = count; - setMaximumBlockCount(count / 100); } -int OutputWindow::maxCharCount() const -{ - return d->maxCharCount; -} - -void OutputWindow::appendMessage(const QString &output, OutputFormat format) +void OutputWindow::handleOutputChunk(const QString &output, OutputFormat format) { QString out = output; - if (d->prependCarriageReturn) { - d->prependCarriageReturn = false; - out.prepend('\r'); - } - out = SynchronousProcess::normalizeNewlines(out); - if (out.endsWith('\r')) { - d->prependCarriageReturn = true; - out.chop(1); - } - if (out.size() > d->maxCharCount) { // Current line alone exceeds limit, we need to cut it. out.truncate(d->maxCharCount); @@ -426,86 +428,42 @@ void OutputWindow::appendMessage(const QString &output, OutputFormat format) } } - const bool atBottom = isScrollbarAtBottom() || m_scrollTimer.isActive(); - - if (format == ErrorMessageFormat || format == NormalMessageFormat) { - if (d->formatter) - d->formatter->appendMessage(doNewlineEnforcement(out), format); - } else { - - bool sameLine = format == StdOutFormatSameLine - || format == StdErrFormatSameLine; - - if (sameLine) { - d->scrollToBottom = true; - - bool enforceNewline = d->enforceNewline; - d->enforceNewline = false; - - if (enforceNewline) { - out.prepend('\n'); - } else { - const int newline = out.indexOf('\n'); - moveCursor(QTextCursor::End); - if (newline != -1) { - if (d->formatter) - d->formatter->appendMessage(out.left(newline), format);// doesn't enforce new paragraph like appendPlainText - out = out.mid(newline); - } - } - - if (out.isEmpty()) { - d->enforceNewline = true; - } else { - if (out.endsWith('\n')) { - d->enforceNewline = true; - out.chop(1); - } - if (d->formatter) - d->formatter->appendMessage(out, format); - } - } else { - if (d->formatter) - d->formatter->appendMessage(doNewlineEnforcement(out), format); - } - } + const bool atBottom = isScrollbarAtBottom() || d->scrollTimer.isActive(); + d->scrollToBottom = true; + d->formatter.appendMessage(out, format); if (atBottom) { - if (m_lastMessage.elapsed() < 5) { - m_scrollTimer.start(); + if (d->lastMessage.elapsed() < 5) { + d->scrollTimer.start(); } else { - m_scrollTimer.stop(); + d->scrollTimer.stop(); scrollToBottom(); } } - m_lastMessage.start(); + d->lastMessage.start(); enableUndoRedo(); } -// TODO rename -void OutputWindow::appendText(const QString &textIn, const QTextCharFormat &format) +void OutputWindow::setMaxCharCount(int count) { - const QString text = SynchronousProcess::normalizeNewlines(textIn); - if (d->maxCharCount > 0 && document()->characterCount() >= d->maxCharCount) - return; - const bool atBottom = isScrollbarAtBottom(); - if (!d->cursor.atEnd()) - d->cursor.movePosition(QTextCursor::End); - d->cursor.beginEditBlock(); - d->cursor.insertText(doNewlineEnforcement(text), format); - - if (d->maxCharCount > 0 && document()->characterCount() >= d->maxCharCount) { - QTextCharFormat tmp; - tmp.setFontWeight(QFont::Bold); - d->cursor.insertText(doNewlineEnforcement(tr("Additional output omitted. You can increase " - "the limit in the \"Build & Run\" settings.") - + '\n'), tmp); - } + d->maxCharCount = count; + setMaximumBlockCount(count / 100); +} - d->cursor.endEditBlock(); - if (atBottom) - scrollToBottom(); +int OutputWindow::maxCharCount() const +{ + return d->maxCharCount; +} + +void OutputWindow::appendMessage(const QString &output, OutputFormat format) +{ + if (d->queuedOutput.isEmpty() || d->queuedOutput.last().second != format) + d->queuedOutput << qMakePair(output, format); + else + d->queuedOutput.last().first.append(output); + if (!d->queueTimer.isActive()) + d->queueTimer.start(); } bool OutputWindow::isScrollbarAtBottom() const @@ -542,11 +500,35 @@ QMimeData *OutputWindow::createMimeDataFromSelection() const void OutputWindow::clear() { - d->enforceNewline = false; - d->prependCarriageReturn = false; - QPlainTextEdit::clear(); - if (d->formatter) - d->formatter->clear(); + d->formatter.clear(); +} + +void OutputWindow::flush() +{ + const int totalQueuedSize = std::accumulate(d->queuedOutput.cbegin(), d->queuedOutput.cend(), 0, + [](int val, const QPair<QString, OutputFormat> &c) { return val + c.first.size(); }); + if (totalQueuedSize > 5 * chunkSize) { + d->flushRequested = true; + return; + } + d->queueTimer.stop(); + for (const auto &chunk : d->queuedOutput) + handleOutputChunk(chunk.first, chunk.second); + d->queuedOutput.clear(); + d->formatter.flush(); +} + +void OutputWindow::reset() +{ + flush(); + d->queueTimer.stop(); + d->formatter.reset(); + if (!d->queuedOutput.isEmpty()) { + d->queuedOutput.clear(); + d->formatter.appendMessage(tr("[Discarding excessive amount of pending output.]\n"), + ErrorMessageFormat); + } + d->flushRequested = false; } void OutputWindow::scrollToBottom() @@ -595,4 +577,83 @@ void OutputWindow::setWordWrapEnabled(bool wrap) setWordWrapMode(QTextOption::NoWrap); } +#ifdef WITH_TESTS + +// Handles all lines starting with "A" and the following ones up to and including the next +// one starting with "A". +class TestFormatterA : public OutputLineParser +{ +private: + Result handleLine(const QString &text, OutputFormat) override + { + static const QString replacement = "handled by A\n"; + if (m_handling) { + if (text.startsWith("A")) { + m_handling = false; + return {Status::Done, {}, replacement}; + } + return {Status::InProgress, {}, replacement}; + } + if (text.startsWith("A")) { + m_handling = true; + return {Status::InProgress, {}, replacement}; + } + return Status::NotHandled; + } + + bool m_handling = false; +}; + +// Handles all lines starting with "B". No continuation logic. +class TestFormatterB : public OutputLineParser +{ +private: + Result handleLine(const QString &text, OutputFormat) override + { + if (text.startsWith("B")) + return {Status::Done, {}, QString("handled by B\n")}; + return Status::NotHandled; + } +}; + +void Internal::CorePlugin::testOutputFormatter() +{ + const QString input = + "B to be handled by B\r\n" + "not to be handled\n" + "A to be handled by A\n" + "continuation for A\r\n" + "B looks like B, but still continuation for A\r\n" + "A end of A\n" + "A next A\n" + "A end of next A\n" + " A trick\r\n" + "line with \r embedded carriage return\n" + "B to be handled by B\n"; + const QString output = + "handled by B\n" + "not to be handled\n" + "handled by A\n" + "handled by A\n" + "handled by A\n" + "handled by A\n" + "handled by A\n" + "handled by A\n" + " A trick\n" + " embedded carriage return\n" + "handled by B\n"; + + // Stress-test the implementation by providing the input in chunks, splitting at all possible + // offsets. + for (int i = 0; i < input.length(); ++i) { + OutputFormatter formatter; + QPlainTextEdit textEdit; + formatter.setPlainTextEdit(&textEdit); + formatter.setLineParsers({new TestFormatterB, new TestFormatterA}); + formatter.appendMessage(input.left(i), StdOutFormat); + formatter.appendMessage(input.mid(i), StdOutFormat); + QCOMPARE(textEdit.toPlainText(), output); + } +} +#endif // WITH_TESTS } // namespace Core |