Skip to content

Commit

Permalink
implement syntax highlighting with QTextLayout
Browse files Browse the repository at this point in the history
Using QTextDocument was a little bit overkill since we don't need rich
text support. This replaces it with a simple QTextLayout implementation.

closed: #522, #505
  • Loading branch information
lievenhey committed Nov 30, 2023
1 parent 37146a0 commit 85abc75
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 231 deletions.
6 changes: 4 additions & 2 deletions src/models/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
add_library(
models STATIC
models
STATIC
../settings.cpp
../util.cpp
callercalleemodel.cpp
Expand All @@ -12,7 +13,7 @@ add_library(
eventmodel.cpp
filterandzoomstack.cpp
frequencymodel.cpp
highlighter.cpp
highlightedtext.cpp
processfiltermodel.cpp
processlist_unix.cpp
processmodel.cpp
Expand All @@ -21,6 +22,7 @@ add_library(
timelinedelegate.cpp
topproxy.cpp
treemodel.cpp
formattingutils.cpp
)

target_link_libraries(
Expand Down
39 changes: 14 additions & 25 deletions src/models/disassemblymodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,15 @@
SPDX-License-Identifier: GPL-2.0-or-later
*/

#include <QFontDatabase>
#include <QTextBlock>
#include <QTextDocument>

#include "disassemblymodel.h"

#include "highlighter.hpp"
#include "search.h"
#include "sourcecodemodel.h"

DisassemblyModel::DisassemblyModel(KSyntaxHighlighting::Repository* repository, QObject* parent)
: QAbstractTableModel(parent)
, m_document(new QTextDocument(this))
, m_highlighter(new Highlighter(m_document, repository, this))
, m_highlightedText(repository)
{
m_document->setUndoRedoEnabled(false);
}

DisassemblyModel::~DisassemblyModel() = default;
Expand Down Expand Up @@ -56,17 +49,13 @@ void DisassemblyModel::setDisassembly(const DisassemblyOutput& disassemblyOutput
m_results = results;
m_numTypes = results.selfCosts.numTypes();

m_document->clear();

QTextCursor cursor(m_document);
cursor.beginEditBlock();
for (const auto& it : disassemblyOutput.disassemblyLines) {
cursor.insertText(it.disassembly);
cursor.insertBlock();
}
cursor.endEditBlock();
QStringList assemblyLines;
assemblyLines.reserve(disassemblyOutput.disassemblyLines.size());
std::transform(disassemblyOutput.disassemblyLines.cbegin(), disassemblyOutput.disassemblyLines.cend(),
std::back_inserter(assemblyLines),
[](const DisassemblyOutput::DisassemblyLine& line) { return line.disassembly; });

m_document->setTextWidth(m_document->idealWidth());
m_highlightedText.setText(assemblyLines);

endResetModel();
}
Expand Down Expand Up @@ -112,6 +101,7 @@ QVariant DisassemblyModel::data(const QModelIndex& index, int role) const
}

const auto& data = m_data.disassemblyLines.at(index.row());
const auto& line = m_highlightedText.textAt(index.row());
if (role == AddrRole)
return data.addr;

Expand All @@ -127,10 +117,10 @@ QVariant DisassemblyModel::data(const QModelIndex& index, int role) const
} else if (index.column() == HexdumpColumn) {
return data.hexdump;
} else if (index.column() == DisassemblyColumn) {
const auto block = m_document->findBlockByLineNumber(index.row());
if (role == SyntaxHighlightRole)
return QVariant::fromValue(block.layout()->lineAt(0));
return block.text();
if (role == SyntaxHighlightRole) {
return QVariant::fromValue(m_highlightedText.lineAt(index.row()));
}
return m_highlightedText.textAt(index.row());
}
}

Expand All @@ -153,7 +143,7 @@ QVariant DisassemblyModel::data(const QModelIndex& index, int role) const
return totalCost;
} else if (role == Qt::ToolTipRole) {
auto tooltip = tr("addr: <tt>%1</tt><br/>assembly: <tt>%2</tt><br/>disassembly: <tt>%3</tt>")
.arg(QString::number(data.addr, 16), data.disassembly);
.arg(QString::number(data.addr, 16), line);
return Util::formatTooltip(tooltip, locationCost, m_results.selfCosts);
}

Expand All @@ -162,8 +152,7 @@ QVariant DisassemblyModel::data(const QModelIndex& index, int role) const
return Util::formatCostRelative(costLine, totalCost, true);
} else {
if (role == Qt::ToolTipRole)
return tr("<qt><tt>%1</tt><hr/>No samples at this location.</qt>")
.arg(data.disassembly.toHtmlEscaped());
return tr("<qt><tt>%1</tt><hr/>No samples at this location.</qt>").arg(line.toHtmlEscaped());
else
return QString();
}
Expand Down
11 changes: 4 additions & 7 deletions src/models/disassemblymodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@

#include "data.h"
#include "disassemblyoutput.h"

class QTextDocument;
class Highlighter;
#include "highlightedtext.h"

namespace KSyntaxHighlighting {
class Definition;
Expand Down Expand Up @@ -47,9 +45,9 @@ class DisassemblyModel : public QAbstractTableModel
Data::FileLine fileLineForIndex(const QModelIndex& index) const;
QModelIndex indexForFileLine(const Data::FileLine& line) const;

Highlighter* highlighter() const
HighlightedText* highlightedText()
{
return m_highlighter;
return &m_highlightedText;
}

enum Columns
Expand Down Expand Up @@ -82,8 +80,7 @@ public slots:
void find(const QString& search, Direction direction, int offset);

private:
QTextDocument* m_document;
Highlighter* m_highlighter;
HighlightedText m_highlightedText;
DisassemblyOutput m_data;
Data::CallerCalleeResults m_results;
int m_numTypes = 0;
Expand Down
23 changes: 23 additions & 0 deletions src/models/formattingutils.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
SPDX-FileCopyrightText: Lieven Hey <[email protected]>
SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "formattingutils.h"

QString Util::removeAnsi(const QString& stringWithAnsi)
{
if (!stringWithAnsi.contains(escapeChar)) {
return stringWithAnsi;
}

QString ansiFreeString = stringWithAnsi;
while (ansiFreeString.contains(escapeChar)) {
const auto escapeStart = ansiFreeString.indexOf(escapeChar);
const auto escapeEnd = ansiFreeString.indexOf(QLatin1Char('m'), escapeStart);
ansiFreeString.remove(escapeStart, escapeEnd - escapeStart + 1);
}
return ansiFreeString;
}
17 changes: 17 additions & 0 deletions src/models/formattingutils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
SPDX-FileCopyrightText: Lieven Hey <[email protected]>
SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
SPDX-License-Identifier: GPL-2.0-or-later
*/

#pragma once

#include <QString>

namespace Util {
// escape character also known as \033 and \e it signals the start of an ansi escape sequence
constexpr auto escapeChar = QLatin1Char('\u001B');

QString removeAnsi(const QString& stringWithAnsi);
}
200 changes: 200 additions & 0 deletions src/models/highlightedtext.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
SPDX-FileCopyrightText: Lieven Hey <[email protected]>
SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "highlightedtext.h"

#include <QGuiApplication>
#include <QPalette>

#include <memory>

#include <KColorScheme>

#if KFSyntaxHighlighting_FOUND
#include <KSyntaxHighlighting/AbstractHighlighter>
#include <KSyntaxHighlighting/Definition>
#include <KSyntaxHighlighting/Format>
#include <KSyntaxHighlighting/Repository>
#include <KSyntaxHighlighting/State>
#include <KSyntaxHighlighting/SyntaxHighlighter>
#include <KSyntaxHighlighting/Theme>
#endif

#include "formattingutils.h"

#if KFSyntaxHighlighting_FOUND
class HighlightingImplementation : public KSyntaxHighlighting::AbstractHighlighter
{
public:
HighlightingImplementation(KSyntaxHighlighting::Repository* repository)
: m_repository(repository)
{
}
~HighlightingImplementation() override = default;

QVector<QTextLayout::FormatRange> format(const QStringList& 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();
}

return m_formats;
}

void themeChanged()
{
if (!m_repository) {
return;
}

KSyntaxHighlighting::Repository::DefaultTheme theme;
const auto palette = QGuiApplication::palette();
if (palette.base().color().lightness() < 128) {
theme = KSyntaxHighlighting::Repository::DarkTheme;
} else {
theme = KSyntaxHighlighting::Repository::LightTheme;
}
setTheme(m_repository->defaultTheme(theme));
}

void setHighlightingDefinition(const KSyntaxHighlighting::Definition& definition)
{
setDefinition(definition);
}

QString definitionName() const
{
return definition().name();
}

protected:
void applyFormat(int offset, int length, const KSyntaxHighlighting::Format& format) override
{
QTextCharFormat textCharFormat;
textCharFormat.setForeground(format.textColor(theme()));
m_formats.push_back({m_offset + offset, length, textCharFormat});
}

private:
KSyntaxHighlighting::Repository* m_repository;
QVector<QTextLayout::FormatRange> m_formats;
int m_offset = 0;
};
#else
class HighlightingImplementation
{
public:
HighlightingImplementation(KSyntaxHighlighting::Repository*) = default;
~HighlightingImplementation() override = default;

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

void themeChanged() override { }

void setHighlightingDefinition(const KSyntaxHighlighting::Definition& /*definition*/) override { }
QString definitionName() const override
{
return {};
};
#endif

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;

void HighlightedText::setText(const QStringList& text)
{
if (!m_highlighter) {
m_highlighter = std::make_unique<HighlightingImplementation>(m_repository);
}

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);

applyFormatting();
}

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();
}

QString HighlightedText::definition() const
{
if (!m_highlighter)
return {};
return m_highlighter->definitionName();
}
Loading

0 comments on commit 85abc75

Please sign in to comment.