Skip to content

Commit

Permalink
use cache for layout since QTextLayout::setFormats is slow
Browse files Browse the repository at this point in the history
QTextLayout::setFormats is really slow so this patch introduces a cache
named HighlightedLine which will only call QTextLayout::setFormats if
necessary
  • Loading branch information
lievenhey committed Dec 5, 2023
1 parent 565a132 commit 8963c85
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 113 deletions.
207 changes: 117 additions & 90 deletions src/models/highlightedtext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,11 @@ class HighlightingImplementation : public KSyntaxHighlighting::AbstractHighlight
}
~HighlightingImplementation() override = default;

virtual QVector<QTextLayout::FormatRange> format(const QStringList& text)
virtual QVector<QTextLayout::FormatRange> format(const QString& text)
{
m_formats.clear();
m_offset = 0;

KSyntaxHighlighting::State state;
for (const auto& line : text) {
state = highlightLine(line, state);

// KSyntaxHighlighting uses line offsets but QTextLayout uses global offsets
m_offset += line.size();
}
highlightLine(text, {});

return m_formats;
}
Expand Down Expand Up @@ -84,13 +77,12 @@ class HighlightingImplementation : public KSyntaxHighlighting::AbstractHighlight
{
QTextCharFormat textCharFormat;
textCharFormat.setForeground(format.textColor(theme()));
m_formats.push_back({m_offset + offset, length, textCharFormat});
m_formats.push_back({offset, length, textCharFormat});
}

private:
KSyntaxHighlighting::Repository* m_repository;
QVector<QTextLayout::FormatRange> m_formats;
int m_offset = 0;
};
#else
class HighlightingImplementation
Expand Down Expand Up @@ -122,7 +114,7 @@ class AnsiHighlightingImplementation : public HighlightingImplementation
}
~AnsiHighlightingImplementation() override = default;

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

Expand All @@ -133,41 +125,39 @@ class AnsiHighlightingImplementation : public HighlightingImplementation
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);
auto lastToken = text.begin();
for (auto escapeIt = std::find(text.cbegin(), text.cend(), Util::escapeChar); escapeIt != text.cend();
escapeIt = std::find(escapeIt, text.cend(), Util::escapeChar)) {

Q_ASSERT(*(escapeIt + 1) == QLatin1Char('['));

offset += std::distance(lastToken, escapeIt);

// 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;
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 - format.start;
if (format.length) {
formats.push_back(format);
}
lastToken = escapeIt;

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

lastToken = escapeIt;
}

return formats;
Expand All @@ -188,18 +178,76 @@ class AnsiHighlightingImplementation : public HighlightingImplementation
KColorScheme m_colorScheme;
};

class HighlightedLine : public QObject
{
Q_OBJECT
public:
HighlightedLine(HighlightingImplementation* highlighter, QString text, HighlightedText* parent)
: QObject(parent)
, m_highlighter(highlighter)
, m_text(std::move(text))
, m_layout(nullptr)
{
connect(parent, &HighlightedText::definitionChanged, this, &HighlightedLine::updateHighlighting);
}

~HighlightedLine() override = default;

QTextLayout* layout()
{
if (!m_layout) {
doLayout();
}
return m_layout.get();
}

public slots:
void updateHighlighting()
{
doLayout();
}

private:
void doLayout()
{
if (!m_layout) {
m_layout = std::make_unique<QTextLayout>();
m_layout->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
const auto& ansiFreeLine = Util::removeAnsi(m_text);
m_layout->setText(ansiFreeLine);
}

m_layout->setFormats(m_highlighter->format(m_text));

m_layout->beginLayout();

while (true) {
QTextLine line = m_layout->createLine();
if (!line.isValid())
break;
line.setNumColumns(100);
line.setPosition(QPointF(0, 0));
}
m_layout->endLayout();
}

HighlightingImplementation* m_highlighter;
QString m_text;
std::unique_ptr<QTextLayout> m_layout;
};

HighlightedText::HighlightedText(KSyntaxHighlighting::Repository* repository, QObject* parent)
: QObject(parent)
#if KFSyntaxHighlighting_FOUND
, m_repository(repository)
#endif
, m_layout(std::make_unique<QTextLayout>())
{
m_layout->setCacheEnabled(true);
m_layout->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
}

HighlightedText::~HighlightedText() = default;
HighlightedText::~HighlightedText()
{
deleteHighlightedLines();
}

void HighlightedText::setText(const QStringList& text)
{
Expand All @@ -217,64 +265,33 @@ void HighlightedText::setText(const QStringList& text)
}

m_highlighter->themeChanged();
m_highlighter->format(text);

m_lines.reserve(text.size());
m_cleanedLines.reserve(text.size());

QString formattedText;

for (const auto& line : text) {
const auto& lineWithNewline = QLatin1String("%1%2").arg(line, QChar::LineSeparator);
const auto& ansiFreeLine = Util::removeAnsi(lineWithNewline);
m_cleanedLines.push_back(ansiFreeLine);
m_lines.push_back(lineWithNewline);
formattedText += ansiFreeLine;
}

m_layout->setText(formattedText);
deleteHighlightedLines();
m_highlightedLines.reserve(text.size());
std::transform(text.cbegin(), text.cend(), std::back_inserter(m_highlightedLines),
[this](const QString& text) { return new HighlightedLine(m_highlighter.get(), text, this); });

applyFormatting();
m_cleanedLines.reserve(text.size());
std::transform(text.cbegin(), text.cend(), std::back_inserter(m_cleanedLines), Util::removeAnsi);
}

void HighlightedText::setDefinition(const KSyntaxHighlighting::Definition& definition)
{
Q_ASSERT(m_highlighter);
m_highlighter->setHighlightingDefinition(definition);
emit definitionChanged(definition.name());
applyFormatting();
}

QString HighlightedText::textAt(int index) const
{
Q_ASSERT(m_highlighter);
Q_ASSERT(index < m_cleanedLines.size());
return m_cleanedLines.at(index);
}

QTextLine HighlightedText::lineAt(int index) const
{
Q_ASSERT(m_layout);
return m_layout->lineAt(index);
}

void HighlightedText::applyFormatting()
{
Q_ASSERT(m_highlighter);

m_layout->setFormats(m_highlighter->format(m_lines));

m_layout->clearLayout();
m_layout->beginLayout();

while (true) {
QTextLine line = m_layout->createLine();
if (!line.isValid())
break;

line.setPosition(QPointF(0, 0));
}
m_layout->endLayout();
auto& line = m_highlightedLines[index];
return line->layout()->lineAt(0);
}

QString HighlightedText::definition() const
Expand All @@ -284,7 +301,17 @@ QString HighlightedText::definition() const
return m_highlighter->definitionName();
}

QTextLayout* HighlightedText::layout() const
void HighlightedText::deleteHighlightedLines()
{
// we have to manually clean up those
std::for_each(m_highlightedLines.cbegin(), m_highlightedLines.cend(),
[](const HighlightedLine* line) { delete line; });
m_highlightedLines.clear();
}

QTextLayout* HighlightedText::layoutForLine(int index)
{
return m_layout.get();
return m_highlightedLines[index]->layout();
}

#include "highlightedtext.moc"
9 changes: 4 additions & 5 deletions src/models/highlightedtext.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Repository;
}

class HighlightingImplementation;
class HighlightedLine;

class HighlightedText : public QObject
{
Expand All @@ -44,23 +45,21 @@ class HighlightedText : public QObject
}

// for testing
QTextLayout* layout() const;
QTextLayout* layoutForLine(int index);

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

private slots:
void applyFormatting();

private:
void deleteHighlightedLines();
void updateHighlighting();

#if KFSyntaxHighlighting_FOUND
KSyntaxHighlighting::Repository* m_repository;
#endif
std::unique_ptr<HighlightingImplementation> m_highlighter;
std::unique_ptr<QTextLayout> m_layout;
mutable QVector<HighlightedLine*> m_highlightedLines;
QStringList m_lines;
QStringList m_cleanedLines;
bool m_isUsingAnsi = false;
Expand Down
39 changes: 21 additions & 18 deletions tests/modeltests/tst_formatting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,42 +44,45 @@ private slots:
void testFormattingValidAnsiSequences_data()
{
QTest::addColumn<QStringList>("ansiStrings");
QTest::addColumn<QVector<QTextLayout::FormatRange>>("formatting");
QTest::addColumn<QVector<QVector<QTextLayout::FormatRange>>>("formatting");

QTest::addRow("no ansi sequence") << QStringList {QStringLiteral(" A B C D E ")}
<< QVector<QTextLayout::FormatRange> {{0, 16, {}}}; // only default formatting
QTest::addRow("no ansi sequence")
<< QStringList {QStringLiteral(" A B C D E ")}
<< QVector<QVector<QTextLayout::FormatRange>> {{{0, 15, {}}}}; // only default formatting
QTest::addRow("one ansi sequence") << QStringList {QStringLiteral("\u001B[33mHello World\u001B[0m")}
<< QVector<QTextLayout::FormatRange> {{0, 11, {}}};
<< QVector<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, {}}};
<< QVector<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, {}}, {7, 5, {}}};

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

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

HighlightedText highlighter(nullptr);

highlighter.setText(ansiStrings);
// we are testing the ansi highlighter, verify it is used
QVERIFY(highlighter.isUsingAnsi());

for (int ansiStringIndex = 0; ansiStringIndex < ansiStrings.count(); ansiStringIndex++) {
auto layout = highlighter.layoutForLine(ansiStringIndex);
QVERIFY(layout);
auto format = layout->formats();

auto layout = highlighter.layout();
QVERIFY(layout);
auto format = layout->formats();
QCOMPARE(format.size(), formatting.size());
QCOMPARE(format.size(), formatting[ansiStringIndex].size());

for (int i = 0; i < format.size(); i++) {
QCOMPARE(format[i].start, formatting[i].start);
QCOMPARE(format[i].length, formatting[i].length);
for (int i = 0; i < format.size(); i++) {
auto& formattingLine = formatting[ansiStringIndex];
QCOMPARE(format[i].start, formattingLine[i].start);
QCOMPARE(format[i].length, formattingLine[i].length);
}
}
}
};
Expand Down

0 comments on commit 8963c85

Please sign in to comment.