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 0000000..0694339 Binary files /dev/null and b/src/fonts/codicon.ttf differ 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() {