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