Fold arrows, pointer format, teal custom types, collapsed pointers by default

- Change fold indicators from +/- to arrow icons (▸ collapsed, ▾ expanded)
- Move ▾ dropdown arrow after text on command rows (source▾, struct▾)
- Change pointer display from ptr64<Type> to Type* format
- Color custom/user-defined types teal (#4EC9B0) via lexer GlobalClass
- Keep built-in types blue (#569cd6) via KeywordSet2
- Remove underline from root class name on CommandRow2
- Pointer children start collapsed by default (lazy expansion)
- Demo data updated accordingly
This commit is contained in:
megablox
2026-02-08 11:00:11 -07:00
committed by sysadmin
parent 105ad398b6
commit 6a9641edc5
12 changed files with 610 additions and 393 deletions

View File

@@ -39,10 +39,12 @@ struct ComposeState {
if (currentLine > 0) text += '\n'; if (currentLine > 0) text += '\n';
// 3-char fold indicator column: " - " expanded, " + " collapsed, " " other // 3-char fold indicator column: " - " expanded, " + " collapsed, " " other
// CommandRow has no fold prefix (flush left) // CommandRow has no fold prefix (flush left)
if (lm.lineKind == LineKind::CommandRow || lm.lineKind == LineKind::CommandRow2) { if (lm.lineKind == LineKind::CommandRow || lm.lineKind == LineKind::Blank
// no prefix || lm.lineKind == LineKind::CommandRow2
|| (lm.lineKind == LineKind::Footer && lm.isRootHeader)) {
// no prefix — flush left like CommandRow2
} else if (lm.foldHead) } else if (lm.foldHead)
text += lm.foldCollapsed ? QStringLiteral(" + ") : QStringLiteral(" - "); text += lm.foldCollapsed ? QStringLiteral(" \u25B8 ") : QStringLiteral(" \u25BE ");
else else
text += QStringLiteral(" "); text += QStringLiteral(" ");
text += lineText; text += lineText;
@@ -286,6 +288,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.depth = depth; lm.depth = depth;
lm.lineKind = LineKind::Footer; lm.lineKind = LineKind::Footer;
lm.nodeKind = node.kind; lm.nodeKind = node.kind;
lm.isRootHeader = isRootHeader; // root footer: flush left (no fold prefix)
lm.offsetText.clear(); lm.offsetText.clear();
lm.foldLevel = computeFoldLevel(depth, false); lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0; lm.markerMask = 0;
@@ -313,7 +316,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
QString ptrTargetName = resolvePointerTarget(tree, node.refId); QString ptrTargetName = resolvePointerTarget(tree, node.refId);
QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName); QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName);
// Emit merged fold header: "ptr64<Type> Name {" (expanded) or "ptr64<Type> Name -> val" (collapsed) // Emit merged fold header: "Type* Name {" (expanded) or "Type* Name -> val" (collapsed)
{ {
LineMeta lm; LineMeta lm;
lm.nodeIdx = nodeIdx; lm.nodeIdx = nodeIdx;
@@ -336,32 +339,31 @@ void composeNode(ComposeState& state, const NodeTree& tree,
if (!node.collapsed) { if (!node.collapsed) {
int sz = node.byteSize(); int sz = node.byteSize();
uint64_t ptrVal = 0;
if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) { if (prov.isValid() && sz > 0 && prov.isReadable(absAddr, sz)) {
uint64_t ptrVal = (node.kind == NodeKind::Pointer32) ptrVal = (node.kind == NodeKind::Pointer32)
? (uint64_t)prov.readU32(absAddr) : prov.readU64(absAddr); ? (uint64_t)prov.readU32(absAddr) : prov.readU64(absAddr);
if (ptrVal != 0) { if (ptrVal != 0) {
uint64_t pBase = ptrToProviderAddr(tree, ptrVal); uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
} }
if (ptrVal != 0) { }
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
qulonglong key = pBase ^ (node.refId * kGoldenRatio); // Show referenced struct children: at dereferenced address if non-NULL,
if (!state.ptrVisiting.contains(key)) { // otherwise at offset 0 as a struct template preview
state.ptrVisiting.insert(key); uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0;
int refIdx = tree.indexOfId(node.refId); qulonglong key = pBase ^ (node.refId * kGoldenRatio);
if (refIdx >= 0) { if (!state.ptrVisiting.contains(key)) {
const Node& ref = tree.nodes[refIdx]; state.ptrVisiting.insert(key);
if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) int refIdx = tree.indexOfId(node.refId);
// isArrayChild=true skips header/footer, emits children only if (refIdx >= 0) {
// depth (not depth+1): pointer header replaces struct header, const Node& ref = tree.nodes[refIdx];
// so children should be at depth+1, not depth+2 if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array)
composeParent(state, tree, prov, refIdx, composeParent(state, tree, prov, refIdx,
depth, pBase, ref.id, depth, pBase, ref.id,
/*isArrayChild=*/true); /*isArrayChild=*/true);
}
state.ptrVisiting.remove(key);
}
} }
state.ptrVisiting.remove(key);
} }
// Footer for pointer fold // Footer for pointer fold
@@ -473,6 +475,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
} }
// Emit CommandRow as line 0 (synthetic UI line) // Emit CommandRow as line 0 (synthetic UI line)
const QString cmdRowText = QStringLiteral("source\u25BE \u203A 0x0");
{ {
LineMeta lm; LineMeta lm;
lm.nodeIdx = -1; lm.nodeIdx = -1;
@@ -485,10 +488,29 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
lm.markerMask = 0; lm.markerMask = 0;
lm.effectiveTypeW = state.typeW; lm.effectiveTypeW = state.typeW;
lm.effectiveNameW = state.nameW; lm.effectiveNameW = state.nameW;
state.emitLine(QStringLiteral("File Address: 0x0"), lm); state.emitLine(cmdRowText, lm);
} }
// Emit CommandRow2 as line 1 (root class type + name) // Emit dotted separator (line 1) — fixed-width spaced dots in text + margin
{
// ~20 dots in text area, ~10 dots in margin, using middle dot · (U+00B7)
QString dots(20, QChar(0x00B7));
QString marginDots(10, QChar(0x00B7));
LineMeta lm;
lm.nodeIdx = -1;
lm.nodeId = kCommandRowId;
lm.depth = 0;
lm.lineKind = LineKind::Blank;
lm.foldLevel = SC_FOLDLEVELBASE;
lm.foldHead = false;
lm.offsetText = marginDots;
lm.markerMask = 0;
lm.effectiveTypeW = state.typeW;
lm.effectiveNameW = state.nameW;
state.emitLine(dots, lm);
}
// Emit CommandRow2 as line 2 (root class type + name)
{ {
LineMeta lm; LineMeta lm;
lm.nodeIdx = -1; lm.nodeIdx = -1;
@@ -501,7 +523,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
lm.markerMask = 0; lm.markerMask = 0;
lm.effectiveTypeW = state.typeW; lm.effectiveTypeW = state.typeW;
lm.effectiveNameW = state.nameW; lm.effectiveNameW = state.nameW;
state.emitLine(QStringLiteral("struct <no class> {"), lm); state.emitLine(QStringLiteral("struct\u25BE <no class> {"), lm);
} }
QVector<int> roots = state.childMap.value(0); QVector<int> roots = state.childMap.value(0);

View File

@@ -808,8 +808,11 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
} }
} else if constexpr (std::is_same_v<T, cmd::ChangePointerRef>) { } else if constexpr (std::is_same_v<T, cmd::ChangePointerRef>) {
int idx = tree.indexOfId(c.nodeId); int idx = tree.indexOfId(c.nodeId);
if (idx >= 0) if (idx >= 0) {
tree.nodes[idx].refId = isUndo ? c.oldRefId : c.newRefId; tree.nodes[idx].refId = isUndo ? c.oldRefId : c.newRefId;
if (tree.nodes[idx].refId != 0)
tree.nodes[idx].collapsed = true;
}
} else if constexpr (std::is_same_v<T, cmd::ChangeStructTypeName>) { } else if constexpr (std::is_same_v<T, cmd::ChangeStructTypeName>) {
int idx = tree.indexOfId(c.nodeId); int idx = tree.indexOfId(c.nodeId);
if (idx >= 0) if (idx >= 0)
@@ -837,7 +840,7 @@ void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
const Node& node = m_doc->tree.nodes[nodeIdx]; const Node& node = m_doc->tree.nodes[nodeIdx];
uint64_t addr = m_doc->tree.computeOffset(nodeIdx); uint64_t addr = m_doc->tree.computeOffset(nodeIdx);
// For vector sub-components, redirect to float parsing at sub-offset // For vector components, redirect to float parsing at sub-offset
NodeKind editKind = node.kind; NodeKind editKind = node.kind;
if ((node.kind == NodeKind::Vec2 || node.kind == NodeKind::Vec3 || if ((node.kind == NodeKind::Vec2 || node.kind == NodeKind::Vec3 ||
node.kind == NodeKind::Vec4) && subLine >= 0) { node.kind == NodeKind::Vec4) && subLine >= 0) {
@@ -1370,10 +1373,10 @@ void RcxController::updateCommandRow() {
QString src; QString src;
QString provName = m_doc->provider->name(); QString provName = m_doc->provider->name();
if (provName.isEmpty()) { if (provName.isEmpty()) {
src = QStringLiteral("<Select Source>"); src = QStringLiteral("source\u25BE");
} else { } else {
src = QStringLiteral("%1 '%2'") src = QStringLiteral("'%1'\u25BE")
.arg(m_doc->provider->kind(), provName); .arg(provName);
} }
// -- Symbol for selected node (getSymbol integration) -- // -- Symbol for selected node (getSymbol integration) --
@@ -1394,10 +1397,10 @@ void RcxController::updateCommandRow() {
// Build the row. If we have a symbol, append it after the address. // Build the row. If we have a symbol, append it after the address.
QString row; QString row;
if (sym.isEmpty()) { if (sym.isEmpty()) {
row = QStringLiteral("%1 Address: %2") row = QStringLiteral("%1 \u203A %2")
.arg(elide(src, 40), elide(addr, 24)); .arg(elide(src, 40), elide(addr, 24));
} else { } else {
row = QStringLiteral("%1 Address: %2 %3") row = QStringLiteral("%1 \u203A %2 %3")
.arg(elide(src, 40), elide(addr, 24), elide(sym, 40)); .arg(elide(src, 40), elide(addr, 24), elide(sym, 40));
} }
@@ -1408,13 +1411,13 @@ void RcxController::updateCommandRow() {
if (n.parentId == 0 && n.kind == NodeKind::Struct) { if (n.parentId == 0 && n.kind == NodeKind::Struct) {
QString keyword = n.resolvedClassKeyword(); QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName; QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
row2 = QStringLiteral("%1 %2 {") row2 = QStringLiteral("%1\u25BE %2 {")
.arg(keyword, className); .arg(keyword, className);
break; break;
} }
} }
if (row2.isEmpty()) if (row2.isEmpty())
row2 = QStringLiteral("struct <no class> {"); row2 = QStringLiteral("struct\u25BE <no class> {");
for (auto* ed : m_editors) { for (auto* ed : m_editors) {
ed->setCommandRowText(row); ed->setCommandRowText(row);

View File

@@ -55,10 +55,10 @@ struct KindMeta {
inline constexpr KindMeta kKindMeta[] = { inline constexpr KindMeta kKindMeta[] = {
// kind name typeName sz ln al flags // kind name typeName sz ln al flags
{NodeKind::Hex8, "Hex8", "Hex8", 1, 1, 1, KF_HexPreview}, {NodeKind::Hex8, "Hex8", "hex8", 1, 1, 1, KF_HexPreview},
{NodeKind::Hex16, "Hex16", "Hex16", 2, 1, 2, KF_HexPreview}, {NodeKind::Hex16, "Hex16", "hex16", 2, 1, 2, KF_HexPreview},
{NodeKind::Hex32, "Hex32", "Hex32", 4, 1, 4, KF_HexPreview}, {NodeKind::Hex32, "Hex32", "hex32", 4, 1, 4, KF_HexPreview},
{NodeKind::Hex64, "Hex64", "Hex64", 8, 1, 8, KF_HexPreview}, {NodeKind::Hex64, "Hex64", "hex64", 8, 1, 8, KF_HexPreview},
{NodeKind::Int8, "Int8", "int8_t", 1, 1, 1, KF_None}, {NodeKind::Int8, "Int8", "int8_t", 1, 1, 1, KF_None},
{NodeKind::Int16, "Int16", "int16_t", 2, 1, 2, KF_None}, {NodeKind::Int16, "Int16", "int16_t", 2, 1, 2, KF_None},
{NodeKind::Int32, "Int32", "int32_t", 4, 1, 4, KF_None}, {NodeKind::Int32, "Int32", "int32_t", 4, 1, 4, KF_None},
@@ -72,10 +72,10 @@ inline constexpr KindMeta kKindMeta[] = {
{NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None}, {NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None},
{NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None}, {NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None},
{NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None}, {NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None},
{NodeKind::Vec2, "Vec2", "Vec2", 8, 1, 4, KF_Vector}, {NodeKind::Vec2, "Vec2", "vec2", 8, 1, 4, KF_Vector},
{NodeKind::Vec3, "Vec3", "Vec3", 12, 1, 4, KF_Vector}, {NodeKind::Vec3, "Vec3", "vec3", 12, 1, 4, KF_Vector},
{NodeKind::Vec4, "Vec4", "Vec4", 16, 1, 4, KF_Vector}, {NodeKind::Vec4, "Vec4", "vec4", 16, 1, 4, KF_Vector},
{NodeKind::Mat4x4, "Mat4x4", "Mat4x4", 64, 4, 4, KF_None}, {NodeKind::Mat4x4, "Mat4x4", "mat4x4", 64, 4, 4, KF_None},
{NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String}, {NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String},
{NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String}, {NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String},
{NodeKind::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview}, {NodeKind::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview},
@@ -125,6 +125,9 @@ inline constexpr bool isHexPreview(NodeKind k) {
inline constexpr bool isHexNode(NodeKind k) { inline constexpr bool isHexNode(NodeKind k) {
return k >= NodeKind::Hex8 && k <= NodeKind::Hex64; return k >= NodeKind::Hex8 && k <= NodeKind::Hex64;
} }
inline constexpr bool isVectorKind(NodeKind k) {
return k == NodeKind::Vec2 || k == NodeKind::Vec3 || k == NodeKind::Vec4;
}
inline QStringList allTypeNamesForUI(bool stripBrackets = false) { inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
QStringList out; QStringList out;
@@ -391,15 +394,17 @@ struct NodeTree {
enum class LineKind : uint8_t { enum class LineKind : uint8_t {
CommandRow, // line 0: source + address CommandRow, // line 0: source + address
CommandRow2, // line 1: root class type + name Blank, // line 1: dotted separator
CommandRow2, // line 2: root class type + name
Header, Field, Continuation, Footer, ArrayElementSeparator Header, Field, Continuation, Footer, ArrayElementSeparator
}; };
static constexpr uint64_t kCommandRowId = UINT64_MAX; static constexpr uint64_t kCommandRowId = UINT64_MAX;
static constexpr uint64_t kCommandRow2Id = UINT64_MAX - 1; static constexpr uint64_t kCommandRow2Id = UINT64_MAX - 1;
static constexpr int kCommandRowLine = 0; static constexpr int kCommandRowLine = 0;
static constexpr int kCommandRow2Line = 1; static constexpr int kBlankLine = 1;
static constexpr int kFirstDataLine = 2; static constexpr int kCommandRow2Line = 2;
static constexpr int kFirstDataLine = 3;
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL; static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
struct LineMeta { struct LineMeta {
@@ -430,7 +435,7 @@ struct LineMeta {
}; };
inline bool isSyntheticLine(const LineMeta& lm) { inline bool isSyntheticLine(const LineMeta& lm) {
return lm.lineKind == LineKind::CommandRow || lm.lineKind == LineKind::CommandRow2; return lm.lineKind == LineKind::CommandRow || lm.lineKind == LineKind::Blank || lm.lineKind == LineKind::CommandRow2;
} }
// ── Layout Info ── // ── Layout Info ──
@@ -568,22 +573,26 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
} }
// ── CommandRow spans ── // ── CommandRow spans ──
// Line format: " File 'name' Address: 0x140000000" // Line format: "source▾ 0x140000000"
inline ColumnSpan commandRowSrcSpan(const QString& lineText) { inline ColumnSpan commandRowSrcSpan(const QString& lineText) {
int idx = lineText.indexOf(QStringLiteral(" Address: ")); int idx = lineText.indexOf(QStringLiteral(" \u203A"));
if (idx < 0) return {}; if (idx < 0) return {};
int start = 0; int start = 0;
while (start < idx && !lineText[start].isLetterOrNumber() while (start < idx && !lineText[start].isLetterOrNumber()
&& lineText[start] != '<') start++; && lineText[start] != '<' && lineText[start] != '\'') start++;
if (start >= idx) return {}; if (start >= idx) return {};
return {start, idx, true}; // Exclude trailing ▾ from the editable span
int end = idx;
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
if (end <= start) return {};
return {start, end, true};
} }
inline ColumnSpan commandRowAddrSpan(const QString& lineText) { inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
int tag = lineText.indexOf(QStringLiteral(" Address: ")); int tag = lineText.indexOf(QStringLiteral(" \u203A"));
if (tag < 0) return {}; if (tag < 0) return {};
int start = tag + 10; // after " Address: " int start = tag + 3; // after " "
int end = start; int end = start;
while (end < lineText.size() && !lineText[end].isSpace()) end++; while (end < lineText.size() && !lineText[end].isSpace()) end++;
if (end <= start) return {}; if (end <= start) return {};
@@ -591,13 +600,14 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
} }
// ── CommandRow2 spans ── // ── CommandRow2 spans ──
// Line format: "struct ClassName" // Line format: "struct ClassName {"
inline ColumnSpan commandRow2TypeSpan(const QString& lineText) { inline ColumnSpan commandRow2TypeSpan(const QString& lineText) {
int start = 0; int start = 0;
while (start < lineText.size() && lineText[start].isSpace()) start++; while (start < lineText.size() && lineText[start].isSpace()) start++;
if (start >= lineText.size()) return {}; if (start >= lineText.size()) return {};
int end = lineText.indexOf(' ', start); int end = start;
while (end < lineText.size() && lineText[end] != QChar(' ') && lineText[end] != QChar(0x25BE)) end++;
if (end <= start) return {start, (int)lineText.size(), true}; if (end <= start) return {start, (int)lineText.size(), true};
return {start, end, true}; return {start, end, true};
} }
@@ -642,27 +652,20 @@ inline ColumnSpan arrayElemCountSpanFor(const LineMeta& lm, const QString& lineT
} }
// ── Pointer kind/target spans (within type column of pointer fields) ── // ── Pointer kind/target spans (within type column of pointer fields) ──
// Line format: " ptr64<void> name -> 0x..." // Line format: " void* name -> 0x..."
// pointerKindSpan covers "ptr64" or "ptr32", pointerTargetSpan covers the target name inside <> // pointerTargetSpan covers the target name before '*'
inline ColumnSpan pointerKindSpanFor(const LineMeta& lm, const QString& lineText) { inline ColumnSpan pointerKindSpanFor(const LineMeta& /*lm*/, const QString& /*lineText*/) {
if ((lm.lineKind != LineKind::Field && lm.lineKind != LineKind::Header) || lm.isContinuation) return {}; return {}; // No separate kind span in "Type*" format
if (lm.nodeKind != NodeKind::Pointer32 && lm.nodeKind != NodeKind::Pointer64) return {};
int ind = kFoldCol + lm.depth * 3;
// Find '<' in the type portion
int lt = lineText.indexOf('<', ind);
if (lt <= ind) return {};
return {ind, lt, true};
} }
inline ColumnSpan pointerTargetSpanFor(const LineMeta& lm, const QString& lineText) { inline ColumnSpan pointerTargetSpanFor(const LineMeta& lm, const QString& lineText) {
if ((lm.lineKind != LineKind::Field && lm.lineKind != LineKind::Header) || lm.isContinuation) return {}; if ((lm.lineKind != LineKind::Field && lm.lineKind != LineKind::Header) || lm.isContinuation) return {};
if (lm.nodeKind != NodeKind::Pointer32 && lm.nodeKind != NodeKind::Pointer64) return {}; if (lm.nodeKind != NodeKind::Pointer32 && lm.nodeKind != NodeKind::Pointer64) return {};
int ind = kFoldCol + lm.depth * 3; int ind = kFoldCol + lm.depth * 3;
int lt = lineText.indexOf('<', ind); int star = lineText.indexOf('*', ind);
int gt = lineText.indexOf('>', lt); if (star <= ind) return {};
if (lt < 0 || gt < 0 || gt <= lt + 1) return {}; return {ind, star, true};
return {lt + 1, gt, true};
} }
// ── Array navigation spans ── // ── Array navigation spans ──

View File

@@ -18,7 +18,7 @@ namespace rcx {
// ── Theme constants ── // ── Theme constants ──
static const QColor kBgText("#1e1e1e"); static const QColor kBgText("#1e1e1e");
static const QColor kBgMargin("#252526"); static const QColor kBgMargin("#1e1e1e"); // matches regular editor background
static const QColor kFgMargin("#858585"); static const QColor kFgMargin("#858585");
static const QColor kFgMarginDim("#505050"); static const QColor kFgMarginDim("#505050");
@@ -28,6 +28,8 @@ static constexpr int IND_BASE_ADDR = 10; // Green color for base address
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like) static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans
static constexpr int IND_DATA_CHANGED = 13; // Amber text for changed data values static constexpr int IND_DATA_CHANGED = 13; // Amber text for changed data values
static constexpr int IND_CLASS_NAME = 14; // Teal text for root class name
static constexpr int IND_CLASS_ULINE = 15; // Underline for root class name
static QString g_fontName = "Consolas"; static QString g_fontName = "Consolas";
@@ -160,11 +162,11 @@ void RcxEditor::setupScintilla() {
// Command-row pill background (shadcn-ish chip) // Command-row pill background (shadcn-ish chip)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_CMD_PILL, 7 /*INDIC_ROUNDBOX*/); IND_CMD_PILL, 8 /*INDIC_STRAIGHTBOX*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_CMD_PILL, QColor("#2a2a2a")); IND_CMD_PILL, QColor("#2a2a2a"));
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETALPHA, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETALPHA,
IND_CMD_PILL, (long)90); IND_CMD_PILL, (long)100);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
IND_CMD_PILL, (long)1); IND_CMD_PILL, (long)1);
@@ -174,6 +176,18 @@ void RcxEditor::setupScintilla() {
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_DATA_CHANGED, QColor("#8fbc7a")); IND_DATA_CHANGED, QColor("#8fbc7a"));
// Root class name — teal (VS Code type color)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_CLASS_NAME, 17 /*INDIC_TEXTFORE*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_CLASS_NAME, QColor("#4EC9B0"));
// Root class name underline
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_CLASS_ULINE, (long)0 /*INDIC_PLAIN*/);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_CLASS_ULINE, QColor(124, 180, 226));
} }
void RcxEditor::setupLexer() { void RcxEditor::setupLexer() {
@@ -201,15 +215,25 @@ void RcxEditor::setupLexer() {
m_lexer->setFont(font, i); m_lexer->setFont(font, i);
} }
// Custom / user-defined types → teal (VS Code #4EC9B0)
m_lexer->setColor(QColor("#4EC9B0"), QsciLexerCPP::GlobalClass);
m_sci->setLexer(m_lexer); m_sci->setLexer(m_lexer);
m_sci->setBraceMatching(QsciScintilla::NoBraceMatch); // Disable - this is a structured viewer m_sci->setBraceMatching(QsciScintilla::NoBraceMatch); // Disable - this is a structured viewer
// Add type names to keyword set 2teal coloring (distinct from identifiers) // Add built-in type names to keyword set 1blue coloring
QByteArray kw2 = allTypeNamesForUI(/*stripBrackets=*/true).join(' ').toLatin1(); QByteArray kw2 = allTypeNamesForUI(/*stripBrackets=*/true).join(' ').toLatin1();
m_sci->SendScintilla(QsciScintillaBase::SCI_SETKEYWORDS, m_sci->SendScintilla(QsciScintillaBase::SCI_SETKEYWORDS,
(uintptr_t)1, kw2.constData()); (uintptr_t)1, kw2.constData());
} }
void RcxEditor::setCustomTypeNames(const QStringList& names) {
m_customTypeNames = names;
QByteArray kw = names.join(' ').toLatin1();
m_sci->SendScintilla(QsciScintillaBase::SCI_SETKEYWORDS,
(uintptr_t)3, kw.constData());
}
void RcxEditor::setupMargins() { void RcxEditor::setupMargins() {
m_sci->setMarginsFont(editorFont()); m_sci->setMarginsFont(editorFont());
@@ -282,7 +306,7 @@ void RcxEditor::setupMarkers() {
// M_CMD_ROW (8): distinct background for CommandRow bar // M_CMD_ROW (8): distinct background for CommandRow bar
m_sci->markerDefine(QsciScintilla::Background, M_CMD_ROW); m_sci->markerDefine(QsciScintilla::Background, M_CMD_ROW);
m_sci->setMarkerBackgroundColor(QColor("#252526"), M_CMD_ROW); m_sci->setMarkerBackgroundColor(QColor("#1e1e1e"), M_CMD_ROW);
} }
void RcxEditor::allocateMarginStyles() { void RcxEditor::allocateMarginStyles() {
@@ -293,7 +317,7 @@ void RcxEditor::allocateMarginStyles() {
m_marginStyleBase = (int)base; m_marginStyleBase = (int)base;
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLEOFFSET, base); m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLEOFFSET, base);
const long bgrMargin = 0x262525; // BGR for #252526 const long bgrMargin = 0x1e1e1e; // BGR for #1e1e1e (matches editor bg)
QByteArray fontName = editorFont().family().toUtf8(); QByteArray fontName = editorFont().family().toUtf8();
int fontSize = editorFont().pointSize(); int fontSize = editorFont().pointSize();
@@ -349,7 +373,6 @@ void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) {
m_sci->clearMarginText(-1); m_sci->clearMarginText(-1);
for (int i = 0; i < meta.size(); i++) { for (int i = 0; i < meta.size(); i++) {
if (isSyntheticLine(meta[i])) continue;
const auto& lm = meta[i]; const auto& lm = meta[i];
if (lm.offsetText.isEmpty()) continue; if (lm.offsetText.isEmpty()) continue;
@@ -368,10 +391,13 @@ void RcxEditor::applyMarkers(const QVector<LineMeta>& meta) {
} }
m_sci->markerDeleteAll(M_CMD_ROW); m_sci->markerDeleteAll(M_CMD_ROW);
for (int i = 0; i < meta.size(); i++) { for (int i = 0; i < meta.size(); i++) {
if (meta[i].lineKind == LineKind::CommandRow || meta[i].lineKind == LineKind::CommandRow2) { if (meta[i].lineKind == LineKind::CommandRow) {
m_sci->markerAdd(i, M_CMD_ROW); m_sci->markerAdd(i, M_CMD_ROW);
continue; continue;
} }
if (meta[i].lineKind == LineKind::CommandRow2) {
continue; // regular background (no special marker)
}
uint32_t mask = meta[i].markerMask; uint32_t mask = meta[i].markerMask;
for (int m = M_CONT; m <= M_STRUCT_BG; m++) { for (int m = M_CONT; m <= M_STRUCT_BG; m++) {
if (mask & (1u << m)) { if (mask & (1u << m)) {
@@ -610,10 +636,7 @@ void RcxEditor::applyBaseAddressColoring(const QVector<LineMeta>& meta) {
if (meta.isEmpty() || meta[0].lineKind != LineKind::CommandRow) return; if (meta.isEmpty() || meta[0].lineKind != LineKind::CommandRow) return;
clearIndicatorLine(IND_BASE_ADDR, 0); clearIndicatorLine(IND_BASE_ADDR, 0);
QString lineText = getLineText(m_sci, 0); // Address in command bar is not colored green (only field values get green)
ColumnSpan span = commandRowAddrSpan(lineText);
if (span.valid)
fillIndicatorCols(IND_BASE_ADDR, 0, span.start, span.end);
} }
void RcxEditor::applyCommandRowPills() { void RcxEditor::applyCommandRowPills() {
@@ -635,7 +658,7 @@ void RcxEditor::applyCommandRowPills() {
fillPadded(commandRowSrcSpan(t)); fillPadded(commandRowSrcSpan(t));
fillPadded(commandRowAddrSpan(t)); fillPadded(commandRowAddrSpan(t));
// Dim label text: provider kind ("File"/"Process") and "Address:" // Dim label text: source arrow/placeholder and ", address="
ColumnSpan srcSpan = commandRowSrcSpan(t); ColumnSpan srcSpan = commandRowSrcSpan(t);
if (srcSpan.valid) { if (srcSpan.valid) {
int quotePos = t.indexOf('\'', srcSpan.start); int quotePos = t.indexOf('\'', srcSpan.start);
@@ -644,33 +667,34 @@ void RcxEditor::applyCommandRowPills() {
if (kindEnd > srcSpan.start) if (kindEnd > srcSpan.start)
fillIndicatorCols(IND_HEX_DIM, line, srcSpan.start, kindEnd); fillIndicatorCols(IND_HEX_DIM, line, srcSpan.start, kindEnd);
} }
int addrTag = t.indexOf(QStringLiteral(" Address: ")); int addrTag = t.indexOf(QStringLiteral(" \u203A"));
if (addrTag >= 0) if (addrTag >= 0)
fillIndicatorCols(IND_HEX_DIM, line, addrTag + 1, addrTag + 9); fillIndicatorCols(IND_HEX_DIM, line, addrTag, addrTag + 3);
// Style CommandRow2 (line 1) if present // Style CommandRow2 (line kCommandRow2Line) if present
if (m_meta.size() > 1 && m_meta[1].lineKind == LineKind::CommandRow2) { if (m_meta.size() > kCommandRow2Line && m_meta[kCommandRow2Line].lineKind == LineKind::CommandRow2) {
constexpr int line2 = 1; int line2 = kCommandRow2Line;
QString t2 = getLineText(m_sci, line2); QString t2 = getLineText(m_sci, line2);
clearIndicatorLine(IND_CMD_PILL, line2);
clearIndicatorLine(IND_HEX_DIM, line2); clearIndicatorLine(IND_HEX_DIM, line2);
clearIndicatorLine(IND_CLASS_NAME, line2);
auto fillPadded2 = [&](ColumnSpan s) { clearIndicatorLine(IND_CLASS_ULINE, line2);
if (!s.valid) return;
int a = qMax(0, s.start - 1);
int b = qMin(t2.size(), s.end + 1);
fillIndicatorCols(IND_CMD_PILL, line2, a, b);
};
ColumnSpan typeSpan = commandRow2TypeSpan(t2); ColumnSpan typeSpan = commandRow2TypeSpan(t2);
fillPadded2(typeSpan);
if (typeSpan.valid) if (typeSpan.valid)
fillIndicatorCols(IND_HEX_DIM, line2, typeSpan.start, typeSpan.end); fillIndicatorCols(IND_HEX_DIM, line2, typeSpan.start, typeSpan.end);
ColumnSpan nameSpan = commandRow2NameSpan(t2); ColumnSpan nameSpan = commandRow2NameSpan(t2);
fillPadded2(nameSpan); if (nameSpan.valid) {
fillIndicatorCols(IND_CLASS_NAME, line2, nameSpan.start, nameSpan.end);
}
}
// Dim the dotted separator line
if (m_meta.size() > kBlankLine && m_meta[kBlankLine].lineKind == LineKind::Blank) {
QString sep = getLineText(m_sci, kBlankLine);
if (!sep.isEmpty())
fillIndicatorCols(IND_HEX_DIM, kBlankLine, 0, sep.size());
} }
} }
@@ -1398,17 +1422,55 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
QString trimmed = lineText.mid(norm.start, norm.end - norm.start); QString trimmed = lineText.mid(norm.start, norm.end - norm.start);
int vecComponent = 0; // which vector component (0-3)
// For vector value editing: narrow span to the clicked component
if (target == EditTarget::Value && isVectorKind(lm->nodeKind)) {
int cursorCol = col; // col from getCursorPosition above
// Find comma positions within the value span to identify components
QVector<int> compStarts, compEnds;
int pos = 0;
for (int i = 0; i < trimmed.size(); i++) {
if (trimmed[i] == ',') {
compEnds.append(i);
// skip ", " separator
int next = i + 1;
while (next < trimmed.size() && trimmed[next] == ' ') next++;
compStarts.append(next);
}
}
compStarts.prepend(0);
compEnds.append(trimmed.size());
// Find which component the cursor is in
int relCol = cursorCol - norm.start;
vecComponent = 0;
for (int i = 0; i < compStarts.size(); i++) {
if (relCol >= compStarts[i] && (i == compStarts.size() - 1 || relCol < compStarts[i + 1]))
{ vecComponent = i; break; }
}
if (vecComponent >= compStarts.size()) vecComponent = compStarts.size() - 1;
// Narrow span to just this component
int cStart = norm.start + compStarts[vecComponent];
int cEnd = norm.start + compEnds[vecComponent];
// Trim trailing spaces from component
while (cEnd > cStart && lineText[cEnd - 1] == ' ') cEnd--;
norm.start = cStart;
norm.end = cEnd;
trimmed = lineText.mid(norm.start, norm.end - norm.start);
}
m_editState.active = true; m_editState.active = true;
m_editState.line = line; m_editState.line = line;
m_editState.nodeIdx = lm->nodeIdx; m_editState.nodeIdx = lm->nodeIdx;
m_editState.subLine = lm->subLine; m_editState.subLine = isVectorKind(lm->nodeKind) ? vecComponent : lm->subLine;
m_editState.target = target; m_editState.target = target;
m_editState.spanStart = norm.start; m_editState.spanStart = norm.start;
m_editState.original = trimmed; m_editState.original = trimmed;
m_editState.linelenAfterReplace = lineText.size(); m_editState.linelenAfterReplace = lineText.size();
m_editState.editKind = lm->nodeKind; m_editState.editKind = lm->nodeKind;
if ((lm->nodeKind == NodeKind::Vec2 || lm->nodeKind == NodeKind::Vec3 || if (isVectorKind(lm->nodeKind))
lm->nodeKind == NodeKind::Vec4) && lm->subLine >= 0)
m_editState.editKind = NodeKind::Float; m_editState.editKind = NodeKind::Float;
// Store fixed comment column position for value editing // Store fixed comment column position for value editing
@@ -2004,7 +2066,7 @@ void RcxEditor::setCommandRowText(const QString& line) {
} }
void RcxEditor::setCommandRow2Text(const QString& line) { void RcxEditor::setCommandRow2Text(const QString& line) {
if (m_sci->lines() <= 1) return; if (m_sci->lines() <= kCommandRow2Line) return;
QString s = line; QString s = line;
s.replace('\n', ' '); s.replace('\n', ' ');
s.replace('\r', ' '); s.replace('\r', ' ');
@@ -2017,15 +2079,15 @@ void RcxEditor::setCommandRow2Text(const QString& line) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, 0);
m_sci->setReadOnly(false); m_sci->setReadOnly(false);
long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, 1); long start = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, kCommandRow2Line);
long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, 1); long end = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, kCommandRow2Line);
long oldLen = end - start; long oldLen = end - start;
QByteArray utf8 = s.toUtf8(); QByteArray utf8 = s.toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, start); m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, start);
m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, end); m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, end);
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData()); m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData());
// Adjust saved cursor/anchor for length change in line 1 // Adjust saved cursor/anchor for length change in CommandRow2 line
long delta = (long)utf8.size() - oldLen; long delta = (long)utf8.size() - oldLen;
if (savedPos > end) savedPos += delta; if (savedPos > end) savedPos += delta;
if (savedAnchor > end) savedAnchor += delta; if (savedAnchor > end) savedAnchor += delta;

View File

@@ -49,8 +49,8 @@ public:
void setEditorFont(const QString& fontName); void setEditorFont(const QString& fontName);
static void setGlobalFontName(const QString& fontName); static void setGlobalFontName(const QString& fontName);
// Custom type names (struct types from the tree) shown in type picker // Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
void setCustomTypeNames(const QStringList& names) { m_customTypeNames = names; } void setCustomTypeNames(const QStringList& names);
// Saved sources for quick-switch in source picker // Saved sources for quick-switch in source picker
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; } void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }

View File

@@ -1,6 +1,6 @@
{ {
"baseAddress": "400000", "baseAddress": "400000",
"nextId": "23", "nextId": "29",
"nodes": [ "nodes": [
{ {
"arrayLen": 1, "arrayLen": 1,
@@ -13,7 +13,7 @@
"parentId": "0", "parentId": "0",
"refId": "0", "refId": "0",
"strLen": 64, "strLen": 64,
"structTypeName": "Ball" "structTypeName": "ball"
}, },
{ {
"arrayLen": 1, "arrayLen": 1,
@@ -92,8 +92,8 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "8", "id": "8",
"kind": "Hex32", "kind": "UInt32",
"name": "field_34", "name": "color",
"offset": 52, "offset": 52,
"parentId": "1", "parentId": "1",
"refId": "0", "refId": "0",
@@ -104,8 +104,8 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "9", "id": "9",
"kind": "UInt32", "kind": "Float",
"name": "color", "name": "radius",
"offset": 56, "offset": 56,
"parentId": "1", "parentId": "1",
"refId": "0", "refId": "0",
@@ -129,7 +129,7 @@
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "11", "id": "11",
"kind": "Float", "kind": "Float",
"name": "radius", "name": "mass",
"offset": 64, "offset": 64,
"parentId": "1", "parentId": "1",
"refId": "0", "refId": "0",
@@ -140,7 +140,7 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "12", "id": "12",
"kind": "Hex32", "kind": "Hex64",
"name": "field_44", "name": "field_44",
"offset": 68, "offset": 68,
"parentId": "1", "parentId": "1",
@@ -152,9 +152,9 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "13", "id": "13",
"kind": "Double", "kind": "Bool",
"name": "mass", "name": "bouncy",
"offset": 72, "offset": 76,
"parentId": "1", "parentId": "1",
"refId": "0", "refId": "0",
"strLen": 64 "strLen": 64
@@ -164,9 +164,9 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "14", "id": "14",
"kind": "Hex64", "kind": "Hex8",
"name": "field_50", "name": "field_4D",
"offset": 80, "offset": 77,
"parentId": "1", "parentId": "1",
"refId": "0", "refId": "0",
"strLen": 64 "strLen": 64
@@ -176,9 +176,9 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "15", "id": "15",
"kind": "Bool", "kind": "Hex16",
"name": "bouncy", "name": "field_4E",
"offset": 88, "offset": 78,
"parentId": "1", "parentId": "1",
"refId": "0", "refId": "0",
"strLen": 64 "strLen": 64
@@ -188,9 +188,9 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "16", "id": "16",
"kind": "Hex8", "kind": "UInt32",
"name": "field_59", "name": "color",
"offset": 89, "offset": 80,
"parentId": "1", "parentId": "1",
"refId": "0", "refId": "0",
"strLen": 64 "strLen": 64
@@ -200,9 +200,9 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "17", "id": "17",
"kind": "Hex16", "kind": "Hex32",
"name": "field_5A", "name": "field_54",
"offset": 90, "offset": 84,
"parentId": "1", "parentId": "1",
"refId": "0", "refId": "0",
"strLen": 64 "strLen": 64
@@ -212,9 +212,9 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "18", "id": "18",
"kind": "UInt32", "kind": "Hex64",
"name": "bounceCount", "name": "field_58",
"offset": 92, "offset": 88,
"parentId": "1", "parentId": "1",
"refId": "0", "refId": "0",
"strLen": 64 "strLen": 64
@@ -224,7 +224,7 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "19", "id": "19",
"kind": "Hex32", "kind": "Hex64",
"name": "field_60", "name": "field_60",
"offset": 96, "offset": 96,
"parentId": "1", "parentId": "1",
@@ -236,12 +236,13 @@
"collapsed": false, "collapsed": false,
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "20", "id": "20",
"kind": "Hex64", "kind": "Struct",
"name": "field_68", "name": "aPhysics",
"offset": 100, "offset": 0,
"parentId": "1", "parentId": "0",
"refId": "0", "refId": "0",
"strLen": 64 "strLen": 64,
"structTypeName": "Physics"
}, },
{ {
"arrayLen": 1, "arrayLen": 1,
@@ -249,9 +250,9 @@
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "21", "id": "21",
"kind": "Hex64", "kind": "Hex64",
"name": "field_70", "name": "field_00",
"offset": 108, "offset": 0,
"parentId": "1", "parentId": "20",
"refId": "0", "refId": "0",
"strLen": 64 "strLen": 64
}, },
@@ -261,8 +262,80 @@
"elementKind": "UInt8", "elementKind": "UInt8",
"id": "22", "id": "22",
"kind": "Hex64", "kind": "Hex64",
"name": "field_08",
"offset": 8,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "23",
"kind": "Hex64",
"name": "field_10",
"offset": 16,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "24",
"kind": "Hex64",
"name": "field_18",
"offset": 24,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "25",
"kind": "Hex64",
"name": "field_20",
"offset": 32,
"parentId": "20",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": true,
"elementKind": "UInt8",
"id": "26",
"kind": "Pointer64",
"name": "physics",
"offset": 104,
"parentId": "1",
"refId": "20",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "27",
"kind": "Hex64",
"name": "field_70",
"offset": 112,
"parentId": "1",
"refId": "0",
"strLen": 64
},
{
"arrayLen": 1,
"collapsed": false,
"elementKind": "UInt8",
"id": "28",
"kind": "Hex64",
"name": "field_78", "name": "field_78",
"offset": 116, "offset": 120,
"parentId": "1", "parentId": "1",
"refId": "0", "refId": "0",
"strLen": 64 "strLen": 64

View File

@@ -51,12 +51,11 @@ QString arrayTypeName(NodeKind elemKind, int count) {
return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]"); return elem + QStringLiteral("[") + QString::number(count) + QStringLiteral("]");
} }
// Pointer type string: "ptr64<void>" or "ptr64<StructName>" // Pointer type string: "void*" or "StructName*"
QString pointerTypeName(NodeKind kind, const QString& targetName) { QString pointerTypeName(NodeKind kind, const QString& targetName) {
auto* m = kindMeta(kind); Q_UNUSED(kind);
QString base = m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
QString target = targetName.isEmpty() ? QStringLiteral("void") : targetName; QString target = targetName.isEmpty() ? QStringLiteral("void") : targetName;
return base + QStringLiteral("<") + target + QStringLiteral(">"); return target + QStringLiteral("*");
} }
// ── Value formatting ── // ── Value formatting ──
@@ -78,8 +77,20 @@ QString fmtUInt16(uint16_t v) { return hexVal(v); }
QString fmtUInt32(uint32_t v) { return hexVal(v); } QString fmtUInt32(uint32_t v) { return hexVal(v); }
QString fmtUInt64(uint64_t v) { return hexVal(v); } QString fmtUInt64(uint64_t v) { return hexVal(v); }
QString fmtFloat(float v) { return QString::number(v, 'g', 4); } // 4 sig figs keeps it short QString fmtFloat(float v) {
QString fmtDouble(double v) { return QString::number(v, 'g', 6); } QString s = QString::number(v, 'g', 4);
if (!s.contains('.') && !s.contains('e') && !s.contains('E'))
s += QStringLiteral(".f");
else
s += QLatin1Char('f');
return s;
}
QString fmtDouble(double v) {
QString s = QString::number(v, 'g', 6);
if (!s.contains('.') && !s.contains('e') && !s.contains('E'))
s += QStringLiteral(".0");
return s;
}
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); } QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
QString fmtPointer32(uint32_t v) { QString fmtPointer32(uint32_t v) {
@@ -497,12 +508,15 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return parseIntChecked<uint32_t>(val, ok); } case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return parseIntChecked<uint32_t>(val, ok); }
case NodeKind::UInt64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return *ok ? toBytes<uint64_t>(val) : QByteArray{}; } case NodeKind::UInt64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return *ok ? toBytes<uint64_t>(val) : QByteArray{}; }
case NodeKind::Float: { case NodeKind::Float: {
QString n = s; n.replace(',', '.'); // Accept EU decimal separator QString n = s.trimmed();
if (n.endsWith('f', Qt::CaseInsensitive)) n.chop(1);
n.replace(',', '.');
float val = n.toFloat(ok); float val = n.toFloat(ok);
return *ok ? toBytes<float>(val) : QByteArray{}; return *ok ? toBytes<float>(val) : QByteArray{};
} }
case NodeKind::Double: { case NodeKind::Double: {
QString n = s; n.replace(',', '.'); // Accept EU decimal separator QString n = s.trimmed();
n.replace(',', '.');
double val = n.toDouble(ok); double val = n.toDouble(ok);
return *ok ? toBytes<double>(val) : QByteArray{}; return *ok ? toBytes<double>(val) : QByteArray{};
} }

View File

@@ -387,6 +387,14 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
}); });
}); });
// Auto-focus on first root struct (don't show all roots)
for (const auto& n : doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
ctrl->setViewRootId(n.id);
break;
}
}
ctrl->refresh(); ctrl->refresh();
rebuildWorkspaceModel(); rebuildWorkspaceModel();
return sub; return sub;
@@ -408,7 +416,7 @@ void MainWindow::selfTest() {
Node ball; Node ball;
ball.kind = NodeKind::Struct; ball.kind = NodeKind::Struct;
ball.name = "aBall"; ball.name = "aBall";
ball.structTypeName = "Ball"; ball.structTypeName = "ball";
ball.parentId = 0; ball.parentId = 0;
ball.offset = 0; ball.offset = 0;
int bi = doc->tree.addNode(ball); int bi = doc->tree.addNode(ball);
@@ -420,21 +428,39 @@ void MainWindow::selfTest() {
{ Node n; n.kind = NodeKind::Vec3; n.name = "velocity"; n.parentId = ballId; n.offset = 32; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Vec3; n.name = "velocity"; n.parentId = ballId; n.offset = 32; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_2C"; n.parentId = ballId; n.offset = 44; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Hex32; n.name = "field_2C"; n.parentId = ballId; n.offset = 44; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 48; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 48; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_34"; n.parentId = ballId; n.offset = 52; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 52; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 56; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Float; n.name = "radius"; n.parentId = ballId; n.offset = 56; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_3C"; n.parentId = ballId; n.offset = 60; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Hex32; n.name = "field_3C"; n.parentId = ballId; n.offset = 60; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "radius"; n.parentId = ballId; n.offset = 64; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Float; n.name = "mass"; n.parentId = ballId; n.offset = 64; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_44"; n.parentId = ballId; n.offset = 68; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Hex64; n.name = "field_44"; n.parentId = ballId; n.offset = 68; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Double; n.name = "mass"; n.parentId = ballId; n.offset = 72; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 76; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_50"; n.parentId = ballId; n.offset = 80; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Hex8; n.name = "field_4D"; n.parentId = ballId; n.offset = 77; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 88; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Hex16; n.name = "field_4E"; n.parentId = ballId; n.offset = 78; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex8; n.name = "field_59"; n.parentId = ballId; n.offset = 89; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 80; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex16; n.name = "field_5A"; n.parentId = ballId; n.offset = 90; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Hex32; n.name = "field_54"; n.parentId = ballId; n.offset = 84; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::UInt32; n.name = "bounceCount"; n.parentId = ballId; n.offset = 92; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Hex64; n.name = "field_58"; n.parentId = ballId; n.offset = 88; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "field_60"; n.parentId = ballId; n.offset = 96; doc->tree.addNode(n); } { Node n; n.kind = NodeKind::Hex64; n.name = "field_60"; n.parentId = ballId; n.offset = 96; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_68"; n.parentId = ballId; n.offset = 100; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_70"; n.parentId = ballId; n.offset = 108; doc->tree.addNode(n); } // Physics struct (defined at root level)
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_78"; n.parentId = ballId; n.offset = 116; doc->tree.addNode(n); } Node phys;
phys.kind = NodeKind::Struct;
phys.name = "aPhysics";
phys.structTypeName = "Physics";
phys.parentId = 0;
phys.offset = 0;
int pi = doc->tree.addNode(phys);
uint64_t physId = doc->tree.nodes[pi].id;
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = physId; n.offset = 0; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = physId; n.offset = 8; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_10"; n.parentId = physId; n.offset = 16; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_18"; n.parentId = physId; n.offset = 24; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_20"; n.parentId = physId; n.offset = 32; doc->tree.addNode(n); }
// Pointer to Physics in ball struct
{ Node n; n.kind = NodeKind::Pointer64; n.name = "physics"; n.parentId = ballId; n.offset = 104; n.refId = physId; n.collapsed = true; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_70"; n.parentId = ballId; n.offset = 112; doc->tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "field_78"; n.parentId = ballId; n.offset = 120; doc->tree.addNode(n); }
doc->save(demoPath); doc->save(demoPath);
doc->load(demoPath); doc->load(demoPath);

View File

@@ -13,15 +13,15 @@ using namespace rcx;
static QString buildSourceLabel(const Provider& prov) { static QString buildSourceLabel(const Provider& prov) {
QString provName = prov.name(); QString provName = prov.name();
if (provName.isEmpty()) if (provName.isEmpty())
return QStringLiteral("<Select Source>"); return QStringLiteral("source\u25BE");
return QStringLiteral("%1 '%2'").arg(prov.kind(), provName); return QStringLiteral("'%1'\u25BE").arg(provName);
} }
static QString buildCommandRow(const Provider& prov, uint64_t baseAddress) { static QString buildCommandRow(const Provider& prov, uint64_t baseAddress) {
QString src = buildSourceLabel(prov); QString src = buildSourceLabel(prov);
QString addr = QStringLiteral("0x") + QString addr = QStringLiteral("0x") +
QString::number(baseAddress, 16).toUpper(); QString::number(baseAddress, 16).toUpper();
return QStringLiteral(" %1 Address: %2").arg(src, addr); return QStringLiteral(" %1 \u203A %2").arg(src, addr);
} }
// -- Replicate commandRowSrcSpan for testing // -- Replicate commandRowSrcSpan for testing
@@ -32,13 +32,17 @@ struct TestColumnSpan {
}; };
static TestColumnSpan commandRowSrcSpan(const QString& lineText) { static TestColumnSpan commandRowSrcSpan(const QString& lineText) {
int idx = lineText.indexOf(QStringLiteral(" Address: ")); int idx = lineText.indexOf(QStringLiteral(" \u203A"));
if (idx < 0) return {}; if (idx < 0) return {};
int start = 0; int start = 0;
while (start < idx && !lineText[start].isLetterOrNumber() while (start < idx && !lineText[start].isLetterOrNumber()
&& lineText[start] != '<') start++; && lineText[start] != '<' && lineText[start] != '\'') start++;
if (start >= idx) return {}; if (start >= idx) return {};
return {start, idx, true}; // Exclude trailing ▾ from the editable span
int end = idx;
while (end > start && lineText[end - 1] == QChar(0x25BE)) end--;
if (end <= start) return {};
return {start, end, true};
} }
class TestCommandRow : public QObject { class TestCommandRow : public QObject {
@@ -52,18 +56,18 @@ private slots:
void label_nullProvider_showsSelectSource() { void label_nullProvider_showsSelectSource() {
NullProvider p; NullProvider p;
QCOMPARE(buildSourceLabel(p), QStringLiteral("<Select Source>")); QCOMPARE(buildSourceLabel(p), QStringLiteral("source\u25BE"));
} }
void label_bufferNoName_showsSelectSource() { void label_bufferNoName_showsSelectSource() {
// BufferProvider with empty name also triggers <Select Source> // BufferProvider with empty name also triggers source
BufferProvider p(QByteArray(4, '\0')); BufferProvider p(QByteArray(4, '\0'));
QCOMPARE(buildSourceLabel(p), QStringLiteral("<Select Source>")); QCOMPARE(buildSourceLabel(p), QStringLiteral("source\u25BE"));
} }
void label_bufferWithName_showsFileAndName() { void label_bufferWithName_showsFileAndName() {
BufferProvider p(QByteArray(4, '\0'), "dump.bin"); BufferProvider p(QByteArray(4, '\0'), "dump.bin");
QCOMPARE(buildSourceLabel(p), QStringLiteral("File 'dump.bin'")); QCOMPARE(buildSourceLabel(p), QStringLiteral("'dump.bin'\u25BE"));
} }
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -73,13 +77,13 @@ private slots:
void row_nullProvider() { void row_nullProvider() {
NullProvider p; NullProvider p;
QString row = buildCommandRow(p, 0); QString row = buildCommandRow(p, 0);
QCOMPARE(row, QStringLiteral(" <Select Source> Address: 0x0")); QCOMPARE(row, QStringLiteral(" source\u25BE \u203A 0x0"));
} }
void row_fileProvider() { void row_fileProvider() {
BufferProvider p(QByteArray(4, '\0'), "test.bin"); BufferProvider p(QByteArray(4, '\0'), "test.bin");
QString row = buildCommandRow(p, 0x140000000ULL); QString row = buildCommandRow(p, 0x140000000ULL);
QCOMPARE(row, QStringLiteral(" File 'test.bin' Address: 0x140000000")); QCOMPARE(row, QStringLiteral(" 'test.bin'\u25BE \u203A 0x140000000"));
} }
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -91,7 +95,7 @@ private slots:
auto span = commandRowSrcSpan(row); auto span = commandRowSrcSpan(row);
QVERIFY(span.valid); QVERIFY(span.valid);
QString extracted = row.mid(span.start, span.end - span.start); QString extracted = row.mid(span.start, span.end - span.start);
QCOMPARE(extracted, QStringLiteral("<Select Source>")); QCOMPARE(extracted, QStringLiteral("source"));
} }
void span_fileProvider() { void span_fileProvider() {
@@ -100,17 +104,17 @@ private slots:
auto span = commandRowSrcSpan(row); auto span = commandRowSrcSpan(row);
QVERIFY(span.valid); QVERIFY(span.valid);
QString extracted = row.mid(span.start, span.end - span.start); QString extracted = row.mid(span.start, span.end - span.start);
QCOMPARE(extracted, QStringLiteral("File 'dump.bin'")); QCOMPARE(extracted, QStringLiteral("'dump.bin'"));
} }
void span_processProvider_simulated() { void span_processProvider_simulated() {
// Simulate a process provider without needing Windows APIs // Simulate a process provider without needing Windows APIs
// by building the string directly // by building the string directly
QString row = QStringLiteral(" Process 'notepad.exe' Address: 0x7FF600000000"); QString row = QStringLiteral(" 'notepad.exe'\u25BE \u203A 0x7FF600000000");
auto span = commandRowSrcSpan(row); auto span = commandRowSrcSpan(row);
QVERIFY(span.valid); QVERIFY(span.valid);
QString extracted = row.mid(span.start, span.end - span.start); QString extracted = row.mid(span.start, span.end - span.start);
QCOMPARE(extracted, QStringLiteral("Process 'notepad.exe'")); QCOMPARE(extracted, QStringLiteral("'notepad.exe'"));
} }
// --------------------------------------------------------------- // ---------------------------------------------------------------
@@ -120,11 +124,11 @@ private slots:
void switching_nullToFileToProcess() { void switching_nullToFileToProcess() {
// Start with NullProvider // Start with NullProvider
std::unique_ptr<Provider> prov = std::make_unique<NullProvider>(); std::unique_ptr<Provider> prov = std::make_unique<NullProvider>();
QCOMPARE(buildSourceLabel(*prov), QStringLiteral("<Select Source>")); QCOMPARE(buildSourceLabel(*prov), QStringLiteral("source\u25BE"));
// User loads a file // User loads a file
prov = std::make_unique<BufferProvider>(QByteArray(64, '\0'), "game.exe"); prov = std::make_unique<BufferProvider>(QByteArray(64, '\0'), "game.exe");
QCOMPARE(buildSourceLabel(*prov), QStringLiteral("File 'game.exe'")); QCOMPARE(buildSourceLabel(*prov), QStringLiteral("'game.exe'\u25BE"));
// User switches to a "process" -- simulate with a named BufferProvider // User switches to a "process" -- simulate with a named BufferProvider
// (ProcessProvider needs Windows, but the label logic is the same) // (ProcessProvider needs Windows, but the label logic is the same)

View File

@@ -35,27 +35,30 @@ private slots:
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + 2 fields + root footer = 5 // CommandRow + Blank + CommandRow2 + 2 fields + root footer = 6
QCOMPARE(result.meta.size(), 5); QCOMPARE(result.meta.size(), 6);
// Line 0 is CommandRow // Line 0 is CommandRow
QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow); QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow);
// Line 1 is CommandRow2 // Line 1 is Blank separator
QCOMPARE(result.meta[1].lineKind, LineKind::CommandRow2); QCOMPARE(result.meta[1].lineKind, LineKind::Blank);
// Line 2 is CommandRow2
QCOMPARE(result.meta[2].lineKind, LineKind::CommandRow2);
// Fields at depth 1 // Fields at depth 1
QVERIFY(!result.meta[2].foldHead);
QCOMPARE(result.meta[2].depth, 1);
QVERIFY(!result.meta[3].foldHead); QVERIFY(!result.meta[3].foldHead);
QCOMPARE(result.meta[3].depth, 1); QCOMPARE(result.meta[3].depth, 1);
QVERIFY(!result.meta[4].foldHead);
QCOMPARE(result.meta[4].depth, 1);
// Offset text // Offset text
QCOMPARE(result.meta[2].offsetText, QString("0")); QCOMPARE(result.meta[3].offsetText, QString("0"));
QCOMPARE(result.meta[3].offsetText, QString("4")); QCOMPARE(result.meta[4].offsetText, QString("4"));
// Line 4 is root footer // Line 5 is root footer
QCOMPARE(result.meta[4].lineKind, LineKind::Footer); QCOMPARE(result.meta[5].lineKind, LineKind::Footer);
} }
void testVec3SingleLine() { void testVec3SingleLine() {
@@ -79,17 +82,17 @@ private slots:
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + 1 Vec3 line + root footer = 4 // CommandRow + Blank + CommandRow2 + 1 Vec3 line + root footer = 5
QCOMPARE(result.meta.size(), 4); QCOMPARE(result.meta.size(), 5);
// Line 2: single Vec3 line, not continuation, depth 1 // Line 3: single Vec3 line, not continuation, depth 1
QVERIFY(!result.meta[2].isContinuation); QVERIFY(!result.meta[3].isContinuation);
QCOMPARE(result.meta[2].offsetText, QString("0")); QCOMPARE(result.meta[3].offsetText, QString("0"));
QCOMPARE(result.meta[2].depth, 1); QCOMPARE(result.meta[3].depth, 1);
QCOMPARE(result.meta[2].nodeKind, NodeKind::Vec3); QCOMPARE(result.meta[3].nodeKind, NodeKind::Vec3);
// Line 3 is root footer // Line 4 is root footer
QCOMPARE(result.meta[3].lineKind, LineKind::Footer); QCOMPARE(result.meta[4].lineKind, LineKind::Footer);
} }
void testPaddingMarker() { void testPaddingMarker() {
@@ -113,13 +116,13 @@ private slots:
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + padding + root footer = 4 // CommandRow + Blank + CommandRow2 + padding + root footer = 5
QCOMPARE(result.meta.size(), 4); QCOMPARE(result.meta.size(), 5);
QVERIFY(result.meta[2].markerMask & (1u << M_PAD)); QVERIFY(result.meta[3].markerMask & (1u << M_PAD));
QCOMPARE(result.meta[2].depth, 1); QCOMPARE(result.meta[3].depth, 1);
// Line 3 is root footer // Line 4 is root footer
QCOMPARE(result.meta[3].lineKind, LineKind::Footer); QCOMPARE(result.meta[4].lineKind, LineKind::Footer);
} }
void testNullPointerMarker() { void testNullPointerMarker() {
@@ -145,14 +148,14 @@ private slots:
BufferProvider prov(data); BufferProvider prov(data);
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + ptr + root footer = 4 // CommandRow + Blank + CommandRow2 + ptr + root footer = 5
QCOMPARE(result.meta.size(), 4); QCOMPARE(result.meta.size(), 5);
// No ambient validation markers — M_PTR0 is no longer set // No ambient validation markers — M_PTR0 is no longer set
QVERIFY(!(result.meta[2].markerMask & (1u << M_PTR0))); QVERIFY(!(result.meta[3].markerMask & (1u << M_PTR0)));
QCOMPARE(result.meta[2].depth, 1); QCOMPARE(result.meta[3].depth, 1);
// Line 3 is root footer // Line 4 is root footer
QCOMPARE(result.meta[3].lineKind, LineKind::Footer); QCOMPARE(result.meta[4].lineKind, LineKind::Footer);
} }
void testCollapsedStruct() { void testCollapsedStruct() {
@@ -178,11 +181,11 @@ private slots:
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// Collapsed root: isRootHeader overrides collapse, so children + footer still render // Collapsed root: isRootHeader overrides collapse, so children + footer still render
// CommandRow + CommandRow2 + field + root footer = 4 // CommandRow + Blank + CommandRow2 + field + root footer = 5
QCOMPARE(result.meta.size(), 4); QCOMPARE(result.meta.size(), 5);
QCOMPARE(result.meta[2].lineKind, LineKind::Field); QCOMPARE(result.meta[3].lineKind, LineKind::Field);
QCOMPARE(result.meta[2].depth, 1); QCOMPARE(result.meta[3].depth, 1);
QCOMPARE(result.meta[3].lineKind, LineKind::Footer); QCOMPARE(result.meta[4].lineKind, LineKind::Footer);
} }
void testUnreadablePointerNoRead() { void testUnreadablePointerNoRead() {
@@ -209,15 +212,15 @@ private slots:
BufferProvider prov(data); BufferProvider prov(data);
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + ptr + root footer = 4 // CommandRow + Blank + CommandRow2 + ptr + root footer = 5
QCOMPARE(result.meta.size(), 4); QCOMPARE(result.meta.size(), 5);
// No ambient validation markers // No ambient validation markers
QVERIFY(!(result.meta[2].markerMask & (1u << M_ERR))); QVERIFY(!(result.meta[3].markerMask & (1u << M_ERR)));
QVERIFY(!(result.meta[2].markerMask & (1u << M_PTR0))); QVERIFY(!(result.meta[3].markerMask & (1u << M_PTR0)));
QCOMPARE(result.meta[2].depth, 1); QCOMPARE(result.meta[3].depth, 1);
// Line 3 is root footer // Line 4 is root footer
QCOMPARE(result.meta[3].lineKind, LineKind::Footer); QCOMPARE(result.meta[4].lineKind, LineKind::Footer);
} }
void testFoldLevels() { void testFoldLevels() {
@@ -250,13 +253,13 @@ private slots:
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// Child header (depth 1, fold head) — root header no longer emitted // Child header (depth 1, fold head) — root header no longer emitted
QCOMPARE(result.meta[2].foldLevel, 0x401 | 0x2000); QCOMPARE(result.meta[3].foldLevel, 0x401 | 0x2000);
QCOMPARE(result.meta[2].depth, 1); QCOMPARE(result.meta[3].depth, 1);
QVERIFY(result.meta[2].foldHead); QVERIFY(result.meta[3].foldHead);
// Leaf (depth 2, not head) // Leaf (depth 2, not head)
QCOMPARE(result.meta[3].foldLevel, 0x402); QCOMPARE(result.meta[4].foldLevel, 0x402);
QCOMPARE(result.meta[3].depth, 2); QCOMPARE(result.meta[4].depth, 2);
} }
void testNestedStruct() { void testNestedStruct() {
@@ -303,31 +306,31 @@ private slots:
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + flags + Inner header + x + y + Inner footer + root footer = 8 // CommandRow + Blank + CommandRow2 + flags + Inner header + x + y + Inner footer + root footer = 9
QCOMPARE(result.meta.size(), 8); QCOMPARE(result.meta.size(), 9);
// flags field (depth 1) // flags field (depth 1)
QCOMPARE(result.meta[2].lineKind, LineKind::Field); QCOMPARE(result.meta[3].lineKind, LineKind::Field);
QCOMPARE(result.meta[2].depth, 1); QCOMPARE(result.meta[3].depth, 1);
// Inner header (depth 1, fold head) // Inner header (depth 1, fold head)
QCOMPARE(result.meta[3].lineKind, LineKind::Header); QCOMPARE(result.meta[4].lineKind, LineKind::Header);
QCOMPARE(result.meta[3].depth, 1); QCOMPARE(result.meta[4].depth, 1);
QVERIFY(result.meta[3].foldHead); QVERIFY(result.meta[4].foldHead);
QCOMPARE(result.meta[3].foldLevel, 0x401 | 0x2000); QCOMPARE(result.meta[4].foldLevel, 0x401 | 0x2000);
// Inner fields at depth 2 // Inner fields at depth 2
QCOMPARE(result.meta[4].depth, 2);
QCOMPARE(result.meta[4].foldLevel, 0x402);
QCOMPARE(result.meta[5].depth, 2); QCOMPARE(result.meta[5].depth, 2);
QCOMPARE(result.meta[5].foldLevel, 0x402);
QCOMPARE(result.meta[6].depth, 2);
// Inner footer // Inner footer
QCOMPARE(result.meta[6].lineKind, LineKind::Footer); QCOMPARE(result.meta[7].lineKind, LineKind::Footer);
QCOMPARE(result.meta[6].depth, 1); QCOMPARE(result.meta[7].depth, 1);
// Root footer // Root footer
QCOMPARE(result.meta[7].lineKind, LineKind::Footer); QCOMPARE(result.meta[8].lineKind, LineKind::Footer);
QCOMPARE(result.meta[7].depth, 0); QCOMPARE(result.meta[8].depth, 0);
} }
void testPointerDerefExpansion() { void testPointerDerefExpansion() {
@@ -395,28 +398,28 @@ private slots:
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + magic + ptr(merged fold header) + fn1 + fn2 + ptr footer + Main footer = 8 // CommandRow + Blank + CommandRow2 + magic + ptr(merged fold header) + fn1 + fn2 + ptr footer + Main footer = 9
// VTable standalone: header + fn1 + fn2 + footer = 4 // VTable standalone: header + fn1 + fn2 + footer = 4
// Total = 12 // Total = 13
QCOMPARE(result.meta.size(), 12); QCOMPARE(result.meta.size(), 13);
// magic field (depth 1) // magic field (depth 1)
QCOMPARE(result.meta[2].lineKind, LineKind::Field); QCOMPARE(result.meta[3].lineKind, LineKind::Field);
QCOMPARE(result.meta[2].depth, 1);
// Pointer as merged fold header: "ptr64<VTable> ptr {"
QCOMPARE(result.meta[3].lineKind, LineKind::Header);
QCOMPARE(result.meta[3].depth, 1); QCOMPARE(result.meta[3].depth, 1);
QVERIFY(result.meta[3].foldHead);
QCOMPARE(result.meta[3].nodeKind, NodeKind::Pointer64); // Pointer as merged fold header: "VTable* ptr {"
QCOMPARE(result.meta[4].lineKind, LineKind::Header);
QCOMPARE(result.meta[4].depth, 1);
QVERIFY(result.meta[4].foldHead);
QCOMPARE(result.meta[4].nodeKind, NodeKind::Pointer64);
// Expanded fields at depth 2 (struct header merged into pointer) // Expanded fields at depth 2 (struct header merged into pointer)
QCOMPARE(result.meta[4].depth, 2);
QCOMPARE(result.meta[5].depth, 2); QCOMPARE(result.meta[5].depth, 2);
QCOMPARE(result.meta[6].depth, 2);
// Pointer fold footer // Pointer fold footer
QCOMPARE(result.meta[6].lineKind, LineKind::Footer); QCOMPARE(result.meta[7].lineKind, LineKind::Footer);
QCOMPARE(result.meta[6].depth, 1); QCOMPARE(result.meta[7].depth, 1);
} }
void testPointerDerefNull() { void testPointerDerefNull() {
@@ -460,18 +463,22 @@ private slots:
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + ptr(merged fold header) + ptr footer + Main footer = 5 // CommandRow + Blank + CommandRow2 + ptr(merged fold header) + target field + ptr footer + Main footer = 7
// Target standalone: header + field + footer = 3 // Target standalone: header + field + footer = 3
// Total = 8 // Total = 10 (null ptr still shows template preview)
QCOMPARE(result.meta.size(), 8); QCOMPARE(result.meta.size(), 10);
// Pointer as merged fold header (expanded but empty — null ptr) // Pointer as merged fold header (expanded — shows template at offset 0)
QCOMPARE(result.meta[2].lineKind, LineKind::Header); QCOMPARE(result.meta[3].lineKind, LineKind::Header);
QCOMPARE(result.meta[2].depth, 1); QCOMPARE(result.meta[3].depth, 1);
QVERIFY(result.meta[2].foldHead); QVERIFY(result.meta[3].foldHead);
// Pointer fold footer (empty expansion) // Target field shown as template preview
QCOMPARE(result.meta[3].lineKind, LineKind::Footer); QCOMPARE(result.meta[4].lineKind, LineKind::Field);
QCOMPARE(result.meta[4].depth, 2);
// Pointer fold footer
QCOMPARE(result.meta[5].lineKind, LineKind::Footer);
} }
void testPointerDerefCollapsed() { void testPointerDerefCollapsed() {
@@ -518,14 +525,14 @@ private slots:
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + ptr(fold head, collapsed) + Main footer = 4 // CommandRow + Blank + CommandRow2 + ptr(fold head, collapsed) + Main footer = 5
// Target standalone: header + field + footer = 3 // Target standalone: header + field + footer = 3
// Total = 7 // Total = 8
QCOMPARE(result.meta.size(), 7); QCOMPARE(result.meta.size(), 8);
// Pointer is fold head (depth 1) // Pointer is fold head (depth 1)
QVERIFY(result.meta[2].foldHead); QVERIFY(result.meta[3].foldHead);
QCOMPARE(result.meta[2].depth, 1); QCOMPARE(result.meta[3].depth, 1);
} }
void testPointerDerefCycle() { void testPointerDerefCycle() {
@@ -588,12 +595,12 @@ private slots:
QVERIFY(result.meta.size() > 0); QVERIFY(result.meta.size() > 0);
QVERIFY(result.meta.size() < 100); // sanity: bounded output QVERIFY(result.meta.size() < 100); // sanity: bounded output
// CommandRow + CommandRow2 + ptr merged header + data + self merged header // CommandRow + Blank + CommandRow2 + ptr merged header + data + self merged header
// Second expansion blocked by cycle guard: no children under self // Second expansion blocked by cycle guard: no children under self
// Then: self footer + ptr footer + Main footer + standalone Recursive rendering // Then: self footer + ptr footer + Main footer + standalone Recursive rendering
QVERIFY(result.meta[2].foldHead); // ptr merged fold head QVERIFY(result.meta[3].foldHead); // ptr merged fold head
QCOMPARE(result.meta[2].lineKind, LineKind::Header); // ptr merged header QCOMPARE(result.meta[3].lineKind, LineKind::Header); // ptr merged header
QCOMPARE(result.meta[3].lineKind, LineKind::Field); // data field (first child of Recursive) QCOMPARE(result.meta[4].lineKind, LineKind::Field); // data field (first child of Recursive)
} }
void testStructFooterSimple() { void testStructFooterSimple() {
@@ -657,12 +664,16 @@ private slots:
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
for (int i = 0; i < result.meta.size(); i++) { for (int i = 0; i < result.meta.size(); i++) {
// Skip CommandRow / CommandRow2 (synthetic lines with sentinel nodeId) // Skip CommandRow / Blank / CommandRow2 (synthetic lines with sentinel nodeId)
if (result.meta[i].lineKind == LineKind::CommandRow) { if (result.meta[i].lineKind == LineKind::CommandRow) {
QCOMPARE(result.meta[i].nodeId, kCommandRowId); QCOMPARE(result.meta[i].nodeId, kCommandRowId);
QCOMPARE(result.meta[i].nodeIdx, -1); QCOMPARE(result.meta[i].nodeIdx, -1);
continue; continue;
} }
if (result.meta[i].lineKind == LineKind::Blank) {
QCOMPARE(result.meta[i].nodeIdx, -1);
continue;
}
if (result.meta[i].lineKind == LineKind::CommandRow2) { if (result.meta[i].lineKind == LineKind::CommandRow2) {
QCOMPARE(result.meta[i].nodeId, kCommandRow2Id); QCOMPARE(result.meta[i].nodeId, kCommandRow2Id);
QCOMPARE(result.meta[i].nodeIdx, -1); QCOMPARE(result.meta[i].nodeIdx, -1);
@@ -944,16 +955,16 @@ private slots:
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + Array header(collapsed) + root footer = 4 // CommandRow + Blank + CommandRow2 + Array header(collapsed) + root footer = 5
QCOMPARE(result.meta.size(), 4); QCOMPARE(result.meta.size(), 5);
// Array header is collapsed (at index 2) // Array header is collapsed (at index 3)
int arrLine = -1; int arrLine = -1;
for (int i = 0; i < result.meta.size(); i++) { for (int i = 0; i < result.meta.size(); i++) {
if (result.meta[i].isArrayHeader) { arrLine = i; break; } if (result.meta[i].isArrayHeader) { arrLine = i; break; }
} }
QVERIFY(arrLine >= 0); QVERIFY(arrLine >= 0);
QCOMPARE(arrLine, 2); QCOMPARE(arrLine, 3);
QVERIFY(result.meta[arrLine].foldCollapsed); QVERIFY(result.meta[arrLine].foldCollapsed);
// Header text should NOT contain "{" // Header text should NOT contain "{"
@@ -1024,7 +1035,7 @@ private slots:
// ═════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════
void testPointerDefaultVoid() { void testPointerDefaultVoid() {
// Pointer64 with no refId should display as "ptr64<void>" // Pointer64 with no refId should display as "void*"
NodeTree tree; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -1059,8 +1070,8 @@ private slots:
QStringList lines = result.text.split('\n'); QStringList lines = result.text.split('\n');
QString text = lines[ptrLine]; QString text = lines[ptrLine];
QVERIFY2(text.contains("ptr64<void>"), QVERIFY2(text.contains("void*"),
qPrintable("Pointer with no refId should show 'ptr64<void>': " + text)); qPrintable("Pointer with no refId should show 'void*': " + text));
// pointerTargetName should be empty (void) // pointerTargetName should be empty (void)
QVERIFY(result.meta[ptrLine].pointerTargetName.isEmpty()); QVERIFY(result.meta[ptrLine].pointerTargetName.isEmpty());
@@ -1094,13 +1105,13 @@ private slots:
QStringList lines = result.text.split('\n'); QStringList lines = result.text.split('\n');
bool foundPtr32 = false; bool foundPtr32 = false;
for (const QString& l : lines) { for (const QString& l : lines) {
if (l.contains("ptr32<void>")) { foundPtr32 = true; break; } if (l.contains("void*")) { foundPtr32 = true; break; }
} }
QVERIFY2(foundPtr32, "Pointer32 with no refId should show 'ptr32<void>'"); QVERIFY2(foundPtr32, "Pointer32 with no refId should show 'void*'");
} }
void testPointerDisplaysTargetName() { void testPointerDisplaysTargetName() {
// Pointer64 with refId displays "ptr64<TargetName>" // Pointer64 with refId displays "TargetName*"
NodeTree tree; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -1153,8 +1164,8 @@ private slots:
QVERIFY(ptrLine >= 0); QVERIFY(ptrLine >= 0);
QStringList lines = result.text.split('\n'); QStringList lines = result.text.split('\n');
QVERIFY2(lines[ptrLine].contains("ptr64<PlayerData>"), QVERIFY2(lines[ptrLine].contains("PlayerData*"),
qPrintable("Should show 'ptr64<PlayerData>': " + lines[ptrLine])); qPrintable("Should show 'PlayerData*': " + lines[ptrLine]));
// pointerTargetName metadata // pointerTargetName metadata
QCOMPARE(result.meta[ptrLine].pointerTargetName, QString("PlayerData")); QCOMPARE(result.meta[ptrLine].pointerTargetName, QString("PlayerData"));
@@ -1200,7 +1211,7 @@ private slots:
QStringList lines = result.text.split('\n'); QStringList lines = result.text.split('\n');
bool found = false; bool found = false;
for (const QString& l : lines) { for (const QString& l : lines) {
if (l.contains("ptr64<MyStruct>")) { found = true; break; } if (l.contains("MyStruct*")) { found = true; break; }
} }
QVERIFY2(found, "Should use struct name when structTypeName is empty"); QVERIFY2(found, "Should use struct name when structTypeName is empty");
} }
@@ -1252,22 +1263,19 @@ private slots:
QString lineText = lines[ptrLine]; QString lineText = lines[ptrLine];
const LineMeta& lm = result.meta[ptrLine]; const LineMeta& lm = result.meta[ptrLine];
// Kind span: covers "ptr64" // Kind span: no longer applicable in "Type*" format
ColumnSpan kindSpan = pointerKindSpanFor(lm, lineText); ColumnSpan kindSpan = pointerKindSpanFor(lm, lineText);
QVERIFY2(kindSpan.valid, "pointerKindSpanFor must return valid span"); QVERIFY2(!kindSpan.valid, "pointerKindSpanFor should return invalid in Type* format");
QString kindText = lineText.mid(kindSpan.start, kindSpan.end - kindSpan.start);
QVERIFY2(kindText.contains("ptr64"),
qPrintable("Kind span should cover 'ptr64', got: '" + kindText + "'"));
// Target span: covers "VTable" // Target span: covers "VTable" (before the '*')
ColumnSpan targetSpan = pointerTargetSpanFor(lm, lineText); ColumnSpan targetSpan = pointerTargetSpanFor(lm, lineText);
QVERIFY2(targetSpan.valid, "pointerTargetSpanFor must return valid span"); QVERIFY2(targetSpan.valid, "pointerTargetSpanFor must return valid span");
QString targetText = lineText.mid(targetSpan.start, targetSpan.end - targetSpan.start); QString targetText = lineText.mid(targetSpan.start, targetSpan.end - targetSpan.start).trimmed();
QCOMPARE(targetText, QString("VTable")); QCOMPARE(targetText, QString("VTable"));
} }
void testPointerVoidSpans() { void testPointerVoidSpans() {
// Even void* pointer should have valid kind and target spans // void* pointer should have valid target span but no kind span
NodeTree tree; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -1302,19 +1310,19 @@ private slots:
QString lineText = lines[ptrLine]; QString lineText = lines[ptrLine];
const LineMeta& lm = result.meta[ptrLine]; const LineMeta& lm = result.meta[ptrLine];
// Kind span: "ptr64" // Kind span: no longer applicable in "Type*" format
ColumnSpan kindSpan = pointerKindSpanFor(lm, lineText); ColumnSpan kindSpan = pointerKindSpanFor(lm, lineText);
QVERIFY2(kindSpan.valid, "void* pointer should have valid kind span"); QVERIFY2(!kindSpan.valid, "Kind span should be invalid in Type* format");
// Target span: "void" // Target span: "void" (before the '*')
ColumnSpan targetSpan = pointerTargetSpanFor(lm, lineText); ColumnSpan targetSpan = pointerTargetSpanFor(lm, lineText);
QVERIFY2(targetSpan.valid, "void* pointer should have valid target span"); QVERIFY2(targetSpan.valid, "void* pointer should have valid target span");
QString targetText = lineText.mid(targetSpan.start, targetSpan.end - targetSpan.start); QString targetText = lineText.mid(targetSpan.start, targetSpan.end - targetSpan.start).trimmed();
QCOMPARE(targetText, QString("void")); QCOMPARE(targetText, QString("void"));
} }
void testPointerToPointerChain() { void testPointerToPointerChain() {
// ptr64<StructB> → StructB { ptr64<StructC> } → StructC { field } // StructB* → StructB { StructC* } → StructC { field }
NodeTree tree; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -1390,18 +1398,18 @@ private slots:
QVERIFY(result.meta.size() > 0); QVERIFY(result.meta.size() > 0);
QVERIFY(result.meta.size() < 200); QVERIFY(result.meta.size() < 200);
// Check that ptr64<Wrapper> and ptr64<InnerData> both appear in text // Check that Wrapper* and InnerData* both appear in text
bool foundWrapper = false, foundInner = false; bool foundWrapper = false, foundInner = false;
QStringList lines = result.text.split('\n'); QStringList lines = result.text.split('\n');
for (const QString& l : lines) { for (const QString& l : lines) {
if (l.contains("ptr64<Wrapper>")) foundWrapper = true; if (l.contains("Wrapper*")) foundWrapper = true;
if (l.contains("ptr64<InnerData>")) foundInner = true; if (l.contains("InnerData*")) foundInner = true;
} }
QVERIFY2(foundWrapper, "Should display 'ptr64<Wrapper>'"); QVERIFY2(foundWrapper, "Should display 'Wrapper*'");
QVERIFY2(foundInner, "Should display 'ptr64<InnerData>'"); QVERIFY2(foundInner, "Should display 'InnerData*'");
// The chain: Root → ptr64<Wrapper>(fold head) → Wrapper expanded → // The chain: Root → Wrapper*(fold head) → Wrapper expanded →
// ptr64<InnerData>(fold head) → InnerData expanded // InnerData*(fold head) → InnerData expanded
int foldHeadCount = 0; int foldHeadCount = 0;
for (const LineMeta& lm : result.meta) { for (const LineMeta& lm : result.meta) {
if (lm.foldHead && lm.nodeKind == NodeKind::Pointer64) if (lm.foldHead && lm.nodeKind == NodeKind::Pointer64)
@@ -1490,15 +1498,15 @@ private slots:
qPrintable(QString("Cycle should be bounded, got %1 lines") qPrintable(QString("Cycle should be bounded, got %1 lines")
.arg(result.meta.size()))); .arg(result.meta.size())));
// Both ptr64<StructB> and ptr64<Main> should appear // Both StructB* and Main* should appear
bool foundToB = false, foundToMain = false; bool foundToB = false, foundToMain = false;
QStringList lines = result.text.split('\n'); QStringList lines = result.text.split('\n');
for (const QString& l : lines) { for (const QString& l : lines) {
if (l.contains("ptr64<StructB>")) foundToB = true; if (l.contains("StructB*")) foundToB = true;
if (l.contains("ptr64<Main>")) foundToMain = true; if (l.contains("Main*")) foundToMain = true;
} }
QVERIFY2(foundToB, "Should display 'ptr64<StructB>'"); QVERIFY2(foundToB, "Should display 'StructB*'");
QVERIFY2(foundToMain, "Should display 'ptr64<Main>'"); QVERIFY2(foundToMain, "Should display 'Main*'");
// The first expansion of each pointer works; // The first expansion of each pointer works;
// the cycle is caught on the second attempt. // the cycle is caught on the second attempt.
@@ -1555,10 +1563,10 @@ private slots:
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// Every struct name should appear in a "ptr64<Name>" format // Every struct name should appear in a "Name*" format
QStringList lines = result.text.split('\n'); QStringList lines = result.text.split('\n');
for (const QString& sname : structNames) { for (const QString& sname : structNames) {
QString expected = QString("ptr64<%1>").arg(sname); QString expected = QString("%1*").arg(sname);
bool found = false; bool found = false;
for (const QString& l : lines) { for (const QString& l : lines) {
if (l.contains(expected)) { found = true; break; } if (l.contains(expected)) { found = true; break; }
@@ -1594,9 +1602,9 @@ private slots:
QStringList lines = result.text.split('\n'); QStringList lines = result.text.split('\n');
bool foundVoid = false; bool foundVoid = false;
for (const QString& l : lines) { for (const QString& l : lines) {
if (l.contains("ptr64<void>")) { foundVoid = true; break; } if (l.contains("void*")) { foundVoid = true; break; }
} }
QVERIFY2(foundVoid, "Dangling refId should degrade to ptr64<void>"); QVERIFY2(foundVoid, "Dangling refId should degrade to void*");
} }
void testPointerCollapsedNoExpansion() { void testPointerCollapsedNoExpansion() {
@@ -1662,7 +1670,7 @@ private slots:
} }
void testPointerWidthComputation() { void testPointerWidthComputation() {
// Type column must be wide enough for "ptr64<LongStructName>" // Type column must be wide enough for "LongStructName*"
NodeTree tree; NodeTree tree;
tree.baseAddress = 0; tree.baseAddress = 0;
@@ -1698,7 +1706,7 @@ private slots:
QStringList lines = result.text.split('\n'); QStringList lines = result.text.split('\n');
bool foundFull = false; bool foundFull = false;
for (const QString& l : lines) { for (const QString& l : lines) {
if (l.contains("ptr64<VeryLongStructNameForTesting>")) { if (l.contains("VeryLongStructNameForTesting*")) {
foundFull = true; foundFull = true;
break; break;
} }
@@ -1707,9 +1715,9 @@ private slots:
"Type column should be wide enough for long pointer target names"); "Type column should be wide enough for long pointer target names");
// Layout type width should accommodate the long name // Layout type width should accommodate the long name
// "ptr64<VeryLongStructNameForTesting>" = 35 chars // "VeryLongStructNameForTesting*" = 29 chars
QVERIFY2(result.layout.typeW >= 35, QVERIFY2(result.layout.typeW >= 29,
qPrintable(QString("typeW=%1, should be >= 35").arg(result.layout.typeW))); qPrintable(QString("typeW=%1, should be >= 29").arg(result.layout.typeW)));
} }
// ═════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════
@@ -1820,7 +1828,7 @@ private slots:
void testCommandRow2NameSpan() { void testCommandRow2NameSpan() {
// Name span should cover the class name // Name span should cover the class name
QString text = "struct MyClass"; QString text = "struct\u25BE MyClass";
ColumnSpan nameSpan = commandRow2NameSpan(text); ColumnSpan nameSpan = commandRow2NameSpan(text);
QVERIFY(nameSpan.valid); QVERIFY(nameSpan.valid);
@@ -1873,10 +1881,11 @@ private slots:
QVERIFY2(result.meta.size() >= 5, QVERIFY2(result.meta.size() >= 5,
qPrintable(QString("Expected >= 5 lines, got %1").arg(result.meta.size()))); qPrintable(QString("Expected >= 5 lines, got %1").arg(result.meta.size())));
// Every line should have text content // Every non-blank line should have text content
QStringList lines = result.text.split('\n'); QStringList lines = result.text.split('\n');
QCOMPARE(lines.size(), result.meta.size()); QCOMPARE(lines.size(), result.meta.size());
for (int i = 0; i < lines.size(); i++) { for (int i = 0; i < lines.size(); i++) {
if (result.meta[i].lineKind == LineKind::Blank) continue;
QVERIFY2(!lines[i].isEmpty(), QVERIFY2(!lines[i].isEmpty(),
qPrintable(QString("Line %1 is empty").arg(i))); qPrintable(QString("Line %1 is empty").arg(i)));
} }

View File

@@ -376,7 +376,7 @@ private slots:
// Set CommandRow text with an ADDR value (simulates controller.updateCommandRow) // Set CommandRow text with an ADDR value (simulates controller.updateCommandRow)
m_editor->setCommandRowText( m_editor->setCommandRowText(
QStringLiteral(" File Address: 0xD87B5E5000")); QStringLiteral("source\u25BE \u203A 0xD87B5E5000"));
// BaseAddress should be ALLOWED on CommandRow (ADDR field) // BaseAddress should be ALLOWED on CommandRow (ADDR field)
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
@@ -394,14 +394,14 @@ private slots:
// ── Test: inline edit lifecycle (begin → commit → re-edit) ── // ── Test: inline edit lifecycle (begin → commit → re-edit) ──
void testInlineEditReEntry() { void testInlineEditReEntry() {
// Move cursor to line 2 (first field; line 0=CommandRow, 1=CommandRow2, root header suppressed) // Move cursor to line 3 (first field; 0=CommandRow, 1=Blank, 2=CommandRow2, root header suppressed)
m_editor->scintilla()->setCursorPosition(2, 0); m_editor->scintilla()->setCursorPosition(kFirstDataLine, 0);
// Should not be editing // Should not be editing
QVERIFY(!m_editor->isEditing()); QVERIFY(!m_editor->isEditing());
// Begin edit on Name column // Begin edit on Name column
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2); bool ok = m_editor->beginInlineEdit(EditTarget::Name, kFirstDataLine);
QVERIFY(ok); QVERIFY(ok);
QVERIFY(m_editor->isEditing()); QVERIFY(m_editor->isEditing());
@@ -413,7 +413,7 @@ private slots:
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Should be able to edit again // Should be able to edit again
ok = m_editor->beginInlineEdit(EditTarget::Name, 2); ok = m_editor->beginInlineEdit(EditTarget::Name, kFirstDataLine);
QVERIFY(ok); QVERIFY(ok);
QVERIFY(m_editor->isEditing()); QVERIFY(m_editor->isEditing());
@@ -425,10 +425,10 @@ private slots:
// ── Test: commit inline edit then re-edit same line ── // ── Test: commit inline edit then re-edit same line ──
void testCommitThenReEdit() { void testCommitThenReEdit() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
m_editor->scintilla()->setCursorPosition(2, 0); m_editor->scintilla()->setCursorPosition(kFirstDataLine, 0);
// Begin value edit // Begin value edit
bool ok = m_editor->beginInlineEdit(EditTarget::Value, 2); bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine);
QVERIFY(ok); QVERIFY(ok);
QVERIFY(m_editor->isEditing()); QVERIFY(m_editor->isEditing());
@@ -445,7 +445,7 @@ private slots:
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Must be able to edit the same line again // Must be able to edit the same line again
ok = m_editor->beginInlineEdit(EditTarget::Value, 2); ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine);
QVERIFY(ok); QVERIFY(ok);
QVERIFY(m_editor->isEditing()); QVERIFY(m_editor->isEditing());
@@ -456,7 +456,7 @@ private slots:
void testMouseClickCommitsEdit() { void testMouseClickCommitsEdit() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2); bool ok = m_editor->beginInlineEdit(EditTarget::Name, kFirstDataLine);
QVERIFY(ok); QVERIFY(ok);
QVERIFY(m_editor->isEditing()); QVERIFY(m_editor->isEditing());
@@ -478,7 +478,7 @@ private slots:
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Begin type edit on a field line // Begin type edit on a field line
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2); bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine);
QVERIFY(ok); QVERIFY(ok);
QVERIFY(m_editor->isEditing()); QVERIFY(m_editor->isEditing());
@@ -598,7 +598,7 @@ private slots:
void testTypeAutocompleteTypingAndCommit() { void testTypeAutocompleteTypingAndCommit() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2); bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine);
QVERIFY(ok); QVERIFY(ok);
// Autocomplete is deferred via QTimer::singleShot(0) — poll until active // Autocomplete is deferred via QTimer::singleShot(0) — poll until active
@@ -635,7 +635,7 @@ private slots:
void testTypeEditClickAwayNoChange() { void testTypeEditClickAwayNoChange() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2); bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine);
QVERIFY(ok); QVERIFY(ok);
// Process deferred autocomplete // Process deferred autocomplete
@@ -652,7 +652,7 @@ private slots:
QCOMPARE(commitSpy.count(), 1); QCOMPARE(commitSpy.count(), 1);
// The committed text should be the original typeName (no change) // The committed text should be the original typeName (no change)
// First field at line 2 is InheritedAddressSpace (UInt8 → "uint8_t") // First field at kFirstDataLine is InheritedAddressSpace (UInt8 → "uint8_t")
QList<QVariant> args = commitSpy.first(); QList<QVariant> args = commitSpy.first();
QString committedText = args.at(3).toString(); QString committedText = args.at(3).toString();
QVERIFY2(committedText == "uint8_t", QVERIFY2(committedText == "uint8_t",
@@ -665,8 +665,8 @@ private slots:
void testColumnSpanHitTest() { void testColumnSpanHitTest() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Line 2 is a field line (UInt8), verify spans are valid (line 0=CommandRow, 1=CommandRow2) // kFirstDataLine is a field line (UInt8), verify spans are valid
const LineMeta* lm = m_editor->metaForLine(2); const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
QVERIFY(lm); QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::Field); QCOMPARE(lm->lineKind, LineKind::Field);
@@ -683,7 +683,7 @@ private slots:
// Value span should be valid for field lines // Value span should be valid for field lines
QString lineText; QString lineText;
int len = (int)m_editor->scintilla()->SendScintilla( int len = (int)m_editor->scintilla()->SendScintilla(
QsciScintillaBase::SCI_LINELENGTH, (unsigned long)2); QsciScintillaBase::SCI_LINELENGTH, (unsigned long)kFirstDataLine);
QVERIFY(len > 0); QVERIFY(len > 0);
ColumnSpan vs = RcxEditor::valueSpan(*lm, len); ColumnSpan vs = RcxEditor::valueSpan(*lm, len);
QVERIFY(vs.valid); QVERIFY(vs.valid);
@@ -712,13 +712,13 @@ private slots:
void testSelectedNodeIndices() { void testSelectedNodeIndices() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Put cursor on first field line (line 2; 0=CommandRow, 1=CommandRow2, root header suppressed) // Put cursor on first field line (kFirstDataLine; 0=CommandRow, 1=Blank, 2=CommandRow2)
m_editor->scintilla()->setCursorPosition(2, 0); m_editor->scintilla()->setCursorPosition(kFirstDataLine, 0);
QSet<int> sel = m_editor->selectedNodeIndices(); QSet<int> sel = m_editor->selectedNodeIndices();
QCOMPARE(sel.size(), 1); QCOMPARE(sel.size(), 1);
// The node index should match the first field // The node index should match the first field
const LineMeta* lm = m_editor->metaForLine(2); const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
QVERIFY(lm); QVERIFY(lm);
QVERIFY(sel.contains(lm->nodeIdx)); QVERIFY(sel.contains(lm->nodeIdx));
} }
@@ -736,8 +736,8 @@ private slots:
QVERIFY2(!result.text.contains("// base:"), QVERIFY2(!result.text.contains("// base:"),
"Composed text should not contain '// base:' (consolidated into cmd bar)"); "Composed text should not contain '// base:' (consolidated into cmd bar)");
// Line 2 should be the first field (root header suppressed) // kFirstDataLine should be the first field (root header suppressed)
const LineMeta* lm = m_editor->metaForLine(2); const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
QVERIFY(lm); QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::Field); QCOMPARE(lm->lineKind, LineKind::Field);
@@ -750,7 +750,7 @@ private slots:
// Set CommandRow text with ADDR value (simulates controller) // Set CommandRow text with ADDR value (simulates controller)
m_editor->setCommandRowText( m_editor->setCommandRowText(
QStringLiteral(" File Address: 0xD87B5E5000")); QStringLiteral("source\u25BE \u203A 0xD87B5E5000"));
// Line 0 is CommandRow // Line 0 is CommandRow
const LineMeta* lm = m_editor->metaForLine(0); const LineMeta* lm = m_editor->metaForLine(0);
@@ -852,14 +852,14 @@ private slots:
void testValueEditCommitUpdatesSignal() { void testValueEditCommitUpdatesSignal() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Line 2 = first UInt8 field (InheritedAddressSpace, root header suppressed) // kFirstDataLine = first UInt8 field (InheritedAddressSpace, root header suppressed)
const LineMeta* lm = m_editor->metaForLine(2); const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
QVERIFY(lm); QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::Field); QCOMPARE(lm->lineKind, LineKind::Field);
QVERIFY(lm->nodeKind != NodeKind::Padding); QVERIFY(lm->nodeKind != NodeKind::Padding);
// Begin value edit // Begin value edit
bool ok = m_editor->beginInlineEdit(EditTarget::Value, 2); bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine);
QVERIFY(ok); QVERIFY(ok);
QVERIFY(m_editor->isEditing()); QVERIFY(m_editor->isEditing());
@@ -902,7 +902,7 @@ private slots:
// Set CommandRow text with ADDR value (simulates controller) // Set CommandRow text with ADDR value (simulates controller)
m_editor->setCommandRowText( m_editor->setCommandRowText(
QStringLiteral(" File Address: 0xD87B5E5000")); QStringLiteral("source\u25BE \u203A 0xD87B5E5000"));
// Begin base address edit on line 0 (CommandRow ADDR field) // Begin base address edit on line 0 (CommandRow ADDR field)
bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0);
@@ -919,7 +919,7 @@ private slots:
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Click on a field line at the indent area (col 0 — not over editable text) // Click on a field line at the indent area (col 0 — not over editable text)
QPoint clickPos = colToViewport(m_editor->scintilla(), 2, 0); QPoint clickPos = colToViewport(m_editor->scintilla(), kFirstDataLine, 0);
sendLeftClick(m_editor->scintilla()->viewport(), clickPos); sendLeftClick(m_editor->scintilla()->viewport(), clickPos);
QApplication::processEvents(); QApplication::processEvents();
@@ -932,8 +932,8 @@ private slots:
void testCursorShapeOverText() { void testCursorShapeOverText() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Line 2 is a field (UInt8 InheritedAddressSpace) // kFirstDataLine is a field (UInt8 InheritedAddressSpace)
const LineMeta* lm = m_editor->metaForLine(2); const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
QVERIFY(lm); QVERIFY(lm);
// Get the name span (padded to kColName width) // Get the name span (padded to kColName width)
@@ -941,14 +941,14 @@ private slots:
QVERIFY(ns.valid); QVERIFY(ns.valid);
// Move mouse to the start of the name span (should be over text) // Move mouse to the start of the name span (should be over text)
QPoint textPos = colToViewport(m_editor->scintilla(), 2, ns.start + 1); QPoint textPos = colToViewport(m_editor->scintilla(), kFirstDataLine, ns.start + 1);
sendMouseMove(m_editor->scintilla()->viewport(), textPos); sendMouseMove(m_editor->scintilla()->viewport(), textPos);
QApplication::processEvents(); QApplication::processEvents();
QCOMPARE(viewportCursor(m_editor), Qt::IBeamCursor); QCOMPARE(viewportCursor(m_editor), Qt::IBeamCursor);
// Move mouse to far padding area (past end of text, within padded span) // Move mouse to far padding area (past end of text, within padded span)
// The padded span ends at ns.end but the trimmed text is shorter // The padded span ends at ns.end but the trimmed text is shorter
QPoint padPos = colToViewport(m_editor->scintilla(), 2, ns.end - 1); QPoint padPos = colToViewport(m_editor->scintilla(), kFirstDataLine, ns.end - 1);
sendMouseMove(m_editor->scintilla()->viewport(), padPos); sendMouseMove(m_editor->scintilla()->viewport(), padPos);
QApplication::processEvents(); QApplication::processEvents();
// Should be Arrow (padding whitespace, not actual text) // Should be Arrow (padding whitespace, not actual text)
@@ -959,7 +959,7 @@ private slots:
void testCursorShapeOverType() { void testCursorShapeOverType() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
const LineMeta* lm = m_editor->metaForLine(2); const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
QVERIFY(lm); QVERIFY(lm);
// Type span starts after the fold column + indent // Type span starts after the fold column + indent
@@ -967,7 +967,7 @@ private slots:
QVERIFY(ts.valid); QVERIFY(ts.valid);
// Move to start of type text (e.g. "uint8_t") // Move to start of type text (e.g. "uint8_t")
QPoint typePos = colToViewport(m_editor->scintilla(), 2, ts.start + 1); QPoint typePos = colToViewport(m_editor->scintilla(), kFirstDataLine, ts.start + 1);
sendMouseMove(m_editor->scintilla()->viewport(), typePos); sendMouseMove(m_editor->scintilla()->viewport(), typePos);
QApplication::processEvents(); QApplication::processEvents();
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor); QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
@@ -1013,18 +1013,18 @@ private slots:
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Click on a field to select the node // Click on a field to select the node
const LineMeta* lm = m_editor->metaForLine(2); const LineMeta* lm = m_editor->metaForLine(kFirstDataLine);
QVERIFY(lm); QVERIFY(lm);
ColumnSpan ns = RcxEditor::nameSpan(*lm, lm->effectiveTypeW, lm->effectiveNameW); ColumnSpan ns = RcxEditor::nameSpan(*lm, lm->effectiveTypeW, lm->effectiveNameW);
QVERIFY(ns.valid); QVERIFY(ns.valid);
// Click in the name area (selects the node) // Click in the name area (selects the node)
QPoint clickPos = colToViewport(m_editor->scintilla(), 2, ns.start + 1); QPoint clickPos = colToViewport(m_editor->scintilla(), kFirstDataLine, ns.start + 1);
sendLeftClick(m_editor->scintilla()->viewport(), clickPos); sendLeftClick(m_editor->scintilla()->viewport(), clickPos);
QApplication::processEvents(); QApplication::processEvents();
// Now move mouse to col 0 (indent area — non-editable) // Now move mouse to col 0 (indent area — non-editable)
QPoint emptyPos = colToViewport(m_editor->scintilla(), 2, 0); QPoint emptyPos = colToViewport(m_editor->scintilla(), kFirstDataLine, 0);
sendMouseMove(m_editor->scintilla()->viewport(), emptyPos); sendMouseMove(m_editor->scintilla()->viewport(), emptyPos);
QApplication::processEvents(); QApplication::processEvents();
@@ -1033,26 +1033,26 @@ private slots:
QVERIFY(!m_editor->isEditing()); QVERIFY(!m_editor->isEditing());
} }
// ── Test: CommandRow2 exists at line 1 ── // ── Test: CommandRow2 exists at kCommandRow2Line ──
void testCommandRow2Exists() { void testCommandRow2Exists() {
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Line 1 should be CommandRow2 // kCommandRow2Line should be CommandRow2
const LineMeta* lm = m_editor->metaForLine(1); const LineMeta* lm = m_editor->metaForLine(kCommandRow2Line);
QVERIFY(lm); QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::CommandRow2); QCOMPARE(lm->lineKind, LineKind::CommandRow2);
QCOMPARE(lm->nodeId, kCommandRow2Id); QCOMPARE(lm->nodeId, kCommandRow2Id);
QCOMPARE(lm->nodeIdx, -1); QCOMPARE(lm->nodeIdx, -1);
// Type/Name/Value should be rejected on CommandRow2 // Type/Name/Value should be rejected on CommandRow2
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Type, 1)); QVERIFY(!m_editor->beginInlineEdit(EditTarget::Type, kCommandRow2Line));
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Name, 1)); QVERIFY(!m_editor->beginInlineEdit(EditTarget::Name, kCommandRow2Line));
QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, 1)); QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, kCommandRow2Line));
QVERIFY(!m_editor->isEditing()); QVERIFY(!m_editor->isEditing());
// RootClassName should be allowed on CommandRow2 // RootClassName should be allowed on CommandRow2
m_editor->setCommandRow2Text(QStringLiteral("struct _PEB64")); m_editor->setCommandRow2Text(QStringLiteral("struct\u25BE _PEB64"));
bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 1); bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, kCommandRow2Line);
QVERIFY2(ok, "RootClassName edit should be allowed on CommandRow2"); QVERIFY2(ok, "RootClassName edit should be allowed on CommandRow2");
QVERIFY(m_editor->isEditing()); QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit(); m_editor->cancelInlineEdit();
@@ -1063,15 +1063,15 @@ private slots:
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Set CommandRow2 without alignas // Set CommandRow2 without alignas
m_editor->setCommandRow2Text(QStringLiteral("struct _PEB64")); m_editor->setCommandRow2Text(QStringLiteral("struct\u25BE _PEB64"));
// Line 1 is CommandRow2 // kCommandRow2Line is CommandRow2
const LineMeta* lm = m_editor->metaForLine(1); const LineMeta* lm = m_editor->metaForLine(kCommandRow2Line);
QVERIFY(lm); QVERIFY(lm);
QCOMPARE(lm->lineKind, LineKind::CommandRow2); QCOMPARE(lm->lineKind, LineKind::CommandRow2);
// RootClassName should work // RootClassName should work
QVERIFY(m_editor->beginInlineEdit(EditTarget::RootClassName, 1)); QVERIFY(m_editor->beginInlineEdit(EditTarget::RootClassName, kCommandRow2Line));
QVERIFY(m_editor->isEditing()); QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit(); m_editor->cancelInlineEdit();
@@ -1083,15 +1083,15 @@ private slots:
m_editor->applyDocument(m_result); m_editor->applyDocument(m_result);
// Root struct header is completely suppressed from output. // Root struct header is completely suppressed from output.
// Line 0 = CommandRow, Line 1 = CommandRow2, Line 2 = first field. // Line 0 = CommandRow, Line 1 = Blank, Line 2 = CommandRow2, Line 3 = first field.
const LineMeta* lm2 = m_editor->metaForLine(2); const LineMeta* lm2 = m_editor->metaForLine(kFirstDataLine);
QVERIFY(lm2); QVERIFY(lm2);
QCOMPARE(lm2->lineKind, LineKind::Field); QCOMPARE(lm2->lineKind, LineKind::Field);
// Verify no root header exists anywhere in the output // Verify no root header line exists in the output (footer may have isRootHeader for flush-left)
bool foundRootHeader = false; bool foundRootHeader = false;
for (int i = 0; i < m_result.meta.size(); i++) { for (int i = 0; i < m_result.meta.size(); i++) {
if (m_result.meta[i].isRootHeader) { if (m_result.meta[i].isRootHeader && m_result.meta[i].lineKind == LineKind::Header) {
foundRootHeader = true; foundRootHeader = true;
break; break;
} }

View File

@@ -185,7 +185,7 @@ private slots:
QCOMPARE(name, QString("float")); QCOMPARE(name, QString("float"));
name = doc.resolveTypeName(NodeKind::Hex64); name = doc.resolveTypeName(NodeKind::Hex64);
QCOMPARE(name, QString("Hex64")); QCOMPARE(name, QString("hex64"));
} }
void testResolveTypeName_withAlias() { void testResolveTypeName_withAlias() {
@@ -441,10 +441,11 @@ private slots:
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov, 99999); ComposeResult result = compose(tree, prov, 99999);
// Only command rows // Only command rows + blank
QCOMPARE(result.meta.size(), 2); QCOMPARE(result.meta.size(), 3);
QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow); QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow);
QCOMPARE(result.meta[1].lineKind, LineKind::CommandRow2); QCOMPARE(result.meta[1].lineKind, LineKind::Blank);
QCOMPARE(result.meta[2].lineKind, LineKind::CommandRow2);
} }
void testCompose_viewRootId_singleRoot() { void testCompose_viewRootId_singleRoot() {
@@ -881,23 +882,23 @@ private slots:
NullProvider prov; NullProvider prov;
ComposeResult result = compose(tree, prov); ComposeResult result = compose(tree, prov);
// CommandRow + CommandRow2 + 1 Vec4 line + footer = 4 // CommandRow + Blank + CommandRow2 + 1 Vec4 line + footer = 5
QCOMPARE(result.meta.size(), 4); QCOMPARE(result.meta.size(), 5);
// The Vec4 line (index 2) is a single field line, not continuation // The Vec4 line (index 3) is a single field line, not continuation
QCOMPARE(result.meta[2].lineKind, LineKind::Field); QCOMPARE(result.meta[3].lineKind, LineKind::Field);
QCOMPARE(result.meta[2].nodeKind, NodeKind::Vec4); QCOMPARE(result.meta[3].nodeKind, NodeKind::Vec4);
QVERIFY(!result.meta[2].isContinuation); QVERIFY(!result.meta[3].isContinuation);
// Copy text (equivalent to editor's "Copy All as Text") // Copy text (equivalent to editor's "Copy All as Text")
QString text = result.text; QString text = result.text;
// NullProvider reads 0 for all floats, so values are "0, 0, 0, 0" // NullProvider reads 0 for all floats, values are "0.f, 0.f, 0.f, 0.f"
QVERIFY(text.contains("0, 0, 0, 0")); QVERIFY(text.contains("0.f, 0.f, 0.f, 0.f"));
// Confirm type, name, and values all on the same line // Confirm type, name, and values all on the same line
QStringList lines = text.split('\n'); QStringList lines = text.split('\n');
QVERIFY(lines[2].contains("Vec4")); QVERIFY(lines[3].contains("vec4"));
QVERIFY(lines[2].contains("position")); QVERIFY(lines[3].contains("position"));
QVERIFY(lines[2].contains("0, 0, 0, 0")); QVERIFY(lines[3].contains("0.f, 0.f, 0.f, 0.f"));
} }
}; };