CompilerExplorer: Add assembly => source links

When hovering over the assembly, the matching source lines
are highlighted. Links inside the assembly are now clickable.

Change-Id: I22479a2e1badcfd95e0f341b2556fc93c8469625
Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
Marcus Tillmanns
2023-12-14 09:35:42 +01:00
parent 7b1213d9f8
commit 3425959e21
3 changed files with 296 additions and 52 deletions

View File

@@ -226,7 +226,7 @@ struct CompilerResult
struct CompileResult : CompilerResult struct CompileResult : CompilerResult
{ {
struct Asm struct AssemblyLine
{ {
// A part of the asm that is a (hyper)link to a label (the name references labelDefinitions) // A part of the asm that is a (hyper)link to a label (the name references labelDefinitions)
struct Label struct Label
@@ -246,34 +246,68 @@ struct CompileResult : CompilerResult
label.range.endCol = object["range"]["endCol"].toInt(); label.range.endCol = object["range"]["endCol"].toInt();
return label; return label;
} }
bool operator==(const Label &other) const
{
return name == other.name && range.startCol == other.range.startCol
&& range.endCol == other.range.endCol;
}
bool operator!=(const Label &other) const { return !(*this == other); }
}; };
QList<Label> labels; QList<Label> labels;
// The part of the source that generated this asm // The part of the source that generated this asm
struct struct Source
{ {
int column; std::optional<int> column;
QString file; QString file;
int line; int line;
} source; bool operator==(const Source &other) const
{
return column == other.column && file == other.file && line == other.line;
}
bool operator!=(const Source &other) const { return !(*this == other); }
};
std::optional<Source> source;
QString text; QString text;
QStringList opcodes; QStringList opcodes;
static Asm fromJson(const QJsonObject &object) static AssemblyLine fromJson(const QJsonObject &object)
{ {
Asm asm_; AssemblyLine line;
asm_.text = object["text"].toString(); line.text = object["text"].toString();
auto opcodes = object["opcodes"].toArray(); auto opcodes = object["opcodes"].toArray();
for (const auto &opcode : opcodes) for (const auto &opcode : opcodes)
asm_.opcodes.append(opcode.toString()); line.opcodes.append(opcode.toString());
asm_.source.column = object["source"]["column"].toInt();
asm_.source.file = object["source"]["file"].toString(); auto itSource = object.find("source");
asm_.source.line = object["source"]["line"].toInt(); if (itSource != object.end() && !itSource->isNull()) {
std::optional<int> columnOpt;
auto source = itSource->toObject();
auto itColumn = source.find("column");
if (itColumn != source.end() && !itColumn->isNull())
columnOpt = itColumn->toInt();
line.source = Source{
columnOpt,
source["file"].toString(),
source["line"].toInt(),
};
}
for (const auto &label : object["labels"].toArray()) { for (const auto &label : object["labels"].toArray()) {
asm_.labels.append(Label::fromJson(label.toObject())); line.labels.append(Label::fromJson(label.toObject()));
} }
return asm_; return line;
} }
bool operator==(const AssemblyLine &other) const
{
return source == other.source && text == other.text && opcodes == other.opcodes;
}
bool operator!=(const AssemblyLine &other) const { return !(*this == other); }
}; };
struct ExecResult struct ExecResult
@@ -304,7 +338,7 @@ struct CompileResult : CompilerResult
}; };
QMap<QString, int> labelDefinitions; QMap<QString, int> labelDefinitions;
QList<Asm> assemblyLines; QList<AssemblyLine> assemblyLines;
std::optional<ExecResult> execResult; std::optional<ExecResult> execResult;
@@ -327,7 +361,7 @@ struct CompileResult : CompilerResult
if (object.contains("asm")) { if (object.contains("asm")) {
for (const auto &asmLine : object["asm"].toArray()) for (const auto &asmLine : object["asm"].toArray())
result.assemblyLines.append(Asm::fromJson(asmLine.toObject())); result.assemblyLines.append(AssemblyLine::fromJson(asmLine.toObject()));
} }
if (object.contains("execResult")) if (object.contains("execResult"))

View File

@@ -15,9 +15,11 @@
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h> #include <coreplugin/messagemanager.h>
#include <texteditor/fontsettings.h>
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <texteditor/texteditoractionhandler.h> #include <texteditor/texteditoractionhandler.h>
#include <texteditor/texteditorsettings.h>
#include <texteditor/textmark.h> #include <texteditor/textmark.h>
#include <projectexplorer/projectexplorerconstants.h> #include <projectexplorer/projectexplorerconstants.h>
@@ -55,6 +57,13 @@ using namespace Utils;
namespace CompilerExplorer { namespace CompilerExplorer {
enum {
LinkProperty = QTextFormat::UserProperty + 10,
};
constexpr char AsmEditorLinks[] = "AsmEditor.Links";
constexpr char SourceEditorHoverLine[] = "SourceEditor.HoveredLine";
CodeEditorWidget::CodeEditorWidget(const std::shared_ptr<SourceSettings> &settings, CodeEditorWidget::CodeEditorWidget(const std::shared_ptr<SourceSettings> &settings,
QUndoStack *undoStack) QUndoStack *undoStack)
: m_settings(settings) : m_settings(settings)
@@ -264,6 +273,57 @@ QString SourceEditorWidget::sourceCode()
return {}; return {};
} }
void SourceEditorWidget::markSourceLocation(
const std::optional<Api::CompileResult::AssemblyLine> &assemblyLine)
{
if (!assemblyLine || !assemblyLine->source) {
m_codeEditor->setExtraSelections(SourceEditorHoverLine, {});
return;
}
auto source = *assemblyLine->source;
// If this is a location in a different file we cannot highlight it
if (!source.file.isEmpty()) {
m_codeEditor->setExtraSelections(SourceEditorHoverLine, {});
return;
}
// Lines are 1-based, so if we get 0 it means we don't have a valid location
if (source.line == 0) {
m_codeEditor->setExtraSelections(SourceEditorHoverLine, {});
return;
}
QList<QTextEdit::ExtraSelection> selections;
const TextEditor::FontSettings fs = TextEditor::TextEditorSettings::fontSettings();
QTextCharFormat background = fs.toTextCharFormat(TextEditor::C_CURRENT_LINE);
QTextCharFormat column = fs.toTextCharFormat(TextEditor::C_OCCURRENCES);
QTextBlock block = m_codeEditor->textDocument()->document()->findBlockByLineNumber(source.line
- 1);
QTextEdit::ExtraSelection selection;
selection.cursor = QTextCursor(m_codeEditor->textDocument()->document());
selection.cursor.setPosition(block.position());
selection.cursor.setPosition(qMax(block.position(), block.position() + block.length() - 1),
QTextCursor::KeepAnchor);
selection.cursor.setKeepPositionOnInsert(true);
selection.format = background;
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selections.append(selection);
if (source.column) {
selection.cursor.setPosition(block.position() + *source.column - 1);
selection.cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
selection.format = column;
selections.append(selection);
}
m_codeEditor->setExtraSelections(SourceEditorHoverLine, selections);
}
CompilerWidget::CompilerWidget(const std::shared_ptr<SourceSettings> &sourceSettings, CompilerWidget::CompilerWidget(const std::shared_ptr<SourceSettings> &sourceSettings,
const std::shared_ptr<CompilerSettings> &compilerSettings, const std::shared_ptr<CompilerSettings> &compilerSettings,
QUndoStack *undoStack) QUndoStack *undoStack)
@@ -286,8 +346,14 @@ CompilerWidget::CompilerWidget(const std::shared_ptr<SourceSettings> &sourceSett
auto toolBar = new StyledBar; auto toolBar = new StyledBar;
m_asmEditor = new AsmEditorWidget(undoStack); m_asmEditor = new AsmEditorWidget(undoStack);
m_asmDocument = QSharedPointer<TextDocument>(new TextDocument); m_asmDocument = QSharedPointer<AsmDocument>(new AsmDocument);
m_asmEditor->setTextDocument(m_asmDocument); m_asmEditor->setTextDocument(m_asmDocument);
connect(m_asmEditor,
&AsmEditorWidget::hoveredLineChanged,
this,
&CompilerWidget::hoveredLineChanged);
QTC_ASSERT_EXPECTED(m_asmEditor->configureGenericHighlighter("Intel x86 (NASM)"), QTC_ASSERT_EXPECTED(m_asmEditor->configureGenericHighlighter("Intel x86 (NASM)"),
m_asmEditor->configureGenericHighlighter( m_asmEditor->configureGenericHighlighter(
Utils::mimeTypeForName("text/x-asm"))); Utils::mimeTypeForName("text/x-asm")));
@@ -464,28 +530,9 @@ void CompilerWidget::doCompile()
m_resultTerminal->writeToTerminal((out + "\r\n").toUtf8(), false); m_resultTerminal->writeToTerminal((out + "\r\n").toUtf8(), false);
} }
} }
qDeleteAll(m_marks);
m_marks.clear();
QString asmText; const QList<QTextEdit::ExtraSelection> links = m_asmDocument->setCompileResult(r);
for (auto l : r.assemblyLines) m_asmEditor->setExtraSelections(AsmEditorLinks, links);
asmText += l.text + "\n";
m_asmDocument->setPlainText(asmText);
int i = 0;
for (auto l : r.assemblyLines) {
i++;
if (l.opcodes.empty())
continue;
auto mark = new TextMark(m_asmDocument.get(),
i,
TextMarkCategory{Tr::tr("Bytes"), "Bytes"});
m_asmDocument->addMark(mark);
mark->setLineAnnotation(l.opcodes.join(' '));
m_marks.append(mark);
}
} catch (const std::exception &e) { } catch (const std::exception &e) {
Core::MessageManager::writeDisrupting( Core::MessageManager::writeDisrupting(
Tr::tr("Failed to compile: \"%1\".").arg(QString::fromUtf8(e.what()))); Tr::tr("Failed to compile: \"%1\".").arg(QString::fromUtf8(e.what())));
@@ -541,7 +588,7 @@ void EditorWidget::focusInEvent(QFocusEvent *event)
FancyMainWindow::focusInEvent(event); FancyMainWindow::focusInEvent(event);
} }
void EditorWidget::addCompiler(const std::shared_ptr<SourceSettings> &sourceSettings, CompilerWidget *EditorWidget::addCompiler(const std::shared_ptr<SourceSettings> &sourceSettings,
const std::shared_ptr<CompilerSettings> &compilerSettings, const std::shared_ptr<CompilerSettings> &compilerSettings,
int idx) int idx)
{ {
@@ -563,6 +610,8 @@ void EditorWidget::addCompiler(const std::shared_ptr<SourceSettings> &sourceSett
connect(compiler, &CompilerWidget::gotFocus, this, [this] { connect(compiler, &CompilerWidget::gotFocus, this, [this] {
m_actionHandler.updateCurrentEditor(); m_actionHandler.updateCurrentEditor();
}); });
return compiler;
} }
QVariantMap EditorWidget::windowStateCallback() QVariantMap EditorWidget::windowStateCallback()
@@ -608,15 +657,27 @@ void EditorWidget::addSourceEditor(const std::shared_ptr<SourceSettings> &source
addDockWidget(Qt::LeftDockWidgetArea, dockWidget); addDockWidget(Qt::LeftDockWidgetArea, dockWidget);
sourceSettings->compilers.forEachItem<CompilerSettings>( sourceSettings->compilers.forEachItem<CompilerSettings>(
[this, sourceSettings](const std::shared_ptr<CompilerSettings> &compilerSettings, int idx) { [this,
addCompiler(sourceSettings, compilerSettings, idx + 1); sourceEditor,
sourceSettings](const std::shared_ptr<CompilerSettings> &compilerSettings, int idx) {
auto compilerWidget = addCompiler(sourceSettings, compilerSettings, idx + 1);
connect(compilerWidget,
&CompilerWidget::hoveredLineChanged,
sourceEditor,
&SourceEditorWidget::markSourceLocation);
}); });
sourceSettings->compilers.setItemAddedCallback<CompilerSettings>( sourceSettings->compilers.setItemAddedCallback<CompilerSettings>(
[this, sourceSettings](const std::shared_ptr<CompilerSettings> &compilerSettings) { [this, sourceEditor, sourceSettings](
addCompiler(sourceSettings->shared_from_this(), const std::shared_ptr<CompilerSettings> &compilerSettings) {
auto compilerWidget = addCompiler(sourceSettings->shared_from_this(),
compilerSettings, compilerSettings,
sourceSettings->compilers.size()); sourceSettings->compilers.size());
connect(compilerWidget,
&CompilerWidget::hoveredLineChanged,
sourceEditor,
&SourceEditorWidget::markSourceLocation);
}); });
sourceSettings->compilers.setItemRemovedCallback<CompilerSettings>( sourceSettings->compilers.setItemRemovedCallback<CompilerSettings>(
@@ -885,8 +946,129 @@ EditorFactory::EditorFactory()
setEditorCreator([this]() { return new Editor(m_actionHandler); }); setEditorCreator([this]() { return new Editor(m_actionHandler); });
} }
QList<QTextEdit::ExtraSelection> AsmDocument::setCompileResult(
const Api::CompileResult &compileResult)
{
m_assemblyLines = compileResult.assemblyLines;
document()->clear();
qDeleteAll(m_marks);
m_marks.clear();
QString asmText;
QTextCursor cursor(document());
QTextCharFormat linkFormat = TextEditor::TextEditorSettings::fontSettings().toTextCharFormat(
TextEditor::C_LINK);
QList<QTextEdit::ExtraSelection> links;
auto labelRow = [&labels = std::as_const(compileResult.labelDefinitions)](
const QString &labelName) -> std::optional<int> {
auto it = labels.find(labelName);
if (it != labels.end())
return *it;
return std::nullopt;
};
setPlainText(
Utils::transform(m_assemblyLines, [](const auto &line) { return line.text; }).join('\n'));
int currentLine = 0;
for (auto l : m_assemblyLines) {
currentLine++;
auto createLabelLink = [currentLine, &linkFormat, &cursor, labelRow](
const Api::CompileResult::AssemblyLine::Label &label) {
QTextEdit::ExtraSelection selection;
selection.cursor = cursor;
QTextBlock block = cursor.document()->findBlockByLineNumber(currentLine - 1);
selection.cursor.setPosition(block.position() + label.range.startCol - 1);
selection.cursor.setPosition(block.position() + label.range.endCol - 1,
QTextCursor::KeepAnchor);
selection.cursor.setKeepPositionOnInsert(true);
selection.format = linkFormat;
if (auto lRow = labelRow(label.name))
selection.format.setProperty(LinkProperty, *lRow);
return selection;
};
links.append(Utils::transform(l.labels, createLabelLink));
if (!l.opcodes.empty()) {
auto mark = new TextMark(this, currentLine, TextMarkCategory{Tr::tr("Bytes"), "Bytes"});
addMark(mark);
mark->setLineAnnotation(l.opcodes.join(' '));
m_marks.append(mark);
}
}
emit contentsChanged();
return links;
}
AsmEditorWidget::AsmEditorWidget(QUndoStack *stack) AsmEditorWidget::AsmEditorWidget(QUndoStack *stack)
: m_undoStack(stack) : m_undoStack(stack)
{} {}
void AsmEditorWidget::mouseMoveEvent(QMouseEvent *event)
{
const QTextCursor cursor = cursorForPosition(event->pos());
int line = cursor.block().blockNumber();
auto document = static_cast<AsmDocument *>(textDocument());
std::optional<Api::CompileResult::AssemblyLine> newLine;
if (line < document->asmLines().size())
newLine = document->asmLines()[line];
if (m_currentlyHoveredLine != newLine) {
m_currentlyHoveredLine = newLine;
emit hoveredLineChanged(newLine);
}
TextEditorWidget::mouseMoveEvent(event);
}
void AsmEditorWidget::leaveEvent(QEvent *event)
{
if (m_currentlyHoveredLine) {
m_currentlyHoveredLine = std::nullopt;
emit hoveredLineChanged(std::nullopt);
}
TextEditorWidget::leaveEvent(event);
}
void AsmEditorWidget::findLinkAt(const QTextCursor &cursor,
const Utils::LinkHandler &processLinkCallback,
bool,
bool)
{
QList<QTextEdit::ExtraSelection> links = this->extraSelections(AsmEditorLinks);
auto contains = [cursor](const QTextEdit::ExtraSelection &selection) {
if (selection.format.hasProperty(LinkProperty)
&& selection.cursor.selectionStart() <= cursor.position()
&& selection.cursor.selectionEnd() >= cursor.position()) {
return true;
}
return false;
};
if (std::optional<QTextEdit::ExtraSelection> selection = Utils::findOr(links,
std::nullopt,
contains)) {
const int row = selection->format.property(LinkProperty).toInt();
Link link{{}, row, 0};
link.linkTextStart = selection->cursor.selectionStart();
link.linkTextEnd = selection->cursor.selectionEnd();
processLinkCallback(link);
}
}
} // namespace CompilerExplorer } // namespace CompilerExplorer

View File

@@ -30,6 +30,7 @@ namespace CompilerExplorer {
class JsonSettingsDocument; class JsonSettingsDocument;
class SourceEditorWidget; class SourceEditorWidget;
class AsmDocument;
class CodeEditorWidget : public TextEditor::TextEditorWidget class CodeEditorWidget : public TextEditor::TextEditorWidget
{ {
@@ -56,6 +57,19 @@ private:
QUndoStack *m_undoStack; QUndoStack *m_undoStack;
}; };
class AsmDocument : public TextEditor::TextDocument
{
public:
using TextEditor::TextDocument::TextDocument;
QList<QTextEdit::ExtraSelection> setCompileResult(const Api::CompileResult &compileResult);
QList<Api::CompileResult::AssemblyLine> &asmLines() { return m_assemblyLines; }
private:
QList<Api::CompileResult::AssemblyLine> m_assemblyLines;
QList<TextEditor::TextMark *> m_marks;
};
class AsmEditorWidget : public TextEditor::TextEditorWidget class AsmEditorWidget : public TextEditor::TextEditorWidget
{ {
Q_OBJECT Q_OBJECT
@@ -69,14 +83,25 @@ public:
emit gotFocus(); emit gotFocus();
} }
void findLinkAt(const QTextCursor &,
const Utils::LinkHandler &processLinkCallback,
bool resolveTarget = true,
bool inNextSplit = false) override;
void undo() override { m_undoStack->undo(); } void undo() override { m_undoStack->undo(); }
void redo() override { m_undoStack->redo(); } void redo() override { m_undoStack->redo(); }
protected:
void mouseMoveEvent(QMouseEvent *event) override;
void leaveEvent(QEvent *event) override;
signals: signals:
void gotFocus(); void gotFocus();
void hoveredLineChanged(const std::optional<Api::CompileResult::AssemblyLine> &assemblyLine);
private: private:
QUndoStack *m_undoStack; QUndoStack *m_undoStack;
std::optional<Api::CompileResult::AssemblyLine> m_currentlyHoveredLine;
}; };
class JsonSettingsDocument : public Core::IDocument class JsonSettingsDocument : public Core::IDocument
@@ -128,6 +153,9 @@ public:
TextEditor::TextEditorWidget *textEditor() { return m_codeEditor; } TextEditor::TextEditorWidget *textEditor() { return m_codeEditor; }
public slots:
void markSourceLocation(const std::optional<Api::CompileResult::AssemblyLine> &assemblyLine);
signals: signals:
void sourceCodeChanged(); void sourceCodeChanged();
void remove(); void remove();
@@ -163,19 +191,19 @@ private:
signals: signals:
void remove(); void remove();
void gotFocus(); void gotFocus();
void hoveredLineChanged(const std::optional<Api::CompileResult::AssemblyLine> &assemblyLine);
private: private:
AsmEditorWidget *m_asmEditor{nullptr}; AsmEditorWidget *m_asmEditor{nullptr};
Core::SearchableTerminal *m_resultTerminal{nullptr}; Core::SearchableTerminal *m_resultTerminal{nullptr};
SpinnerSolution::Spinner *m_spinner{nullptr}; SpinnerSolution::Spinner *m_spinner{nullptr};
QSharedPointer<TextEditor::TextDocument> m_asmDocument; QSharedPointer<AsmDocument> m_asmDocument;
std::unique_ptr<QFutureWatcher<Api::CompileResult>> m_compileWatcher; std::unique_ptr<QFutureWatcher<Api::CompileResult>> m_compileWatcher;
QString m_source; QString m_source;
QTimer *m_delayTimer{nullptr}; QTimer *m_delayTimer{nullptr};
QList<TextEditor::TextMark *> m_marks;
}; };
class HelperWidget : public QWidget class HelperWidget : public QWidget
@@ -213,7 +241,7 @@ protected:
void setupHelpWidget(); void setupHelpWidget();
QWidget *createHelpWidget() const; QWidget *createHelpWidget() const;
void addCompiler(const std::shared_ptr<SourceSettings> &sourceSettings, CompilerWidget *addCompiler(const std::shared_ptr<SourceSettings> &sourceSettings,
const std::shared_ptr<CompilerSettings> &compilerSettings, const std::shared_ptr<CompilerSettings> &compilerSettings,
int idx); int idx);