diff --git a/src/libs/utils/ansiescapecodehandler.cpp b/src/libs/utils/ansiescapecodehandler.cpp index 74e14f2c46d..fe9d8a115ec 100644 --- a/src/libs/utils/ansiescapecodehandler.cpp +++ b/src/libs/utils/ansiescapecodehandler.cpp @@ -9,6 +9,10 @@ namespace Utils { +static const QString s_escape = "\x1b["; +static const QChar s_semicolon = ';'; +static const QChar s_colorTerminator = 'm'; + /*! \class Utils::AnsiEscapeCodeHandler \inmodule QtCreator @@ -58,9 +62,6 @@ QList AnsiEscapeCodeHandler::parseText(const FormattedText &input DefaultBackgroundColor = 49 }; - const QString escape = "\x1b["; - const QChar semicolon = ';'; - const QChar colorTerminator = 'm'; const QChar eraseToEol = 'K'; QList outputData; @@ -93,7 +94,7 @@ QList AnsiEscapeCodeHandler::parseText(const FormattedText &input if (strippedText.isEmpty()) break; } - const int escapePos = strippedText.indexOf(escape.at(0)); + const int escapePos = strippedText.indexOf(s_escape.at(0)); if (escapePos < 0) { outputData << FormattedText(strippedText, charFormat); break; @@ -101,16 +102,16 @@ QList AnsiEscapeCodeHandler::parseText(const FormattedText &input outputData << FormattedText(strippedText.left(escapePos), charFormat); strippedText.remove(0, escapePos); } - QTC_ASSERT(strippedText.at(0) == escape.at(0), break); + QTC_ASSERT(strippedText.at(0) == s_escape.at(0), break); - while (!strippedText.isEmpty() && escape.at(0) == strippedText.at(0)) { - if (escape.startsWith(strippedText)) { + while (!strippedText.isEmpty() && s_escape.at(0) == strippedText.at(0)) { + if (s_escape.startsWith(strippedText)) { // control secquence is not complete m_pendingText += strippedText; strippedText.clear(); break; } - if (!strippedText.startsWith(escape)) { + if (!strippedText.startsWith(s_escape)) { switch (strippedText.at(1).toLatin1()) { case '\\': // Unexpected terminator sequence. QTC_CHECK(false); @@ -134,8 +135,8 @@ QList AnsiEscapeCodeHandler::parseText(const FormattedText &input } break; } - m_pendingText += strippedText.mid(0, escape.length()); - strippedText.remove(0, escape.length()); + m_pendingText += strippedText.mid(0, s_escape.length()); + strippedText.remove(0, s_escape.length()); // \e[K is not supported. Just strip it. if (strippedText.startsWith(eraseToEol)) { @@ -152,7 +153,7 @@ QList AnsiEscapeCodeHandler::parseText(const FormattedText &input } else { if (!strNumber.isEmpty()) numbers << strNumber; - if (strNumber.isEmpty() || strippedText.at(0) != semicolon) + if (strNumber.isEmpty() || strippedText.at(0) != s_semicolon) break; strNumber.clear(); } @@ -163,7 +164,7 @@ QList AnsiEscapeCodeHandler::parseText(const FormattedText &input break; // remove terminating char - if (!strippedText.startsWith(colorTerminator)) { + if (!strippedText.startsWith(s_colorTerminator)) { m_pendingText.clear(); strippedText.remove(0, 1); break; @@ -284,6 +285,22 @@ void AnsiEscapeCodeHandler::setTextInEditor(QPlainTextEdit *editor, const QStrin editor->document()->setModified(false); } +QString AnsiEscapeCodeHandler::ansiFromColor(const QColor &color) +{ + // RGB color is ESC[38;2;;;m + // https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit + return QStringLiteral("%1;2;%2;%3;%4m") + .arg(s_escape + "38") + .arg(color.red()) + .arg(color.green()) + .arg(color.blue()); +} + +QString AnsiEscapeCodeHandler::noColor() +{ + return s_escape + s_colorTerminator; +} + void AnsiEscapeCodeHandler::setFormatScope(const QTextCharFormat &charFormat) { m_previousFormat = charFormat; diff --git a/src/libs/utils/ansiescapecodehandler.h b/src/libs/utils/ansiescapecodehandler.h index ef0aef2b2f0..a693c8a175c 100644 --- a/src/libs/utils/ansiescapecodehandler.h +++ b/src/libs/utils/ansiescapecodehandler.h @@ -30,6 +30,8 @@ public: QList parseText(const FormattedText &input); void endFormatScope(); static void setTextInEditor(QPlainTextEdit *editor, const QString &text); + static QString ansiFromColor(const QColor &color); + static QString noColor(); private: void setFormatScope(const QTextCharFormat &charFormat); diff --git a/src/plugins/diffeditor/diffeditor.cpp b/src/plugins/diffeditor/diffeditor.cpp index a80e0cfb44a..92164f435de 100644 --- a/src/plugins/diffeditor/diffeditor.cpp +++ b/src/plugins/diffeditor/diffeditor.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -442,7 +443,11 @@ void DiffEditor::updateDescription() QTC_ASSERT(m_toolBar, return); const QString description = m_document->description(); - m_descriptionWidget->setPlainText(description); + + if (m_document->isDescriptionAnsiEnabled()) + AnsiEscapeCodeHandler::setTextInEditor(m_descriptionWidget, description); + else + m_descriptionWidget->setPlainText(description); m_descriptionWidget->setVisible(m_showDescription && !description.isEmpty()); const QString actionText = m_showDescription ? Tr::tr("Hide Change Description") diff --git a/src/plugins/diffeditor/diffeditorcontroller.cpp b/src/plugins/diffeditor/diffeditorcontroller.cpp index c172c9f0971..d746e5bcb68 100644 --- a/src/plugins/diffeditor/diffeditorcontroller.cpp +++ b/src/plugins/diffeditor/diffeditorcontroller.cpp @@ -9,6 +9,7 @@ #include #include +#include #include using namespace Core; @@ -82,6 +83,11 @@ void DiffEditorController::setDiffFiles(const QList &diffFileList) m_document->setDiffFiles(diffFileList); } +void DiffEditorController::setAnsiEnabled(bool enabled) +{ + m_document->setDescriptionAnsiEnabled(enabled); +} + void DiffEditorController::setDescription(const QString &description) { m_document->setDescription(description); diff --git a/src/plugins/diffeditor/diffeditorcontroller.h b/src/plugins/diffeditor/diffeditorcontroller.h index e4fc4e4c31b..6ef7b8a7925 100644 --- a/src/plugins/diffeditor/diffeditorcontroller.h +++ b/src/plugins/diffeditor/diffeditorcontroller.h @@ -63,6 +63,7 @@ protected: void setDiffFiles(const QList &diffFileList); // Optional: void setDisplayName(const QString &name) { m_displayName = name; } + void setAnsiEnabled(bool enabled); void setDescription(const QString &description); void setDescriptionSyntaxHighlighterCreator( const std::function &creator); diff --git a/src/plugins/diffeditor/diffeditordocument.h b/src/plugins/diffeditor/diffeditordocument.h index 8d38c4e2ea8..dcace5932fa 100644 --- a/src/plugins/diffeditor/diffeditordocument.h +++ b/src/plugins/diffeditor/diffeditordocument.h @@ -47,6 +47,8 @@ public: void setDescription(const QString &description); QString description() const; + void setDescriptionAnsiEnabled(bool enabled) { m_descriptionAnsiEnabled = enabled; } + bool isDescriptionAnsiEnabled() const { return m_descriptionAnsiEnabled; } void setDescriptionSyntaxHighlighterCreator( const std::function &creator); std::function descriptionSyntaxHighlighterCreator() const { @@ -97,6 +99,7 @@ private: int m_contextLineCount = 3; bool m_isContextLineCountForced = false; bool m_ignoreWhitespace = false; + bool m_descriptionAnsiEnabled = false; State m_state = LoadOK; friend class ::DiffEditor::DiffEditorController; diff --git a/src/plugins/git/gitclient.cpp b/src/plugins/git/gitclient.cpp index ce05c3c8c8f..4a2b56a8c27 100644 --- a/src/plugins/git/gitclient.cpp +++ b/src/plugins/git/gitclient.cpp @@ -22,9 +22,9 @@ #include #include -#include #include +#include #include #include #include @@ -68,110 +68,15 @@ const char colorOption[] = "--color=always"; const char patchOption[] = "--patch"; const char graphOption[] = "--graph"; const char decorateOption[] = "--decorate"; -const char showFormatC[] = - "--pretty=format:commit %H%d%n" - "Author: %aN <%aE>, %ad (%ar)%n" - "Committer: %cN <%cE>, %cd (%cr)%n" - "%n" - "%B"; using namespace Core; using namespace DiffEditor; using namespace Tasking; -using namespace TextEditor; using namespace Utils; using namespace VcsBase; namespace Git::Internal { -static const QRegularExpression s_commitPattern("commit ([a-f0-9]{7,40})(.*)"); -static const QRegularExpression s_authorPattern("(?:Author|Committer): (.*>), (.*)"); -static const QRegularExpression s_branchesPattern("(?:Branches|Precedes|Follows): (.*)"); -static const QRegularExpression s_keywordPattern("^[\\w-]+:"); -static const QRegularExpression s_changeNumberPattern("\\b[a-f0-9]{7,40}\\b"); - -static QColor colorForStyle(TextStyle style) -{ - const ColorScheme &scheme = TextEditorSettings::fontSettings().colorScheme(); - const QColor color = scheme.formatFor(style).foreground(); - if (color.isValid()) - return color; - return scheme.formatFor(C_TEXT).foreground(); -} - -class GitDescriptionHighlighter : public SyntaxHighlighter -{ -public: - void highlightBlock(const QString &text) final - { - const QRegularExpressionMatch commitMatch = s_commitPattern.match(text); - if (commitMatch.hasMatch()) { - m_state = Commit; - setStyle(commitMatch.capturedStart(1), commitMatch.capturedLength(1), C_LOG_COMMIT_HASH); - setStyle(commitMatch.capturedStart(2), commitMatch.capturedLength(2), C_LOG_DECORATION); - return; - } - - const QRegularExpressionMatch authorMatch = s_authorPattern.match(text); - if (authorMatch.hasMatch()) { - m_state = Author; - setStyle(authorMatch.capturedStart(1), authorMatch.capturedLength(1), C_LOG_AUTHOR_NAME); - setStyle(authorMatch.capturedStart(2), authorMatch.capturedLength(2), C_LOG_COMMIT_DATE); - return; - } - - const QRegularExpressionMatch branchesMatch = s_branchesPattern.match(text); - if (branchesMatch.hasMatch()) { - m_state = Branches; - setStyle(branchesMatch.capturedStart(1), branchesMatch.capturedLength(1), C_LOG_DECORATION); - return; - } - - if (m_state == Branches) { - if (text.isEmpty()) - m_state = Subject; - else - setStyle(0, text.length(), C_LOG_DECORATION); - return; - } - - if ((m_state == Subject) && !text.isEmpty()) { - setStyle(0, text.length(), C_LOG_COMMIT_SUBJECT); - m_state = Body; - return; - } - - setStyle(0, text.length(), C_TEXT); - - const QRegularExpressionMatch keywordMatch = s_keywordPattern.match(text); - if (keywordMatch.hasMatch() && keywordMatch.capturedStart() == 0) { - QTextCharFormat charFormat = format(0); - charFormat.setFontItalic(true); - setFormat(0, keywordMatch.capturedLength(), charFormat); - } - - QRegularExpressionMatchIterator it = s_changeNumberPattern.globalMatch(text); - while (it.hasNext()) { - const QRegularExpressionMatch match = it.next(); - setStyle(match.capturedStart(), match.capturedLength(), C_LOG_COMMIT_HASH); - } - } - -private: - void setStyle(int start, int size, TextStyle style) - { - setFormat(start, size, colorForStyle(style)); - } - - enum State { - Commit, - Author, - Branches, - Subject, - Body - } m_state = Commit; -}; - static QString branchesDisplay(const QString &prefix, QStringList *branches, bool *first) { const int limit = 12; @@ -198,6 +103,17 @@ static QString branchesDisplay(const QString &prefix, QStringList *branches, boo return output; } +static QString logColorName(TextEditor::TextStyle style) +{ + using namespace TextEditor; + + const ColorScheme &scheme = TextEditorSettings::fontSettings().colorScheme(); + QColor color = scheme.formatFor(style).foreground(); + if (!color.isValid()) + color = scheme.formatFor(C_TEXT).foreground(); + return color.name(); +}; + /////////////////////////////// static void stage(DiffEditorController *diffController, const QString &patch, bool revert) @@ -348,7 +264,6 @@ GitDiffEditorController::GitDiffEditorController(IDocument *document, GitBaseDiffEditorController::GitBaseDiffEditorController(IDocument *document) : VcsBaseDiffEditorController(document) { - setDescriptionSyntaxHighlighterCreator([] { return new GitDescriptionHighlighter; }); setDisplayName("Git Diff"); } @@ -440,7 +355,6 @@ FileListDiffController::FileListDiffController(IDocument *document, const QStrin class ShowController : public GitBaseDiffEditorController { - Q_OBJECT public: ShowController(IDocument *document, const QString &id); }; @@ -449,7 +363,11 @@ ShowController::ShowController(IDocument *document, const QString &id) : GitBaseDiffEditorController(document) { setDisplayName("Git Show"); + setAnsiEnabled(true); static const QString busyMessage = Tr::tr(""); + const QColor color = QColor::fromString(logColorName(TextEditor::C_LOG_DECORATION)); + const QString decorateColor = AnsiEscapeCodeHandler::ansiFromColor(color); + const QString noColor = AnsiEscapeCodeHandler::noColor(); struct ReloadStorage { bool m_postProcessDescription = false; @@ -464,26 +382,39 @@ ShowController::ShowController(IDocument *document, const QString &id) const Storage storage; - const auto updateDescription = [this](const ReloadStorage &storage) { + const auto updateDescription = [this, decorateColor, noColor](const ReloadStorage &storage) { QString desc = storage.m_header; if (!storage.m_branches.isEmpty()) desc.append(BRANCHES_PREFIX + storage.m_branches + '\n'); if (!storage.m_precedes.isEmpty()) - desc.append("Precedes: " + storage.m_precedes + '\n'); + desc.append("Precedes: " + decorateColor + storage.m_precedes + noColor + '\n'); QStringList follows; for (const QString &str : storage.m_follows) { if (!str.isEmpty()) follows.append(str); } if (!follows.isEmpty()) - desc.append("Follows: " + follows.join(", ") + '\n'); + desc.append("Follows: " + decorateColor + follows.join(", ") + noColor + '\n'); desc.append('\n' + storage.m_body); setDescription(desc); }; const auto onDescriptionSetup = [this, id](Process &process) { process.setCodec(gitClient().encoding(GitClient::EncodingCommit, workingDirectory())); - setupCommand(process, {"show", "-s", noColorOption, showFormatC, id}); + const QString authorName = logColorName(TextEditor::C_LOG_AUTHOR_NAME); + const QString commitDate = logColorName(TextEditor::C_LOG_COMMIT_DATE); + const QString commitHash = logColorName(TextEditor::C_LOG_COMMIT_HASH); + const QString commitSubject = logColorName(TextEditor::C_LOG_COMMIT_SUBJECT); + const QString decoration = logColorName(TextEditor::C_LOG_DECORATION); + + const QString showFormat = QStringLiteral( + "--pretty=format:" + "commit %C(%1)%H%Creset %C(%2)%d%Creset%n" + "Author: %C(%3)%aN <%aE>%Creset, %C(%4)%ad (%ar)%Creset%n" + "Committer: %C(%3)%cN <%cE>%Creset, %C(%4)%cd (%cr)%Creset%n" + "%n%C(%5)%B%Creset" + ).arg(commitHash, decoration, authorName, commitDate, commitSubject); + setupCommand(process, {"show", "-s", colorOption, showFormat, id}); VcsOutputWindow::appendCommand(process.workingDirectory(), process.commandLine()); setDescription(Tr::tr("Waiting for data...")); }; @@ -496,7 +427,8 @@ ShowController::ShowController(IDocument *document, const QString &id) return; } const int lastHeaderLine = output.indexOf("\n\n") + 1; - data->m_commit = output.mid(7, 12); + const int commitPos = output.indexOf('m', 8) + 1; + data->m_commit = output.mid(commitPos, 12); data->m_header = output.left(lastHeaderLine); data->m_body = output.mid(lastHeaderLine + 1); updateDescription(*data); @@ -510,10 +442,20 @@ ShowController::ShowController(IDocument *document, const QString &id) const auto onBranchesSetup = [this, storage](Process &process) { storage->m_branches = busyMessage; - setupCommand(process, {"branch", noColorOption, "-a", "--contains", storage->m_commit}); + const QString branchesFormat = QStringLiteral( + "--format=" + "%(if:equals=refs/remotes)%(refname:rstrip=-2)%(then)" + "%(refname:lstrip=1)" + "%(else)" + "%(refname:lstrip=2)" + "%(end)" + ); + setupCommand(process, {"branch", noColorOption, "-a", branchesFormat, + "--contains", storage->m_commit}); VcsOutputWindow::appendCommand(process.workingDirectory(), process.commandLine()); }; - const auto onBranchesDone = [storage, updateDescription](const Process &process, DoneWith result) { + const auto onBranchesDone = [storage, updateDescription, decorateColor, noColor]( + const Process &process, DoneWith result) { ReloadStorage *data = storage.activeStorage(); data->m_branches.clear(); if (result == DoneWith::Success) { @@ -525,30 +467,30 @@ ShowController::ShowController(IDocument *document, const QString &id) bool first = true; const QStringList branchList = process.cleanedStdOut().split('\n'); for (const QString &branch : branchList) { - const QString b = branch.mid(2).trimmed(); - if (b.isEmpty()) + if (branch.isEmpty()) continue; - if (b.startsWith(remotePrefix)) { - const int nextSlash = b.indexOf('/', prefixLength); + if (branch.startsWith(remotePrefix)) { + const int nextSlash = branch.indexOf('/', prefixLength); if (nextSlash < 0) continue; - const QString remote = b.mid(prefixLength, nextSlash - prefixLength); + const QString remote = branch.mid(prefixLength, nextSlash - prefixLength); if (remote != previousRemote) { - data->m_branches += branchesDisplay(previousRemote, &branches, &first) - + '\n'; + data->m_branches += decorateColor + branchesDisplay(previousRemote, &branches, &first) + + noColor + '\n'; branches.clear(); previousRemote = remote; } - branches << b.mid(nextSlash + 1); + branches << branch.mid(nextSlash + 1); } else { - branches << b; + branches << branch; } } if (branches.isEmpty()) { if (previousRemote == localPrefix) - data->m_branches += Tr::tr(""); + data->m_branches += decorateColor + Tr::tr("") + noColor; } else { - data->m_branches += branchesDisplay(previousRemote, &branches, &first); + data->m_branches += decorateColor + branchesDisplay(previousRemote, &branches, &first) + + noColor; } data->m_branches = data->m_branches.trimmed(); } @@ -724,11 +666,6 @@ static bool gitHasRgbColors() return gitClient().gitVersion().result() >= QVersionNumber{2, 3}; } -static QString logColorName(TextEditor::TextStyle style) -{ - return colorForStyle(style).name(); -} - class GitLogArgumentsWidget : public BaseGitLogArgumentsWidget { Q_OBJECT