Git: Use ANSI colors for show description

Change-Id: I763e2283110b540b5ccebc30d685e5ea07357a73
Reviewed-by: Jarek Kobus <jaroslaw.kobus@qt.io>
Reviewed-by: André Hartmann <aha_1980@gmx.de>
This commit is contained in:
Orgad Shaneh
2024-08-05 21:37:36 +03:00
committed by Orgad Shaneh
parent e88f65866c
commit bf823eb581
7 changed files with 105 additions and 134 deletions

View File

@@ -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<FormattedText> AnsiEscapeCodeHandler::parseText(const FormattedText &input
DefaultBackgroundColor = 49
};
const QString escape = "\x1b[";
const QChar semicolon = ';';
const QChar colorTerminator = 'm';
const QChar eraseToEol = 'K';
QList<FormattedText> outputData;
@@ -93,7 +94,7 @@ QList<FormattedText> 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<FormattedText> 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<FormattedText> 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<FormattedText> 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<FormattedText> 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;<r>;<g>;<b>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;

View File

@@ -30,6 +30,8 @@ public:
QList<FormattedText> 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);

View File

@@ -26,6 +26,7 @@
#include <texteditor/texteditorsettings.h>
#include <utils/algorithm.h>
#include <utils/ansiescapecodehandler.h>
#include <utils/fileutils.h>
#include <utils/guard.h>
#include <utils/qtcassert.h>
@@ -442,6 +443,10 @@ void DiffEditor::updateDescription()
QTC_ASSERT(m_toolBar, return);
const QString description = m_document->description();
if (m_document->isDescriptionAnsiEnabled())
AnsiEscapeCodeHandler::setTextInEditor(m_descriptionWidget, description);
else
m_descriptionWidget->setPlainText(description);
m_descriptionWidget->setVisible(m_showDescription && !description.isEmpty());

View File

@@ -9,6 +9,7 @@
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/progressmanager/taskprogress.h>
#include <utils/ansiescapecodehandler.h>
#include <utils/qtcassert.h>
using namespace Core;
@@ -82,6 +83,11 @@ void DiffEditorController::setDiffFiles(const QList<FileData> &diffFileList)
m_document->setDiffFiles(diffFileList);
}
void DiffEditorController::setAnsiEnabled(bool enabled)
{
m_document->setDescriptionAnsiEnabled(enabled);
}
void DiffEditorController::setDescription(const QString &description)
{
m_document->setDescription(description);

View File

@@ -63,6 +63,7 @@ protected:
void setDiffFiles(const QList<FileData> &diffFileList);
// Optional:
void setDisplayName(const QString &name) { m_displayName = name; }
void setAnsiEnabled(bool enabled);
void setDescription(const QString &description);
void setDescriptionSyntaxHighlighterCreator(
const std::function<TextEditor::SyntaxHighlighter *()> &creator);

View File

@@ -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<TextEditor::SyntaxHighlighter *()> &creator);
std::function<TextEditor::SyntaxHighlighter *()> 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;

View File

@@ -22,9 +22,9 @@
#include <diffeditor/diffeditorconstants.h>
#include <texteditor/fontsettings.h>
#include <texteditor/syntaxhighlighter.h>
#include <texteditor/texteditorsettings.h>
#include <utils/ansiescapecodehandler.h>
#include <utils/async.h>
#include <utils/algorithm.h>
#include <utils/checkablemessagebox.h>
@@ -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("<resolving>");
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<ReloadStorage> 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("<None>");
data->m_branches += decorateColor + Tr::tr("<None>") + 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