diff options
author | Marcus Tillmanns <marcus.tillmanns@qt.io> | 2023-12-14 09:35:42 +0100 |
---|---|---|
committer | Marcus Tillmanns <marcus.tillmanns@qt.io> | 2023-12-14 10:14:02 +0000 |
commit | 3425959e21ac959b414390bccb36fb928e5953e0 (patch) | |
tree | 3331cdb5908b93e360060bb9c42e097cc5c2d20d /src/plugins/compilerexplorer | |
parent | 7b1213d9f8cec3d8a58c3f163c4ea3241b578095 (diff) |
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>
Diffstat (limited to 'src/plugins/compilerexplorer')
-rw-r--r-- | src/plugins/compilerexplorer/api/compile.h | 64 | ||||
-rw-r--r-- | src/plugins/compilerexplorer/compilerexplorereditor.cpp | 246 | ||||
-rw-r--r-- | src/plugins/compilerexplorer/compilerexplorereditor.h | 38 |
3 files changed, 296 insertions, 52 deletions
diff --git a/src/plugins/compilerexplorer/api/compile.h b/src/plugins/compilerexplorer/api/compile.h index 618af39a246..0f0493739d2 100644 --- a/src/plugins/compilerexplorer/api/compile.h +++ b/src/plugins/compilerexplorer/api/compile.h @@ -226,7 +226,7 @@ struct 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) struct Label @@ -246,34 +246,68 @@ struct CompileResult : CompilerResult label.range.endCol = object["range"]["endCol"].toInt(); 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; // The part of the source that generated this asm - struct + struct Source { - int column; + std::optional<int> column; QString file; 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; QStringList opcodes; - static Asm fromJson(const QJsonObject &object) + static AssemblyLine fromJson(const QJsonObject &object) { - Asm asm_; - asm_.text = object["text"].toString(); + AssemblyLine line; + line.text = object["text"].toString(); auto opcodes = object["opcodes"].toArray(); for (const auto &opcode : opcodes) - asm_.opcodes.append(opcode.toString()); - asm_.source.column = object["source"]["column"].toInt(); - asm_.source.file = object["source"]["file"].toString(); - asm_.source.line = object["source"]["line"].toInt(); + line.opcodes.append(opcode.toString()); + + auto itSource = object.find("source"); + 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()) { - 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 @@ -304,7 +338,7 @@ struct CompileResult : CompilerResult }; QMap<QString, int> labelDefinitions; - QList<Asm> assemblyLines; + QList<AssemblyLine> assemblyLines; std::optional<ExecResult> execResult; @@ -327,7 +361,7 @@ struct CompileResult : CompilerResult if (object.contains("asm")) { 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")) diff --git a/src/plugins/compilerexplorer/compilerexplorereditor.cpp b/src/plugins/compilerexplorer/compilerexplorereditor.cpp index dea0c439df9..4c0af4f73ea 100644 --- a/src/plugins/compilerexplorer/compilerexplorereditor.cpp +++ b/src/plugins/compilerexplorer/compilerexplorereditor.cpp @@ -15,9 +15,11 @@ #include <coreplugin/icore.h> #include <coreplugin/messagemanager.h> +#include <texteditor/fontsettings.h> #include <texteditor/textdocument.h> #include <texteditor/texteditor.h> #include <texteditor/texteditoractionhandler.h> +#include <texteditor/texteditorsettings.h> #include <texteditor/textmark.h> #include <projectexplorer/projectexplorerconstants.h> @@ -55,6 +57,13 @@ using namespace Utils; 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, QUndoStack *undoStack) : m_settings(settings) @@ -264,6 +273,57 @@ QString SourceEditorWidget::sourceCode() 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, const std::shared_ptr<CompilerSettings> &compilerSettings, QUndoStack *undoStack) @@ -286,8 +346,14 @@ CompilerWidget::CompilerWidget(const std::shared_ptr<SourceSettings> &sourceSett auto toolBar = new StyledBar; m_asmEditor = new AsmEditorWidget(undoStack); - m_asmDocument = QSharedPointer<TextDocument>(new TextDocument); + m_asmDocument = QSharedPointer<AsmDocument>(new AsmDocument); m_asmEditor->setTextDocument(m_asmDocument); + + connect(m_asmEditor, + &AsmEditorWidget::hoveredLineChanged, + this, + &CompilerWidget::hoveredLineChanged); + QTC_ASSERT_EXPECTED(m_asmEditor->configureGenericHighlighter("Intel x86 (NASM)"), m_asmEditor->configureGenericHighlighter( Utils::mimeTypeForName("text/x-asm"))); @@ -464,28 +530,9 @@ void CompilerWidget::doCompile() m_resultTerminal->writeToTerminal((out + "\r\n").toUtf8(), false); } } - qDeleteAll(m_marks); - m_marks.clear(); - - QString asmText; - for (auto l : r.assemblyLines) - 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); - } + + const QList<QTextEdit::ExtraSelection> links = m_asmDocument->setCompileResult(r); + m_asmEditor->setExtraSelections(AsmEditorLinks, links); } catch (const std::exception &e) { Core::MessageManager::writeDisrupting( Tr::tr("Failed to compile: \"%1\".").arg(QString::fromUtf8(e.what()))); @@ -541,9 +588,9 @@ void EditorWidget::focusInEvent(QFocusEvent *event) FancyMainWindow::focusInEvent(event); } -void EditorWidget::addCompiler(const std::shared_ptr<SourceSettings> &sourceSettings, - const std::shared_ptr<CompilerSettings> &compilerSettings, - int idx) +CompilerWidget *EditorWidget::addCompiler(const std::shared_ptr<SourceSettings> &sourceSettings, + const std::shared_ptr<CompilerSettings> &compilerSettings, + int idx) { auto compiler = new CompilerWidget(sourceSettings, compilerSettings, m_undoStack); compiler->setWindowTitle("Compiler #" + QString::number(idx)); @@ -563,6 +610,8 @@ void EditorWidget::addCompiler(const std::shared_ptr<SourceSettings> &sourceSett connect(compiler, &CompilerWidget::gotFocus, this, [this] { m_actionHandler.updateCurrentEditor(); }); + + return compiler; } QVariantMap EditorWidget::windowStateCallback() @@ -608,15 +657,27 @@ void EditorWidget::addSourceEditor(const std::shared_ptr<SourceSettings> &source addDockWidget(Qt::LeftDockWidgetArea, dockWidget); sourceSettings->compilers.forEachItem<CompilerSettings>( - [this, sourceSettings](const std::shared_ptr<CompilerSettings> &compilerSettings, int idx) { - addCompiler(sourceSettings, compilerSettings, idx + 1); + [this, + 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>( - [this, sourceSettings](const std::shared_ptr<CompilerSettings> &compilerSettings) { - addCompiler(sourceSettings->shared_from_this(), - compilerSettings, - sourceSettings->compilers.size()); + [this, sourceEditor, sourceSettings]( + const std::shared_ptr<CompilerSettings> &compilerSettings) { + auto compilerWidget = addCompiler(sourceSettings->shared_from_this(), + compilerSettings, + sourceSettings->compilers.size()); + + connect(compilerWidget, + &CompilerWidget::hoveredLineChanged, + sourceEditor, + &SourceEditorWidget::markSourceLocation); }); sourceSettings->compilers.setItemRemovedCallback<CompilerSettings>( @@ -885,8 +946,129 @@ EditorFactory::EditorFactory() 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) : 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 diff --git a/src/plugins/compilerexplorer/compilerexplorereditor.h b/src/plugins/compilerexplorer/compilerexplorereditor.h index 978bda4164f..b7ce26a5c39 100644 --- a/src/plugins/compilerexplorer/compilerexplorereditor.h +++ b/src/plugins/compilerexplorer/compilerexplorereditor.h @@ -30,6 +30,7 @@ namespace CompilerExplorer { class JsonSettingsDocument; class SourceEditorWidget; +class AsmDocument; class CodeEditorWidget : public TextEditor::TextEditorWidget { @@ -56,6 +57,19 @@ private: 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 { Q_OBJECT @@ -69,14 +83,25 @@ public: emit gotFocus(); } + void findLinkAt(const QTextCursor &, + const Utils::LinkHandler &processLinkCallback, + bool resolveTarget = true, + bool inNextSplit = false) override; + void undo() override { m_undoStack->undo(); } void redo() override { m_undoStack->redo(); } +protected: + void mouseMoveEvent(QMouseEvent *event) override; + void leaveEvent(QEvent *event) override; + signals: void gotFocus(); + void hoveredLineChanged(const std::optional<Api::CompileResult::AssemblyLine> &assemblyLine); private: QUndoStack *m_undoStack; + std::optional<Api::CompileResult::AssemblyLine> m_currentlyHoveredLine; }; class JsonSettingsDocument : public Core::IDocument @@ -128,6 +153,9 @@ public: TextEditor::TextEditorWidget *textEditor() { return m_codeEditor; } +public slots: + void markSourceLocation(const std::optional<Api::CompileResult::AssemblyLine> &assemblyLine); + signals: void sourceCodeChanged(); void remove(); @@ -163,19 +191,19 @@ private: signals: void remove(); void gotFocus(); + void hoveredLineChanged(const std::optional<Api::CompileResult::AssemblyLine> &assemblyLine); private: AsmEditorWidget *m_asmEditor{nullptr}; Core::SearchableTerminal *m_resultTerminal{nullptr}; SpinnerSolution::Spinner *m_spinner{nullptr}; - QSharedPointer<TextEditor::TextDocument> m_asmDocument; + QSharedPointer<AsmDocument> m_asmDocument; std::unique_ptr<QFutureWatcher<Api::CompileResult>> m_compileWatcher; QString m_source; QTimer *m_delayTimer{nullptr}; - QList<TextEditor::TextMark *> m_marks; }; class HelperWidget : public QWidget @@ -213,9 +241,9 @@ protected: void setupHelpWidget(); QWidget *createHelpWidget() const; - void addCompiler(const std::shared_ptr<SourceSettings> &sourceSettings, - const std::shared_ptr<CompilerSettings> &compilerSettings, - int idx); + CompilerWidget *addCompiler(const std::shared_ptr<SourceSettings> &sourceSettings, + const std::shared_ptr<CompilerSettings> &compilerSettings, + int idx); void addSourceEditor(const std::shared_ptr<SourceSettings> &sourceSettings); void removeSourceEditor(const std::shared_ptr<SourceSettings> &sourceSettings); |