From 95faf027a990345aca287d15717eee21f1c47256 Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Sat, 28 Feb 2026 08:21:00 -0700 Subject: [PATCH] refactor: rename helpers to static fields, block-style rendering, sibling insert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename isHelper/ToggleHelper to isStatic/ToggleStatic across core, compose, controller, editor, and generator. Static fields now render with block syntax (static Type name { return expr } → 0xADDR) and support collapsed/expanded display. Add "Add Static Field" context menu for sibling nodes. Update expression span parser, completions, C++ generator comments, and all tests. --- CMakeLists.txt | 5 + src/compose.cpp | 194 ++++++++++++++++++++-------------- src/controller.cpp | 90 +++++++++++----- src/core.h | 41 +++++--- src/editor.cpp | 32 ++++-- src/editor.h | 4 +- src/generator.cpp | 18 ++-- tests/test_compose.cpp | 142 ++++++++++++------------- tests/test_controller.cpp | 178 ++++++++++++++++---------------- tests/test_core.cpp | 60 +++++------ tests/test_editor.cpp | 212 ++++++++++++++++++++++++++++++++++++++ tests/test_generator.cpp | 76 +++++++------- 12 files changed, 685 insertions(+), 367 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 995ab8f..bf6ae75 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -230,6 +230,11 @@ if(BUILD_TESTING) target_link_libraries(test_addressparser PRIVATE ${QT}::Core ${QT}::Test) add_test(NAME test_addressparser COMMAND test_addressparser) + add_executable(test_static_fields tests/test_static_fields.cpp src/compose.cpp src/format.cpp src/addressparser.cpp) + target_include_directories(test_static_fields PRIVATE src) + target_link_libraries(test_static_fields PRIVATE ${QT}::Core ${QT}::Test) + add_test(NAME test_static_fields COMMAND test_static_fields) + if(WIN32) add_executable(test_import_pdb tests/test_import_pdb.cpp src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp) diff --git a/src/compose.cpp b/src/compose.cpp index 1a81435..9fd862f 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -397,11 +397,11 @@ void composeParent(ComposeState& state, const NodeTree& tree, const QVector& allChildren = childIndices(state, node.id); - // Split children into regular nodes and helpers (helpers render at the end) - QVector regular, helperIdxs; + // Split children into regular nodes and static fields (static fields render at the end) + QVector regular, staticIdxs; for (int ci : allChildren) { - if (tree.nodes[ci].isHelper) - helperIdxs.append(ci); + if (tree.nodes[ci].isStatic) + staticIdxs.append(ci); else regular.append(ci); } @@ -523,24 +523,9 @@ void composeParent(ComposeState& state, const NodeTree& tree, childrenAreArrayElements ? absAddr : 0); } - // ── Static helpers: render after regular children, before footer ── - if (!helperIdxs.isEmpty() && !node.collapsed) { - // Separator line - { - LineMeta lm; - lm.nodeIdx = nodeIdx; - lm.nodeId = node.id; - lm.depth = childDepth; - lm.lineKind = LineKind::Field; - lm.nodeKind = NodeKind::Hex8; // neutral kind for separator - lm.foldLevel = computeFoldLevel(childDepth, false); - lm.markerMask = 0; - lm.offsetText = QString(state.offsetHexDigits, QChar(' ')); - state.emitLine(fmt::indent(childDepth) - + QStringLiteral("\u2500\u2500\u2500 helpers \u2500\u2500\u2500"), lm); - } - - // Build identifier resolver for helper expressions + // ── Static fields: render after regular children, before footer ── + if (!staticIdxs.isEmpty() && !node.collapsed) { + // Build identifier resolver for static field expressions auto makeResolver = [&](uint64_t parentAbsAddr) { AddressParserCallbacks cbs; cbs.resolveIdentifier = [&tree, &prov, ®ular, parentAbsAddr] @@ -582,92 +567,143 @@ void composeParent(ComposeState& state, const NodeTree& tree, auto cbs = makeResolver(absAddr); - for (int hi : helperIdxs) { - const Node& helper = tree.nodes[hi]; + for (int si : staticIdxs) { + const Node& sf = tree.nodes[si]; // Evaluate expression → absolute address - uint64_t helperAddr = 0; + uint64_t staticAddr = 0; bool exprOk = false; - if (!helper.offsetExpr.isEmpty()) { - auto result = AddressParser::evaluate(helper.offsetExpr, 8, &cbs); + if (!sf.offsetExpr.isEmpty()) { + auto result = AddressParser::evaluate(sf.offsetExpr, 8, &cbs); exprOk = result.ok; if (result.ok) - helperAddr = result.value; + staticAddr = result.value; } - // Format: "▸ type name = expr → 0xADDR" (or "= expr (error)" on failure) - int typeW = state.effectiveTypeW(node.id); - int nameW = state.effectiveNameW(node.id); - + // Resolve type name QString typeName; - if (helper.kind == NodeKind::Struct) - typeName = fmt::structTypeName(helper); - else if (helper.kind == NodeKind::Pointer64 || helper.kind == NodeKind::Pointer32) - typeName = fmt::pointerTypeName(helper.kind, resolvePointerTarget(tree, helper.refId)); + if (sf.kind == NodeKind::Struct) + typeName = fmt::structTypeName(sf); + else if (sf.kind == NodeKind::Pointer64 || sf.kind == NodeKind::Pointer32) + typeName = fmt::pointerTypeName(sf.kind, resolvePointerTarget(tree, sf.refId)); else - typeName = fmt::typeNameRaw(helper.kind); + typeName = fmt::typeNameRaw(sf.kind); - bool overflow = state.compactColumns && typeName.size() > typeW; - QString type = overflow ? typeName : typeName.leftJustified(typeW); - QString name = overflow ? helper.name : helper.name.leftJustified(nameW); + bool isCollapsed = sf.collapsed; - QString exprPart; - if (!helper.offsetExpr.isEmpty()) { - if (exprOk) - exprPart = QStringLiteral("= %1 \u2192 0x%2") - .arg(helper.offsetExpr) - .arg(QString::number(helperAddr, 16).toUpper()); - else - exprPart = QStringLiteral("= %1 (error)").arg(helper.offsetExpr); + // ── Header line: "static {" or collapsed: "static { return ; }" + QString headerLine; + if (isCollapsed) { + QString exprPart; + if (!sf.offsetExpr.isEmpty()) { + if (exprOk) + exprPart = QStringLiteral("return %1 } \u2192 0x%2") + .arg(sf.offsetExpr) + .arg(QString::number(staticAddr, 16).toUpper()); + else + exprPart = QStringLiteral("return %1 } (error)").arg(sf.offsetExpr); + } else { + exprPart = QStringLiteral("}"); + } + headerLine = fmt::indent(childDepth) + + QStringLiteral("static ") + typeName + + QStringLiteral(" ") + sf.name + + QStringLiteral(" { ") + exprPart; + } else { + headerLine = fmt::indent(childDepth) + + QStringLiteral("static ") + typeName + + QStringLiteral(" ") + sf.name + + QStringLiteral(" {"); } - QString line = fmt::indent(childDepth) + type - + QStringLiteral(" ") + name - + QStringLiteral(" ") + exprPart; - LineMeta lm; - lm.nodeIdx = hi; - lm.nodeId = helper.id; + lm.nodeIdx = si; + lm.nodeId = sf.id; lm.depth = childDepth; lm.lineKind = LineKind::Header; - lm.nodeKind = helper.kind; + lm.nodeKind = sf.kind; lm.foldHead = true; - lm.foldCollapsed = true; // helpers always start collapsed - lm.isHelperLine = true; + lm.foldCollapsed = isCollapsed; + lm.isStaticLine = true; lm.foldLevel = computeFoldLevel(childDepth, true); lm.markerMask = (1u << M_STRUCT_BG); - lm.offsetText = QStringLiteral("~") + QString::number(helperAddr, 16) + lm.offsetText = QStringLiteral("~") + QString::number(staticAddr, 16) .toUpper().rightJustified(state.offsetHexDigits - 1, '0'); - lm.offsetAddr = helperAddr; + lm.offsetAddr = staticAddr; lm.ptrBase = state.currentPtrBase; - lm.effectiveTypeW = overflow ? typeName.size() : typeW; - lm.effectiveNameW = nameW; - state.emitLine(line, lm); + lm.effectiveTypeW = typeName.size() + 7; // "static " prefix + lm.effectiveNameW = sf.name.size(); + state.emitLine(headerLine, lm); - // If helper is expanded (user clicked to expand), compose its children - if (!helper.collapsed && exprOk) { - if (helper.kind == NodeKind::Struct || helper.kind == NodeKind::Array) { - // Compose helper's children at the evaluated address - const QVector& helperKids = childIndices(state, helper.id); - for (int hci : helperKids) { - composeNode(state, tree, prov, hci, childDepth + 1, - helperAddr, helper.id, false, helper.id); + // ── Body + children (only when expanded) ── + if (!isCollapsed) { + // Body line: " return → 0xADDR" + { + QString bodyLine; + if (!sf.offsetExpr.isEmpty()) { + if (exprOk) + bodyLine = fmt::indent(childDepth + 1) + + QStringLiteral("return %1").arg(sf.offsetExpr); + else + bodyLine = fmt::indent(childDepth + 1) + + QStringLiteral("return %1 (error)").arg(sf.offsetExpr); + } else { + bodyLine = fmt::indent(childDepth + 1) + + QStringLiteral("return 0"); } - // Helper footer + + // Right-align resolved address + if (exprOk && !sf.offsetExpr.isEmpty()) { + bodyLine += QStringLiteral(" \u2192 0x") + + QString::number(staticAddr, 16).toUpper(); + } + + LineMeta blm; + blm.nodeIdx = si; + blm.nodeId = sf.id; + blm.depth = childDepth + 1; + blm.lineKind = LineKind::Field; + blm.nodeKind = sf.kind; + blm.isStaticLine = true; + blm.foldLevel = computeFoldLevel(childDepth + 1, false); + blm.markerMask = 0; + blm.offsetText = QString(state.offsetHexDigits, QChar(' ')); + blm.offsetAddr = staticAddr; + blm.ptrBase = state.currentPtrBase; + state.emitLine(bodyLine, blm); + } + + // If struct/array, compose children at evaluated address + if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) { + const QVector& staticKids = childIndices(state, sf.id); + for (int sci : staticKids) { + composeNode(state, tree, prov, sci, childDepth + 1, + staticAddr, sf.id, false, sf.id); + } + } + + // Footer line: "};" + { LineMeta flm; - flm.nodeIdx = hi; - flm.nodeId = helper.id; + flm.nodeIdx = si; + flm.nodeId = sf.id; flm.depth = childDepth; flm.lineKind = LineKind::Footer; - flm.nodeKind = helper.kind; + flm.nodeKind = sf.kind; + flm.isStaticLine = true; flm.foldLevel = computeFoldLevel(childDepth, false); flm.markerMask = 0; - int hSpan = tree.structSpan(helper.id, &state.childMap); - flm.offsetText = fmt::fmtOffsetMargin(helperAddr + hSpan, false, - state.offsetHexDigits); - flm.offsetAddr = helperAddr + hSpan; + if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) { + int sSpan = tree.structSpan(sf.id, &state.childMap); + flm.offsetText = fmt::fmtOffsetMargin(staticAddr + sSpan, false, + state.offsetHexDigits); + flm.offsetAddr = staticAddr + sSpan; + } else { + flm.offsetText = QString(state.offsetHexDigits, QChar(' ')); + flm.offsetAddr = staticAddr; + } flm.ptrBase = state.currentPtrBase; - state.emitLine(fmt::fmtStructFooter(helper, childDepth, hSpan), flm); + state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), flm); } } } diff --git a/src/controller.cpp b/src/controller.cpp index 90cedd6..0d33672 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -481,10 +481,10 @@ void RcxController::connectEditor(RcxEditor* editor) { } break; } - case EditTarget::HelperExpr: { + case EditTarget::StaticExpr: { if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) { const Node& node = m_doc->tree.nodes[nodeIdx]; - if (node.isHelper && text != node.offsetExpr) { + if (node.isStatic && text != node.offsetExpr) { m_doc->undoStack.push(new RcxCommand(this, cmd::ChangeOffsetExpr{node.id, node.offsetExpr, text})); } @@ -1191,10 +1191,10 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { int idx = tree.indexOfId(c.nodeId); if (idx >= 0) tree.nodes[idx].offsetExpr = isUndo ? c.oldExpr : c.newExpr; - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { int idx = tree.indexOfId(c.nodeId); if (idx >= 0) - tree.nodes[idx].isHelper = isUndo ? c.oldVal : c.newVal; + tree.nodes[idx].isStatic = isUndo ? c.oldVal : c.newVal; } }, command); @@ -1849,18 +1849,18 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, menu.addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() { insertNode(nodeId, 0, NodeKind::Hex64, "newField"); }); - // Add Helper — inserts a static helper child - menu.addAction("Add Helper", [this, nodeId]() { - Node helper; - helper.id = m_doc->tree.m_nextId++; - helper.kind = NodeKind::Hex64; - helper.name = QStringLiteral("helper"); - helper.parentId = nodeId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); + // Add Static Field — inserts a static field child + menu.addAction("Add Static Field", [this, nodeId]() { + Node sf; + sf.id = m_doc->tree.m_nextId++; + sf.kind = NodeKind::Hex64; + sf.name = QStringLiteral("static_field"); + sf.parentId = nodeId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); m_doc->undoStack.push(new RcxCommand(this, - cmd::Insert{helper, {}})); + cmd::Insert{sf, {}})); }); if (node.collapsed) { menu.addAction(icon("expand-all.svg"), "&Expand", [this, nodeId]() { @@ -1876,8 +1876,29 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, } - // Helper-specific: Edit Expression inline - if (node.isHelper) { + // Add Static Field as sibling (for child nodes of a struct) + if (node.parentId != 0 && node.kind != NodeKind::Struct && node.kind != NodeKind::Array) { + uint64_t parentId = node.parentId; + int pi = m_doc->tree.indexOfId(parentId); + if (pi >= 0 && (m_doc->tree.nodes[pi].kind == NodeKind::Struct + || m_doc->tree.nodes[pi].kind == NodeKind::Array)) { + menu.addAction("Add Static Field", [this, parentId]() { + Node sf; + sf.id = m_doc->tree.m_nextId++; + sf.kind = NodeKind::Hex64; + sf.name = QStringLiteral("static_field"); + sf.parentId = parentId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + m_doc->undoStack.push(new RcxCommand(this, + cmd::Insert{sf, {}})); + }); + } + } + + // Static field: Edit Expression inline + if (node.isStatic) { menu.addAction("Edit E&xpression", [this, editor, line, nodeId]() { // Build completions list: "base" + sibling field names QStringList completions; @@ -1886,12 +1907,12 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, if (ni >= 0) { uint64_t parentId = m_doc->tree.nodes[ni].parentId; for (const Node& sib : m_doc->tree.nodes) { - if (sib.parentId == parentId && !sib.isHelper && !sib.name.isEmpty()) + if (sib.parentId == parentId && !sib.isStatic && !sib.name.isEmpty()) completions << sib.name; } } - editor->setHelperCompletions(completions); - editor->beginInlineEdit(EditTarget::HelperExpr, line); + editor->setStaticCompletions(completions); + editor->beginInlineEdit(EditTarget::StaticExpr, line); }); } @@ -1948,6 +1969,27 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, // ── Always-available actions ── + // Add Static Field to current view root (struct) + if (m_viewRootId != 0) { + int ri = m_doc->tree.indexOfId(m_viewRootId); + if (ri >= 0 && (m_doc->tree.nodes[ri].kind == NodeKind::Struct + || m_doc->tree.nodes[ri].kind == NodeKind::Array)) { + uint64_t rootId = m_viewRootId; + menu.addAction("Add Static Field", [this, rootId]() { + Node sf; + sf.id = m_doc->tree.m_nextId++; + sf.kind = NodeKind::Hex64; + sf.name = QStringLiteral("static_field"); + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + m_doc->undoStack.push(new RcxCommand(this, + cmd::Insert{sf, {}})); + }); + } + } + menu.addAction(icon("diff-added.svg"), "Append bytes...", [this, &menu]() { bool ok; QString input = QInputDialog::getText(menu.parentWidget(), @@ -2359,12 +2401,12 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode, e.sizeBytes = m_doc->tree.structSpan(n.id); QVector kids = m_doc->tree.childrenOf(n.id); - int nonHelperCount = 0; + int nonStaticCount = 0; int maxAlign = 1; for (int i = 0; i < kids.size(); i++) { const Node& child = m_doc->tree.nodes[kids[i]]; - if (child.isHelper) continue; - nonHelperCount++; + if (child.isStatic) continue; + nonStaticCount++; int childAlign = alignmentFor(child.kind); if (childAlign > maxAlign) maxAlign = childAlign; if (e.fieldSummary.size() < 6) { @@ -2384,7 +2426,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode, .arg(typeName, child.name); } } - e.fieldCount = nonHelperCount; + e.fieldCount = nonStaticCount; e.alignment = maxAlign; entries.append(e); diff --git a/src/core.h b/src/core.h index 1991138..271926f 100644 --- a/src/core.h +++ b/src/core.h @@ -197,8 +197,8 @@ struct Node { QString classKeyword; // "struct", "class", or "enum" (empty = "struct") uint64_t parentId = 0; // 0 = root (no parent) int offset = 0; - bool isHelper = false; // static helper — excluded from struct layout - QString offsetExpr; // C/C++ expression → absolute address (helpers only) + bool isStatic = false; // static field — excluded from struct layout + QString offsetExpr; // C/C++ expression → absolute address (static fields only) int arrayLen = 1; // Array: element count int strLen = 64; bool collapsed = false; @@ -240,8 +240,8 @@ struct Node { o["classKeyword"] = classKeyword; o["parentId"] = QString::number(parentId); o["offset"] = offset; - if (isHelper) - o["isHelper"] = true; + if (isStatic) + o["isStatic"] = true; if (!offsetExpr.isEmpty()) o["offsetExpr"] = offsetExpr; o["arrayLen"] = arrayLen; @@ -283,7 +283,7 @@ struct Node { n.classKeyword = o["classKeyword"].toString(); n.parentId = o["parentId"].toString("0").toULongLong(); n.offset = o["offset"].toInt(0); - n.isHelper = o["isHelper"].toBool(false); + n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false)); n.offsetExpr = o["offsetExpr"].toString(); n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000); n.strLen = qBound(1, o["strLen"].toInt(64), 1000000); @@ -445,7 +445,7 @@ struct NodeTree { QVector kids = childMap ? childMap->value(structId) : childrenOf(structId); for (int ci : kids) { const Node& c = nodes[ci]; - if (c.isHelper) continue; // helpers don't affect struct size + if (c.isStatic) continue; // static fields don't affect struct size int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) ? structSpan(c.id, childMap, visited) : c.byteSize(); int end = c.offset + sz; @@ -600,7 +600,7 @@ struct LineMeta { QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void") bool isArrayElement = false; // true for synthesized primitive array element lines bool isMemberLine = false; // true for enum member / bitfield member lines - bool isHelperLine = false; // true for static helper node lines + bool isStaticLine = false; // true for static field node lines }; inline bool isSyntheticLine(const LineMeta& lm) { @@ -648,7 +648,7 @@ namespace cmd { struct ChangeEnumMembers { uint64_t nodeId; QVector> oldMembers, newMembers; }; struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; }; - struct ToggleHelper { uint64_t nodeId; bool oldVal, newVal; }; + struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; }; } using Command = std::variant< @@ -656,7 +656,7 @@ using Command = std::variant< cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes, cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName, cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers, - cmd::ChangeOffsetExpr, cmd::ToggleHelper + cmd::ChangeOffsetExpr, cmd::ToggleStatic >; // ── Column spans (for inline editing) ── @@ -669,7 +669,7 @@ struct ColumnSpan { enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount, ArrayElementType, ArrayElementCount, PointerTarget, - RootClassType, RootClassName, TypeSelector, HelperExpr }; + RootClassType, RootClassName, TypeSelector, StaticExpr }; // Column layout constants (shared with format.cpp span computation) inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line @@ -747,13 +747,20 @@ inline ColumnSpan memberValueSpanFor(const LineMeta& lm, const QString& lineText return {valStart, valEnd, true}; } -// Helper expression span: locates text between "= " and " →" (or end of line) -inline ColumnSpan helperExprSpanFor(const LineMeta& /*lm*/, const QString& lineText) { - int eq = lineText.indexOf(QLatin1String("= ")); - if (eq < 0) return {}; - int exprStart = eq + 2; - int arrow = lineText.indexOf(QChar(0x2192), exprStart); // → - int exprEnd = (arrow > exprStart) ? arrow - 1 : lineText.size(); +// Static field expression span: locates text between "return " and "→" / "(error)" / end +inline ColumnSpan staticExprSpanFor(const LineMeta& /*lm*/, const QString& lineText) { + int ret = lineText.indexOf(QLatin1String("return ")); + if (ret < 0) return {}; + int exprStart = ret + 7; + // End: before arrow, before "(error)", or line end + int exprEnd = lineText.size(); + int arrow = lineText.indexOf(QChar(0x2192), exprStart); + if (arrow > exprStart) exprEnd = arrow; + int err = lineText.indexOf(QLatin1String("(error)"), exprStart); + if (err > exprStart && err < exprEnd) exprEnd = err; + // Also stop at " }" for collapsed format + int brace = lineText.indexOf(QLatin1String(" }"), exprStart); + if (brace > exprStart && brace < exprEnd) exprEnd = brace; while (exprEnd > exprStart && lineText[exprEnd - 1] == ' ') exprEnd--; return {exprStart, exprEnd, true}; } diff --git a/src/editor.cpp b/src/editor.cpp index 6b243fb..93d4f01 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -504,14 +504,14 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { if (m_editState.target == EditTarget::Value) QTimer::singleShot(0, this, &RcxEditor::validateEditLive); - // Autocomplete for helper expressions — show field names as user types - if (m_editState.target == EditTarget::HelperExpr && !m_helperCompletions.isEmpty()) { + // Autocomplete for static field expressions — show field names as user types + if (m_editState.target == EditTarget::StaticExpr && !m_staticCompletions.isEmpty()) { // Get word at cursor long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS); long wordStart = m_sci->SendScintilla(QsciScintillaBase::SCI_WORDSTARTPOSITION, pos, (long)1); int wordLen = (int)(pos - wordStart); if (wordLen >= 1) { - QByteArray list = m_helperCompletions.join(' ').toUtf8(); + QByteArray list = m_staticCompletions.join(' ').toUtf8(); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' '); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSHOW, (uintptr_t)wordLen, list.constData()); } @@ -1501,6 +1501,20 @@ static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText }; if (kKeywords.contains(typeCol)) return {}; + // Static field headers: "static hex64 target {" — skip "static " prefix + if (lm.isStaticLine) { + int cursor = ind; + while (cursor < typeEnd && lineText[cursor] == ' ') cursor++; + if (lineText.mid(cursor, 7) == QLatin1String("static ")) + cursor += 7; + while (cursor < typeEnd && lineText[cursor] == ' ') cursor++; + int tStart = cursor; + while (cursor < typeEnd && lineText[cursor] != ' ') cursor++; + if (cursor > tStart) + return {tStart, cursor, true}; + return {}; + } + // Named struct: entire type column is the type name (e.g. "_MMPTE") // Find the actual text bounds within the padded column int start = ind; @@ -1586,7 +1600,8 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, if (lm->nodeIdx < 0) return false; // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only) - if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind)) + // Exception: static field names are always editable (they're function names) + if ((t == EditTarget::Name || t == EditTarget::Value) && isHexNode(lm->nodeKind) && !lm->isStaticLine) return false; QString lineText = getLineText(m_sci, line); @@ -1612,9 +1627,9 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, s = arrayElemCountSpanFor(*lm, lineText); break; case EditTarget::PointerTarget: s = pointerTargetSpanFor(*lm, lineText); break; - case EditTarget::HelperExpr: - if (lm->isHelperLine) - s = helperExprSpanFor(*lm, lineText); + case EditTarget::StaticExpr: + if (lm->isStaticLine) + s = staticExprSpanFor(*lm, lineText); break; case EditTarget::Source: break; } @@ -2245,7 +2260,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { || target == EditTarget::RootClassType || target == EditTarget::RootClassName))) return false; // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only) - if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind)) + // Exception: static field names are always editable (they're function names, not hex labels) + if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind) && !lm->isStaticLine) return false; QString lineText; diff --git a/src/editor.h b/src/editor.h index de6e2cb..466521a 100644 --- a/src/editor.h +++ b/src/editor.h @@ -45,7 +45,7 @@ public: bool isEditing() const { return m_editState.active; } bool beginInlineEdit(EditTarget target, int line = -1, int col = -1); void cancelInlineEdit(); - void setHelperCompletions(const QStringList& words) { m_helperCompletions = words; } + void setStaticCompletions(const QStringList& words) { m_staticCompletions = words; } void applySelectionOverlay(const QSet& selIds); void setCommandRowText(const QString& line); @@ -134,7 +134,7 @@ private: bool lastValidationOk = true; // track state to avoid redundant updates }; InlineEditState m_editState; - QStringList m_helperCompletions; // autocomplete words for HelperExpr editing + QStringList m_staticCompletions; // autocomplete words for StaticExpr editing // ── Tab cycling state ── EditTarget m_lastTabTarget = EditTarget::Value; diff --git a/src/generator.cpp b/src/generator.cpp index 39c381a..f44d582 100644 --- a/src/generator.cpp +++ b/src/generator.cpp @@ -173,10 +173,10 @@ static void emitStructBody(GenContext& ctx, uint64_t structId, QString ind = indent(depth); QVector allChildren = ctx.childMap.value(structId); - QVector children, helperIdxs; + QVector children, staticIdxs; for (int ci : allChildren) { - if (tree.nodes[ci].isHelper) - helperIdxs.append(ci); + if (tree.nodes[ci].isStatic) + staticIdxs.append(ci); else children.append(ci); } @@ -318,12 +318,12 @@ static void emitStructBody(GenContext& ctx, uint64_t structId, if (!isUnion && cursor < structSize) emitPadRun(cursor, structSize - cursor); - // Emit helper comments (helpers are runtime-only, not part of struct layout) - for (int hi : helperIdxs) { - const Node& h = tree.nodes[hi]; - QString hType = h.structTypeName.isEmpty() ? ctx.cType(h.kind) : h.structTypeName; - ctx.output += ind + QStringLiteral("// helper: %1 %2 @ %3\n") - .arg(hType, sanitizeIdent(h.name), h.offsetExpr); + // Emit static field comments (static fields are runtime-only, not part of struct layout) + for (int si : staticIdxs) { + const Node& sf = tree.nodes[si]; + QString sfType = sf.structTypeName.isEmpty() ? ctx.cType(sf.kind) : sf.structTypeName; + ctx.output += ind + QStringLiteral("// static: %1 %2 @ %3\n") + .arg(sfType, sanitizeIdent(sf.name), sf.offsetExpr); } } diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index bd3f2b7..d42b7ce 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -2435,9 +2435,9 @@ private slots: QCOMPARE(n.byteSize(), 8); } - // ── Helper node compose tests ── + // ── Static field node compose tests ── - void testHelperSeparatorLine() { + void testStaticFieldHeaderLine() { NodeTree tree; tree.baseAddress = 0; @@ -2456,27 +2456,27 @@ private slots: f1.offset = 0; tree.addNode(f1); - // Helper node - Node helper; - helper.kind = NodeKind::Hex64; - helper.name = "my_helper"; - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); - tree.addNode(helper); + // Static field node + Node sf; + sf.kind = NodeKind::Hex64; + sf.name = "my_static"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + tree.addNode(sf); NullProvider prov; ComposeResult result = compose(tree, prov); - // Separator with "helpers" text and box-drawing chars should appear - QVERIFY2(result.text.contains(QStringLiteral("helpers")), - qPrintable("Expected 'helpers' separator in:\n" + result.text)); - QVERIFY2(result.text.contains(QStringLiteral("\u2500")), - qPrintable("Expected box-drawing separator char in:\n" + result.text)); + // Header with "static" keyword and opening brace should appear + QVERIFY2(result.text.contains(QStringLiteral("static ")) + && result.text.contains(QStringLiteral("my_static")) + && result.text.contains(QStringLiteral("{")), + qPrintable("Expected static field header in:\n" + result.text)); } - void testHelperDoesNotAffectStructSize() { + void testStaticFieldDoesNotAffectStructSize() { NodeTree tree; tree.baseAddress = 0; @@ -2494,24 +2494,24 @@ private slots: f1.offset = 0; tree.addNode(f1); - // Struct span without helper + // Struct span without static field int spanBefore = tree.structSpan(rootId); - // Add helper - Node helper; - helper.kind = NodeKind::Struct; - helper.name = "helper"; - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base + 100"); - tree.addNode(helper); + // Add static field + Node sf; + sf.kind = NodeKind::Struct; + sf.name = "static_field"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base + 100"); + tree.addNode(sf); int spanAfter = tree.structSpan(rootId); QCOMPARE(spanAfter, spanBefore); } - void testHelperIsHelperLineFlag() { + void testStaticFieldIsStaticLineFlag() { NodeTree tree; tree.baseAddress = 0; @@ -2529,30 +2529,30 @@ private slots: f1.offset = 0; tree.addNode(f1); - Node helper; - helper.kind = NodeKind::Hex64; - helper.name = "my_helper"; - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); - tree.addNode(helper); + Node sf; + sf.kind = NodeKind::Hex64; + sf.name = "my_static"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + tree.addNode(sf); NullProvider prov; ComposeResult result = compose(tree, prov); - // At least one line should have isHelperLine set - bool foundHelper = false; + // At least one line should have isStaticLine set + bool foundStaticField = false; for (const auto& lm : result.meta) { - if (lm.isHelperLine) { - foundHelper = true; + if (lm.isStaticLine) { + foundStaticField = true; break; } } - QVERIFY2(foundHelper, "Expected at least one LineMeta with isHelperLine=true"); + QVERIFY2(foundStaticField, "Expected at least one LineMeta with isStaticLine=true"); } - void testHelperCollapsedByDefault() { + void testStaticFieldCollapsed() { NodeTree tree; tree.baseAddress = 0; @@ -2563,42 +2563,42 @@ private slots: int ri = tree.addNode(root); uint64_t rootId = tree.nodes[ri].id; - // Helper struct with a child (should still appear collapsed) - Node helper; - helper.kind = NodeKind::Struct; - helper.name = "inner"; - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); - helper.collapsed = true; - int hi = tree.addNode(helper); - uint64_t helperId = tree.nodes[hi].id; + // Static field struct with a child (should still appear collapsed) + Node sf; + sf.kind = NodeKind::Struct; + sf.name = "inner"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + sf.collapsed = true; + int hi = tree.addNode(sf); + uint64_t sfId = tree.nodes[hi].id; - Node hChild; - hChild.kind = NodeKind::UInt32; - hChild.name = "x"; - hChild.parentId = helperId; - hChild.offset = 0; - tree.addNode(hChild); + Node sfChild; + sfChild.kind = NodeKind::UInt32; + sfChild.name = "x"; + sfChild.parentId = sfId; + sfChild.offset = 0; + tree.addNode(sfChild); NullProvider prov; ComposeResult result = compose(tree, prov); - // The helper's child should NOT have a visible line (it's collapsed) + // The static field's child should NOT have a visible line (it's collapsed) bool foundChildLine = false; for (const auto& lm : result.meta) { if (lm.nodeIdx >= 0 && lm.nodeIdx < tree.nodes.size() && tree.nodes[lm.nodeIdx].name == QStringLiteral("x") - && tree.nodes[lm.nodeIdx].parentId == helperId) { + && tree.nodes[lm.nodeIdx].parentId == sfId) { foundChildLine = true; } } QVERIFY2(!foundChildLine, - "Helper's children should not be visible when collapsed"); + "Static field's children should not be visible when collapsed"); } - void testHelperExpressionShownInText() { + void testStaticFieldExpressionShownInText() { NodeTree tree; tree.baseAddress = 0; @@ -2609,14 +2609,14 @@ private slots: int ri = tree.addNode(root); uint64_t rootId = tree.nodes[ri].id; - Node helper; - helper.kind = NodeKind::Hex64; - helper.name = "my_helper"; - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base + 0x10"); - tree.addNode(helper); + Node sf; + sf.kind = NodeKind::Hex64; + sf.name = "my_static"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base + 0x10"); + tree.addNode(sf); NullProvider prov; ComposeResult result = compose(tree, prov); diff --git a/tests/test_controller.cpp b/tests/test_controller.cpp index d5fb3cb..3eb0d2b 100644 --- a/tests/test_controller.cpp +++ b/tests/test_controller.cpp @@ -668,179 +668,179 @@ private slots: QVERIFY(newIdx >= 0); QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::UInt32); } - // ── Helper node controller tests ── + // ── Static field node controller tests ── - void testAddHelper() { + void testAddStaticField() { uint64_t rootId = m_doc->tree.nodes[0].id; int origSize = m_doc->tree.nodes.size(); - // Simulate "Add Helper" — same code as context menu action - Node helper; - helper.id = m_doc->tree.m_nextId++; - helper.kind = NodeKind::Hex64; - helper.name = QStringLiteral("helper"); - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); - m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); + // Simulate "Add Static Field" — same code as context menu action + Node sf; + sf.id = m_doc->tree.m_nextId++; + sf.kind = NodeKind::Hex64; + sf.name = QStringLiteral("static_field"); + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}})); QApplication::processEvents(); QCOMPARE(m_doc->tree.nodes.size(), origSize + 1); const auto& h = m_doc->tree.nodes.back(); - QCOMPARE(h.isHelper, true); + QCOMPARE(h.isStatic, true); QCOMPARE(h.offsetExpr, QStringLiteral("base")); - QCOMPARE(h.name, QStringLiteral("helper")); + QCOMPARE(h.name, QStringLiteral("static_field")); QCOMPARE(h.parentId, rootId); } - void testAddHelperUndo() { + void testAddStaticFieldUndo() { uint64_t rootId = m_doc->tree.nodes[0].id; int origSize = m_doc->tree.nodes.size(); - Node helper; - helper.id = m_doc->tree.m_nextId++; - helper.kind = NodeKind::Hex64; - helper.name = QStringLiteral("helper"); - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); - m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); + Node sf; + sf.id = m_doc->tree.m_nextId++; + sf.kind = NodeKind::Hex64; + sf.name = QStringLiteral("static_field"); + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}})); QApplication::processEvents(); QCOMPARE(m_doc->tree.nodes.size(), origSize + 1); - // Undo: helper should be gone + // Undo: static field should be gone m_doc->undoStack.undo(); QApplication::processEvents(); QCOMPARE(m_doc->tree.nodes.size(), origSize); - // Redo: helper should be back + // Redo: static field should be back m_doc->undoStack.redo(); QApplication::processEvents(); QCOMPARE(m_doc->tree.nodes.size(), origSize + 1); - QCOMPARE(m_doc->tree.nodes.back().isHelper, true); + QCOMPARE(m_doc->tree.nodes.back().isStatic, true); } - void testChangeHelperExpression() { + void testChangeStaticFieldExpression() { uint64_t rootId = m_doc->tree.nodes[0].id; - // Add a helper - Node helper; - helper.id = m_doc->tree.m_nextId++; - helper.kind = NodeKind::Hex64; - helper.name = QStringLiteral("helper"); - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); - m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); + // Add a static field + Node sf; + sf.id = m_doc->tree.m_nextId++; + sf.kind = NodeKind::Hex64; + sf.name = QStringLiteral("static_field"); + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}})); QApplication::processEvents(); - uint64_t helperId = m_doc->tree.nodes.back().id; + uint64_t sfId = m_doc->tree.nodes.back().id; // Change expression m_doc->undoStack.push(new RcxCommand(m_ctrl, - cmd::ChangeOffsetExpr{helperId, QStringLiteral("base"), QStringLiteral("base + 0x10")})); + cmd::ChangeOffsetExpr{sfId, QStringLiteral("base"), QStringLiteral("base + 0x10")})); QApplication::processEvents(); - int idx = m_doc->tree.indexOfId(helperId); + int idx = m_doc->tree.indexOfId(sfId); QVERIFY(idx >= 0); QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + 0x10")); // Undo: old expression restored m_doc->undoStack.undo(); QApplication::processEvents(); - idx = m_doc->tree.indexOfId(helperId); + idx = m_doc->tree.indexOfId(sfId); QVERIFY(idx >= 0); QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base")); } - void testDeleteHelperPreservesStructSize() { + void testDeleteStaticFieldPreservesStructSize() { uint64_t rootId = m_doc->tree.nodes[0].id; int spanBefore = m_doc->tree.structSpan(rootId); - // Add a helper - Node helper; - helper.id = m_doc->tree.m_nextId++; - helper.kind = NodeKind::Hex64; - helper.name = QStringLiteral("helper"); - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); - m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); + // Add a static field + Node sf; + sf.id = m_doc->tree.m_nextId++; + sf.kind = NodeKind::Hex64; + sf.name = QStringLiteral("static_field"); + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}})); QApplication::processEvents(); - // Struct size unchanged after adding helper + // Struct size unchanged after adding static field QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore); - // Remove helper - uint64_t helperId = m_doc->tree.nodes.back().id; - m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Remove{helperId})); + // Remove static field + uint64_t sfId = m_doc->tree.nodes.back().id; + m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Remove{sfId})); QApplication::processEvents(); // Struct size still unchanged QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore); } - void testHelperRenamePreservesExpression() { + void testStaticFieldRenamePreservesExpression() { uint64_t rootId = m_doc->tree.nodes[0].id; - // Add a helper - Node helper; - helper.id = m_doc->tree.m_nextId++; - helper.kind = NodeKind::Hex64; - helper.name = QStringLiteral("my_helper"); - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base + field_u32"); - m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); + // Add a static field + Node sf; + sf.id = m_doc->tree.m_nextId++; + sf.kind = NodeKind::Hex64; + sf.name = QStringLiteral("my_static"); + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base + field_u32"); + m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}})); QApplication::processEvents(); - uint64_t helperId = m_doc->tree.nodes.back().id; + uint64_t sfId = m_doc->tree.nodes.back().id; - // Rename the helper + // Rename the static field m_doc->undoStack.push(new RcxCommand(m_ctrl, - cmd::Rename{helperId, QStringLiteral("my_helper"), QStringLiteral("renamed_helper")})); + cmd::Rename{sfId, QStringLiteral("my_static"), QStringLiteral("renamed_static")})); QApplication::processEvents(); - int idx = m_doc->tree.indexOfId(helperId); + int idx = m_doc->tree.indexOfId(sfId); QVERIFY(idx >= 0); - QCOMPARE(m_doc->tree.nodes[idx].name, QStringLiteral("renamed_helper")); + QCOMPARE(m_doc->tree.nodes[idx].name, QStringLiteral("renamed_static")); // Expression should be preserved QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + field_u32")); - QCOMPARE(m_doc->tree.nodes[idx].isHelper, true); + QCOMPARE(m_doc->tree.nodes[idx].isStatic, true); } - void testHelperTypeChangePreservesFlags() { + void testStaticFieldTypeChangePreservesFlags() { uint64_t rootId = m_doc->tree.nodes[0].id; - Node helper; - helper.id = m_doc->tree.m_nextId++; - helper.kind = NodeKind::Hex64; - helper.name = QStringLiteral("helper"); - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); - m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); + Node sf; + sf.id = m_doc->tree.m_nextId++; + sf.kind = NodeKind::Hex64; + sf.name = QStringLiteral("static_field"); + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}})); QApplication::processEvents(); - uint64_t helperId = m_doc->tree.nodes.back().id; + uint64_t sfId = m_doc->tree.nodes.back().id; // Change kind to UInt32 m_doc->undoStack.push(new RcxCommand(m_ctrl, - cmd::ChangeKind{helperId, NodeKind::Hex64, NodeKind::UInt32})); + cmd::ChangeKind{sfId, NodeKind::Hex64, NodeKind::UInt32})); QApplication::processEvents(); - int idx = m_doc->tree.indexOfId(helperId); + int idx = m_doc->tree.indexOfId(sfId); QVERIFY(idx >= 0); QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); - // Helper flags must survive type change - QCOMPARE(m_doc->tree.nodes[idx].isHelper, true); + // Static field flags must survive type change + QCOMPARE(m_doc->tree.nodes[idx].isStatic, true); QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base")); } }; diff --git a/tests/test_core.cpp b/tests/test_core.cpp index 94e0253..44682da 100644 --- a/tests/test_core.cpp +++ b/tests/test_core.cpp @@ -672,9 +672,9 @@ private slots: QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range) } - // ── Helper node serialization ── + // ── Static field node serialization ── - void testHelperJsonRoundTrip() { + void testStaticFieldJsonRoundTrip() { rcx::NodeTree tree; tree.baseAddress = 0x14000000; @@ -692,27 +692,27 @@ private slots: field.offset = 0x3C; tree.addNode(field); - rcx::Node helper; - helper.kind = rcx::NodeKind::Struct; - helper.name = "nt_hdr"; - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base + e_lfanew"); - tree.addNode(helper); + rcx::Node sf; + sf.kind = rcx::NodeKind::Struct; + sf.name = "nt_hdr"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base + e_lfanew"); + tree.addNode(sf); QJsonObject json = tree.toJson(); rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json); QCOMPARE(tree2.nodes.size(), 3); const auto& h = tree2.nodes[2]; - QCOMPARE(h.isHelper, true); + QCOMPARE(h.isStatic, true); QCOMPARE(h.offsetExpr, QStringLiteral("base + e_lfanew")); QCOMPARE(h.name, QStringLiteral("nt_hdr")); } - void testHelperJsonBackwardCompat() { - // Old JSON without isHelper/offsetExpr should load with defaults + void testStaticFieldJsonBackwardCompat() { + // Old JSON without isStatic/offsetExpr should load with defaults rcx::NodeTree tree; rcx::Node root; root.kind = rcx::NodeKind::Struct; @@ -723,11 +723,11 @@ private slots: QJsonObject json = tree.toJson(); rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json); - QCOMPARE(tree2.nodes[0].isHelper, false); + QCOMPARE(tree2.nodes[0].isStatic, false); QCOMPARE(tree2.nodes[0].offsetExpr, QString()); } - void testStructSpanExcludesHelpers() { + void testStructSpanExcludesStaticFields() { using namespace rcx; NodeTree tree; @@ -754,27 +754,27 @@ private slots: f2.offset = 4; tree.addNode(f2); - // Helper: should NOT affect span - Node helper; - helper.kind = NodeKind::Struct; - helper.name = "helper"; - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); - tree.addNode(helper); + // Static field: should NOT affect span + Node sf; + sf.kind = NodeKind::Struct; + sf.name = "static_field"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + tree.addNode(sf); - // Span should be max(0+4, 4+8) = 12, same as without helper + // Span should be max(0+4, 4+8) = 12, same as without static field QCOMPARE(tree.structSpan(rootId), 12); } - void testHelperExprSpanFor() { + void testStaticExprSpanFor() { using namespace rcx; - // Simulate a helper header line: " ▸ struct NT_HEADERS nt_hdr = base + e_lfanew → 0x1400000E8" + // Simulate a static field body line: " return base + e_lfanew → 0x1400000E8" LineMeta lm; - lm.isHelperLine = true; - QString lineText = QStringLiteral(" \u25B8 struct NT_HEADERS nt_hdr = base + e_lfanew \u2192 0x1400000E8"); - ColumnSpan span = helperExprSpanFor(lm, lineText); + lm.isStaticLine = true; + QString lineText = QStringLiteral(" return base + e_lfanew \u2192 0x1400000E8"); + ColumnSpan span = staticExprSpanFor(lm, lineText); QVERIFY(span.valid); QString expr = lineText.mid(span.start, span.end - span.start); QCOMPARE(expr.trimmed(), QStringLiteral("base + e_lfanew")); diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index 10587b0..ef83788 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -2556,6 +2556,218 @@ private slots: QApplication::processEvents(); QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor); } + // ── Static field: name must be editable (it's a function name, not hex label) ── + + void testStaticFieldNameEditable() { + // Build a tree with one regular field + one static field + NodeTree tree; + tree.baseAddress = 0; + Node root; + root.kind = NodeKind::Struct; + root.name = "Test"; + root.structTypeName = "Test"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + Node f; + f.kind = NodeKind::UInt32; + f.name = "field_a"; + f.parentId = rootId; + f.offset = 0; + tree.addNode(f); + + Node sf; + sf.kind = NodeKind::Hex64; + sf.name = "my_target"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + tree.addNode(sf); + + NullProvider prov; + ComposeResult result = compose(tree, prov); + m_editor->applyDocument(result); + QApplication::processEvents(); + + // Find the static field header line + int headerLine = -1; + for (int i = 0; i < result.meta.size(); i++) { + if (result.meta[i].isStaticLine && result.meta[i].lineKind == LineKind::Header) { + headerLine = i; + break; + } + } + QVERIFY2(headerLine >= 0, "Should have a static field header line"); + + const LineMeta* lm = m_editor->metaForLine(headerLine); + QVERIFY(lm); + QVERIFY(lm->isStaticLine); + + // Verify the header text contains the name + QString text = m_editor->textWithMargins(); + QStringList lines = text.split('\n'); + QVERIFY2(headerLine < lines.size(), "header line in range"); + QString hdrText = lines[headerLine]; + QVERIFY2(hdrText.contains("my_target"), qPrintable("Header line should contain name: " + hdrText)); + + // The name should be inline-editable despite being a hex node kind + int nameStart = kFoldCol + lm->depth * 3 + lm->effectiveTypeW + kSepWidth; + bool ok = m_editor->beginInlineEdit(EditTarget::Name, headerLine, nameStart); + QVERIFY2(ok, qPrintable(QString("Static field name must be editable. line=%1 col=%2 depth=%3 typeW=%4 text='%5'") + .arg(headerLine).arg(nameStart).arg(lm->depth).arg(lm->effectiveTypeW).arg(hdrText))); + m_editor->cancelInlineEdit(); + } + + // ── Static field: type in header triggers type picker, not inline edit ── + + void testStaticFieldTypeClickable() { + // Build same tree as above + NodeTree tree; + tree.baseAddress = 0; + Node root; + root.kind = NodeKind::Struct; + root.name = "Test"; + root.structTypeName = "Test"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + Node f; + f.kind = NodeKind::UInt32; + f.name = "field_a"; + f.parentId = rootId; + f.offset = 0; + tree.addNode(f); + + Node sf; + sf.kind = NodeKind::Hex64; + sf.name = "my_target"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + tree.addNode(sf); + + NullProvider prov; + ComposeResult result = compose(tree, prov); + m_editor->applyDocument(result); + QApplication::processEvents(); + + // Find the static field header line + int headerLine = -1; + for (int i = 0; i < result.meta.size(); i++) { + if (result.meta[i].isStaticLine && result.meta[i].lineKind == LineKind::Header) { + headerLine = i; + break; + } + } + QVERIFY(headerLine >= 0); + + const LineMeta* lm = m_editor->metaForLine(headerLine); + QVERIFY(lm); + + // Scroll to ensure visible + m_editor->scintilla()->SendScintilla( + QsciScintillaBase::SCI_ENSUREVISIBLE, (unsigned long)headerLine); + m_editor->scintilla()->SendScintilla( + QsciScintillaBase::SCI_GOTOLINE, (unsigned long)headerLine); + QApplication::processEvents(); + + // Hover over the type column (after "static " prefix) — should be PointingHandCursor + // "static " is 7 chars, so the actual type starts at indent + 7 + int typeCol = kFoldCol + lm->depth * 3 + 7; + QPoint typePos = colToViewport(m_editor->scintilla(), headerLine, typeCol + 1); + if (typePos.y() > 0) { + sendMouseMove(m_editor->scintilla()->viewport(), typePos); + QApplication::processEvents(); + QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor); + } + } + + // ── Static field: body line expression is editable ── + + void testStaticFieldExprEditable() { + NodeTree tree; + tree.baseAddress = 0; + Node root; + root.kind = NodeKind::Struct; + root.name = "Test"; + root.structTypeName = "Test"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + Node sf; + sf.kind = NodeKind::Hex64; + sf.name = "target"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base + 0x10"); + tree.addNode(sf); + + NullProvider prov; + ComposeResult result = compose(tree, prov); + m_editor->applyDocument(result); + QApplication::processEvents(); + + // Find the body line (Field with isStaticLine) + int bodyLine = -1; + for (int i = 0; i < result.meta.size(); i++) { + if (result.meta[i].isStaticLine && result.meta[i].lineKind == LineKind::Field) { + bodyLine = i; + break; + } + } + QVERIFY2(bodyLine >= 0, "Should have a static field body line"); + + // The expression should be editable via StaticExpr target + bool ok = m_editor->beginInlineEdit(EditTarget::StaticExpr, bodyLine); + QVERIFY2(ok, "Static field expression must be editable"); + m_editor->cancelInlineEdit(); + } + + // ── No separator line for static fields ── + + void testStaticFieldNoSeparator() { + NodeTree tree; + tree.baseAddress = 0; + Node root; + root.kind = NodeKind::Struct; + root.name = "Test"; + root.structTypeName = "Test"; + root.parentId = 0; + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + Node f; + f.kind = NodeKind::UInt32; + f.name = "a"; + f.parentId = rootId; + f.offset = 0; + tree.addNode(f); + + Node sf; + sf.kind = NodeKind::Hex64; + sf.name = "target"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + tree.addNode(sf); + + NullProvider prov; + ComposeResult result = compose(tree, prov); + + // No separator line with box-drawing characters should exist + QStringList lines = result.text.split('\n'); + for (const auto& line : lines) { + QVERIFY2(!line.contains(QStringLiteral("\u2500\u2500\u2500\u2500 static \u2500\u2500\u2500\u2500")), + "Static fields should not have a separator line"); + } + } }; QTEST_MAIN(TestEditor) diff --git a/tests/test_generator.cpp b/tests/test_generator.cpp index a9d3014..8ba9a9c 100644 --- a/tests/test_generator.cpp +++ b/tests/test_generator.cpp @@ -758,9 +758,9 @@ private slots: QVERIFY(!result.contains("struct _LIST_ENTRY\n{")); QVERIFY(!result.contains("uint8_t _pad")); } - // ── Helper node generator tests ── + // ── Static field node generator tests ── - void testHelperNotInStructBody() { + void testStaticFieldNotInStructBody() { rcx::NodeTree tree; rcx::Node root; @@ -778,32 +778,32 @@ private slots: f1.offset = 0; tree.addNode(f1); - rcx::Node helper; - helper.kind = rcx::NodeKind::Struct; - helper.name = "nt_hdr"; - helper.structTypeName = "IMAGE_NT_HEADERS"; - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base + e_lfanew"); - tree.addNode(helper); + rcx::Node sf; + sf.kind = rcx::NodeKind::Struct; + sf.name = "nt_hdr"; + sf.structTypeName = "IMAGE_NT_HEADERS"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base + e_lfanew"); + tree.addNode(sf); QString result = rcx::renderCpp(tree, rootId); - // Helper should NOT appear as a member in the struct body + // Static field should NOT appear as a member in the struct body QVERIFY2(!result.contains("IMAGE_NT_HEADERS nt_hdr;"), - qPrintable("Helper should not be in struct body:\n" + result)); + qPrintable("Static field should not be in struct body:\n" + result)); - // Helper SHOULD appear as a comment - QVERIFY2(result.contains("// helper:"), - qPrintable("Helper comment missing:\n" + result)); + // Static field SHOULD appear as a comment + QVERIFY2(result.contains("// static:"), + qPrintable("Static field comment missing:\n" + result)); QVERIFY2(result.contains("nt_hdr"), - qPrintable("Helper name missing from comment:\n" + result)); + qPrintable("Static field name missing from comment:\n" + result)); QVERIFY2(result.contains("base + e_lfanew"), - qPrintable("Helper expression missing from comment:\n" + result)); + qPrintable("Static field expression missing from comment:\n" + result)); } - void testHelperCommentFormat() { + void testStaticFieldCommentFormat() { rcx::NodeTree tree; rcx::Node root; @@ -821,26 +821,26 @@ private slots: f1.offset = 0; tree.addNode(f1); - rcx::Node helper; - helper.kind = rcx::NodeKind::Hex64; - helper.name = "ptr"; - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base + 0xFF"); - tree.addNode(helper); + rcx::Node sf; + sf.kind = rcx::NodeKind::Hex64; + sf.name = "ptr"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base + 0xFF"); + tree.addNode(sf); QString result = rcx::renderCpp(tree, rootId); // The regular field should be in the struct body QVERIFY(result.contains("uint64_t base_field;")); - // Helper emitted as comment after struct body - QVERIFY(result.contains("// helper:")); + // Static field emitted as comment after struct body + QVERIFY(result.contains("// static:")); QVERIFY(result.contains("@ base + 0xFF")); } - void testStructSizeUnchangedByHelper() { + void testStructSizeUnchangedByStaticField() { rcx::NodeTree tree; rcx::Node root; @@ -858,14 +858,14 @@ private slots: f1.offset = 0; tree.addNode(f1); - rcx::Node helper; - helper.kind = rcx::NodeKind::Struct; - helper.name = "big_helper"; - helper.parentId = rootId; - helper.offset = 0; - helper.isHelper = true; - helper.offsetExpr = QStringLiteral("base"); - tree.addNode(helper); + rcx::Node sf; + sf.kind = rcx::NodeKind::Struct; + sf.name = "big_static"; + sf.parentId = rootId; + sf.offset = 0; + sf.isStatic = true; + sf.offsetExpr = QStringLiteral("base"); + tree.addNode(sf); QString result = rcx::renderCpp(tree, rootId, nullptr, true);