aboutsummaryrefslogtreecommitdiffstats
path: root/src/libs/utils/outputformatter.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/libs/utils/outputformatter.cpp')
-rw-r--r--src/libs/utils/outputformatter.cpp528
1 files changed, 491 insertions, 37 deletions
diff --git a/src/libs/utils/outputformatter.cpp b/src/libs/utils/outputformatter.cpp
index ad077755ca..355fcb24ab 100644
--- a/src/libs/utils/outputformatter.cpp
+++ b/src/libs/utils/outputformatter.cpp
@@ -23,36 +23,203 @@
**
****************************************************************************/
-#include "ansiescapecodehandler.h"
#include "outputformatter.h"
+
+#include "algorithm.h"
+#include "ansiescapecodehandler.h"
+#include "fileinprojectfinder.h"
+#include "qtcassert.h"
+#include "synchronousprocess.h"
#include "theme/theme.h"
+#include <QDir>
+#include <QFileInfo>
+#include <QPair>
#include <QPlainTextEdit>
+#include <QPointer>
+#include <QRegularExpressionMatch>
#include <QTextCursor>
+#include <numeric>
+
namespace Utils {
-namespace Internal {
+class OutputLineParser::Private
+{
+public:
+ FilePaths searchDirs;
+ QPointer<const OutputLineParser> redirectionDetector;
+ bool skipFileExistsCheck = false;
+ bool demoteErrorsToWarnings = false;
+ FileInProjectFinder *fileFinder = nullptr;
+};
+
+OutputLineParser::OutputLineParser() : d(new Private) { }
+
+OutputLineParser::~OutputLineParser() { delete d; }
+
+Q_GLOBAL_STATIC_WITH_ARGS(QString, linkPrefix, {"olpfile://"})
+Q_GLOBAL_STATIC_WITH_ARGS(QString, linkSep, {"::"})
+
+QString OutputLineParser::createLinkTarget(const FilePath &filePath, int line = -1, int column = -1)
+{
+ return *linkPrefix() + filePath.toString() + *linkSep() + QString::number(line)
+ + *linkSep() + QString::number(column);
+}
+
+bool OutputLineParser::isLinkTarget(const QString &target)
+{
+ return target.startsWith(*linkPrefix());
+}
+
+void OutputLineParser::parseLinkTarget(const QString &target, FilePath &filePath, int &line,
+ int &column)
+{
+ const QStringList parts = target.mid(linkPrefix()->length()).split(*linkSep());
+ if (parts.isEmpty())
+ return;
+ filePath = FilePath::fromString(parts.first());
+ line = parts.length() > 1 ? parts.at(1).toInt() : 0;
+ column = parts.length() > 2 ? parts.at(2).toInt() : 0;
+}
+
+// The redirection mechanism is needed for broken build tools (e.g. xcodebuild) that get invoked
+// indirectly as part of the build process and redirect their child processes' stderr output
+// to stdout. A parser might be able to detect this condition and inform interested
+// other parsers that they need to interpret stdout data as stderr.
+void OutputLineParser::setRedirectionDetector(const OutputLineParser *detector)
+{
+ d->redirectionDetector = detector;
+}
+
+bool OutputLineParser::needsRedirection() const
+{
+ return d->redirectionDetector && (d->redirectionDetector->hasDetectedRedirection()
+ || d->redirectionDetector->needsRedirection());
+}
+
+void OutputLineParser::addSearchDir(const FilePath &dir)
+{
+ d->searchDirs << dir;
+}
+
+void OutputLineParser::dropSearchDir(const FilePath &dir)
+{
+ const int idx = d->searchDirs.lastIndexOf(dir);
+
+ // TODO: This apparently triggers. Find out why and either remove the assertion (if it's legit)
+ // or fix the culprit.
+ QTC_ASSERT(idx != -1, return);
+
+ d->searchDirs.removeAt(idx);
+}
+
+const FilePaths OutputLineParser::searchDirectories() const
+{
+ return d->searchDirs;
+}
+
+void OutputLineParser::setFileFinder(FileInProjectFinder *finder)
+{
+ d->fileFinder = finder;
+}
+
+void OutputLineParser::setDemoteErrorsToWarnings(bool demote)
+{
+ d->demoteErrorsToWarnings = demote;
+}
+
+bool OutputLineParser::demoteErrorsToWarnings() const
+{
+ return d->demoteErrorsToWarnings;
+}
-class OutputFormatterPrivate
+FilePath OutputLineParser::absoluteFilePath(const FilePath &filePath)
+{
+ if (filePath.isEmpty() || filePath.toFileInfo().isAbsolute())
+ return filePath;
+ FilePaths candidates;
+ for (const FilePath &dir : searchDirectories()) {
+ const FilePath candidate = dir.pathAppended(filePath.toString());
+ if (candidate.exists() || d->skipFileExistsCheck)
+ candidates << candidate;
+ }
+ if (candidates.count() == 1)
+ return FilePath::fromString(QDir::cleanPath(candidates.first().toString()));
+
+ QString fp = filePath.toString();
+ while (fp.startsWith("../"))
+ fp.remove(0, 3);
+ bool found = false;
+ candidates = d->fileFinder->findFile(QUrl::fromLocalFile(fp), &found);
+ if (found && candidates.size() == 1)
+ return candidates.first();
+
+ return filePath;
+}
+
+void OutputLineParser::addLinkSpecForAbsoluteFilePath(OutputLineParser::LinkSpecs &linkSpecs,
+ const FilePath &filePath, int lineNo, int pos, int len)
+{
+ if (filePath.toFileInfo().isAbsolute())
+ linkSpecs.append({pos, len, createLinkTarget(filePath, lineNo)});
+}
+
+void OutputLineParser::addLinkSpecForAbsoluteFilePath(OutputLineParser::LinkSpecs &linkSpecs,
+ const FilePath &filePath, int lineNo, const QRegularExpressionMatch &match,
+ int capIndex)
+{
+ addLinkSpecForAbsoluteFilePath(linkSpecs, filePath, lineNo, match.capturedStart(capIndex),
+ match.capturedLength(capIndex));
+}
+void OutputLineParser::addLinkSpecForAbsoluteFilePath(OutputLineParser::LinkSpecs &linkSpecs,
+ const FilePath &filePath, int lineNo, const QRegularExpressionMatch &match,
+ const QString &capName)
+{
+ addLinkSpecForAbsoluteFilePath(linkSpecs, filePath, lineNo, match.capturedStart(capName),
+ match.capturedLength(capName));
+}
+
+QString OutputLineParser::rightTrimmed(const QString &in)
+{
+ int pos = in.length();
+ for (; pos > 0; --pos) {
+ if (!in.at(pos - 1).isSpace())
+ break;
+ }
+ return in.mid(0, pos);
+}
+
+#ifdef WITH_TESTS
+void OutputLineParser::skipFileExistsCheck()
+{
+ d->skipFileExistsCheck = true;
+}
+#endif
+
+
+class OutputFormatter::Private
{
public:
QPlainTextEdit *plainTextEdit = nullptr;
QTextCharFormat formats[NumberOfFormats];
QTextCursor cursor;
AnsiEscapeCodeHandler escapeCodeHandler;
+ QPair<QString, OutputFormat> incompleteLine;
+ optional<QTextCharFormat> formatOverride;
+ QList<OutputLineParser *> lineParsers;
+ OutputLineParser *nextParser = nullptr;
+ FileInProjectFinder fileFinder;
+ PostPrintAction postPrintAction;
bool boldFontEnabled = true;
+ bool prependCarriageReturn = false;
};
-} // namespace Internal
-
-OutputFormatter::OutputFormatter()
- : d(new Internal::OutputFormatterPrivate)
-{
-}
+OutputFormatter::OutputFormatter() : d(new Private) { }
OutputFormatter::~OutputFormatter()
{
+ qDeleteAll(d->lineParsers);
delete d;
}
@@ -69,23 +236,118 @@ void OutputFormatter::setPlainTextEdit(QPlainTextEdit *plainText)
initFormats();
}
-void OutputFormatter::appendMessage(const QString &text, OutputFormat format)
+void OutputFormatter::setLineParsers(const QList<OutputLineParser *> &parsers)
{
- if (!d->cursor.atEnd() && text.startsWith('\n'))
- d->cursor.movePosition(QTextCursor::End);
- appendMessage(text, d->formats[format]);
+ flush();
+ qDeleteAll(d->lineParsers);
+ d->lineParsers.clear();
+ d->nextParser = nullptr;
+ addLineParsers(parsers);
+}
+
+void OutputFormatter::addLineParsers(const QList<OutputLineParser *> &parsers)
+{
+ for (OutputLineParser * const p : qAsConst(parsers))
+ addLineParser(p);
+}
+
+void OutputFormatter::addLineParser(OutputLineParser *parser)
+{
+ setupLineParser(parser);
+ d->lineParsers << parser;
+}
+
+void OutputFormatter::setupLineParser(OutputLineParser *parser)
+{
+ parser->setFileFinder(&d->fileFinder);
+ connect(parser, &OutputLineParser::newSearchDir, this, &OutputFormatter::addSearchDir);
+ connect(parser, &OutputLineParser::searchDirExpired, this, &OutputFormatter::dropSearchDir);
+}
+
+void OutputFormatter::setFileFinder(const FileInProjectFinder &finder)
+{
+ d->fileFinder = finder;
+}
+
+void OutputFormatter::setDemoteErrorsToWarnings(bool demote)
+{
+ for (OutputLineParser * const p : qAsConst(d->lineParsers))
+ p->setDemoteErrorsToWarnings(demote);
+}
+
+void OutputFormatter::overridePostPrintAction(const PostPrintAction &postPrintAction)
+{
+ d->postPrintAction = postPrintAction;
}
-void OutputFormatter::appendMessage(const QString &text, const QTextCharFormat &format)
+void OutputFormatter::doAppendMessage(const QString &text, OutputFormat format)
{
- const QList<FormattedText> formattedTextList = parseAnsi(text, format);
- for (const FormattedText &output : formattedTextList)
+ const QTextCharFormat charFmt = charFormat(format);
+ const QList<FormattedText> formattedText = parseAnsi(text, charFmt);
+ const QString cleanLine = std::accumulate(formattedText.begin(), formattedText.end(), QString(),
+ [](const FormattedText &t1, const FormattedText &t2) { return t1.text + t2.text; });
+ QList<OutputLineParser *> involvedParsers;
+ const OutputLineParser::Result res = handleMessage(cleanLine, format, involvedParsers);
+ if (res.newContent) {
+ append(res.newContent.value(), charFmt);
+ return;
+ }
+ for (const FormattedText &output : linkifiedText(formattedText, res.linkSpecs))
append(output.text, output.format);
+ for (OutputLineParser * const p : qAsConst(involvedParsers)) {
+ if (d->postPrintAction)
+ d->postPrintAction(p);
+ else
+ p->runPostPrintActions();
+ }
+}
+
+OutputLineParser::Result OutputFormatter::handleMessage(const QString &text, OutputFormat format,
+ QList<OutputLineParser *> &involvedParsers)
+{
+ // We only invoke the line parsers for stdout and stderr
+ if (format != StdOutFormat && format != StdErrFormat)
+ return OutputLineParser::Status::NotHandled;
+ const OutputLineParser * const oldNextParser = d->nextParser;
+ if (d->nextParser) {
+ involvedParsers << d->nextParser;
+ const OutputLineParser::Result res
+ = d->nextParser->handleLine(text, outputTypeForParser(d->nextParser, format));
+ switch (res.status) {
+ case OutputLineParser::Status::Done:
+ d->nextParser = nullptr;
+ return res;
+ case OutputLineParser::Status::InProgress:
+ return res;
+ case OutputLineParser::Status::NotHandled:
+ d->nextParser = nullptr;
+ break;
+ }
+ }
+ QTC_CHECK(!d->nextParser);
+ for (OutputLineParser * const parser : qAsConst(d->lineParsers)) {
+ if (parser == oldNextParser) // We tried that one already.
+ continue;
+ const OutputLineParser::Result res
+ = parser->handleLine(text, outputTypeForParser(parser, format));
+ switch (res.status) {
+ case OutputLineParser::Status::Done:
+ involvedParsers << parser;
+ return res;
+ case OutputLineParser::Status::InProgress:
+ involvedParsers << parser;
+ d->nextParser = parser;
+ return res;
+ case OutputLineParser::Status::NotHandled:
+ break;
+ }
+ }
+ return OutputLineParser::Status::NotHandled;
}
QTextCharFormat OutputFormatter::charFormat(OutputFormat format) const
{
- return d->formats[format];
+ return d->formatOverride ? d->formatOverride.value() : d->formats[format];
}
QList<FormattedText> OutputFormatter::parseAnsi(const QString &text, const QTextCharFormat &format)
@@ -93,16 +355,67 @@ QList<FormattedText> OutputFormatter::parseAnsi(const QString &text, const QText
return d->escapeCodeHandler.parseText(FormattedText(text, format));
}
+const QList<FormattedText> OutputFormatter::linkifiedText(
+ const QList<FormattedText> &text, const OutputLineParser::LinkSpecs &linkSpecs)
+{
+ if (linkSpecs.isEmpty())
+ return text;
+
+ QList<FormattedText> linkified;
+ int totalTextLengthSoFar = 0;
+ int nextLinkSpecIndex = 0;
+
+ for (const FormattedText &t : text) {
+ const int totalPreviousTextLength = totalTextLengthSoFar;
+
+ // There is no more linkification work to be done. Just copy the text as-is.
+ if (nextLinkSpecIndex >= linkSpecs.size()) {
+ linkified << t;
+ continue;
+ }
+
+ for (int nextLocalTextPos = 0; nextLocalTextPos < t.text.size(); ) {
+
+ // There are no more links in this part, so copy the rest of the text as-is.
+ if (nextLinkSpecIndex >= linkSpecs.size()) {
+ linkified << FormattedText(t.text.mid(nextLocalTextPos), t.format);
+ totalTextLengthSoFar += t.text.length() - nextLocalTextPos;
+ break;
+ }
+
+ const OutputLineParser::LinkSpec &linkSpec = linkSpecs.at(nextLinkSpecIndex);
+ const int localLinkStartPos = linkSpec.startPos - totalPreviousTextLength;
+ ++nextLinkSpecIndex;
+
+ // We ignore links that would cross format boundaries.
+ if (localLinkStartPos < nextLocalTextPos
+ || localLinkStartPos + linkSpec.length > t.text.length()) {
+ linkified << FormattedText(t.text.mid(nextLocalTextPos), t.format);
+ totalTextLengthSoFar += t.text.length() - nextLocalTextPos;
+ break;
+ }
+
+ // Now we know we have a link that is fully inside this part of the text.
+ // Split the text so that the link part gets the appropriate format.
+ const int prefixLength = localLinkStartPos - nextLocalTextPos;
+ const QString textBeforeLink = t.text.mid(nextLocalTextPos, prefixLength);
+ linkified << FormattedText(textBeforeLink, t.format);
+ const QString linkedText = t.text.mid(localLinkStartPos, linkSpec.length);
+ linkified << FormattedText(linkedText, linkFormat(t.format, linkSpec.target));
+ nextLocalTextPos = localLinkStartPos + linkSpec.length;
+ totalTextLengthSoFar += prefixLength + linkSpec.length;
+ }
+ }
+ return linkified;
+}
+
void OutputFormatter::append(const QString &text, const QTextCharFormat &format)
{
+ if (!plainTextEdit())
+ return;
int startPos = 0;
int crPos = -1;
while ((crPos = text.indexOf('\r', startPos)) >= 0) {
- if (text.size() > crPos + 1 && text.at(crPos + 1) == '\n') {
- d->cursor.insertText(text.mid(startPos, crPos - startPos) + '\n', format);
- startPos = crPos + 2;
- continue;
- }
d->cursor.insertText(text.mid(startPos, crPos - startPos), format);
d->cursor.clearSelection();
d->cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
@@ -122,8 +435,23 @@ QTextCharFormat OutputFormatter::linkFormat(const QTextCharFormat &inputFormat,
return result;
}
+#ifdef WITH_TESTS
+void OutputFormatter::overrideTextCharFormat(const QTextCharFormat &fmt)
+{
+ d->formatOverride = fmt;
+}
+
+QList<OutputLineParser *> OutputFormatter::lineParsers() const
+{
+ return d->lineParsers;
+}
+#endif // WITH_TESTS
+
void OutputFormatter::clearLastLine()
{
+ // Note that this approach will fail if the text edit is not read-only and users
+ // have messed with the last line between programmatic inputs.
+ // We live with this risk, as all the alternatives are worse.
if (!d->cursor.atEnd())
d->cursor.movePosition(QTextCursor::End);
d->cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
@@ -136,32 +464,73 @@ void OutputFormatter::initFormats()
return;
Theme *theme = creatorTheme();
-
- // NormalMessageFormat
d->formats[NormalMessageFormat].setForeground(theme->color(Theme::OutputPanes_NormalMessageTextColor));
-
- // ErrorMessageFormat
d->formats[ErrorMessageFormat].setForeground(theme->color(Theme::OutputPanes_ErrorMessageTextColor));
-
- // LogMessageFormat
d->formats[LogMessageFormat].setForeground(theme->color(Theme::OutputPanes_WarningMessageTextColor));
-
- // StdOutFormat
d->formats[StdOutFormat].setForeground(theme->color(Theme::OutputPanes_StdOutTextColor));
- d->formats[StdOutFormatSameLine] = d->formats[StdOutFormat];
-
- // StdErrFormat
d->formats[StdErrFormat].setForeground(theme->color(Theme::OutputPanes_StdErrTextColor));
- d->formats[StdErrFormatSameLine] = d->formats[StdErrFormat];
-
d->formats[DebugFormat].setForeground(theme->color(Theme::OutputPanes_DebugTextColor));
-
setBoldFontEnabled(d->boldFontEnabled);
}
+void OutputFormatter::flushIncompleteLine()
+{
+ clearLastLine();
+ doAppendMessage(d->incompleteLine.first, d->incompleteLine.second);
+ d->incompleteLine.first.clear();
+}
+
+void OutputFormatter::dumpIncompleteLine(const QString &line, OutputFormat format)
+{
+ if (line.isEmpty())
+ return;
+ append(line, charFormat(format));
+ d->incompleteLine.first.append(line);
+ d->incompleteLine.second = format;
+}
+
+bool OutputFormatter::handleFileLink(const QString &href)
+{
+ if (!OutputLineParser::isLinkTarget(href))
+ return false;
+ FilePath filePath;
+ int line;
+ int column;
+ OutputLineParser::parseLinkTarget(href, filePath, line, column);
+ QTC_ASSERT(!filePath.isEmpty(), return false);
+ emit openInEditorRequested(filePath, line, column);
+ return true;
+}
+
void OutputFormatter::handleLink(const QString &href)
{
- Q_UNUSED(href)
+ QTC_ASSERT(!href.isEmpty(), return);
+ // We can handle absolute file paths ourselves. Other types of references are forwarded
+ // to the line parsers.
+ if (handleFileLink(href))
+ return;
+ for (OutputLineParser * const f : qAsConst(d->lineParsers)) {
+ if (f->handleLink(href))
+ return;
+ }
+}
+
+void OutputFormatter::clear()
+{
+ if (plainTextEdit())
+ plainTextEdit()->clear();
+}
+
+void OutputFormatter::reset()
+{
+ d->prependCarriageReturn = false;
+ d->incompleteLine.first.clear();
+ d->nextParser = nullptr;
+ qDeleteAll(d->lineParsers);
+ d->lineParsers.clear();
+ d->fileFinder = FileInProjectFinder();
+ d->formatOverride.reset();
+ d->escapeCodeHandler = AnsiEscapeCodeHandler();
}
void OutputFormatter::setBoldFontEnabled(bool enabled)
@@ -174,7 +543,92 @@ void OutputFormatter::setBoldFontEnabled(bool enabled)
void OutputFormatter::flush()
{
+ if (!d->incompleteLine.first.isEmpty())
+ flushIncompleteLine();
d->escapeCodeHandler.endFormatScope();
+ for (OutputLineParser * const p : qAsConst(d->lineParsers))
+ p->flush();
+ if (d->nextParser)
+ d->nextParser->runPostPrintActions();
+}
+
+bool OutputFormatter::hasFatalErrors() const
+{
+ return anyOf(d->lineParsers, [](const OutputLineParser *p) {
+ return p->hasFatalErrors();
+ });
+}
+
+void OutputFormatter::addSearchDir(const FilePath &dir)
+{
+ for (OutputLineParser * const p : qAsConst(d->lineParsers))
+ p->addSearchDir(dir);
+}
+
+void OutputFormatter::dropSearchDir(const FilePath &dir)
+{
+ for (OutputLineParser * const p : qAsConst(d->lineParsers))
+ p->dropSearchDir(dir);
+}
+
+OutputFormat OutputFormatter::outputTypeForParser(const OutputLineParser *parser,
+ OutputFormat type) const
+{
+ if (type == StdOutFormat && parser->needsRedirection())
+ return StdErrFormat;
+ return type;
+}
+
+void OutputFormatter::appendMessage(const QString &text, OutputFormat format)
+{
+ if (text.isEmpty())
+ return;
+
+ // If we have an existing incomplete line and its format is different from this one,
+ // then we consider the two messages unrelated. We re-insert the previous incomplete line,
+ // possibly formatted now, and start from scratch with the new input.
+ if (!d->incompleteLine.first.isEmpty() && d->incompleteLine.second != format)
+ flushIncompleteLine();
+
+ QString out = text;
+ if (d->prependCarriageReturn) {
+ d->prependCarriageReturn = false;
+ out.prepend('\r');
+ }
+ out = SynchronousProcess::normalizeNewlines(out);
+ if (out.endsWith('\r')) {
+ d->prependCarriageReturn = true;
+ out.chop(1);
+ }
+
+ // If the input is a single incomplete line, we do not forward it to the specialized
+ // formatting code, but simply dump it as-is. Once it becomes complete or it needs to
+ // be flushed for other reasons, we remove the unformatted part and re-insert it, this
+ // time with proper formatting.
+ if (!out.contains('\n')) {
+ dumpIncompleteLine(out, format);
+ return;
+ }
+
+ // We have at least one complete line, so let's remove the previously dumped
+ // incomplete line and prepend it to the first line of our new input.
+ if (!d->incompleteLine.first.isEmpty()) {
+ clearLastLine();
+ out.prepend(d->incompleteLine.first);
+ d->incompleteLine.first.clear();
+ }
+
+ // Forward all complete lines to the specialized formatting code, and handle a
+ // potential trailing incomplete line the same way as above.
+ for (int startPos = 0; ;) {
+ const int eolPos = out.indexOf('\n', startPos);
+ if (eolPos == -1) {
+ dumpIncompleteLine(out.mid(startPos), format);
+ break;
+ }
+ doAppendMessage(out.mid(startPos, eolPos - startPos + 1), format);
+ startPos = eolPos + 1;
+ }
}
} // namespace Utils