mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
@@ -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)
|
||||||
|
|||||||
186
src/compose.cpp
186
src/compose.cpp
@@ -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, ®ular, parentAbsAddr]
|
cbs.resolveIdentifier = [&tree, &prov, ®ular, 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);
|
|
||||||
|
|
||||||
|
// ── Header line: "static <type> <name> {" or collapsed: "static <type> <name> { return <expr>; }"
|
||||||
|
QString headerLine;
|
||||||
|
if (isCollapsed) {
|
||||||
QString exprPart;
|
QString exprPart;
|
||||||
if (!helper.offsetExpr.isEmpty()) {
|
if (!sf.offsetExpr.isEmpty()) {
|
||||||
if (exprOk)
|
if (exprOk)
|
||||||
exprPart = QStringLiteral("= %1 \u2192 0x%2")
|
exprPart = QStringLiteral("return %1 } \u2192 0x%2")
|
||||||
.arg(helper.offsetExpr)
|
.arg(sf.offsetExpr)
|
||||||
.arg(QString::number(helperAddr, 16).toUpper());
|
.arg(QString::number(staticAddr, 16).toUpper());
|
||||||
else
|
else
|
||||||
exprPart = QStringLiteral("= %1 (error)").arg(helper.offsetExpr);
|
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);
|
||||||
|
flm.offsetText = fmt::fmtOffsetMargin(staticAddr + sSpan, false,
|
||||||
state.offsetHexDigits);
|
state.offsetHexDigits);
|
||||||
flm.offsetAddr = helperAddr + hSpan;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
41
src/core.h
41
src/core.h
@@ -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};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user