Skip to content

Commit

Permalink
add ansi highlighter
Browse files Browse the repository at this point in the history
this patch adds a syntax highlighter that decoded ansi escape sequences
and colors the text accordingly
  • Loading branch information
lievenhey committed Oct 30, 2023
1 parent f765a2f commit 5273ad4
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 2 deletions.
77 changes: 75 additions & 2 deletions src/models/highlightedtext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,70 @@ class KSyntaxHighlightingImplementation : public HighlightingImplementation
#endif
}

QVector<QTextLayout::FormatRange> AnsiHighlightingImplementation::format(const QStringList& text)
{
QVector<QTextLayout::FormatRange> formats;

int offset = 0;
QTextLayout::FormatRange format;

constexpr int setColorSequenceLength = 5;
constexpr int resetColorSequenceLength = 4;
constexpr int colorCodeLength = 2;

for (const auto& line : text) {
auto lastToken = line.cbegin();
int lineOffset = 0;
for (auto escapeIt = std::find(line.cbegin(), line.cend(), Util::escapeChar); escapeIt != line.cend();
escapeIt = std::find(escapeIt, line.cend(), Util::escapeChar)) {

lineOffset += std::distance(lastToken, escapeIt);
Q_ASSERT(*(escapeIt + 1) == QLatin1Char('['));

// escapeIt + 2 points to the first color code character
auto color = QStringView {escapeIt + 2, colorCodeLength};
bool ok = false;
const uint8_t colorCode = color.toUInt(&ok);
if (ok) {
// only support the 8 default colors
Q_ASSERT(colorCode >= 30 && colorCode <= 37);

format.start = offset + lineOffset;
const auto colorRole = static_cast<KColorScheme::ForegroundRole>(colorCode - 30);
format.format.setForeground(m_colorScheme.foreground(colorRole));

std::advance(escapeIt, setColorSequenceLength);
} else {
// make sure we have a reset sequence
Q_ASSERT(color == QStringLiteral("0m"));
format.length = offset + lineOffset - format.start;
if (format.length) {
formats.push_back(format);
}

std::advance(escapeIt, resetColorSequenceLength);
}
lastToken = escapeIt;
}
offset += lineOffset + std::distance(lastToken, line.cend());
}

return formats;
}

void AnsiHighlightingImplementation::themeChanged()
{
m_colorScheme = KColorScheme(QPalette::Normal, KColorScheme::Complementary);
}

void AnsiHighlightingImplementation::setHighlightingDefinition(const KSyntaxHighlighting::Definition& /*definition*/) {
}

QString AnsiHighlightingImplementation::definitionName() const
{
return {};
}

HighlightedText::HighlightedText(KSyntaxHighlighting::Repository* repository, QObject* parent)
: QObject(parent)
#if KFSyntaxHighlighting_FOUND
Expand All @@ -127,8 +191,17 @@ HighlightedText::~HighlightedText() = default;

void HighlightedText::setText(const QStringList& text)
{
if (!m_highlighter) {
m_highlighter = std::make_unique<KSyntaxHighlightingImplementation>(m_repository);
const bool usesAnsi = std::any_of(text.cbegin(), text.cend(),
[](const QString& line) { return line.contains(QLatin1Char('\u001B')); });

if (!m_highlighter || m_isUsingAnsi != usesAnsi) {
if (usesAnsi) {
m_highlighter = std::make_unique<AnsiHighlightingImplementation>();
} else {
m_highlighter = std::make_unique<KSyntaxHighlightingImplementation>(m_repository);
}
m_isUsingAnsi = usesAnsi;
emit usesAnsiChanged(usesAnsi);
}

m_highlighter->themeChanged();
Expand Down
25 changes: 25 additions & 0 deletions src/models/highlightedtext.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ class HighlightingImplementation
virtual QString definitionName() const = 0;
};

// in public header so we can test it
class AnsiHighlightingImplementation : public HighlightingImplementation
{
public:
AnsiHighlightingImplementation() = default;
~AnsiHighlightingImplementation() override = default;

QVector<QTextLayout::FormatRange> format(const QStringList& text) override;

void themeChanged() override;

void setHighlightingDefinition(const KSyntaxHighlighting::Definition& definition) override;
QString definitionName() const override;

private:
KColorScheme m_colorScheme;
};

class HighlightedText : public QObject
{
Q_OBJECT
Expand All @@ -48,8 +66,14 @@ class HighlightedText : public QObject
QTextLine lineAt(int index) const;
QString definition() const;

bool isUsingAnsi() const
{
return m_isUsingAnsi;
}

signals:
void definitionChanged(const QString& definition);
void usesAnsiChanged(bool usesAnsi);

private slots:
void applyFormatting();
Expand All @@ -64,4 +88,5 @@ private slots:
std::unique_ptr<QTextLayout> m_layout;
QStringList m_lines;
QStringList m_cleanedLines;
bool m_isUsingAnsi = false;
};
3 changes: 3 additions & 0 deletions src/resultsdisassemblypage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,9 @@ ResultsDisassemblyPage::ResultsDisassemblyPage(CostContextMenu* costContextMenu,

connectCompletion(ui->sourceCodeComboBox, m_sourceCodeModel);
connectCompletion(ui->assemblyComboBox, m_disassemblyModel);

connect(m_disassemblyModel->highlightedText(), &HighlightedText::usesAnsiChanged, this,
[this](bool usesAnsi) { ui->customAssemblyHighlighting->setVisible(!usesAnsi); });
#else
ui->customSourceCodeHighlighting->setVisible(false);
ui->customAssemblyHighlighting->setVisible(false);
Expand Down
38 changes: 38 additions & 0 deletions tests/modeltests/tst_formatting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,44 @@ private slots:

QCOMPARE(Util::removeAnsi(ansiString), ansiFreeString);
}

void testFormattingValidAnsiSequences_data()
{
QTest::addColumn<QStringList>("ansiStrings");
QTest::addColumn<QVector<QTextLayout::FormatRange>>("formatting");

QTest::addRow("no ansi sequence")
<< QStringList {QStringLiteral(" A B C D E ")} << QVector<QTextLayout::FormatRange> {};
QTest::addRow("one ansi sequence") << QStringList {QStringLiteral("\u001B[33mHello World\u001B[0m")}
<< QVector<QTextLayout::FormatRange> {{0, 11, {}}};
QTest::addRow("two ansi sequences")
<< QStringList {QStringLiteral("\u001B[33mHello\u001B[0m \u001B[31mWorld\u001B[0m")}
<< QVector<QTextLayout::FormatRange> {{0, 5, {}}, {6, 5, {}}};

QTest::addRow("two ansi lines") << QStringList {QStringLiteral("\u001B[33mHello\u001B[0m\n"),
QStringLiteral("\u001B[31mWorld\u001B[0m")}
<< QVector<QTextLayout::FormatRange> {{0, 5, {}}, {6, 5, {}}};

QTest::addRow("two ansi sequences without break")
<< QStringList {QStringLiteral("\u001B[33m\u001B[0mhello\u001B[33m\u001B[0m")}
<< QVector<QTextLayout::FormatRange> {};
}

void testFormattingValidAnsiSequences()
{
QFETCH(QStringList, ansiStrings);
QFETCH(QVector<QTextLayout::FormatRange>, formatting);

AnsiHighlightingImplementation implementation;

auto format = implementation.format(ansiStrings);
QCOMPARE(format.size(), formatting.size());

for (int i = 0; i < format.size(); i++) {
QCOMPARE(format[i].start, formatting[i].start);
QCOMPARE(format[i].length, formatting[i].length);
}
}
};

QTEST_GUILESS_MAIN(TestFormatting)
Expand Down

0 comments on commit 5273ad4

Please sign in to comment.