From 82e1520ded54753f4a3b6999a122315974fae3b6 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Thu, 5 Feb 2026 17:25:51 -0700 Subject: [PATCH] CommandRow: * icon, SRC: label, remove separator, clean margins - Replace diamond with * for pixel-perfect fold alignment - Add SRC: label prefix to command row source field - Remove vertical bar separator from command row - Clear footer margin text (no more ---) - Remove + prefix from offset margin (0x instead of +0x) - Remove codicon font infrastructure (use editor font chars) --- src/compose.cpp | 88 +++++++++----- src/controller.cpp | 76 +++++++++++- src/controller.h | 2 + src/core.h | 79 ++++++++----- src/editor.cpp | 233 ++++++++++++++++++++++++++----------- src/editor.h | 6 + src/fonts/codicon.ttf | Bin 0 -> 76140 bytes src/format.cpp | 81 ++++++++----- src/main.cpp | 22 ++-- src/resources.qrc | 1 + tests/test_compose.cpp | 184 +++++++++++++++-------------- tests/test_editor.cpp | 258 ++++++++++++++++++++++++++--------------- tests/test_format.cpp | 13 ++- 13 files changed, 690 insertions(+), 353 deletions(-) create mode 100644 src/fonts/codicon.ttf diff --git a/src/compose.cpp b/src/compose.cpp index 55cd72e..85f0e63 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -38,7 +38,9 @@ struct ComposeState { void emitLine(const QString& lineText, LineMeta lm) { if (currentLine > 0) text += '\n'; // 3-char fold indicator column: " - " expanded, " + " collapsed, " " other - if (lm.foldHead) + if (lm.lineKind == LineKind::CommandRow) + text += QStringLiteral(" * "); + else if (lm.foldHead) text += lm.foldCollapsed ? QStringLiteral(" + ") : QStringLiteral(" - "); else text += QStringLiteral(" "); @@ -196,6 +198,10 @@ void composeParent(ComposeState& state, const NodeTree& tree, // Header line (skip for array element structs - condensed display) if (!isArrayChild) { + // Get per-scope widths for this header's parent scope + int typeW = state.effectiveTypeW(scopeId); + int nameW = state.effectiveNameW(scopeId); + LineMeta lm; lm.nodeIdx = nodeIdx; lm.nodeId = node.id; @@ -209,21 +215,20 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.markerMask = (1u << M_STRUCT_BG); lm.isRootHeader = (node.parentId == 0 && node.kind == NodeKind::Struct && !state.baseEmitted); if (lm.isRootHeader) state.baseEmitted = true; + lm.effectiveTypeW = typeW; + lm.effectiveNameW = nameW; QString headerText; if (node.kind == NodeKind::Array) { - // Array header with navigation: "uint32_t[16] name { <0/16>" + // Array header with navigation: "uint32_t[16] name {" (no brace when collapsed) lm.isArrayHeader = true; lm.elementKind = node.elementKind; lm.arrayViewIdx = node.viewIndex; lm.arrayCount = node.arrayLen; - headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex); - } else if (lm.isRootHeader) { - // Root structs show base address - headerText = fmt::fmtStructHeaderWithBase(node, depth, tree.baseAddress); + headerText = fmt::fmtArrayHeader(node, depth, node.viewIndex, node.collapsed, typeW, nameW); } else { - // Nested structs show normal header - headerText = fmt::fmtStructHeader(node, depth); + // All structs (root and nested) use the same header format + headerText = fmt::fmtStructHeader(node, depth, node.collapsed, typeW, nameW); } state.emitLine(headerText, lm); } @@ -254,7 +259,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.depth = depth; lm.lineKind = LineKind::Footer; lm.nodeKind = node.kind; - lm.offsetText = QStringLiteral(" ---"); + lm.offsetText.clear(); lm.foldLevel = computeFoldLevel(depth, false); lm.markerMask = 0; int sz = tree.structSpan(node.id, &state.childMap); @@ -341,27 +346,30 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { state.absOffsets[i] = tree.computeOffset(i); // Compute effective type column width from longest type name + // Include struct/array headers which use "struct TypeName" or "type[count]" format int maxTypeLen = kMinTypeW; for (const Node& node : tree.nodes) { QString typeName; if (node.kind == NodeKind::Array) { // Array type: "int32_t[10]", "char[64]", etc. typeName = fmt::arrayTypeName(node.elementKind, node.arrayLen); + } else if (node.kind == NodeKind::Struct) { + // Struct type: "struct TypeName" or "struct" + typeName = fmt::structTypeName(node); } else { typeName = fmt::typeNameRaw(node.kind); } - maxTypeLen = qMax(maxTypeLen, typeName.size()); + maxTypeLen = qMax(maxTypeLen, (int)typeName.size()); } state.typeW = qBound(kMinTypeW, maxTypeLen, kMaxTypeW); // Compute effective name column width from longest name + // Include struct/array names - they now use columnar layout too int maxNameLen = kMinNameW; for (const Node& node : tree.nodes) { // Skip hex/padding (they show ASCII preview, not name column) if (isHexPreview(node.kind)) continue; - // Skip containers (struct/array headers have different layout) - if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue; - maxNameLen = qMax(maxNameLen, node.name.size()); + maxNameLen = qMax(maxNameLen, (int)node.name.size()); } state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW); @@ -377,17 +385,19 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { for (int childIdx : state.childMap.value(container.id)) { const Node& child = tree.nodes[childIdx]; - // Skip containers - their headers don't use columnar layout - if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) - continue; + // Type width - include struct/array headers too (they now use columnar layout) + QString childTypeName; + if (child.kind == NodeKind::Array) + childTypeName = fmt::arrayTypeName(child.elementKind, child.arrayLen); + else if (child.kind == NodeKind::Struct) + childTypeName = fmt::structTypeName(child); + else + childTypeName = fmt::typeNameRaw(child.kind); + scopeMaxType = qMax(scopeMaxType, (int)childTypeName.size()); - // Type width - QString childTypeName = fmt::typeNameRaw(child.kind); - scopeMaxType = qMax(scopeMaxType, childTypeName.size()); - - // Name width (skip hex/padding) + // Name width (skip hex/padding, but include containers) if (!isHexPreview(child.kind)) { - scopeMaxName = qMax(scopeMaxName, child.name.size()); + scopeMaxName = qMax(scopeMaxName, (int)child.name.size()); } } @@ -396,26 +406,48 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) { } // Compute scope widths for root level (parentId == 0) + // Include struct/array headers - they now use columnar layout too { int rootMaxType = kMinTypeW; int rootMaxName = kMinNameW; for (int childIdx : state.childMap.value(0)) { const Node& child = tree.nodes[childIdx]; - // Skip containers - their headers don't use columnar layout - if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) - continue; + // Type width - include struct/array headers + QString childTypeName; + if (child.kind == NodeKind::Array) + childTypeName = fmt::arrayTypeName(child.elementKind, child.arrayLen); + else if (child.kind == NodeKind::Struct) + childTypeName = fmt::structTypeName(child); + else + childTypeName = fmt::typeNameRaw(child.kind); + rootMaxType = qMax(rootMaxType, (int)childTypeName.size()); - QString childTypeName = fmt::typeNameRaw(child.kind); - rootMaxType = qMax(rootMaxType, childTypeName.size()); + // Name width (skip hex/padding, include containers) if (!isHexPreview(child.kind)) { - rootMaxName = qMax(rootMaxName, child.name.size()); + rootMaxName = qMax(rootMaxName, (int)child.name.size()); } } state.scopeTypeW[0] = qBound(kMinTypeW, rootMaxType, kMaxTypeW); state.scopeNameW[0] = qBound(kMinNameW, rootMaxName, kMaxNameW); } + // Emit CommandRow as line 0 (synthetic UI line) + { + LineMeta lm; + lm.nodeIdx = -1; + lm.nodeId = kCommandRowId; + lm.depth = 0; + lm.lineKind = LineKind::CommandRow; + lm.foldLevel = SC_FOLDLEVELBASE; + lm.foldHead = false; + lm.offsetText.clear(); + lm.markerMask = 0; + lm.effectiveTypeW = state.typeW; + lm.effectiveNameW = state.nameW; + state.emitLine(QStringLiteral("SRC: File : 0x0"), lm); + } + QVector roots = state.childMap.value(0); std::sort(roots.begin(), roots.end(), [&](int a, int b) { return tree.nodes[a].offset < tree.nodes[b].offset; diff --git a/src/controller.cpp b/src/controller.cpp index 4734347..2901526 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -9,12 +10,36 @@ #include #include #include +#include namespace rcx { // Footer selection ID: set high bit to distinguish footer-only selections from node selections static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL; +static QString elide(QString s, int max) { + if (max <= 0) return {}; + if (s.size() <= max) return s; + if (max == 1) return QStringLiteral("\u2026"); + return s.left(max - 1) + QChar(0x2026); +} + +static QString crumbFor(const rcx::NodeTree& t, uint64_t nodeId) { + QStringList parts; + QSet seen; + uint64_t cur = nodeId; + while (cur != 0 && !seen.contains(cur)) { + seen.insert(cur); + int idx = t.indexOfId(cur); + if (idx < 0) break; + const auto& n = t.nodes[idx]; + parts << (n.name.isEmpty() ? QStringLiteral("") : n.name); + cur = n.parentId; + } + std::reverse(parts.begin(), parts.end()); + return parts.join(QStringLiteral(" > ")); +} + // ── RcxDocument ── RcxDocument::RcxDocument(QObject* parent) @@ -62,6 +87,7 @@ void RcxDocument::loadData(const QString& binaryPath) { return; undoStack.clear(); provider = std::make_unique(file.readAll()); + dataPath = binaryPath; tree.baseAddress = 0; emit documentChanged(); } @@ -102,6 +128,7 @@ RcxEditor* RcxController::addSplitEditor(QSplitter* splitter) { if (!m_lastResult.text.isEmpty()) { editor->applyDocument(m_lastResult); } + updateCommandRow(); return editor; } @@ -127,7 +154,8 @@ void RcxController::connectEditor(RcxEditor* editor) { // Inline editing signals connect(editor, &RcxEditor::inlineEditCommitted, this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text) { - if (nodeIdx < 0) { refresh(); return; } + // CommandRow BaseAddress/Source edit has nodeIdx=-1 + if (nodeIdx < 0 && target != EditTarget::BaseAddress && target != EditTarget::Source) { refresh(); return; } switch (target) { case EditTarget::Name: { if (text.isEmpty()) break; @@ -224,6 +252,15 @@ void RcxController::connectEditor(RcxEditor* editor) { } break; } + case EditTarget::Source: { + if (text == QStringLiteral("File")) { + auto* w = qobject_cast(parent()); + QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)"); + if (!path.isEmpty()) m_doc->loadData(path); + } + // "Process" is a placeholder — no action yet + break; + } case EditTarget::ArrayIndex: case EditTarget::ArrayCount: // Array navigation removed - these cases are unreachable @@ -242,8 +279,9 @@ void RcxController::refresh() { // Prune stale selections (nodes removed by undo/redo/delete) QSet valid; for (uint64_t id : m_selIds) { - if (m_doc->tree.indexOfId(id) >= 0) - valid.insert(id); + uint64_t nodeId = id & ~kFooterIdBit; // Strip footer bit for lookup + if (m_doc->tree.indexOfId(nodeId) >= 0) + valid.insert(id); // Keep original ID (with footer bit if present) } m_selIds = valid; @@ -253,6 +291,7 @@ void RcxController::refresh() { editor->restoreViewState(vs); } applySelectionOverlays(); + updateCommandRow(); } void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) { @@ -445,7 +484,8 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { tree.baseAddress = isUndo ? c.oldBase : c.newBase; } else if constexpr (std::is_same_v) { const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes; - m_doc->provider->writeBytes(c.addr, bytes); + if (!m_doc->provider->writeBytes(c.addr, bytes)) + qWarning() << "WriteBytes failed at address" << Qt::hex << c.addr; } else if constexpr (std::is_same_v) { int idx = tree.indexOfId(c.nodeId); if (idx >= 0) { @@ -732,6 +772,7 @@ void RcxController::handleNodeClick(RcxEditor* source, int line, } applySelectionOverlays(); + updateCommandRow(); if (m_selIds.size() == 1) { uint64_t sid = *m_selIds.begin(); @@ -745,6 +786,7 @@ void RcxController::clearSelection() { m_selIds.clear(); m_anchorLine = -1; applySelectionOverlays(); + updateCommandRow(); } void RcxController::applySelectionOverlays() { @@ -752,6 +794,32 @@ void RcxController::applySelectionOverlays() { editor->applySelectionOverlay(m_selIds); } +void RcxController::updateCommandRow() { + QString src; + if (!m_doc->filePath.isEmpty()) + src = QFileInfo(m_doc->filePath).fileName(); + else + src = QStringLiteral("File"); + if (!m_doc->dataPath.isEmpty()) + src += QStringLiteral(" @ ") + QFileInfo(m_doc->dataPath).fileName(); + + QString addr = QStringLiteral("0x") + + QString::number(m_doc->tree.baseAddress, 16).toUpper(); + QString path; + if (m_selIds.size() == 1) { + uint64_t sid = *m_selIds.begin(); + int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit); + if (idx >= 0) + path = crumbFor(m_doc->tree, m_doc->tree.nodes[idx].id); + } + + QString row = QStringLiteral(" * SRC: %1 : %2 %3") + .arg(elide(src, 40), elide(addr, 24), elide(path, 120)); + + for (auto* ed : m_editors) + ed->setCommandRowText(row); +} + void RcxController::handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers) { const LineMeta* lm = editor->metaForLine(line); diff --git a/src/controller.h b/src/controller.h index ffbf3e5..4596e55 100644 --- a/src/controller.h +++ b/src/controller.h @@ -23,6 +23,7 @@ public: std::unique_ptr provider; QUndoStack undoStack; QString filePath; + QString dataPath; bool modified = false; ComposeResult compose() const; @@ -95,6 +96,7 @@ private: void connectEditor(RcxEditor* editor); void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods); + void updateCommandRow(); }; } // namespace rcx diff --git a/src/core.h b/src/core.h index 23ec5fc..33f2cff 100644 --- a/src/core.h +++ b/src/core.h @@ -243,6 +243,7 @@ struct Node { NodeKind elementKind = NodeKind::UInt8; // Array: element type int viewIndex = 0; // Array: current view offset (transient) + // Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size. int byteSize() const { switch (kind) { case NodeKind::UTF8: return strLen; @@ -388,7 +389,14 @@ struct NodeTree { } int structSpan(uint64_t structId, - const QHash>* childMap = nullptr) const { + const QHash>* childMap = nullptr, + QSet* visited = nullptr) const { + QSet localVisited; + if (!visited) visited = &localVisited; + + if (visited->contains(structId)) return 0; // Cycle detected + visited->insert(structId); + int idx = indexOfId(structId); if (idx < 0) return 0; @@ -400,7 +408,7 @@ struct NodeTree { for (int ci : kids) { const Node& c = nodes[ci]; int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) - ? structSpan(c.id, childMap) : c.byteSize(); + ? structSpan(c.id, childMap, visited) : c.byteSize(); int end = c.offset + sz; if (end > maxEnd) maxEnd = end; } @@ -440,9 +448,14 @@ struct NodeTree { // ── LineMeta ── enum class LineKind : uint8_t { + CommandRow, // line 0 only, synthetic UI Header, Field, Continuation, Footer, ArrayElementSeparator }; +static constexpr uint64_t kCommandRowId = UINT64_MAX; +static constexpr int kCommandRowLine = 0; +static constexpr int kFirstDataLine = 1; + struct LineMeta { int nodeIdx = -1; uint64_t nodeId = 0; @@ -466,6 +479,10 @@ struct LineMeta { int effectiveNameW = 22; // Per-line name column width used for rendering }; +inline bool isSyntheticLine(const LineMeta& lm) { + return lm.lineKind == LineKind::CommandRow; +} + // ── Layout Info ── struct LayoutInfo { @@ -513,7 +530,7 @@ struct ColumnSpan { bool valid = false; }; -enum class EditTarget { Name, Type, Value, BaseAddress, ArrayIndex, ArrayCount }; +enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount }; // Column layout constants (shared with format.cpp span computation) inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line @@ -524,9 +541,9 @@ inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address) inline constexpr int kSepWidth = 1; inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t") -inline constexpr int kMaxTypeW = 14; // Maximum type column width (fits "uint64_t[999]") +inline constexpr int kMaxTypeW = 128; // Maximum type column width inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview) -inline constexpr int kMaxNameW = 22; // Maximum name column width (= kColName) +inline constexpr int kMaxNameW = 128; // Maximum name column width inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) { if (lm.lineKind != LineKind::Field || lm.isContinuation) return {}; @@ -592,31 +609,29 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = return {start, lineLength, start < lineLength}; } -// Base address span (only valid for root struct headers) -// Line format: " - struct Name { // base: 0x00400000" -inline ColumnSpan baseAddressSpanFor(const LineMeta& lm, const QString& lineText) { - if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {}; - // Find "// base: " after the opening brace - int baseIdx = lineText.indexOf(QStringLiteral("// base: ")); - if (baseIdx < 0) return {}; - int startPos = baseIdx + 9; // after "// base: " - // Value goes to end of line - int endPos = lineText.size(); - while (endPos > startPos && lineText[endPos-1].isSpace()) - endPos--; - if (endPos <= startPos) return {}; - return {startPos, endPos, true}; +// ── CommandRow spans ── +// Line format: " * SRC: File : 0x140000000 path > here" + +inline ColumnSpan commandRowSrcSpan(const QString& lineText) { + int idx = lineText.indexOf(QStringLiteral(" : ")); + if (idx < 0) return {}; + // Skip past "SRC: " label to expose just the source name + int srcTag = lineText.indexOf(QStringLiteral("SRC: ")); + int start = (srcTag >= 0 && srcTag < idx) ? srcTag + 5 : 0; + while (start < idx && !lineText[start].isLetterOrNumber()) start++; + if (start >= idx) return {}; + return {start, idx, true}; } -// Full "// base: 0x..." span for coloring (includes "// base: " prefix) -inline ColumnSpan baseAddressFullSpanFor(const LineMeta& lm, const QString& lineText) { - if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {}; - int baseIdx = lineText.indexOf(QStringLiteral("// base: ")); - if (baseIdx < 0) return {}; - int endPos = lineText.size(); - while (endPos > baseIdx && lineText[endPos-1].isSpace()) - endPos--; - return {baseIdx, endPos, true}; +inline ColumnSpan commandRowAddrSpan(const QString& lineText) { + int idx = lineText.indexOf(QStringLiteral(" : ")); + if (idx < 0) return {}; + int start = idx + 3; // after " : " + int end = lineText.indexOf(QStringLiteral(" "), start); // next double-space + if (end < 0) end = lineText.size(); + while (end > start && lineText[end-1].isSpace()) end--; + if (end <= start) return {}; + return {start, end, true}; } // ── Array navigation spans ── @@ -683,11 +698,11 @@ namespace fmt { QString fmtNodeLine(const Node& node, const Provider& prov, uint64_t addr, int depth, int subLine = 0, const QString& comment = {}, int colType = kColType, int colName = kColName); - QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation); - QString fmtStructHeader(const Node& node, int depth); - QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress); + QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation); + QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName); QString fmtStructFooter(const Node& node, int depth, int totalSize = -1); - QString fmtArrayHeader(const Node& node, int depth, int viewIdx); + QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName); + QString structTypeName(const Node& node); // Full type string for struct headers QString arrayTypeName(NodeKind elemKind, int count); QString validateBaseAddress(const QString& text); QString indent(int depth); diff --git a/src/editor.cpp b/src/editor.cpp index 6653aec..0458b14 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -77,7 +77,9 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { connect(m_sci, &QsciScintilla::userListActivated, this, [this](int id, const QString& text) { - if (id == 1 && m_editState.active && m_editState.target == EditTarget::Type) { + if (!m_editState.active) return; + if ((id == 1 && m_editState.target == EditTarget::Type) || + (id == 2 && m_editState.target == EditTarget::Source)) { auto info = endInlineEdit(); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text); } @@ -88,6 +90,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { connect(m_sci, &QsciScintilla::textChanged, this, [this]() { if (!m_editState.active) return; + if (m_updatingComment) return; // Skip queuing during comment update if (m_editState.target == EditTarget::Value) QTimer::singleShot(0, this, &RcxEditor::validateEditLive); if (m_editState.target == EditTarget::Type) @@ -147,6 +150,7 @@ void RcxEditor::setupScintilla() { IND_HOVER_SPAN, 17 /*INDIC_TEXTFORE*/); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_HOVER_SPAN, QColor("#3d9c8a")); + } void RcxEditor::setupLexer() { @@ -188,7 +192,7 @@ void RcxEditor::setupMargins() { // Margin 0: Offset text m_sci->setMarginType(0, QsciScintilla::TextMarginRightJustified); - m_sci->setMarginWidth(0, " +0x00000000 "); + m_sci->setMarginWidth(0, " 0x00000000 "); m_sci->setMarginsBackgroundColor(kBgMargin); m_sci->setMarginsForegroundColor(kFgMarginDim); m_sci->setMarginSensitivity(0, true); @@ -307,6 +311,7 @@ void RcxEditor::applyMarginText(const QVector& meta) { m_sci->clearMarginText(-1); for (int i = 0; i < meta.size(); i++) { + if (isSyntheticLine(meta[i])) continue; const auto& lm = meta[i]; if (lm.offsetText.isEmpty()) continue; @@ -324,6 +329,7 @@ void RcxEditor::applyMarkers(const QVector& meta) { m_sci->markerDeleteAll(m); } for (int i = 0; i < meta.size(); i++) { + if (isSyntheticLine(meta[i])) continue; uint32_t mask = meta[i].markerMask; for (int m = M_CONT; m <= M_STRUCT_BG; m++) { if (mask & (1u << m)) { @@ -391,6 +397,7 @@ void RcxEditor::applySelectionOverlay(const QSet& selIds) { m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen); for (int i = 0; i < m_meta.size(); i++) { + if (isSyntheticLine(m_meta[i])) continue; uint64_t nodeId = m_meta[i].nodeId; bool isFooter = (m_meta[i].lineKind == LineKind::Footer); @@ -511,16 +518,16 @@ static QString getLineText(QsciScintilla* sci, int line) { void RcxEditor::applyBaseAddressColoring(const QVector& meta) { m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_BASE_ADDR); - for (int i = 0; i < meta.size(); i++) { - const LineMeta& lm = meta[i]; - if (!lm.isRootHeader) continue; - QString lineText = getLineText(m_sci, i); - ColumnSpan span = baseAddressFullSpanFor(lm, lineText); - if (!span.valid) continue; - long posA = posFromCol(m_sci, i, span.start); - long posB = posFromCol(m_sci, i, span.end); - if (posB > posA) - m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA); + // Color the ADDR span on CommandRow (line 0) + if (!meta.isEmpty() && meta[0].lineKind == LineKind::CommandRow) { + QString lineText = getLineText(m_sci, 0); + ColumnSpan span = commandRowAddrSpan(lineText); + if (span.valid) { + long posA = posFromCol(m_sci, 0, span.start); + long posB = posFromCol(m_sci, 0, span.end); + if (posB > posA) + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA); + } } } @@ -553,55 +560,63 @@ RcxEditor::EndEditInfo RcxEditor::endInlineEdit() { // ── Span helpers ── -// Name span for struct/array headers -// Format: "struct TYPENAME NAME {" or "struct NAME {" or "type[N] NAME {" -// Returns span of the last word before " {" +// Name span for struct/array headers - uses column-based positioning +// Format: [fold][indent][type col][sep][name col][sep][suffix] static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) { if (lm.lineKind != LineKind::Header) return {}; - int bracePos = lineText.lastIndexOf(QStringLiteral(" {")); - if (bracePos <= 0) return {}; - // Find the last space before " {" - the name starts after that - int nameStart = lineText.lastIndexOf(' ', bracePos - 1); - if (nameStart < 0) return {}; - nameStart++; // Move past the space + int ind = kFoldCol + lm.depth * 3; + int typeW = lm.effectiveTypeW; + int nameW = lm.effectiveNameW; + int nameStart = ind + typeW + kSepWidth; + int nameEnd = nameStart + nameW; + + // Clamp to line length + if (nameStart >= lineText.size()) return {}; + if (nameEnd > lineText.size()) nameEnd = lineText.size(); // Don't allow editing array element names like "[0]", "[1]", etc. - QString name = lineText.mid(nameStart, bracePos - nameStart); + QString name = lineText.mid(nameStart, nameEnd - nameStart).trimmed(); if (name.startsWith('[') && name.endsWith(']')) return {}; - return {nameStart, bracePos, true}; + return {nameStart, nameEnd, true}; } // Type name span for struct headers (not arrays) -// Format: "struct TYPENAME NAME {" - returns span of TYPENAME +// Format: "struct TYPENAME NAME {" or collapsed variants // For "struct NAME {" (no typename), returns invalid span static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText) { if (lm.lineKind != LineKind::Header) return {}; if (lm.isArrayHeader) return {}; // Arrays use arrayHeaderTypeSpan instead - int bracePos = lineText.lastIndexOf(QStringLiteral(" {")); - if (bracePos <= 0) return {}; int ind = kFoldCol + lm.depth * 3; + int typeW = lm.effectiveTypeW; + int typeEnd = ind + typeW; + + // Clamp to actual line content + if (typeEnd > lineText.size()) typeEnd = lineText.size(); + + // Extract the type column text and check if it has a typename + // Format: "struct" or "struct TYPENAME" + QString typeCol = lineText.mid(ind, typeEnd - ind).trimmed(); // Find first space (after "struct") - int firstSpace = lineText.indexOf(' ', ind); - if (firstSpace <= ind || firstSpace >= bracePos) return {}; + int firstSpace = typeCol.indexOf(' '); + if (firstSpace < 0) return {}; // Just "struct", no typename - // Find second space (after typename, before name) - int secondSpace = lineText.indexOf(' ', firstSpace + 1); - if (secondSpace <= firstSpace || secondSpace >= bracePos) return {}; // No typename + // If there's content after "struct ", that's the typename + QString typename_ = typeCol.mid(firstSpace + 1).trimmed(); + if (typename_.isEmpty()) return {}; - // Find third space (after name) - if exists, we have typename - int thirdSpace = lineText.indexOf(' ', secondSpace + 1); - if (thirdSpace < 0 || thirdSpace > bracePos) { - // Only two words: "struct NAME {" - no typename to edit - return {}; - } + // Return span of the typename within the type column + int typenameStart = ind + firstSpace + 1; + // Find where the typename actually ends (skip padding) + int typenameEnd = typenameStart; + while (typenameEnd < typeEnd && lineText[typenameEnd] != ' ') + typenameEnd++; - // Three+ words: "struct TYPENAME NAME {" - return typename span - return {firstSpace + 1, secondSpace, true}; + return {typenameStart, typenameEnd, true}; } // Type span for array headers: "int32_t[10]" in "int32_t[10] positions {" @@ -656,7 +671,21 @@ RcxEditor::NormalizedSpan RcxEditor::normalizeSpan( bool RcxEditor::resolvedSpanFor(int line, EditTarget t, NormalizedSpan& out, QString* lineTextOut) const { const LineMeta* lm = metaForLine(line); - if (!lm || lm->nodeIdx < 0) return false; + if (!lm) return false; + + // CommandRow: BaseAddress (ADDR) and Source (SRC) editing + if (lm->lineKind == LineKind::CommandRow) { + if (t != EditTarget::BaseAddress && t != EditTarget::Source) return false; + QString lineText = getLineText(m_sci, line); + ColumnSpan s = (t == EditTarget::Source) + ? commandRowSrcSpan(lineText) + : commandRowAddrSpan(lineText); + out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/(t == EditTarget::BaseAddress)); + if (lineTextOut) *lineTextOut = lineText; + return out.valid; + } + + if (lm->nodeIdx < 0) return false; QString lineText = getLineText(m_sci, line); int textLen = lineText.size(); @@ -670,7 +699,7 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, case EditTarget::Type: s = typeSpan(*lm, typeW); break; case EditTarget::Name: s = nameSpan(*lm, typeW, nameW); break; case EditTarget::Value: s = valueSpan(*lm, textLen, typeW, nameW); break; - case EditTarget::BaseAddress: s = baseAddressSpanFor(*lm, lineText); break; + case EditTarget::BaseAddress: break; // No longer on header lines case EditTarget::ArrayIndex: case EditTarget::ArrayCount: break; // Array navigation removed @@ -741,21 +770,28 @@ static bool hitTestTarget(QsciScintilla* sci, const LineMeta& lm = meta[line]; - // Array element separators are not interactive if (lm.lineKind == LineKind::ArrayElementSeparator) return false; - // Use per-line effective widths from LineMeta - int typeW = lm.effectiveTypeW; - int nameW = lm.effectiveNameW; - auto inSpan = [&](const ColumnSpan& s) { return s.valid && col >= s.start && col < s.end; }; + // CommandRow: SRC and ADDR fields are interactive + if (lm.lineKind == LineKind::CommandRow) { + ColumnSpan ss = commandRowSrcSpan(lineText); + if (inSpan(ss)) { outTarget = EditTarget::Source; outLine = line; return true; } + ColumnSpan as = commandRowAddrSpan(lineText); + if (inSpan(as)) { outTarget = EditTarget::BaseAddress; outLine = line; return true; } + return false; + } + + // Use per-line effective widths from LineMeta + int typeW = lm.effectiveTypeW; + int nameW = lm.effectiveNameW; + ColumnSpan ts = RcxEditor::typeSpan(lm, typeW); ColumnSpan ns = RcxEditor::nameSpan(lm, typeW, nameW); ColumnSpan vs = RcxEditor::valueSpan(lm, textLen, typeW, nameW); - ColumnSpan bs = baseAddressSpanFor(lm, lineText); // Base address for root headers // Fallback spans for header lines if (!ts.valid) { @@ -766,8 +802,7 @@ static bool hitTestTarget(QsciScintilla* sci, if (!ns.valid) ns = headerNameSpan(lm, lineText); - if (inSpan(bs)) outTarget = EditTarget::BaseAddress; - else if (inSpan(ts)) outTarget = EditTarget::Type; + if (inSpan(ts)) outTarget = EditTarget::Type; else if (inSpan(ns)) outTarget = EditTarget::Name; else if (inSpan(vs)) outTarget = EditTarget::Value; else return false; @@ -807,7 +842,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { case EditTarget::Type: raw = typeSpan(*lm, typeW); break; case EditTarget::Name: raw = nameSpan(*lm, typeW, nameW); break; case EditTarget::Value: raw = valueSpan(*lm, lineText.size(), typeW, nameW); break; - case EditTarget::BaseAddress: raw = baseAddressSpanFor(*lm, lineText); break; + case EditTarget::BaseAddress: raw = commandRowAddrSpan(lineText); break; + case EditTarget::Source: raw = commandRowSrcSpan(lineText); break; case EditTarget::ArrayIndex: raw = arrayIndexSpanFor(*lm, lineText); break; case EditTarget::ArrayCount: raw = arrayCountSpanFor(*lm, lineText); break; } @@ -845,6 +881,13 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { emit marginClicked(0, h.line, me->modifiers()); return true; } + // CommandRow: try ADDR edit or consume + if (h.nodeId == kCommandRowId) { + int tLine; EditTarget t; + if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, t)) + beginInlineEdit(t, tLine); + return true; // consume all CommandRow clicks + } if (h.nodeId != 0) { bool alreadySelected = m_currentSelIds.contains(h.nodeId); bool plain = !(me->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier)); @@ -1024,8 +1067,12 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { case Qt::Key_PageUp: case Qt::Key_PageDown: return true; // block line navigation - case Qt::Key_Delete: - return true; // block to prevent eating trailing content + case Qt::Key_Delete: { + int line, col; + m_sci->getCursorPosition(&line, &col); + if (col >= editEndCol()) return true; // block at end + return false; // allow delete within span + } case Qt::Key_Left: case Qt::Key_Backspace: { int line, col; @@ -1067,7 +1114,11 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { int col; m_sci->getCursorPosition(&line, &col); auto* lm = metaForLine(line); - if (!lm || lm->nodeIdx < 0) return false; + if (!lm) return false; + // Allow nodeIdx=-1 only for CommandRow BaseAddress/Source editing + if (lm->nodeIdx < 0 && !(lm->lineKind == LineKind::CommandRow && + (target == EditTarget::BaseAddress || target == EditTarget::Source))) + return false; QString lineText; NormalizedSpan norm; @@ -1134,6 +1185,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { if (target == EditTarget::Type) QTimer::singleShot(0, this, &RcxEditor::showTypeAutocomplete); + if (target == EditTarget::Source) + QTimer::singleShot(0, this, &RcxEditor::showSourcePicker); return true; } @@ -1147,9 +1200,8 @@ int RcxEditor::editEndCol() const { void RcxEditor::clampEditSelection() { if (!m_editState.active) return; - static bool s_clamping = false; - if (s_clamping) return; - s_clamping = true; + if (m_clampingSelection) return; + m_clampingSelection = true; int selStartLine, selStartCol, selEndLine, selEndCol; m_sci->getSelection(&selStartLine, &selStartCol, &selEndLine, &selEndCol); @@ -1159,7 +1211,7 @@ void RcxEditor::clampEditSelection() { // Don't fight cursor positioning - only clamp actual selections if (isCursor) { - s_clamping = false; + m_clampingSelection = false; return; } @@ -1170,7 +1222,7 @@ void RcxEditor::clampEditSelection() { if (selStartLine != m_editState.line || selEndLine != m_editState.line) { m_sci->setSelection(m_editState.line, m_editState.spanStart, m_editState.line, editEnd); - s_clamping = false; + m_clampingSelection = false; return; } @@ -1182,7 +1234,7 @@ void RcxEditor::clampEditSelection() { if (clamped) m_sci->setSelection(selStartLine, selStartCol, selEndLine, selEndCol); - s_clamping = false; + m_clampingSelection = false; } // ── Commit inline edit ── @@ -1254,6 +1306,13 @@ void RcxEditor::showTypeListFiltered(const QString& filter) { // Arrow cursor for popup is handled by applyHoverCursor() via isListActive() } +void RcxEditor::showSourcePicker() { + m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart); + m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' '); + m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW, + (uintptr_t)2, "File Process"); +} + void RcxEditor::updateTypeListFilter() { if (!m_editState.active || m_editState.target != EditTarget::Type) return; @@ -1279,9 +1338,19 @@ void RcxEditor::updateTypeListFilter() { // ── Editable-field text-color indicator ── void RcxEditor::paintEditableSpans(int line) { + const LineMeta* lm = metaForLine(line); + if (!lm) return; + // CommandRow: paint Source and BaseAddress spans + if (isSyntheticLine(*lm)) { + NormalizedSpan norm; + if (resolvedSpanFor(line, EditTarget::Source, norm)) + fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); + if (resolvedSpanFor(line, EditTarget::BaseAddress, norm)) + fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); + return; + } NormalizedSpan norm; - for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value, - EditTarget::BaseAddress}) { + for (EditTarget t : {EditTarget::Type, EditTarget::Name, EditTarget::Value}) { if (resolvedSpanFor(line, t, norm)) fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end); } @@ -1403,9 +1472,8 @@ void RcxEditor::setEditComment(const QString& comment) { if (m_editState.commentCol < 0) return; // Prevent re-entrancy from textChanged signal - static bool s_updating = false; - if (s_updating) return; - s_updating = true; + if (m_updatingComment) return; + m_updatingComment = true; QString lineText = getLineText(m_sci, m_editState.line); @@ -1414,7 +1482,7 @@ void RcxEditor::setEditComment(const QString& comment) { int startCol = valueEnd + 2; // 2 spaces after value int endCol = lineText.size(); int availWidth = endCol - startCol; - if (availWidth <= 0) { s_updating = false; return; } + if (availWidth <= 0) { m_updatingComment = false; return; } // Format as "//" (no space after //) QString formatted = QStringLiteral("//") + comment; @@ -1434,7 +1502,7 @@ void RcxEditor::setEditComment(const QString& comment) { m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_BASE_ADDR); m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA); - s_updating = false; + m_updatingComment = false; } void RcxEditor::validateEditLive() { @@ -1443,7 +1511,9 @@ void RcxEditor::validateEditLive() { int editedLen = m_editState.original.size() + delta; QString text = (editedLen > 0) ? lineText.mid(m_editState.spanStart, editedLen).trimmed() : QString(); - QString errorMsg = fmt::validateValue(m_editState.editKind, text); + QString errorMsg = (m_editState.target == EditTarget::BaseAddress) + ? fmt::validateBaseAddress(text) + : fmt::validateValue(m_editState.editKind, text); const LineMeta* lm = metaForLine(m_editState.line); const bool isSelected = lm && m_currentSelIds.contains(lm->nodeId); @@ -1466,6 +1536,35 @@ void RcxEditor::validateEditLive() { } } +void RcxEditor::setCommandRowText(const QString& line) { + if (m_sci->lines() <= 0) return; + QString s = line; + s.replace('\n', ' '); + s.replace('\r', ' '); + + bool wasReadOnly = m_sci->isReadOnly(); + bool wasModified = m_sci->SendScintilla(QsciScintillaBase::SCI_GETMODIFY); + long savedPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS); + long savedAnchor = m_sci->SendScintilla(QsciScintillaBase::SCI_GETANCHOR); + + m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 0); + m_sci->setReadOnly(false); + + long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 0); + long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 0); + QByteArray utf8 = s.toUtf8(); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, start); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, end); + m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData()); + + if (wasReadOnly) m_sci->setReadOnly(true); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 1); + if (!wasModified) m_sci->SendScintilla(QsciScintillaBase::SCI_SETSAVEPOINT); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETCURRENTPOS, savedPos); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETANCHOR, savedAnchor); + m_sci->SendScintilla(QsciScintillaBase::SCI_COLOURISE, start, start + utf8.size()); +} + void RcxEditor::setEditorFont(const QString& fontName) { g_fontName = fontName; QFont f = editorFont(); diff --git a/src/editor.h b/src/editor.h index d8b3422..03f7b76 100644 --- a/src/editor.h +++ b/src/editor.h @@ -37,6 +37,7 @@ public: void cancelInlineEdit(); void applySelectionOverlay(const QSet& selIds); + void setCommandRowText(const QString& line); void setEditorFont(const QString& fontName); static void setGlobalFontName(const QString& fontName); @@ -98,6 +99,10 @@ private: }; InlineEditState m_editState; + // ── Reentrancy guards ── + bool m_clampingSelection = false; + bool m_updatingComment = false; + void setupScintilla(); void setupLexer(); void setupMargins(); @@ -116,6 +121,7 @@ private: bool handleNormalKey(QKeyEvent* ke); bool handleEditKey(QKeyEvent* ke); void showTypeAutocomplete(); + void showSourcePicker(); void showTypeListFiltered(const QString& filter); void updateTypeListFilter(); void paintEditableSpans(int line); diff --git a/src/fonts/codicon.ttf b/src/fonts/codicon.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0694339a3f89b59dd3b47bc1cac2cd5f526330de GIT binary patch literal 76140 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 D71bIb literal 0 HcmV?d00001 diff --git a/src/format.cpp b/src/format.cpp index b6fd466..3fe0674 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -92,35 +92,30 @@ QString indent(int depth) { // ── Offset margin ── -QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation) { +QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation) { if (isContinuation) return QStringLiteral(" \u00B7"); - if (relativeOffset < 0) - return QStringLiteral("-0x") + QString::number(-relativeOffset, 16).toUpper(); - return QStringLiteral("+0x") + QString::number(relativeOffset, 16).toUpper(); + return QStringLiteral("0x") + QString::number(absoluteOffset, 16).toUpper(); +} + +// ── Struct type name (for width calculation) ── + +QString structTypeName(const Node& node) { + // Full type string: "struct TypeName" or just "struct" if no typename + QString base = typeName(node.kind).trimmed(); // "struct" + if (!node.structTypeName.isEmpty()) + return base + QStringLiteral(" ") + node.structTypeName; + return base; } // ── Struct header / footer ── -QString fmtStructHeader(const Node& node, int depth) { - // Format: "struct TypeName name {" or "struct name {" if no type name - QString type = typeName(node.kind).trimmed(); - if (!node.structTypeName.isEmpty()) - return indent(depth) + type + QStringLiteral(" ") + node.structTypeName + - QStringLiteral(" ") + node.name + QStringLiteral(" {"); - return indent(depth) + type + QStringLiteral(" ") + node.name + QStringLiteral(" {"); -} - -QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress) { - // Format: "struct TypeName Name { // base: 0x..." or "struct Name { // base: 0x..." - QString type = typeName(node.kind).trimmed(); - QString header; - if (!node.structTypeName.isEmpty()) - header = indent(depth) + type + QStringLiteral(" ") + node.structTypeName + - QStringLiteral(" ") + node.name + QStringLiteral(" { "); - else - header = indent(depth) + type + QStringLiteral(" ") + node.name + QStringLiteral(" { "); - QString baseHex = QStringLiteral("0x") + QString::number(baseAddress, 16).toUpper(); - return header + QStringLiteral("// base: ") + baseHex; +QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType, int colName) { + // Columnar format: { (or no brace when collapsed) + QString ind = indent(depth); + QString type = fit(structTypeName(node), colType); + QString name = fit(node.name, colName); + QString suffix = collapsed ? QString() : QStringLiteral("{"); + return ind + type + SEP + name + SEP + suffix; } QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) { @@ -128,10 +123,13 @@ QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) { } // ── Array header ── -// Format: "uint32_t[16] myArray {" (like struct header, no fixed columns) -QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/) { - QString type = arrayTypeName(node.elementKind, node.arrayLen); - return indent(depth) + type + QStringLiteral(" ") + node.name + QStringLiteral(" {"); +// Columnar format: { (or no brace when collapsed) +QString fmtArrayHeader(const Node& node, int depth, int /*viewIdx*/, bool collapsed, int colType, int colName) { + QString ind = indent(depth); + QString type = fit(arrayTypeName(node.elementKind, node.arrayLen), colType); + QString name = fit(node.name, colName); + QString suffix = collapsed ? QString() : QStringLiteral("{"); + return ind + type + SEP + name + SEP + suffix; } // ── Hex / ASCII preview ── @@ -189,6 +187,10 @@ enum class ValueMode { Display, Editable }; static QString readValueImpl(const Node& node, const Provider& prov, uint64_t addr, int subLine, ValueMode mode) { + int sz = node.byteSize(); + if (sz > 0 && !prov.isReadable(addr, sz)) + return (mode == ValueMode::Display) ? QStringLiteral("???") : QString(); + const bool display = (mode == ValueMode::Display); switch (node.kind) { case NodeKind::Hex8: return display ? hexVal(prov.readU8(addr)) : rawHex(prov.readU8(addr), 2); @@ -396,16 +398,31 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) { switch (kind) { case NodeKind::Hex8: return parseHexBytes(stripHex(s), 1, ok); case NodeKind::Hex16: { - uint val = stripHex(s).toUInt(ok, 16); + QString cleaned = stripHex(s); + // Space-separated bytes → raw byte order (display order preserved) + if (cleaned.contains(' ')) + return parseHexBytes(cleaned, 2, ok); + // Single value → native-endian + uint val = cleaned.toUInt(ok, 16); if (*ok && val > 0xFFFF) *ok = false; return *ok ? toBytes(static_cast(val)) : QByteArray{}; } case NodeKind::Hex32: { - uint val = stripHex(s).toUInt(ok, 16); + QString cleaned = stripHex(s); + // Space-separated bytes → raw byte order (display order preserved) + if (cleaned.contains(' ')) + return parseHexBytes(cleaned, 4, ok); + // Single value → native-endian + uint val = cleaned.toUInt(ok, 16); return *ok ? toBytes(val) : QByteArray{}; } case NodeKind::Hex64: { - qulonglong val = stripHex(s).toULongLong(ok, 16); + QString cleaned = stripHex(s); + // Space-separated bytes → raw byte order (display order preserved) + if (cleaned.contains(' ')) + return parseHexBytes(cleaned, 8, ok); + // Single value → native-endian + qulonglong val = cleaned.toULongLong(ok, 16); return *ok ? toBytes(val) : QByteArray{}; } case NodeKind::Int8: { @@ -453,7 +470,7 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) { } case NodeKind::UInt8: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked(val, ok); } case NodeKind::UInt16: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return parseIntChecked(val, ok); } - case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return *ok ? toBytes(val) : QByteArray{}; } + case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return parseIntChecked(val, ok); } case NodeKind::UInt64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return *ok ? toBytes(val) : QByteArray{}; } case NodeKind::Float: { QString n = s; n.replace(',', '.'); // Accept EU decimal separator diff --git a/src/main.cpp b/src/main.cpp index 331ca8d..e5a2b28 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -440,7 +440,7 @@ void MainWindow::newFile() { }; // ── Root: IMAGE_DOS_HEADER ── - uint64_t dosId = addStruct(0, 0x00, "IMAGE_DOS_HEADER", "dosHeader"); + uint64_t dosId = addStruct(0, 0x00, "IMAGE_DOS_HEADER", "DosHeader"); addField(dosId, 0x00, NodeKind::UInt16, "e_magic"); addField(dosId, 0x02, NodeKind::UInt16, "e_cblp"); addField(dosId, 0x04, NodeKind::UInt16, "e_cp"); @@ -458,10 +458,10 @@ void MainWindow::newFile() { addField(dosId, 0x3C, NodeKind::UInt32, "e_lfanew"); // ── PE Signature ── - addField(0, peOff, NodeKind::UInt32, "PE_Signature"); + addField(0, peOff, NodeKind::UInt32, "Signature"); // ── IMAGE_FILE_HEADER ── - uint64_t fhId = addStruct(0, fhOff, "IMAGE_FILE_HEADER", "fileHeader"); + uint64_t fhId = addStruct(0, fhOff, "IMAGE_FILE_HEADER", "FileHeader"); addField(fhId, 0, NodeKind::UInt16, "Machine"); addField(fhId, 2, NodeKind::UInt16, "NumberOfSections"); addField(fhId, 4, NodeKind::UInt32, "TimeDateStamp"); @@ -471,7 +471,7 @@ void MainWindow::newFile() { addField(fhId, 18, NodeKind::UInt16, "Characteristics"); // ── IMAGE_OPTIONAL_HEADER64 ── - uint64_t ohId = addStruct(0, ohOff, "IMAGE_OPTIONAL_HEADER64", "optionalHeader"); + uint64_t ohId = addStruct(0, ohOff, "IMAGE_OPTIONAL_HEADER64", "OptionalHeader"); addField(ohId, 0, NodeKind::UInt16, "Magic"); addField(ohId, 2, NodeKind::UInt8, "MajorLinkerVersion"); addField(ohId, 3, NodeKind::UInt8, "MinorLinkerVersion"); @@ -483,8 +483,8 @@ void MainWindow::newFile() { addField(ohId, 24, NodeKind::UInt64, "ImageBase"); addField(ohId, 32, NodeKind::UInt32, "SectionAlignment"); addField(ohId, 36, NodeKind::UInt32, "FileAlignment"); - addField(ohId, 40, NodeKind::UInt16, "MajorOSVersion"); - addField(ohId, 42, NodeKind::UInt16, "MinorOSVersion"); + addField(ohId, 40, NodeKind::UInt16, "MajorOperatingSystemVersion"); + addField(ohId, 42, NodeKind::UInt16, "MinorOperatingSystemVersion"); addField(ohId, 44, NodeKind::UInt16, "MajorImageVersion"); addField(ohId, 46, NodeKind::UInt16, "MinorImageVersion"); addField(ohId, 48, NodeKind::UInt16, "MajorSubsystemVersion"); @@ -540,6 +540,13 @@ void MainWindow::newFile() { addField(secId, 36, NodeKind::UInt32, "Characteristics"); } + // ── Hex64 fields after headers ── + const int tailOff = shOff + 4 * 40; // 0x228 + addField(0, tailOff + 0, NodeKind::Hex64, "RawData0"); + addField(0, tailOff + 8, NodeKind::Hex64, "RawData1"); + addField(0, tailOff + 16, NodeKind::Hex64, "RawData2"); + addField(0, tailOff + 24, NodeKind::Hex64, "RawData3"); + createTab(doc); } @@ -715,11 +722,10 @@ int main(int argc, char* argv[]) { app.setOrganizationName("ReclassX"); app.setStyle("Fusion"); // Fusion style respects dark palette well - // Load embedded Iosevka font + // Load embedded fonts int fontId = QFontDatabase::addApplicationFont(":/fonts/Iosevka-Regular.ttf"); if (fontId == -1) qWarning("Failed to load embedded Iosevka font"); - // Apply saved font preference before creating any editors { QSettings settings("ReclassX", "ReclassX"); diff --git a/src/resources.qrc b/src/resources.qrc index d037c3d..c400899 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -5,5 +5,6 @@ fonts/Iosevka-Regular.ttf + fonts/codicon.ttf diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index 190481e..43ece8c 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -35,27 +35,30 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // Header + 2 fields + footer = 4 lines - QCOMPARE(result.meta.size(), 4); + // CommandRow + Header + 2 fields + footer = 5 lines + QCOMPARE(result.meta.size(), 5); + + // Line 0 is CommandRow + QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow); // Header is fold head - QVERIFY(result.meta[0].foldHead); - QCOMPARE(result.meta[0].lineKind, LineKind::Header); + QVERIFY(result.meta[1].foldHead); + QCOMPARE(result.meta[1].lineKind, LineKind::Header); // Fields are not fold heads - QVERIFY(!result.meta[1].foldHead); QVERIFY(!result.meta[2].foldHead); + QVERIFY(!result.meta[3].foldHead); // Footer - QCOMPARE(result.meta[3].lineKind, LineKind::Footer); + QCOMPARE(result.meta[4].lineKind, LineKind::Footer); // Offset text - QCOMPARE(result.meta[0].offsetText, QString("+0x0")); - QCOMPARE(result.meta[1].offsetText, QString("+0x0")); - QCOMPARE(result.meta[2].offsetText, QString("+0x4")); + QCOMPARE(result.meta[1].offsetText, QString("0x0")); + QCOMPARE(result.meta[2].offsetText, QString("0x0")); + QCOMPARE(result.meta[3].offsetText, QString("0x4")); // Header is expanded by default (fold indicator in line text) - QVERIFY(!result.meta[0].foldCollapsed); + QVERIFY(!result.meta[1].foldCollapsed); } void testVec3Continuation() { @@ -79,22 +82,22 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // Header + 3 Vec3 lines + footer = 5 lines - QCOMPARE(result.meta.size(), 5); + // CommandRow + Header + 3 Vec3 lines + footer = 6 lines + QCOMPARE(result.meta.size(), 6); - // Line 1 (first Vec3 component): not continuation - QVERIFY(!result.meta[1].isContinuation); - QCOMPARE(result.meta[1].offsetText, QString("+0x0")); + // Line 2 (first Vec3 component): not continuation + QVERIFY(!result.meta[2].isContinuation); + QCOMPARE(result.meta[2].offsetText, QString("0x0")); - // Lines 2-3: continuation - QVERIFY(result.meta[2].isContinuation); - QCOMPARE(result.meta[2].offsetText, QString(" \u00B7")); + // Lines 3-4: continuation QVERIFY(result.meta[3].isContinuation); QCOMPARE(result.meta[3].offsetText, QString(" \u00B7")); + QVERIFY(result.meta[4].isContinuation); + QCOMPARE(result.meta[4].offsetText, QString(" \u00B7")); // Continuation marker - QVERIFY(result.meta[2].markerMask & (1u << M_CONT)); QVERIFY(result.meta[3].markerMask & (1u << M_CONT)); + QVERIFY(result.meta[4].markerMask & (1u << M_CONT)); } void testPaddingMarker() { @@ -118,9 +121,9 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // Header + padding + footer = 3 - QCOMPARE(result.meta.size(), 3); - QVERIFY(result.meta[1].markerMask & (1u << M_PAD)); + // CommandRow + Header + padding + footer = 4 + QCOMPARE(result.meta.size(), 4); + QVERIFY(result.meta[2].markerMask & (1u << M_PAD)); } void testNullPointerMarker() { @@ -146,8 +149,8 @@ private slots: FileProvider prov(data); ComposeResult result = compose(tree, prov); - QCOMPARE(result.meta.size(), 3); - QVERIFY(result.meta[1].markerMask & (1u << M_PTR0)); + QCOMPARE(result.meta.size(), 4); + QVERIFY(result.meta[2].markerMask & (1u << M_PTR0)); } void testCollapsedStruct() { @@ -172,9 +175,10 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // Collapsed: header + footer only = 2 lines + // Collapsed: CommandRow + header only (no children, no footer) QCOMPARE(result.meta.size(), 2); - QVERIFY(result.meta[0].foldHead); + QVERIFY(result.meta[1].foldHead); + QVERIFY(result.meta[1].foldCollapsed); } void testUnreadablePointerNoRead() { @@ -201,10 +205,10 @@ private slots: FileProvider prov(data); ComposeResult result = compose(tree, prov); - QCOMPARE(result.meta.size(), 3); + QCOMPARE(result.meta.size(), 4); // Should have M_ERR, should NOT have M_PTR0 - QVERIFY(result.meta[1].markerMask & (1u << M_ERR)); - QVERIFY(!(result.meta[1].markerMask & (1u << M_PTR0))); + QVERIFY(result.meta[2].markerMask & (1u << M_ERR)); + QVERIFY(!(result.meta[2].markerMask & (1u << M_PTR0))); } void testFoldLevels() { @@ -237,16 +241,16 @@ private slots: ComposeResult result = compose(tree, prov); // Root header (depth 0, head) -> 0x400 | 0x2000 - QCOMPARE(result.meta[0].foldLevel, 0x400 | 0x2000); - QCOMPARE(result.meta[0].depth, 0); + QCOMPARE(result.meta[1].foldLevel, 0x400 | 0x2000); + QCOMPARE(result.meta[1].depth, 0); // Child header (depth 1, head) -> 0x401 | 0x2000 - QCOMPARE(result.meta[1].foldLevel, 0x401 | 0x2000); - QCOMPARE(result.meta[1].depth, 1); + QCOMPARE(result.meta[2].foldLevel, 0x401 | 0x2000); + QCOMPARE(result.meta[2].depth, 1); // Leaf (depth 2, not head) -> 0x402 - QCOMPARE(result.meta[2].foldLevel, 0x402); - QCOMPARE(result.meta[2].depth, 2); + QCOMPARE(result.meta[3].foldLevel, 0x402); + QCOMPARE(result.meta[3].depth, 2); } void testNestedStruct() { @@ -293,36 +297,36 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // Outer header + flags + Inner header + x + y + Inner footer + Outer footer = 7 - QCOMPARE(result.meta.size(), 7); + // CommandRow + Outer header + flags + Inner header + x + y + Inner footer + Outer footer = 8 + QCOMPARE(result.meta.size(), 8); // Outer header - QCOMPARE(result.meta[0].lineKind, LineKind::Header); - QCOMPARE(result.meta[0].depth, 0); - QVERIFY(result.meta[0].foldHead); + QCOMPARE(result.meta[1].lineKind, LineKind::Header); + QCOMPARE(result.meta[1].depth, 0); + QVERIFY(result.meta[1].foldHead); // flags field - QCOMPARE(result.meta[1].lineKind, LineKind::Field); - QCOMPARE(result.meta[1].depth, 1); + QCOMPARE(result.meta[2].lineKind, LineKind::Field); + QCOMPARE(result.meta[2].depth, 1); // Inner header - QCOMPARE(result.meta[2].lineKind, LineKind::Header); - QCOMPARE(result.meta[2].depth, 1); - QVERIFY(result.meta[2].foldHead); - QCOMPARE(result.meta[2].foldLevel, 0x401 | 0x2000); + QCOMPARE(result.meta[3].lineKind, LineKind::Header); + QCOMPARE(result.meta[3].depth, 1); + QVERIFY(result.meta[3].foldHead); + QCOMPARE(result.meta[3].foldLevel, 0x401 | 0x2000); // Inner fields at depth 2 - QCOMPARE(result.meta[3].depth, 2); - QCOMPARE(result.meta[3].foldLevel, 0x402); QCOMPARE(result.meta[4].depth, 2); + QCOMPARE(result.meta[4].foldLevel, 0x402); + QCOMPARE(result.meta[5].depth, 2); // Inner footer - QCOMPARE(result.meta[5].lineKind, LineKind::Footer); - QCOMPARE(result.meta[5].depth, 1); + QCOMPARE(result.meta[6].lineKind, LineKind::Footer); + QCOMPARE(result.meta[6].depth, 1); // Outer footer - QCOMPARE(result.meta[6].lineKind, LineKind::Footer); - QCOMPARE(result.meta[6].depth, 0); + QCOMPARE(result.meta[7].lineKind, LineKind::Footer); + QCOMPARE(result.meta[7].depth, 0); } void testPointerDerefExpansion() { @@ -390,40 +394,40 @@ private slots: ComposeResult result = compose(tree, prov); - // Main: header + magic + ptr(fold head) + VTable header + fn1 + fn2 + VTable footer + Main footer = 8 + // CommandRow + Main: header + magic + ptr(fold head) + VTable header + fn1 + fn2 + VTable footer + Main footer = 9 // VTable standalone: header + fn1 + fn2 + footer = 4 - // Total = 12 - QCOMPARE(result.meta.size(), 12); + // Total = 13 + QCOMPARE(result.meta.size(), 13); // Main header - QCOMPARE(result.meta[0].lineKind, LineKind::Header); - QCOMPARE(result.meta[0].depth, 0); + QCOMPARE(result.meta[1].lineKind, LineKind::Header); + QCOMPARE(result.meta[1].depth, 0); // magic field - QCOMPARE(result.meta[1].lineKind, LineKind::Field); - QCOMPARE(result.meta[1].depth, 1); - - // Pointer as fold head QCOMPARE(result.meta[2].lineKind, LineKind::Field); QCOMPARE(result.meta[2].depth, 1); - QVERIFY(result.meta[2].foldHead); - QCOMPARE(result.meta[2].nodeKind, NodeKind::Pointer64); + + // Pointer as fold head + QCOMPARE(result.meta[3].lineKind, LineKind::Field); + QCOMPARE(result.meta[3].depth, 1); + QVERIFY(result.meta[3].foldHead); + QCOMPARE(result.meta[3].nodeKind, NodeKind::Pointer64); // Expanded VTable header at depth 2 - QCOMPARE(result.meta[3].lineKind, LineKind::Header); - QCOMPARE(result.meta[3].depth, 2); + QCOMPARE(result.meta[4].lineKind, LineKind::Header); + QCOMPARE(result.meta[4].depth, 2); // Expanded fields at depth 3 - QCOMPARE(result.meta[4].depth, 3); QCOMPARE(result.meta[5].depth, 3); + QCOMPARE(result.meta[6].depth, 3); // Expanded VTable footer - QCOMPARE(result.meta[6].lineKind, LineKind::Footer); - QCOMPARE(result.meta[6].depth, 2); + QCOMPARE(result.meta[7].lineKind, LineKind::Footer); + QCOMPARE(result.meta[7].depth, 2); // Main footer - QCOMPARE(result.meta[7].lineKind, LineKind::Footer); - QCOMPARE(result.meta[7].depth, 0); + QCOMPARE(result.meta[8].lineKind, LineKind::Footer); + QCOMPARE(result.meta[8].depth, 0); } void testPointerDerefNull() { @@ -467,18 +471,18 @@ private slots: ComposeResult result = compose(tree, prov); - // Main: header + ptr(fold head, no expansion) + footer = 3 + // CommandRow + Main: header + ptr(fold head, no expansion) + footer = 4 // Target standalone: header + field + footer = 3 - // Total = 6 - QCOMPARE(result.meta.size(), 6); + // Total = 7 + QCOMPARE(result.meta.size(), 7); // Pointer is fold head but has no children (null ptr) - QCOMPARE(result.meta[1].lineKind, LineKind::Field); - QVERIFY(result.meta[1].foldHead); + QCOMPARE(result.meta[2].lineKind, LineKind::Field); + QVERIFY(result.meta[2].foldHead); // Next line is Main footer (no expansion) - QCOMPARE(result.meta[2].lineKind, LineKind::Footer); - QCOMPARE(result.meta[2].depth, 0); + QCOMPARE(result.meta[3].lineKind, LineKind::Footer); + QCOMPARE(result.meta[3].depth, 0); } void testPointerDerefCollapsed() { @@ -525,17 +529,17 @@ private slots: ComposeResult result = compose(tree, prov); - // Main: header + ptr(fold head, collapsed) + footer = 3 + // CommandRow + Main: header + ptr(fold head, collapsed) + footer = 4 // Target standalone: header + field + footer = 3 - // Total = 6 - QCOMPARE(result.meta.size(), 6); + // Total = 7 + QCOMPARE(result.meta.size(), 7); // Pointer is fold head - QVERIFY(result.meta[1].foldHead); + QVERIFY(result.meta[2].foldHead); // No expansion — next is Main footer - QCOMPARE(result.meta[2].lineKind, LineKind::Footer); - QCOMPARE(result.meta[2].depth, 0); + QCOMPARE(result.meta[3].lineKind, LineKind::Footer); + QCOMPARE(result.meta[3].depth, 0); } void testPointerDerefCycle() { @@ -598,14 +602,14 @@ private slots: QVERIFY(result.meta.size() > 0); QVERIFY(result.meta.size() < 100); // sanity: bounded output - // First expansion happens: Main header + ptr fold head + Recursive header + data + backPtr fold head + // First expansion happens: CommandRow + Main header + ptr fold head + Recursive header + data + backPtr fold head // Second expansion blocked by cycle guard: no children under backPtr // Then: Recursive footer + Main footer // Plus standalone Recursive rendering // The exact count depends on cycle guard behavior but must be finite - QCOMPARE(result.meta[0].lineKind, LineKind::Header); // Main header - QVERIFY(result.meta[1].foldHead); // ptr fold head - QCOMPARE(result.meta[2].lineKind, LineKind::Header); // Recursive header (expansion) + QCOMPARE(result.meta[1].lineKind, LineKind::Header); // Main header + QVERIFY(result.meta[2].foldHead); // ptr fold head + QCOMPARE(result.meta[3].lineKind, LineKind::Header); // Recursive header (expansion) } void testStructFooterSimple() { @@ -655,6 +659,12 @@ private slots: ComposeResult result = compose(tree, prov); for (int i = 0; i < result.meta.size(); i++) { + // Skip CommandRow (synthetic line with sentinel nodeId) + if (result.meta[i].lineKind == LineKind::CommandRow) { + QCOMPARE(result.meta[i].nodeId, kCommandRowId); + QCOMPARE(result.meta[i].nodeIdx, -1); + continue; + } QVERIFY2(result.meta[i].nodeId != 0, qPrintable(QString("Line %1 has nodeId=0").arg(i))); int ni = result.meta[i].nodeIdx; diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index cb22612..7637e8c 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -25,43 +25,89 @@ static FileProvider makeTestProvider() { return FileProvider(data); } -// Build a tree covering 0x6000 bytes with Hex64 fields +// Build a PE-like test tree with IMAGE_FILE_HEADER fields static NodeTree makeTestTree() { NodeTree tree; - tree.baseAddress = 0; + tree.baseAddress = 0x140000000; + // Root struct: IMAGE_FILE_HEADER Node root; root.kind = NodeKind::Struct; - root.name = "TestStruct"; + root.structTypeName = "IMAGE_FILE_HEADER"; + root.name = "FileHeader"; root.parentId = 0; root.offset = 0; int ri = tree.addNode(root); uint64_t rootId = tree.nodes[ri].id; - // First two fields for existing tests - Node f1; - f1.kind = NodeKind::UInt16; - f1.name = "field_u16"; - f1.parentId = rootId; - f1.offset = 0; - tree.addNode(f1); + int offset = 0; - Node f2; - f2.kind = NodeKind::Hex64; - f2.name = "field_hex"; - f2.parentId = rootId; - f2.offset = 8; - tree.addNode(f2); + // IMAGE_FILE_HEADER fields (matches Windows PE format) + Node machine; + machine.kind = NodeKind::UInt16; + machine.name = "Machine"; + machine.parentId = rootId; + machine.offset = offset; + tree.addNode(machine); + offset += 2; - // Fill remaining 0x6000 bytes with Hex64 fields (8 bytes each) - // Start at offset 16 (0x10), go to 0x6000 - for (int off = 0x10; off < 0x6000; off += 8) { - Node f; - f.kind = NodeKind::Hex64; - f.name = QString("data_%1").arg(off, 4, 16, QChar('0')); - f.parentId = rootId; - f.offset = off; - tree.addNode(f); + Node numSections; + numSections.kind = NodeKind::UInt16; + numSections.name = "NumberOfSections"; + numSections.parentId = rootId; + numSections.offset = offset; + tree.addNode(numSections); + offset += 2; + + Node timestamp; + timestamp.kind = NodeKind::Hex32; + timestamp.name = "TimeDateStamp"; + timestamp.parentId = rootId; + timestamp.offset = offset; + tree.addNode(timestamp); + offset += 4; + + Node ptrSymbols; + ptrSymbols.kind = NodeKind::Hex32; + ptrSymbols.name = "PointerToSymbolTable"; + ptrSymbols.parentId = rootId; + ptrSymbols.offset = offset; + tree.addNode(ptrSymbols); + offset += 4; + + Node numSymbols; + numSymbols.kind = NodeKind::UInt32; + numSymbols.name = "NumberOfSymbols"; + numSymbols.parentId = rootId; + numSymbols.offset = offset; + tree.addNode(numSymbols); + offset += 4; + + Node optHeaderSize; + optHeaderSize.kind = NodeKind::UInt16; + optHeaderSize.name = "SizeOfOptionalHeader"; + optHeaderSize.parentId = rootId; + optHeaderSize.offset = offset; + tree.addNode(optHeaderSize); + offset += 2; + + Node characteristics; + characteristics.kind = NodeKind::Hex16; + characteristics.name = "Characteristics"; + characteristics.parentId = rootId; + characteristics.offset = offset; + tree.addNode(characteristics); + offset += 2; + + // 8 Hex64 fields for additional test coverage + for (int i = 0; i < 8; i++) { + Node hex; + hex.kind = NodeKind::Hex64; + hex.name = QString("Reserved%1").arg(i); + hex.parentId = rootId; + hex.offset = offset; + tree.addNode(hex); + offset += 8; } return tree; @@ -90,16 +136,50 @@ private slots: delete m_editor; } + // ── Test: CommandRow at line 0 rejects non-ADDR edits ── + void testCommandRowLineRejectsEdits() { + m_editor->applyDocument(m_result); + + // Line 0 should be the CommandRow + const LineMeta* lm = m_editor->metaForLine(0); + QVERIFY(lm); + QCOMPARE(lm->lineKind, LineKind::CommandRow); + QCOMPARE(lm->nodeId, kCommandRowId); + QCOMPARE(lm->nodeIdx, -1); + + // Type/Name/Value should be rejected on CommandRow + QVERIFY(!m_editor->beginInlineEdit(EditTarget::Type, 0)); + QVERIFY(!m_editor->beginInlineEdit(EditTarget::Name, 0)); + QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, 0)); + QVERIFY(!m_editor->isEditing()); + + // Set CommandRow text with an ADDR value (simulates controller.updateCommandRow) + m_editor->setCommandRowText( + QStringLiteral(" * SRC: File : 0x140000000")); + + // BaseAddress should be ALLOWED on CommandRow (ADDR field) + bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); + QVERIFY2(ok, "BaseAddress edit should be allowed on CommandRow"); + QVERIFY(m_editor->isEditing()); + m_editor->cancelInlineEdit(); + + // Source should be ALLOWED on CommandRow (SRC field) + ok = m_editor->beginInlineEdit(EditTarget::Source, 0); + QVERIFY2(ok, "Source edit should be allowed on CommandRow"); + QVERIFY(m_editor->isEditing()); + m_editor->cancelInlineEdit(); + } + // ── Test: inline edit lifecycle (begin → commit → re-edit) ── void testInlineEditReEntry() { - // Move cursor to line 1 (first field inside struct) - m_editor->scintilla()->setCursorPosition(1, 0); + // Move cursor to line 2 (first field inside struct; line 0=CommandRow, 1=header) + m_editor->scintilla()->setCursorPosition(2, 0); // Should not be editing QVERIFY(!m_editor->isEditing()); // Begin edit on Name column - bool ok = m_editor->beginInlineEdit(EditTarget::Name, 1); + bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -111,7 +191,7 @@ private slots: m_editor->applyDocument(m_result); // Should be able to edit again - ok = m_editor->beginInlineEdit(EditTarget::Name, 1); + ok = m_editor->beginInlineEdit(EditTarget::Name, 2); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -123,10 +203,10 @@ private slots: // ── Test: commit inline edit then re-edit same line ── void testCommitThenReEdit() { m_editor->applyDocument(m_result); - m_editor->scintilla()->setCursorPosition(1, 0); + m_editor->scintilla()->setCursorPosition(2, 0); // Begin value edit - bool ok = m_editor->beginInlineEdit(EditTarget::Value, 1); + bool ok = m_editor->beginInlineEdit(EditTarget::Value, 2); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -143,7 +223,7 @@ private slots: m_editor->applyDocument(m_result); // Must be able to edit the same line again - ok = m_editor->beginInlineEdit(EditTarget::Value, 1); + ok = m_editor->beginInlineEdit(EditTarget::Value, 2); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -154,7 +234,7 @@ private slots: void testMouseClickCommitsEdit() { m_editor->applyDocument(m_result); - bool ok = m_editor->beginInlineEdit(EditTarget::Name, 1); + bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -179,7 +259,7 @@ private slots: m_editor->scintilla()->setFocus(); QApplication::processEvents(); - bool ok = m_editor->beginInlineEdit(EditTarget::Name, 1); + bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -207,7 +287,7 @@ private slots: m_editor->applyDocument(m_result); // Begin type edit on a field line - bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1); + bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -226,22 +306,23 @@ private slots: QVERIFY(!m_editor->isEditing()); } - // ── Test: edit on header line (Name is valid, Type/Value invalid) ── + // ── Test: edit on header line (Name and Type valid, Value invalid) ── void testHeaderLineEdit() { m_editor->applyDocument(m_result); - // Line 0 should be the struct header - const LineMeta* lm = m_editor->metaForLine(0); + // Line 1 should be the struct header (line 0 is CommandRow) + const LineMeta* lm = m_editor->metaForLine(1); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::Header); - // Type edit on header should fail (no type span) - bool ok = m_editor->beginInlineEdit(EditTarget::Type, 0); - QVERIFY(!ok); - QVERIFY(!m_editor->isEditing()); + // Type edit on header should succeed (has typename IMAGE_FILE_HEADER) + bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1); + QVERIFY(ok); + QVERIFY(m_editor->isEditing()); + m_editor->cancelInlineEdit(); - // Name edit on header should succeed (dynamic span) - ok = m_editor->beginInlineEdit(EditTarget::Name, 0); + // Name edit on header should succeed + ok = m_editor->beginInlineEdit(EditTarget::Name, 1); QVERIFY(ok); QVERIFY(m_editor->isEditing()); m_editor->cancelInlineEdit(); @@ -271,7 +352,7 @@ private slots: void testTypeAutocompleteShows() { m_editor->applyDocument(m_result); - bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1); + bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2); QVERIFY(ok); // Process deferred timer (autocomplete is deferred) @@ -313,11 +394,12 @@ private slots: QCOMPARE((uint8_t)b[1], (uint8_t)0x5A); QCOMPARE((uint8_t)b[7], (uint8_t)0x00); - // Hex64 continuous (should still work) + // Hex64 continuous - stores as native-endian (numeric value preserved) b = fmt::parseValue(NodeKind::Hex64, "4D5A900000000000", &ok); QVERIFY(ok); - QCOMPARE((uint8_t)b[0], (uint8_t)0x4D); - QCOMPARE((uint8_t)b[1], (uint8_t)0x5A); + uint64_t v64; + memcpy(&v64, b.data(), 8); + QCOMPARE(v64, (uint64_t)0x4D5A900000000000); // Hex64 with 0x prefix and spaces b = fmt::parseValue(NodeKind::Hex64, "0x4D 5A 90 00 00 00 00 00", &ok); @@ -328,7 +410,7 @@ private slots: void testTypeAutocompleteTypingAndCommit() { m_editor->applyDocument(m_result); - bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1); + bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2); QVERIFY(ok); // Process deferred autocomplete @@ -368,7 +450,7 @@ private slots: void testTypeEditClickAwayNoChange() { m_editor->applyDocument(m_result); - bool ok = m_editor->beginInlineEdit(EditTarget::Type, 1); + bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2); QVERIFY(ok); // Process deferred autocomplete @@ -397,8 +479,8 @@ private slots: void testColumnSpanHitTest() { m_editor->applyDocument(m_result); - // Line 1 is a field line (UInt16), verify spans are valid - const LineMeta* lm = m_editor->metaForLine(1); + // Line 2 is a field line (UInt16), verify spans are valid (line 0=CommandRow, 1=header) + const LineMeta* lm = m_editor->metaForLine(2); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::Field); @@ -415,7 +497,7 @@ private slots: // Value span should be valid for field lines QString lineText; int len = (int)m_editor->scintilla()->SendScintilla( - QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1); + QsciScintillaBase::SCI_LINELENGTH, (unsigned long)2); QVERIFY(len > 0); ColumnSpan vs = RcxEditor::valueSpan(*lm, len); QVERIFY(vs.valid); @@ -444,20 +526,19 @@ private slots: void testSelectedNodeIndices() { m_editor->applyDocument(m_result); - // Put cursor on first field line - m_editor->scintilla()->setCursorPosition(1, 0); + // Put cursor on first field line (line 2; 0=CommandRow, 1=header) + m_editor->scintilla()->setCursorPosition(2, 0); QSet sel = m_editor->selectedNodeIndices(); QCOMPARE(sel.size(), 1); // The node index should match the first field - const LineMeta* lm = m_editor->metaForLine(1); + const LineMeta* lm = m_editor->metaForLine(2); QVERIFY(lm); QVERIFY(sel.contains(lm->nodeIdx)); } - // ── Test: base address changes affect header display ── + // ── Test: header line no longer contains "// base:" ── void testBaseAddressDisplay() { - // Create tree with base address 0x10 NodeTree tree = makeTestTree(); tree.baseAddress = 0x10; FileProvider prov = makeTestProvider(); @@ -465,50 +546,45 @@ private slots: m_editor->applyDocument(result); - // Line 0 should be the struct header with isRootHeader=true - const LineMeta* lm = m_editor->metaForLine(0); + // Line 1 should be the struct header (line 0 is CommandRow) + const LineMeta* lm = m_editor->metaForLine(1); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::Header); QVERIFY(lm->isRootHeader); - // Get header line text - should contain "0x10" + // Get header line text — should NOT contain "// base:" (consolidated into cmd bar) QString lineText; int len = (int)m_editor->scintilla()->SendScintilla( - QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0); + QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1); if (len > 0) { QByteArray buf(len + 1, '\0'); m_editor->scintilla()->SendScintilla( - QsciScintillaBase::SCI_GETLINE, (unsigned long)0, (void*)buf.data()); + QsciScintillaBase::SCI_GETLINE, (unsigned long)1, (void*)buf.data()); lineText = QString::fromUtf8(buf.constData(), len).trimmed(); } - // Verify base address appears in header - QVERIFY2(lineText.contains("0x10") || lineText.contains("0X10"), - qPrintable("Header should contain base address 0x10, got: " + lineText)); - - // Verify struct keyword is present + QVERIFY2(!lineText.contains("// base:"), + qPrintable("Header should no longer contain '// base:', got: " + lineText)); QVERIFY2(lineText.contains("struct"), qPrintable("Header should contain 'struct', got: " + lineText)); - // Reset to original result m_editor->applyDocument(m_result); } - // ── Test: base address span is valid for root headers ── + // ── Test: CommandRow ADDR span is valid ── void testBaseAddressSpan() { - NodeTree tree = makeTestTree(); - tree.baseAddress = 0x140000000; // Large address to test span width - FileProvider prov = makeTestProvider(); - ComposeResult result = compose(tree, prov); + m_editor->applyDocument(m_result); - m_editor->applyDocument(result); + // Set CommandRow text with ADDR value (simulates controller) + m_editor->setCommandRowText( + QStringLiteral(" * SRC: File : 0x140000000")); - // Line 0 should be root header + // Line 0 is CommandRow const LineMeta* lm = m_editor->metaForLine(0); QVERIFY(lm); - QVERIFY(lm->isRootHeader); + QCOMPARE(lm->lineKind, LineKind::CommandRow); - // Get line text for span calculation + // Get CommandRow line text QString lineText; int len = (int)m_editor->scintilla()->SendScintilla( QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0); @@ -521,32 +597,30 @@ private slots: lineText.chop(1); } - // Base address span should be valid - ColumnSpan bs = baseAddressSpanFor(*lm, lineText); - QVERIFY2(bs.valid, "Base address span should be valid for root header"); - QVERIFY(bs.start < bs.end); + // ADDR span should be valid (uses commandRowAddrSpan) + ColumnSpan as = commandRowAddrSpan(lineText); + QVERIFY2(as.valid, "ADDR span should be valid on CommandRow"); + QVERIFY(as.start < as.end); // The span should cover the hex address - QString spanText = lineText.mid(bs.start, bs.end - bs.start); + QString spanText = lineText.mid(as.start, as.end - as.start); QVERIFY2(spanText.contains("0x") || spanText.startsWith("0X"), qPrintable("Span should contain hex address, got: " + spanText)); - // Reset m_editor->applyDocument(m_result); } - // ── Test: base address edit begins on root header ── + // ── Test: base address edit begins on CommandRow (line 0) ── void testBaseAddressEditBegins() { - NodeTree tree = makeTestTree(); - tree.baseAddress = 0x10; - FileProvider prov = makeTestProvider(); - ComposeResult result = compose(tree, prov); + m_editor->applyDocument(m_result); - m_editor->applyDocument(result); + // Set CommandRow text with ADDR value (simulates controller) + m_editor->setCommandRowText( + QStringLiteral(" * SRC: File : 0x140000000")); - // Begin base address edit on line 0 (root header) + // Begin base address edit on line 0 (CommandRow ADDR field) bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); - QVERIFY2(ok, "Should be able to begin base address edit on root header"); + QVERIFY2(ok, "Should be able to begin base address edit on CommandRow"); QVERIFY(m_editor->isEditing()); // Cancel and reset diff --git a/tests/test_format.cpp b/tests/test_format.cpp index 9efc2b6..55ad339 100644 --- a/tests/test_format.cpp +++ b/tests/test_format.cpp @@ -39,8 +39,8 @@ private slots: } void testFmtOffsetMargin_primary() { - QCOMPARE(fmt::fmtOffsetMargin(0x10, false), QString("+0x10")); - QCOMPARE(fmt::fmtOffsetMargin(0, false), QString("+0x0")); + QCOMPARE(fmt::fmtOffsetMargin(0x10, false), QString("0x10")); + QCOMPARE(fmt::fmtOffsetMargin(0, false), QString("0x0")); } void testFmtOffsetMargin_continuation() { @@ -51,10 +51,17 @@ private slots: Node n; n.kind = NodeKind::Struct; n.name = "Test"; - QString s = fmt::fmtStructHeader(n, 0); + // Expanded header should contain opening brace + QString s = fmt::fmtStructHeader(n, 0, /*collapsed=*/false); QVERIFY(s.contains("struct")); QVERIFY(s.contains("Test")); QVERIFY(s.contains("{")); + + // Collapsed header should not contain opening brace + QString collapsed = fmt::fmtStructHeader(n, 0, /*collapsed=*/true); + QVERIFY(collapsed.contains("struct")); + QVERIFY(collapsed.contains("Test")); + QVERIFY(!collapsed.contains("{")); } void testFmtStructFooter() {