refactor: rename helpers to static fields, block-style rendering, sibling insert

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.
This commit is contained in:
IChooseYou
2026-02-28 08:21:00 -07:00
committed by IChooseYou
parent 6a51c904de
commit 95faf027a9
12 changed files with 685 additions and 367 deletions

View File

@@ -230,6 +230,11 @@ if(BUILD_TESTING)
target_link_libraries(test_addressparser PRIVATE ${QT}::Core ${QT}::Test) target_link_libraries(test_addressparser PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_addressparser COMMAND test_addressparser) 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) if(WIN32)
add_executable(test_import_pdb tests/test_import_pdb.cpp add_executable(test_import_pdb tests/test_import_pdb.cpp
src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp) src/imports/import_pdb.cpp src/format.cpp src/compose.cpp src/addressparser.cpp)

View File

@@ -397,11 +397,11 @@ void composeParent(ComposeState& state, const NodeTree& tree,
const QVector<int>& allChildren = childIndices(state, node.id); const QVector<int>& allChildren = childIndices(state, node.id);
// Split children into regular nodes and helpers (helpers render at the end) // Split children into regular nodes and static fields (static fields render at the end)
QVector<int> regular, helperIdxs; QVector<int> regular, staticIdxs;
for (int ci : allChildren) { for (int ci : allChildren) {
if (tree.nodes[ci].isHelper) if (tree.nodes[ci].isStatic)
helperIdxs.append(ci); staticIdxs.append(ci);
else else
regular.append(ci); regular.append(ci);
} }
@@ -523,24 +523,9 @@ void composeParent(ComposeState& state, const NodeTree& tree,
childrenAreArrayElements ? absAddr : 0); childrenAreArrayElements ? absAddr : 0);
} }
// ── Static helpers: render after regular children, before footer ── // ── Static fields: render after regular children, before footer ──
if (!helperIdxs.isEmpty() && !node.collapsed) { if (!staticIdxs.isEmpty() && !node.collapsed) {
// Separator line // Build identifier resolver for static field expressions
{
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
auto makeResolver = [&](uint64_t parentAbsAddr) { auto makeResolver = [&](uint64_t parentAbsAddr) {
AddressParserCallbacks cbs; AddressParserCallbacks cbs;
cbs.resolveIdentifier = [&tree, &prov, &regular, parentAbsAddr] cbs.resolveIdentifier = [&tree, &prov, &regular, parentAbsAddr]
@@ -582,92 +567,143 @@ void composeParent(ComposeState& state, const NodeTree& tree,
auto cbs = makeResolver(absAddr); auto cbs = makeResolver(absAddr);
for (int hi : helperIdxs) { for (int si : staticIdxs) {
const Node& helper = tree.nodes[hi]; const Node& sf = tree.nodes[si];
// Evaluate expression → absolute address // Evaluate expression → absolute address
uint64_t helperAddr = 0; uint64_t staticAddr = 0;
bool exprOk = false; bool exprOk = false;
if (!helper.offsetExpr.isEmpty()) { if (!sf.offsetExpr.isEmpty()) {
auto result = AddressParser::evaluate(helper.offsetExpr, 8, &cbs); auto result = AddressParser::evaluate(sf.offsetExpr, 8, &cbs);
exprOk = result.ok; exprOk = result.ok;
if (result.ok) if (result.ok)
helperAddr = result.value; staticAddr = result.value;
} }
// Format: "▸ type name = expr → 0xADDR" (or "= expr (error)" on failure) // Resolve type name
int typeW = state.effectiveTypeW(node.id);
int nameW = state.effectiveNameW(node.id);
QString typeName; QString typeName;
if (helper.kind == NodeKind::Struct) if (sf.kind == NodeKind::Struct)
typeName = fmt::structTypeName(helper); typeName = fmt::structTypeName(sf);
else if (helper.kind == NodeKind::Pointer64 || helper.kind == NodeKind::Pointer32) else if (sf.kind == NodeKind::Pointer64 || sf.kind == NodeKind::Pointer32)
typeName = fmt::pointerTypeName(helper.kind, resolvePointerTarget(tree, helper.refId)); typeName = fmt::pointerTypeName(sf.kind, resolvePointerTarget(tree, sf.refId));
else else
typeName = fmt::typeNameRaw(helper.kind); typeName = fmt::typeNameRaw(sf.kind);
bool overflow = state.compactColumns && typeName.size() > typeW; bool isCollapsed = sf.collapsed;
QString type = overflow ? typeName : typeName.leftJustified(typeW);
QString name = overflow ? helper.name : helper.name.leftJustified(nameW);
QString exprPart; // ── Header line: "static <type> <name> {" or collapsed: "static <type> <name> { return <expr>; }"
if (!helper.offsetExpr.isEmpty()) { QString headerLine;
if (exprOk) if (isCollapsed) {
exprPart = QStringLiteral("= %1 \u2192 0x%2") QString exprPart;
.arg(helper.offsetExpr) if (!sf.offsetExpr.isEmpty()) {
.arg(QString::number(helperAddr, 16).toUpper()); if (exprOk)
else exprPart = QStringLiteral("return %1 } \u2192 0x%2")
exprPart = QStringLiteral("= %1 (error)").arg(helper.offsetExpr); .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; LineMeta lm;
lm.nodeIdx = hi; lm.nodeIdx = si;
lm.nodeId = helper.id; lm.nodeId = sf.id;
lm.depth = childDepth; lm.depth = childDepth;
lm.lineKind = LineKind::Header; lm.lineKind = LineKind::Header;
lm.nodeKind = helper.kind; lm.nodeKind = sf.kind;
lm.foldHead = true; lm.foldHead = true;
lm.foldCollapsed = true; // helpers always start collapsed lm.foldCollapsed = isCollapsed;
lm.isHelperLine = true; lm.isStaticLine = true;
lm.foldLevel = computeFoldLevel(childDepth, true); lm.foldLevel = computeFoldLevel(childDepth, true);
lm.markerMask = (1u << M_STRUCT_BG); 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'); .toUpper().rightJustified(state.offsetHexDigits - 1, '0');
lm.offsetAddr = helperAddr; lm.offsetAddr = staticAddr;
lm.ptrBase = state.currentPtrBase; lm.ptrBase = state.currentPtrBase;
lm.effectiveTypeW = overflow ? typeName.size() : typeW; lm.effectiveTypeW = typeName.size() + 7; // "static " prefix
lm.effectiveNameW = nameW; lm.effectiveNameW = sf.name.size();
state.emitLine(line, lm); state.emitLine(headerLine, lm);
// If helper is expanded (user clicked to expand), compose its children // ── Body + children (only when expanded) ──
if (!helper.collapsed && exprOk) { if (!isCollapsed) {
if (helper.kind == NodeKind::Struct || helper.kind == NodeKind::Array) { // Body line: " return <expr> → 0xADDR"
// Compose helper's children at the evaluated address {
const QVector<int>& helperKids = childIndices(state, helper.id); QString bodyLine;
for (int hci : helperKids) { if (!sf.offsetExpr.isEmpty()) {
composeNode(state, tree, prov, hci, childDepth + 1, if (exprOk)
helperAddr, helper.id, false, helper.id); 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<int>& 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; LineMeta flm;
flm.nodeIdx = hi; flm.nodeIdx = si;
flm.nodeId = helper.id; flm.nodeId = sf.id;
flm.depth = childDepth; flm.depth = childDepth;
flm.lineKind = LineKind::Footer; flm.lineKind = LineKind::Footer;
flm.nodeKind = helper.kind; flm.nodeKind = sf.kind;
flm.isStaticLine = true;
flm.foldLevel = computeFoldLevel(childDepth, false); flm.foldLevel = computeFoldLevel(childDepth, false);
flm.markerMask = 0; flm.markerMask = 0;
int hSpan = tree.structSpan(helper.id, &state.childMap); if (exprOk && (sf.kind == NodeKind::Struct || sf.kind == NodeKind::Array)) {
flm.offsetText = fmt::fmtOffsetMargin(helperAddr + hSpan, false, int sSpan = tree.structSpan(sf.id, &state.childMap);
state.offsetHexDigits); flm.offsetText = fmt::fmtOffsetMargin(staticAddr + sSpan, false,
flm.offsetAddr = helperAddr + hSpan; state.offsetHexDigits);
flm.offsetAddr = staticAddr + sSpan;
} else {
flm.offsetText = QString(state.offsetHexDigits, QChar(' '));
flm.offsetAddr = staticAddr;
}
flm.ptrBase = state.currentPtrBase; flm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(helper, childDepth, hSpan), flm); state.emitLine(fmt::indent(childDepth) + QStringLiteral("};"), flm);
} }
} }
} }

View File

@@ -481,10 +481,10 @@ void RcxController::connectEditor(RcxEditor* editor) {
} }
break; break;
} }
case EditTarget::HelperExpr: { case EditTarget::StaticExpr: {
if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) { if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) {
const Node& node = m_doc->tree.nodes[nodeIdx]; 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, m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeOffsetExpr{node.id, node.offsetExpr, text})); 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); int idx = tree.indexOfId(c.nodeId);
if (idx >= 0) if (idx >= 0)
tree.nodes[idx].offsetExpr = isUndo ? c.oldExpr : c.newExpr; tree.nodes[idx].offsetExpr = isUndo ? c.oldExpr : c.newExpr;
} else if constexpr (std::is_same_v<T, cmd::ToggleHelper>) { } else if constexpr (std::is_same_v<T, cmd::ToggleStatic>) {
int idx = tree.indexOfId(c.nodeId); int idx = tree.indexOfId(c.nodeId);
if (idx >= 0) if (idx >= 0)
tree.nodes[idx].isHelper = isUndo ? c.oldVal : c.newVal; tree.nodes[idx].isStatic = isUndo ? c.oldVal : c.newVal;
} }
}, command); }, command);
@@ -1849,18 +1849,18 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
menu.addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() { menu.addAction(icon("diff-added.svg"), "Add &Child", [this, nodeId]() {
insertNode(nodeId, 0, NodeKind::Hex64, "newField"); insertNode(nodeId, 0, NodeKind::Hex64, "newField");
}); });
// Add Helper — inserts a static helper child // Add Static Field — inserts a static field child
menu.addAction("Add Helper", [this, nodeId]() { menu.addAction("Add Static Field", [this, nodeId]() {
Node helper; Node sf;
helper.id = m_doc->tree.m_nextId++; sf.id = m_doc->tree.m_nextId++;
helper.kind = NodeKind::Hex64; sf.kind = NodeKind::Hex64;
helper.name = QStringLiteral("helper"); sf.name = QStringLiteral("static_field");
helper.parentId = nodeId; sf.parentId = nodeId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
m_doc->undoStack.push(new RcxCommand(this, m_doc->undoStack.push(new RcxCommand(this,
cmd::Insert{helper, {}})); cmd::Insert{sf, {}}));
}); });
if (node.collapsed) { if (node.collapsed) {
menu.addAction(icon("expand-all.svg"), "&Expand", [this, nodeId]() { 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 // Add Static Field as sibling (for child nodes of a struct)
if (node.isHelper) { 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]() { menu.addAction("Edit E&xpression", [this, editor, line, nodeId]() {
// Build completions list: "base" + sibling field names // Build completions list: "base" + sibling field names
QStringList completions; QStringList completions;
@@ -1886,12 +1907,12 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
if (ni >= 0) { if (ni >= 0) {
uint64_t parentId = m_doc->tree.nodes[ni].parentId; uint64_t parentId = m_doc->tree.nodes[ni].parentId;
for (const Node& sib : m_doc->tree.nodes) { 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; completions << sib.name;
} }
} }
editor->setHelperCompletions(completions); editor->setStaticCompletions(completions);
editor->beginInlineEdit(EditTarget::HelperExpr, line); editor->beginInlineEdit(EditTarget::StaticExpr, line);
}); });
} }
@@ -1948,6 +1969,27 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
// ── Always-available actions ── // ── 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]() { menu.addAction(icon("diff-added.svg"), "Append bytes...", [this, &menu]() {
bool ok; bool ok;
QString input = QInputDialog::getText(menu.parentWidget(), 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); e.sizeBytes = m_doc->tree.structSpan(n.id);
QVector<int> kids = m_doc->tree.childrenOf(n.id); QVector<int> kids = m_doc->tree.childrenOf(n.id);
int nonHelperCount = 0; int nonStaticCount = 0;
int maxAlign = 1; int maxAlign = 1;
for (int i = 0; i < kids.size(); i++) { for (int i = 0; i < kids.size(); i++) {
const Node& child = m_doc->tree.nodes[kids[i]]; const Node& child = m_doc->tree.nodes[kids[i]];
if (child.isHelper) continue; if (child.isStatic) continue;
nonHelperCount++; nonStaticCount++;
int childAlign = alignmentFor(child.kind); int childAlign = alignmentFor(child.kind);
if (childAlign > maxAlign) maxAlign = childAlign; if (childAlign > maxAlign) maxAlign = childAlign;
if (e.fieldSummary.size() < 6) { if (e.fieldSummary.size() < 6) {
@@ -2384,7 +2426,7 @@ void RcxController::showTypePopup(RcxEditor* editor, TypePopupMode mode,
.arg(typeName, child.name); .arg(typeName, child.name);
} }
} }
e.fieldCount = nonHelperCount; e.fieldCount = nonStaticCount;
e.alignment = maxAlign; e.alignment = maxAlign;
entries.append(e); entries.append(e);

View File

@@ -197,8 +197,8 @@ struct Node {
QString classKeyword; // "struct", "class", or "enum" (empty = "struct") QString classKeyword; // "struct", "class", or "enum" (empty = "struct")
uint64_t parentId = 0; // 0 = root (no parent) uint64_t parentId = 0; // 0 = root (no parent)
int offset = 0; int offset = 0;
bool isHelper = false; // static helper — excluded from struct layout bool isStatic = false; // static field — excluded from struct layout
QString offsetExpr; // C/C++ expression → absolute address (helpers only) QString offsetExpr; // C/C++ expression → absolute address (static fields only)
int arrayLen = 1; // Array: element count int arrayLen = 1; // Array: element count
int strLen = 64; int strLen = 64;
bool collapsed = false; bool collapsed = false;
@@ -240,8 +240,8 @@ struct Node {
o["classKeyword"] = classKeyword; o["classKeyword"] = classKeyword;
o["parentId"] = QString::number(parentId); o["parentId"] = QString::number(parentId);
o["offset"] = offset; o["offset"] = offset;
if (isHelper) if (isStatic)
o["isHelper"] = true; o["isStatic"] = true;
if (!offsetExpr.isEmpty()) if (!offsetExpr.isEmpty())
o["offsetExpr"] = offsetExpr; o["offsetExpr"] = offsetExpr;
o["arrayLen"] = arrayLen; o["arrayLen"] = arrayLen;
@@ -283,7 +283,7 @@ struct Node {
n.classKeyword = o["classKeyword"].toString(); n.classKeyword = o["classKeyword"].toString();
n.parentId = o["parentId"].toString("0").toULongLong(); n.parentId = o["parentId"].toString("0").toULongLong();
n.offset = o["offset"].toInt(0); 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.offsetExpr = o["offsetExpr"].toString();
n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000); n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000);
n.strLen = qBound(1, o["strLen"].toInt(64), 1000000); n.strLen = qBound(1, o["strLen"].toInt(64), 1000000);
@@ -445,7 +445,7 @@ struct NodeTree {
QVector<int> kids = childMap ? childMap->value(structId) : childrenOf(structId); QVector<int> kids = childMap ? childMap->value(structId) : childrenOf(structId);
for (int ci : kids) { for (int ci : kids) {
const Node& c = nodes[ci]; 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) int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array)
? structSpan(c.id, childMap, visited) : c.byteSize(); ? structSpan(c.id, childMap, visited) : c.byteSize();
int end = c.offset + sz; int end = c.offset + sz;
@@ -600,7 +600,7 @@ struct LineMeta {
QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void") QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void")
bool isArrayElement = false; // true for synthesized primitive array element lines bool isArrayElement = false; // true for synthesized primitive array element lines
bool isMemberLine = false; // true for enum member / bitfield member 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) { inline bool isSyntheticLine(const LineMeta& lm) {
@@ -648,7 +648,7 @@ namespace cmd {
struct ChangeEnumMembers { uint64_t nodeId; struct ChangeEnumMembers { uint64_t nodeId;
QVector<QPair<QString, int64_t>> oldMembers, newMembers; }; QVector<QPair<QString, int64_t>> oldMembers, newMembers; };
struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; }; 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< using Command = std::variant<
@@ -656,7 +656,7 @@ using Command = std::variant<
cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes, cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes,
cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName, cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName,
cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers, cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers,
cmd::ChangeOffsetExpr, cmd::ToggleHelper cmd::ChangeOffsetExpr, cmd::ToggleStatic
>; >;
// ── Column spans (for inline editing) ── // ── Column spans (for inline editing) ──
@@ -669,7 +669,7 @@ struct ColumnSpan {
enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount, enum class EditTarget { Name, Type, Value, BaseAddress, Source, ArrayIndex, ArrayCount,
ArrayElementType, ArrayElementCount, PointerTarget, ArrayElementType, ArrayElementCount, PointerTarget,
RootClassType, RootClassName, TypeSelector, HelperExpr }; RootClassType, RootClassName, TypeSelector, StaticExpr };
// Column layout constants (shared with format.cpp span computation) // Column layout constants (shared with format.cpp span computation)
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line 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}; return {valStart, valEnd, true};
} }
// Helper expression span: locates text between "= " and " →" (or end of line) // Static field expression span: locates text between "return " and "→" / "(error)" / end
inline ColumnSpan helperExprSpanFor(const LineMeta& /*lm*/, const QString& lineText) { inline ColumnSpan staticExprSpanFor(const LineMeta& /*lm*/, const QString& lineText) {
int eq = lineText.indexOf(QLatin1String("= ")); int ret = lineText.indexOf(QLatin1String("return "));
if (eq < 0) return {}; if (ret < 0) return {};
int exprStart = eq + 2; int exprStart = ret + 7;
int arrow = lineText.indexOf(QChar(0x2192), exprStart); // → // End: before arrow, before "(error)", or line end
int exprEnd = (arrow > exprStart) ? arrow - 1 : lineText.size(); 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--; while (exprEnd > exprStart && lineText[exprEnd - 1] == ' ') exprEnd--;
return {exprStart, exprEnd, true}; return {exprStart, exprEnd, true};
} }

View File

@@ -504,14 +504,14 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
if (m_editState.target == EditTarget::Value) if (m_editState.target == EditTarget::Value)
QTimer::singleShot(0, this, &RcxEditor::validateEditLive); QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
// Autocomplete for helper expressions — show field names as user types // Autocomplete for static field expressions — show field names as user types
if (m_editState.target == EditTarget::HelperExpr && !m_helperCompletions.isEmpty()) { if (m_editState.target == EditTarget::StaticExpr && !m_staticCompletions.isEmpty()) {
// Get word at cursor // Get word at cursor
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS); long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS);
long wordStart = m_sci->SendScintilla(QsciScintillaBase::SCI_WORDSTARTPOSITION, pos, (long)1); long wordStart = m_sci->SendScintilla(QsciScintillaBase::SCI_WORDSTARTPOSITION, pos, (long)1);
int wordLen = (int)(pos - wordStart); int wordLen = (int)(pos - wordStart);
if (wordLen >= 1) { 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_AUTOCSETSEPARATOR, (long)' ');
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSHOW, (uintptr_t)wordLen, list.constData()); 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 {}; 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") // Named struct: entire type column is the type name (e.g. "_MMPTE")
// Find the actual text bounds within the padded column // Find the actual text bounds within the padded column
int start = ind; int start = ind;
@@ -1586,7 +1600,8 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
if (lm->nodeIdx < 0) return false; if (lm->nodeIdx < 0) return false;
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only) // 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; return false;
QString lineText = getLineText(m_sci, line); QString lineText = getLineText(m_sci, line);
@@ -1612,9 +1627,9 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t,
s = arrayElemCountSpanFor(*lm, lineText); break; s = arrayElemCountSpanFor(*lm, lineText); break;
case EditTarget::PointerTarget: case EditTarget::PointerTarget:
s = pointerTargetSpanFor(*lm, lineText); break; s = pointerTargetSpanFor(*lm, lineText); break;
case EditTarget::HelperExpr: case EditTarget::StaticExpr:
if (lm->isHelperLine) if (lm->isStaticLine)
s = helperExprSpanFor(*lm, lineText); s = staticExprSpanFor(*lm, lineText);
break; break;
case EditTarget::Source: break; case EditTarget::Source: break;
} }
@@ -2245,7 +2260,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|| target == EditTarget::RootClassType || target == EditTarget::RootClassName))) || target == EditTarget::RootClassType || target == EditTarget::RootClassName)))
return false; return false;
// Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only) // 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; return false;
QString lineText; QString lineText;

View File

@@ -45,7 +45,7 @@ public:
bool isEditing() const { return m_editState.active; } bool isEditing() const { return m_editState.active; }
bool beginInlineEdit(EditTarget target, int line = -1, int col = -1); bool beginInlineEdit(EditTarget target, int line = -1, int col = -1);
void cancelInlineEdit(); void cancelInlineEdit();
void setHelperCompletions(const QStringList& words) { m_helperCompletions = words; } void setStaticCompletions(const QStringList& words) { m_staticCompletions = words; }
void applySelectionOverlay(const QSet<uint64_t>& selIds); void applySelectionOverlay(const QSet<uint64_t>& selIds);
void setCommandRowText(const QString& line); void setCommandRowText(const QString& line);
@@ -134,7 +134,7 @@ private:
bool lastValidationOk = true; // track state to avoid redundant updates bool lastValidationOk = true; // track state to avoid redundant updates
}; };
InlineEditState m_editState; InlineEditState m_editState;
QStringList m_helperCompletions; // autocomplete words for HelperExpr editing QStringList m_staticCompletions; // autocomplete words for StaticExpr editing
// ── Tab cycling state ── // ── Tab cycling state ──
EditTarget m_lastTabTarget = EditTarget::Value; EditTarget m_lastTabTarget = EditTarget::Value;

View File

@@ -173,10 +173,10 @@ static void emitStructBody(GenContext& ctx, uint64_t structId,
QString ind = indent(depth); QString ind = indent(depth);
QVector<int> allChildren = ctx.childMap.value(structId); QVector<int> allChildren = ctx.childMap.value(structId);
QVector<int> children, helperIdxs; QVector<int> children, staticIdxs;
for (int ci : allChildren) { for (int ci : allChildren) {
if (tree.nodes[ci].isHelper) if (tree.nodes[ci].isStatic)
helperIdxs.append(ci); staticIdxs.append(ci);
else else
children.append(ci); children.append(ci);
} }
@@ -318,12 +318,12 @@ static void emitStructBody(GenContext& ctx, uint64_t structId,
if (!isUnion && cursor < structSize) if (!isUnion && cursor < structSize)
emitPadRun(cursor, structSize - cursor); emitPadRun(cursor, structSize - cursor);
// Emit helper comments (helpers are runtime-only, not part of struct layout) // Emit static field comments (static fields are runtime-only, not part of struct layout)
for (int hi : helperIdxs) { for (int si : staticIdxs) {
const Node& h = tree.nodes[hi]; const Node& sf = tree.nodes[si];
QString hType = h.structTypeName.isEmpty() ? ctx.cType(h.kind) : h.structTypeName; QString sfType = sf.structTypeName.isEmpty() ? ctx.cType(sf.kind) : sf.structTypeName;
ctx.output += ind + QStringLiteral("// helper: %1 %2 @ %3\n") ctx.output += ind + QStringLiteral("// static: %1 %2 @ %3\n")
.arg(hType, sanitizeIdent(h.name), h.offsetExpr); .arg(sfType, sanitizeIdent(sf.name), sf.offsetExpr);
} }
} }

View File

@@ -2435,9 +2435,9 @@ private slots:
QCOMPARE(n.byteSize(), 8); QCOMPARE(n.byteSize(), 8);
} }
// ── Helper node compose tests ── // ── Static field node compose tests ──
void testHelperSeparatorLine() { void testStaticFieldHeaderLine() {
NodeTree tree; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -2456,27 +2456,27 @@ private slots:
f1.offset = 0; f1.offset = 0;
tree.addNode(f1); tree.addNode(f1);
// Helper node // Static field node
Node helper; Node sf;
helper.kind = NodeKind::Hex64; sf.kind = NodeKind::Hex64;
helper.name = "my_helper"; sf.name = "my_static";
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
tree.addNode(helper); tree.addNode(sf);
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// Separator with "helpers" text and box-drawing chars should appear // Header with "static" keyword and opening brace should appear
QVERIFY2(result.text.contains(QStringLiteral("helpers")), QVERIFY2(result.text.contains(QStringLiteral("static "))
qPrintable("Expected 'helpers' separator in:\n" + result.text)); && result.text.contains(QStringLiteral("my_static"))
QVERIFY2(result.text.contains(QStringLiteral("\u2500")), && result.text.contains(QStringLiteral("{")),
qPrintable("Expected box-drawing separator char in:\n" + result.text)); qPrintable("Expected static field header in:\n" + result.text));
} }
void testHelperDoesNotAffectStructSize() { void testStaticFieldDoesNotAffectStructSize() {
NodeTree tree; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -2494,24 +2494,24 @@ private slots:
f1.offset = 0; f1.offset = 0;
tree.addNode(f1); tree.addNode(f1);
// Struct span without helper // Struct span without static field
int spanBefore = tree.structSpan(rootId); int spanBefore = tree.structSpan(rootId);
// Add helper // Add static field
Node helper; Node sf;
helper.kind = NodeKind::Struct; sf.kind = NodeKind::Struct;
helper.name = "helper"; sf.name = "static_field";
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base + 100"); sf.offsetExpr = QStringLiteral("base + 100");
tree.addNode(helper); tree.addNode(sf);
int spanAfter = tree.structSpan(rootId); int spanAfter = tree.structSpan(rootId);
QCOMPARE(spanAfter, spanBefore); QCOMPARE(spanAfter, spanBefore);
} }
void testHelperIsHelperLineFlag() { void testStaticFieldIsStaticLineFlag() {
NodeTree tree; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -2529,30 +2529,30 @@ private slots:
f1.offset = 0; f1.offset = 0;
tree.addNode(f1); tree.addNode(f1);
Node helper; Node sf;
helper.kind = NodeKind::Hex64; sf.kind = NodeKind::Hex64;
helper.name = "my_helper"; sf.name = "my_static";
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
tree.addNode(helper); tree.addNode(sf);
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// At least one line should have isHelperLine set // At least one line should have isStaticLine set
bool foundHelper = false; bool foundStaticField = false;
for (const auto& lm : result.meta) { for (const auto& lm : result.meta) {
if (lm.isHelperLine) { if (lm.isStaticLine) {
foundHelper = true; foundStaticField = true;
break; 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; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -2563,42 +2563,42 @@ private slots:
int ri = tree.addNode(root); int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id; uint64_t rootId = tree.nodes[ri].id;
// Helper struct with a child (should still appear collapsed) // Static field struct with a child (should still appear collapsed)
Node helper; Node sf;
helper.kind = NodeKind::Struct; sf.kind = NodeKind::Struct;
helper.name = "inner"; sf.name = "inner";
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
helper.collapsed = true; sf.collapsed = true;
int hi = tree.addNode(helper); int hi = tree.addNode(sf);
uint64_t helperId = tree.nodes[hi].id; uint64_t sfId = tree.nodes[hi].id;
Node hChild; Node sfChild;
hChild.kind = NodeKind::UInt32; sfChild.kind = NodeKind::UInt32;
hChild.name = "x"; sfChild.name = "x";
hChild.parentId = helperId; sfChild.parentId = sfId;
hChild.offset = 0; sfChild.offset = 0;
tree.addNode(hChild); tree.addNode(sfChild);
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, 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; bool foundChildLine = false;
for (const auto& lm : result.meta) { for (const auto& lm : result.meta) {
if (lm.nodeIdx >= 0 && lm.nodeIdx < tree.nodes.size() if (lm.nodeIdx >= 0 && lm.nodeIdx < tree.nodes.size()
&& tree.nodes[lm.nodeIdx].name == QStringLiteral("x") && tree.nodes[lm.nodeIdx].name == QStringLiteral("x")
&& tree.nodes[lm.nodeIdx].parentId == helperId) { && tree.nodes[lm.nodeIdx].parentId == sfId) {
foundChildLine = true; foundChildLine = true;
} }
} }
QVERIFY2(!foundChildLine, 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; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -2609,14 +2609,14 @@ private slots:
int ri = tree.addNode(root); int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id; uint64_t rootId = tree.nodes[ri].id;
Node helper; Node sf;
helper.kind = NodeKind::Hex64; sf.kind = NodeKind::Hex64;
helper.name = "my_helper"; sf.name = "my_static";
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base + 0x10"); sf.offsetExpr = QStringLiteral("base + 0x10");
tree.addNode(helper); tree.addNode(sf);
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);

View File

@@ -668,179 +668,179 @@ private slots:
QVERIFY(newIdx >= 0); QVERIFY(newIdx >= 0);
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::UInt32); 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; uint64_t rootId = m_doc->tree.nodes[0].id;
int origSize = m_doc->tree.nodes.size(); int origSize = m_doc->tree.nodes.size();
// Simulate "Add Helper" — same code as context menu action // Simulate "Add Static Field" — same code as context menu action
Node helper; Node sf;
helper.id = m_doc->tree.m_nextId++; sf.id = m_doc->tree.m_nextId++;
helper.kind = NodeKind::Hex64; sf.kind = NodeKind::Hex64;
helper.name = QStringLiteral("helper"); sf.name = QStringLiteral("static_field");
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
QApplication::processEvents(); QApplication::processEvents();
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1); QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
const auto& h = m_doc->tree.nodes.back(); const auto& h = m_doc->tree.nodes.back();
QCOMPARE(h.isHelper, true); QCOMPARE(h.isStatic, true);
QCOMPARE(h.offsetExpr, QStringLiteral("base")); QCOMPARE(h.offsetExpr, QStringLiteral("base"));
QCOMPARE(h.name, QStringLiteral("helper")); QCOMPARE(h.name, QStringLiteral("static_field"));
QCOMPARE(h.parentId, rootId); QCOMPARE(h.parentId, rootId);
} }
void testAddHelperUndo() { void testAddStaticFieldUndo() {
uint64_t rootId = m_doc->tree.nodes[0].id; uint64_t rootId = m_doc->tree.nodes[0].id;
int origSize = m_doc->tree.nodes.size(); int origSize = m_doc->tree.nodes.size();
Node helper; Node sf;
helper.id = m_doc->tree.m_nextId++; sf.id = m_doc->tree.m_nextId++;
helper.kind = NodeKind::Hex64; sf.kind = NodeKind::Hex64;
helper.name = QStringLiteral("helper"); sf.name = QStringLiteral("static_field");
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
QApplication::processEvents(); QApplication::processEvents();
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1); QCOMPARE(m_doc->tree.nodes.size(), origSize + 1);
// Undo: helper should be gone // Undo: static field should be gone
m_doc->undoStack.undo(); m_doc->undoStack.undo();
QApplication::processEvents(); QApplication::processEvents();
QCOMPARE(m_doc->tree.nodes.size(), origSize); QCOMPARE(m_doc->tree.nodes.size(), origSize);
// Redo: helper should be back // Redo: static field should be back
m_doc->undoStack.redo(); m_doc->undoStack.redo();
QApplication::processEvents(); QApplication::processEvents();
QCOMPARE(m_doc->tree.nodes.size(), origSize + 1); 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; uint64_t rootId = m_doc->tree.nodes[0].id;
// Add a helper // Add a static field
Node helper; Node sf;
helper.id = m_doc->tree.m_nextId++; sf.id = m_doc->tree.m_nextId++;
helper.kind = NodeKind::Hex64; sf.kind = NodeKind::Hex64;
helper.name = QStringLiteral("helper"); sf.name = QStringLiteral("static_field");
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
QApplication::processEvents(); QApplication::processEvents();
uint64_t helperId = m_doc->tree.nodes.back().id; uint64_t sfId = m_doc->tree.nodes.back().id;
// Change expression // Change expression
m_doc->undoStack.push(new RcxCommand(m_ctrl, 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(); QApplication::processEvents();
int idx = m_doc->tree.indexOfId(helperId); int idx = m_doc->tree.indexOfId(sfId);
QVERIFY(idx >= 0); QVERIFY(idx >= 0);
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + 0x10")); QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + 0x10"));
// Undo: old expression restored // Undo: old expression restored
m_doc->undoStack.undo(); m_doc->undoStack.undo();
QApplication::processEvents(); QApplication::processEvents();
idx = m_doc->tree.indexOfId(helperId); idx = m_doc->tree.indexOfId(sfId);
QVERIFY(idx >= 0); QVERIFY(idx >= 0);
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base")); QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base"));
} }
void testDeleteHelperPreservesStructSize() { void testDeleteStaticFieldPreservesStructSize() {
uint64_t rootId = m_doc->tree.nodes[0].id; uint64_t rootId = m_doc->tree.nodes[0].id;
int spanBefore = m_doc->tree.structSpan(rootId); int spanBefore = m_doc->tree.structSpan(rootId);
// Add a helper // Add a static field
Node helper; Node sf;
helper.id = m_doc->tree.m_nextId++; sf.id = m_doc->tree.m_nextId++;
helper.kind = NodeKind::Hex64; sf.kind = NodeKind::Hex64;
helper.name = QStringLiteral("helper"); sf.name = QStringLiteral("static_field");
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
QApplication::processEvents(); QApplication::processEvents();
// Struct size unchanged after adding helper // Struct size unchanged after adding static field
QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore); QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore);
// Remove helper // Remove static field
uint64_t helperId = m_doc->tree.nodes.back().id; uint64_t sfId = m_doc->tree.nodes.back().id;
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Remove{helperId})); m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Remove{sfId}));
QApplication::processEvents(); QApplication::processEvents();
// Struct size still unchanged // Struct size still unchanged
QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore); QCOMPARE(m_doc->tree.structSpan(rootId), spanBefore);
} }
void testHelperRenamePreservesExpression() { void testStaticFieldRenamePreservesExpression() {
uint64_t rootId = m_doc->tree.nodes[0].id; uint64_t rootId = m_doc->tree.nodes[0].id;
// Add a helper // Add a static field
Node helper; Node sf;
helper.id = m_doc->tree.m_nextId++; sf.id = m_doc->tree.m_nextId++;
helper.kind = NodeKind::Hex64; sf.kind = NodeKind::Hex64;
helper.name = QStringLiteral("my_helper"); sf.name = QStringLiteral("my_static");
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base + field_u32"); sf.offsetExpr = QStringLiteral("base + field_u32");
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
QApplication::processEvents(); 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, 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(); QApplication::processEvents();
int idx = m_doc->tree.indexOfId(helperId); int idx = m_doc->tree.indexOfId(sfId);
QVERIFY(idx >= 0); 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 // Expression should be preserved
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base + field_u32")); 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; uint64_t rootId = m_doc->tree.nodes[0].id;
Node helper; Node sf;
helper.id = m_doc->tree.m_nextId++; sf.id = m_doc->tree.m_nextId++;
helper.kind = NodeKind::Hex64; sf.kind = NodeKind::Hex64;
helper.name = QStringLiteral("helper"); sf.name = QStringLiteral("static_field");
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{helper, {}})); m_doc->undoStack.push(new RcxCommand(m_ctrl, cmd::Insert{sf, {}}));
QApplication::processEvents(); QApplication::processEvents();
uint64_t helperId = m_doc->tree.nodes.back().id; uint64_t sfId = m_doc->tree.nodes.back().id;
// Change kind to UInt32 // Change kind to UInt32
m_doc->undoStack.push(new RcxCommand(m_ctrl, m_doc->undoStack.push(new RcxCommand(m_ctrl,
cmd::ChangeKind{helperId, NodeKind::Hex64, NodeKind::UInt32})); cmd::ChangeKind{sfId, NodeKind::Hex64, NodeKind::UInt32}));
QApplication::processEvents(); QApplication::processEvents();
int idx = m_doc->tree.indexOfId(helperId); int idx = m_doc->tree.indexOfId(sfId);
QVERIFY(idx >= 0); QVERIFY(idx >= 0);
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32); QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32);
// Helper flags must survive type change // Static field flags must survive type change
QCOMPARE(m_doc->tree.nodes[idx].isHelper, true); QCOMPARE(m_doc->tree.nodes[idx].isStatic, true);
QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base")); QCOMPARE(m_doc->tree.nodes[idx].offsetExpr, QStringLiteral("base"));
} }
}; };

View File

@@ -672,9 +672,9 @@ private slots:
QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range) QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range)
} }
// ── Helper node serialization ── // ── Static field node serialization ──
void testHelperJsonRoundTrip() { void testStaticFieldJsonRoundTrip() {
rcx::NodeTree tree; rcx::NodeTree tree;
tree.baseAddress = 0x14000000; tree.baseAddress = 0x14000000;
@@ -692,27 +692,27 @@ private slots:
field.offset = 0x3C; field.offset = 0x3C;
tree.addNode(field); tree.addNode(field);
rcx::Node helper; rcx::Node sf;
helper.kind = rcx::NodeKind::Struct; sf.kind = rcx::NodeKind::Struct;
helper.name = "nt_hdr"; sf.name = "nt_hdr";
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base + e_lfanew"); sf.offsetExpr = QStringLiteral("base + e_lfanew");
tree.addNode(helper); tree.addNode(sf);
QJsonObject json = tree.toJson(); QJsonObject json = tree.toJson();
rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json); rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json);
QCOMPARE(tree2.nodes.size(), 3); QCOMPARE(tree2.nodes.size(), 3);
const auto& h = tree2.nodes[2]; const auto& h = tree2.nodes[2];
QCOMPARE(h.isHelper, true); QCOMPARE(h.isStatic, true);
QCOMPARE(h.offsetExpr, QStringLiteral("base + e_lfanew")); QCOMPARE(h.offsetExpr, QStringLiteral("base + e_lfanew"));
QCOMPARE(h.name, QStringLiteral("nt_hdr")); QCOMPARE(h.name, QStringLiteral("nt_hdr"));
} }
void testHelperJsonBackwardCompat() { void testStaticFieldJsonBackwardCompat() {
// Old JSON without isHelper/offsetExpr should load with defaults // Old JSON without isStatic/offsetExpr should load with defaults
rcx::NodeTree tree; rcx::NodeTree tree;
rcx::Node root; rcx::Node root;
root.kind = rcx::NodeKind::Struct; root.kind = rcx::NodeKind::Struct;
@@ -723,11 +723,11 @@ private slots:
QJsonObject json = tree.toJson(); QJsonObject json = tree.toJson();
rcx::NodeTree tree2 = rcx::NodeTree::fromJson(json); 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()); QCOMPARE(tree2.nodes[0].offsetExpr, QString());
} }
void testStructSpanExcludesHelpers() { void testStructSpanExcludesStaticFields() {
using namespace rcx; using namespace rcx;
NodeTree tree; NodeTree tree;
@@ -754,27 +754,27 @@ private slots:
f2.offset = 4; f2.offset = 4;
tree.addNode(f2); tree.addNode(f2);
// Helper: should NOT affect span // Static field: should NOT affect span
Node helper; Node sf;
helper.kind = NodeKind::Struct; sf.kind = NodeKind::Struct;
helper.name = "helper"; sf.name = "static_field";
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
tree.addNode(helper); 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); QCOMPARE(tree.structSpan(rootId), 12);
} }
void testHelperExprSpanFor() { void testStaticExprSpanFor() {
using namespace rcx; 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; LineMeta lm;
lm.isHelperLine = true; lm.isStaticLine = true;
QString lineText = QStringLiteral(" \u25B8 struct NT_HEADERS nt_hdr = base + e_lfanew \u2192 0x1400000E8"); QString lineText = QStringLiteral(" return base + e_lfanew \u2192 0x1400000E8");
ColumnSpan span = helperExprSpanFor(lm, lineText); ColumnSpan span = staticExprSpanFor(lm, lineText);
QVERIFY(span.valid); QVERIFY(span.valid);
QString expr = lineText.mid(span.start, span.end - span.start); QString expr = lineText.mid(span.start, span.end - span.start);
QCOMPARE(expr.trimmed(), QStringLiteral("base + e_lfanew")); QCOMPARE(expr.trimmed(), QStringLiteral("base + e_lfanew"));

View File

@@ -2556,6 +2556,218 @@ private slots:
QApplication::processEvents(); QApplication::processEvents();
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor); 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) QTEST_MAIN(TestEditor)

View File

@@ -758,9 +758,9 @@ private slots:
QVERIFY(!result.contains("struct _LIST_ENTRY\n{")); QVERIFY(!result.contains("struct _LIST_ENTRY\n{"));
QVERIFY(!result.contains("uint8_t _pad")); QVERIFY(!result.contains("uint8_t _pad"));
} }
// ── Helper node generator tests ── // ── Static field node generator tests ──
void testHelperNotInStructBody() { void testStaticFieldNotInStructBody() {
rcx::NodeTree tree; rcx::NodeTree tree;
rcx::Node root; rcx::Node root;
@@ -778,32 +778,32 @@ private slots:
f1.offset = 0; f1.offset = 0;
tree.addNode(f1); tree.addNode(f1);
rcx::Node helper; rcx::Node sf;
helper.kind = rcx::NodeKind::Struct; sf.kind = rcx::NodeKind::Struct;
helper.name = "nt_hdr"; sf.name = "nt_hdr";
helper.structTypeName = "IMAGE_NT_HEADERS"; sf.structTypeName = "IMAGE_NT_HEADERS";
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base + e_lfanew"); sf.offsetExpr = QStringLiteral("base + e_lfanew");
tree.addNode(helper); tree.addNode(sf);
QString result = rcx::renderCpp(tree, rootId); 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;"), 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 // Static field SHOULD appear as a comment
QVERIFY2(result.contains("// helper:"), QVERIFY2(result.contains("// static:"),
qPrintable("Helper comment missing:\n" + result)); qPrintable("Static field comment missing:\n" + result));
QVERIFY2(result.contains("nt_hdr"), 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"), 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::NodeTree tree;
rcx::Node root; rcx::Node root;
@@ -821,26 +821,26 @@ private slots:
f1.offset = 0; f1.offset = 0;
tree.addNode(f1); tree.addNode(f1);
rcx::Node helper; rcx::Node sf;
helper.kind = rcx::NodeKind::Hex64; sf.kind = rcx::NodeKind::Hex64;
helper.name = "ptr"; sf.name = "ptr";
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base + 0xFF"); sf.offsetExpr = QStringLiteral("base + 0xFF");
tree.addNode(helper); tree.addNode(sf);
QString result = rcx::renderCpp(tree, rootId); QString result = rcx::renderCpp(tree, rootId);
// The regular field should be in the struct body // The regular field should be in the struct body
QVERIFY(result.contains("uint64_t base_field;")); QVERIFY(result.contains("uint64_t base_field;"));
// Helper emitted as comment after struct body // Static field emitted as comment after struct body
QVERIFY(result.contains("// helper:")); QVERIFY(result.contains("// static:"));
QVERIFY(result.contains("@ base + 0xFF")); QVERIFY(result.contains("@ base + 0xFF"));
} }
void testStructSizeUnchangedByHelper() { void testStructSizeUnchangedByStaticField() {
rcx::NodeTree tree; rcx::NodeTree tree;
rcx::Node root; rcx::Node root;
@@ -858,14 +858,14 @@ private slots:
f1.offset = 0; f1.offset = 0;
tree.addNode(f1); tree.addNode(f1);
rcx::Node helper; rcx::Node sf;
helper.kind = rcx::NodeKind::Struct; sf.kind = rcx::NodeKind::Struct;
helper.name = "big_helper"; sf.name = "big_static";
helper.parentId = rootId; sf.parentId = rootId;
helper.offset = 0; sf.offset = 0;
helper.isHelper = true; sf.isStatic = true;
helper.offsetExpr = QStringLiteral("base"); sf.offsetExpr = QStringLiteral("base");
tree.addNode(helper); tree.addNode(sf);
QString result = rcx::renderCpp(tree, rootId, nullptr, true); QString result = rcx::renderCpp(tree, rootId, nullptr, true);