From 6a9641edc5bbd1223c7be35326ba58b183695794 Mon Sep 17 00:00:00 2001 From: megablox <> Date: Sun, 8 Feb 2026 11:00:11 -0700 Subject: [PATCH] Fold arrows, pointer format, teal custom types, collapsed pointers by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change fold indicators from +/- to arrow icons (▸ collapsed, ▾ expanded) - Move ▾ dropdown arrow after text on command rows (source▾, struct▾) - Change pointer display from ptr64 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 --- src/compose.cpp | 74 +++++---- src/controller.cpp | 21 +-- src/core.h | 71 ++++----- src/editor.cpp | 132 +++++++++++----- src/editor.h | 4 +- src/examples/demo.rcx | 145 +++++++++++++----- src/format.cpp | 30 +++- src/main.cpp | 56 +++++-- tests/test_command_row.cpp | 40 ++--- tests/test_compose.cpp | 295 +++++++++++++++++++----------------- tests/test_editor.cpp | 104 ++++++------- tests/test_new_features.cpp | 31 ++-- 12 files changed, 610 insertions(+), 393 deletions(-) diff --git a/src/compose.cpp b/src/compose.cpp index d4104d1..4cd2224 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -39,10 +39,12 @@ struct ComposeState { if (currentLine > 0) text += '\n'; // 3-char fold indicator column: " - " expanded, " + " collapsed, " " other // CommandRow has no fold prefix (flush left) - if (lm.lineKind == LineKind::CommandRow || lm.lineKind == LineKind::CommandRow2) { - // no prefix + if (lm.lineKind == LineKind::CommandRow || lm.lineKind == LineKind::Blank + || lm.lineKind == LineKind::CommandRow2 + || (lm.lineKind == LineKind::Footer && lm.isRootHeader)) { + // no prefix — flush left like CommandRow2 } else if (lm.foldHead) - text += lm.foldCollapsed ? QStringLiteral(" + ") : QStringLiteral(" - "); + text += lm.foldCollapsed ? QStringLiteral(" \u25B8 ") : QStringLiteral(" \u25BE "); else text += QStringLiteral(" "); text += lineText; @@ -286,6 +288,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.depth = depth; lm.lineKind = LineKind::Footer; lm.nodeKind = node.kind; + lm.isRootHeader = isRootHeader; // root footer: flush left (no fold prefix) lm.offsetText.clear(); lm.foldLevel = computeFoldLevel(depth, false); lm.markerMask = 0; @@ -313,7 +316,7 @@ void composeNode(ComposeState& state, const NodeTree& tree, QString ptrTargetName = resolvePointerTarget(tree, node.refId); QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName); - // Emit merged fold header: "ptr64 Name {" (expanded) or "ptr64 Name -> val" (collapsed) + // Emit merged fold header: "Type* Name {" (expanded) or "Type* Name -> val" (collapsed) { LineMeta lm; lm.nodeIdx = nodeIdx; @@ -336,32 +339,31 @@ void composeNode(ComposeState& state, const NodeTree& tree, if (!node.collapsed) { int sz = node.byteSize(); + uint64_t ptrVal = 0; 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); if (ptrVal != 0) { uint64_t pBase = ptrToProviderAddr(tree, ptrVal); 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); - if (!state.ptrVisiting.contains(key)) { - state.ptrVisiting.insert(key); - int refIdx = tree.indexOfId(node.refId); - if (refIdx >= 0) { - const Node& ref = tree.nodes[refIdx]; - if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) - // isArrayChild=true skips header/footer, emits children only - // depth (not depth+1): pointer header replaces struct header, - // so children should be at depth+1, not depth+2 - composeParent(state, tree, prov, refIdx, - depth, pBase, ref.id, - /*isArrayChild=*/true); - } - state.ptrVisiting.remove(key); - } + } + + // Show referenced struct children: at dereferenced address if non-NULL, + // otherwise at offset 0 as a struct template preview + uint64_t pBase = (ptrVal != 0) ? ptrToProviderAddr(tree, ptrVal) : 0; + qulonglong key = pBase ^ (node.refId * kGoldenRatio); + if (!state.ptrVisiting.contains(key)) { + state.ptrVisiting.insert(key); + int refIdx = tree.indexOfId(node.refId); + if (refIdx >= 0) { + const Node& ref = tree.nodes[refIdx]; + if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) + composeParent(state, tree, prov, refIdx, + depth, pBase, ref.id, + /*isArrayChild=*/true); } + state.ptrVisiting.remove(key); } // 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) + const QString cmdRowText = QStringLiteral("source\u25BE \u203A 0x0"); { LineMeta lm; lm.nodeIdx = -1; @@ -485,10 +488,29 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR lm.markerMask = 0; lm.effectiveTypeW = state.typeW; 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; lm.nodeIdx = -1; @@ -501,7 +523,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR lm.markerMask = 0; lm.effectiveTypeW = state.typeW; lm.effectiveNameW = state.nameW; - state.emitLine(QStringLiteral("struct {"), lm); + state.emitLine(QStringLiteral("struct\u25BE {"), lm); } QVector roots = state.childMap.value(0); diff --git a/src/controller.cpp b/src/controller.cpp index 269ec91..2813699 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -808,8 +808,11 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { } } else if constexpr (std::is_same_v) { int idx = tree.indexOfId(c.nodeId); - if (idx >= 0) + if (idx >= 0) { 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) { int idx = tree.indexOfId(c.nodeId); 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]; 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; if ((node.kind == NodeKind::Vec2 || node.kind == NodeKind::Vec3 || node.kind == NodeKind::Vec4) && subLine >= 0) { @@ -1370,10 +1373,10 @@ void RcxController::updateCommandRow() { QString src; QString provName = m_doc->provider->name(); if (provName.isEmpty()) { - src = QStringLiteral(""); - return QStringLiteral("%1 '%2'").arg(prov.kind(), provName); + return QStringLiteral("source\u25BE"); + return QStringLiteral("'%1'\u25BE").arg(provName); } static QString buildCommandRow(const Provider& prov, uint64_t baseAddress) { QString src = buildSourceLabel(prov); QString addr = QStringLiteral("0x") + 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 @@ -32,13 +32,17 @@ struct TestColumnSpan { }; static TestColumnSpan commandRowSrcSpan(const QString& lineText) { - int idx = lineText.indexOf(QStringLiteral(" Address: ")); + int idx = lineText.indexOf(QStringLiteral(" \u203A")); if (idx < 0) return {}; int start = 0; while (start < idx && !lineText[start].isLetterOrNumber() - && lineText[start] != '<') start++; + && lineText[start] != '<' && lineText[start] != '\'') start++; 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 { @@ -52,18 +56,18 @@ private slots: void label_nullProvider_showsSelectSource() { NullProvider p; - QCOMPARE(buildSourceLabel(p), QStringLiteral(" + // BufferProvider with empty name also triggers source▾ BufferProvider p(QByteArray(4, '\0')); - QCOMPARE(buildSourceLabel(p), QStringLiteral(" Address: 0x0")); + QCOMPARE(row, QStringLiteral(" source\u25BE \u203A 0x0")); } void row_fileProvider() { BufferProvider p(QByteArray(4, '\0'), "test.bin"); 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); QVERIFY(span.valid); QString extracted = row.mid(span.start, span.end - span.start); - QCOMPARE(extracted, QStringLiteral("")); + QCOMPARE(buildSourceLabel(*prov), QStringLiteral("source\u25BE")); // User loads a file prov = std::make_unique(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 // (ProcessProvider needs Windows, but the label logic is the same) diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index f730b2a..293f56b 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -35,27 +35,30 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + 2 fields + root footer = 5 - QCOMPARE(result.meta.size(), 5); + // CommandRow + Blank + CommandRow2 + 2 fields + root footer = 6 + QCOMPARE(result.meta.size(), 6); // Line 0 is CommandRow QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow); - // Line 1 is CommandRow2 - QCOMPARE(result.meta[1].lineKind, LineKind::CommandRow2); + // Line 1 is Blank separator + QCOMPARE(result.meta[1].lineKind, LineKind::Blank); + + // Line 2 is CommandRow2 + QCOMPARE(result.meta[2].lineKind, LineKind::CommandRow2); // Fields at depth 1 - QVERIFY(!result.meta[2].foldHead); - QCOMPARE(result.meta[2].depth, 1); QVERIFY(!result.meta[3].foldHead); QCOMPARE(result.meta[3].depth, 1); + QVERIFY(!result.meta[4].foldHead); + QCOMPARE(result.meta[4].depth, 1); // Offset text - QCOMPARE(result.meta[2].offsetText, QString("0")); - QCOMPARE(result.meta[3].offsetText, QString("4")); + QCOMPARE(result.meta[3].offsetText, QString("0")); + QCOMPARE(result.meta[4].offsetText, QString("4")); - // Line 4 is root footer - QCOMPARE(result.meta[4].lineKind, LineKind::Footer); + // Line 5 is root footer + QCOMPARE(result.meta[5].lineKind, LineKind::Footer); } void testVec3SingleLine() { @@ -79,17 +82,17 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + 1 Vec3 line + root footer = 4 - QCOMPARE(result.meta.size(), 4); + // CommandRow + Blank + CommandRow2 + 1 Vec3 line + root footer = 5 + QCOMPARE(result.meta.size(), 5); - // Line 2: single Vec3 line, not continuation, depth 1 - QVERIFY(!result.meta[2].isContinuation); - QCOMPARE(result.meta[2].offsetText, QString("0")); - QCOMPARE(result.meta[2].depth, 1); - QCOMPARE(result.meta[2].nodeKind, NodeKind::Vec3); + // Line 3: single Vec3 line, not continuation, depth 1 + QVERIFY(!result.meta[3].isContinuation); + QCOMPARE(result.meta[3].offsetText, QString("0")); + QCOMPARE(result.meta[3].depth, 1); + QCOMPARE(result.meta[3].nodeKind, NodeKind::Vec3); - // Line 3 is root footer - QCOMPARE(result.meta[3].lineKind, LineKind::Footer); + // Line 4 is root footer + QCOMPARE(result.meta[4].lineKind, LineKind::Footer); } void testPaddingMarker() { @@ -113,13 +116,13 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + padding + root footer = 4 - QCOMPARE(result.meta.size(), 4); - QVERIFY(result.meta[2].markerMask & (1u << M_PAD)); - QCOMPARE(result.meta[2].depth, 1); + // CommandRow + Blank + CommandRow2 + padding + root footer = 5 + QCOMPARE(result.meta.size(), 5); + QVERIFY(result.meta[3].markerMask & (1u << M_PAD)); + QCOMPARE(result.meta[3].depth, 1); - // Line 3 is root footer - QCOMPARE(result.meta[3].lineKind, LineKind::Footer); + // Line 4 is root footer + QCOMPARE(result.meta[4].lineKind, LineKind::Footer); } void testNullPointerMarker() { @@ -145,14 +148,14 @@ private slots: BufferProvider prov(data); ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + ptr + root footer = 4 - QCOMPARE(result.meta.size(), 4); + // CommandRow + Blank + CommandRow2 + ptr + root footer = 5 + QCOMPARE(result.meta.size(), 5); // No ambient validation markers — M_PTR0 is no longer set - QVERIFY(!(result.meta[2].markerMask & (1u << M_PTR0))); - QCOMPARE(result.meta[2].depth, 1); + QVERIFY(!(result.meta[3].markerMask & (1u << M_PTR0))); + QCOMPARE(result.meta[3].depth, 1); - // Line 3 is root footer - QCOMPARE(result.meta[3].lineKind, LineKind::Footer); + // Line 4 is root footer + QCOMPARE(result.meta[4].lineKind, LineKind::Footer); } void testCollapsedStruct() { @@ -178,11 +181,11 @@ private slots: ComposeResult result = compose(tree, prov); // Collapsed root: isRootHeader overrides collapse, so children + footer still render - // CommandRow + CommandRow2 + field + root footer = 4 - QCOMPARE(result.meta.size(), 4); - QCOMPARE(result.meta[2].lineKind, LineKind::Field); - QCOMPARE(result.meta[2].depth, 1); - QCOMPARE(result.meta[3].lineKind, LineKind::Footer); + // CommandRow + Blank + CommandRow2 + field + root footer = 5 + QCOMPARE(result.meta.size(), 5); + QCOMPARE(result.meta[3].lineKind, LineKind::Field); + QCOMPARE(result.meta[3].depth, 1); + QCOMPARE(result.meta[4].lineKind, LineKind::Footer); } void testUnreadablePointerNoRead() { @@ -209,15 +212,15 @@ private slots: BufferProvider prov(data); ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + ptr + root footer = 4 - QCOMPARE(result.meta.size(), 4); + // CommandRow + Blank + CommandRow2 + ptr + root footer = 5 + QCOMPARE(result.meta.size(), 5); // No ambient validation markers - QVERIFY(!(result.meta[2].markerMask & (1u << M_ERR))); - QVERIFY(!(result.meta[2].markerMask & (1u << M_PTR0))); - QCOMPARE(result.meta[2].depth, 1); + QVERIFY(!(result.meta[3].markerMask & (1u << M_ERR))); + QVERIFY(!(result.meta[3].markerMask & (1u << M_PTR0))); + QCOMPARE(result.meta[3].depth, 1); - // Line 3 is root footer - QCOMPARE(result.meta[3].lineKind, LineKind::Footer); + // Line 4 is root footer + QCOMPARE(result.meta[4].lineKind, LineKind::Footer); } void testFoldLevels() { @@ -250,13 +253,13 @@ private slots: ComposeResult result = compose(tree, prov); // Child header (depth 1, fold head) — root header no longer emitted - QCOMPARE(result.meta[2].foldLevel, 0x401 | 0x2000); - QCOMPARE(result.meta[2].depth, 1); - QVERIFY(result.meta[2].foldHead); + QCOMPARE(result.meta[3].foldLevel, 0x401 | 0x2000); + QCOMPARE(result.meta[3].depth, 1); + QVERIFY(result.meta[3].foldHead); // Leaf (depth 2, not head) - QCOMPARE(result.meta[3].foldLevel, 0x402); - QCOMPARE(result.meta[3].depth, 2); + QCOMPARE(result.meta[4].foldLevel, 0x402); + QCOMPARE(result.meta[4].depth, 2); } void testNestedStruct() { @@ -303,31 +306,31 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + flags + Inner header + x + y + Inner footer + root footer = 8 - QCOMPARE(result.meta.size(), 8); + // CommandRow + Blank + CommandRow2 + flags + Inner header + x + y + Inner footer + root footer = 9 + QCOMPARE(result.meta.size(), 9); // flags field (depth 1) - QCOMPARE(result.meta[2].lineKind, LineKind::Field); - QCOMPARE(result.meta[2].depth, 1); + QCOMPARE(result.meta[3].lineKind, LineKind::Field); + QCOMPARE(result.meta[3].depth, 1); // Inner header (depth 1, fold head) - QCOMPARE(result.meta[3].lineKind, LineKind::Header); - QCOMPARE(result.meta[3].depth, 1); - QVERIFY(result.meta[3].foldHead); - QCOMPARE(result.meta[3].foldLevel, 0x401 | 0x2000); + QCOMPARE(result.meta[4].lineKind, LineKind::Header); + QCOMPARE(result.meta[4].depth, 1); + QVERIFY(result.meta[4].foldHead); + QCOMPARE(result.meta[4].foldLevel, 0x401 | 0x2000); // 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].foldLevel, 0x402); + QCOMPARE(result.meta[6].depth, 2); // Inner footer - QCOMPARE(result.meta[6].lineKind, LineKind::Footer); - QCOMPARE(result.meta[6].depth, 1); + QCOMPARE(result.meta[7].lineKind, LineKind::Footer); + QCOMPARE(result.meta[7].depth, 1); // Root footer - QCOMPARE(result.meta[7].lineKind, LineKind::Footer); - QCOMPARE(result.meta[7].depth, 0); + QCOMPARE(result.meta[8].lineKind, LineKind::Footer); + QCOMPARE(result.meta[8].depth, 0); } void testPointerDerefExpansion() { @@ -395,28 +398,28 @@ private slots: 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 - // Total = 12 - QCOMPARE(result.meta.size(), 12); + // Total = 13 + QCOMPARE(result.meta.size(), 13); // magic field (depth 1) - QCOMPARE(result.meta[2].lineKind, LineKind::Field); - QCOMPARE(result.meta[2].depth, 1); - - // Pointer as merged fold header: "ptr64 ptr {" - QCOMPARE(result.meta[3].lineKind, LineKind::Header); + QCOMPARE(result.meta[3].lineKind, LineKind::Field); 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) - QCOMPARE(result.meta[4].depth, 2); QCOMPARE(result.meta[5].depth, 2); + QCOMPARE(result.meta[6].depth, 2); // Pointer fold footer - QCOMPARE(result.meta[6].lineKind, LineKind::Footer); - QCOMPARE(result.meta[6].depth, 1); + QCOMPARE(result.meta[7].lineKind, LineKind::Footer); + QCOMPARE(result.meta[7].depth, 1); } void testPointerDerefNull() { @@ -460,18 +463,22 @@ private slots: 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 - // Total = 8 - QCOMPARE(result.meta.size(), 8); + // Total = 10 (null ptr still shows template preview) + QCOMPARE(result.meta.size(), 10); - // Pointer as merged fold header (expanded but empty — null ptr) - QCOMPARE(result.meta[2].lineKind, LineKind::Header); - QCOMPARE(result.meta[2].depth, 1); - QVERIFY(result.meta[2].foldHead); + // Pointer as merged fold header (expanded — shows template at offset 0) + QCOMPARE(result.meta[3].lineKind, LineKind::Header); + QCOMPARE(result.meta[3].depth, 1); + QVERIFY(result.meta[3].foldHead); - // Pointer fold footer (empty expansion) - QCOMPARE(result.meta[3].lineKind, LineKind::Footer); + // Target field shown as template preview + 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() { @@ -518,14 +525,14 @@ private slots: 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 - // Total = 7 - QCOMPARE(result.meta.size(), 7); + // Total = 8 + QCOMPARE(result.meta.size(), 8); // Pointer is fold head (depth 1) - QVERIFY(result.meta[2].foldHead); - QCOMPARE(result.meta[2].depth, 1); + QVERIFY(result.meta[3].foldHead); + QCOMPARE(result.meta[3].depth, 1); } void testPointerDerefCycle() { @@ -588,12 +595,12 @@ private slots: QVERIFY(result.meta.size() > 0); 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 // Then: self footer + ptr footer + Main footer + standalone Recursive rendering - QVERIFY(result.meta[2].foldHead); // ptr merged fold head - QCOMPARE(result.meta[2].lineKind, LineKind::Header); // ptr merged header - QCOMPARE(result.meta[3].lineKind, LineKind::Field); // data field (first child of Recursive) + QVERIFY(result.meta[3].foldHead); // ptr merged fold head + QCOMPARE(result.meta[3].lineKind, LineKind::Header); // ptr merged header + QCOMPARE(result.meta[4].lineKind, LineKind::Field); // data field (first child of Recursive) } void testStructFooterSimple() { @@ -657,12 +664,16 @@ private slots: ComposeResult result = compose(tree, prov); 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) { QCOMPARE(result.meta[i].nodeId, kCommandRowId); QCOMPARE(result.meta[i].nodeIdx, -1); continue; } + if (result.meta[i].lineKind == LineKind::Blank) { + QCOMPARE(result.meta[i].nodeIdx, -1); + continue; + } if (result.meta[i].lineKind == LineKind::CommandRow2) { QCOMPARE(result.meta[i].nodeId, kCommandRow2Id); QCOMPARE(result.meta[i].nodeIdx, -1); @@ -944,16 +955,16 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + Array header(collapsed) + root footer = 4 - QCOMPARE(result.meta.size(), 4); + // CommandRow + Blank + CommandRow2 + Array header(collapsed) + root footer = 5 + QCOMPARE(result.meta.size(), 5); - // Array header is collapsed (at index 2) + // Array header is collapsed (at index 3) int arrLine = -1; for (int i = 0; i < result.meta.size(); i++) { if (result.meta[i].isArrayHeader) { arrLine = i; break; } } QVERIFY(arrLine >= 0); - QCOMPARE(arrLine, 2); + QCOMPARE(arrLine, 3); QVERIFY(result.meta[arrLine].foldCollapsed); // Header text should NOT contain "{" @@ -1024,7 +1035,7 @@ private slots: // ═════════════════════════════════════════════════════════════ void testPointerDefaultVoid() { - // Pointer64 with no refId should display as "ptr64" + // Pointer64 with no refId should display as "void*" NodeTree tree; tree.baseAddress = 0; @@ -1059,8 +1070,8 @@ private slots: QStringList lines = result.text.split('\n'); QString text = lines[ptrLine]; - QVERIFY2(text.contains("ptr64"), - qPrintable("Pointer with no refId should show 'ptr64': " + text)); + QVERIFY2(text.contains("void*"), + qPrintable("Pointer with no refId should show 'void*': " + text)); // pointerTargetName should be empty (void) QVERIFY(result.meta[ptrLine].pointerTargetName.isEmpty()); @@ -1094,13 +1105,13 @@ private slots: QStringList lines = result.text.split('\n'); bool foundPtr32 = false; for (const QString& l : lines) { - if (l.contains("ptr32")) { foundPtr32 = true; break; } + if (l.contains("void*")) { foundPtr32 = true; break; } } - QVERIFY2(foundPtr32, "Pointer32 with no refId should show 'ptr32'"); + QVERIFY2(foundPtr32, "Pointer32 with no refId should show 'void*'"); } void testPointerDisplaysTargetName() { - // Pointer64 with refId displays "ptr64" + // Pointer64 with refId displays "TargetName*" NodeTree tree; tree.baseAddress = 0; @@ -1153,8 +1164,8 @@ private slots: QVERIFY(ptrLine >= 0); QStringList lines = result.text.split('\n'); - QVERIFY2(lines[ptrLine].contains("ptr64"), - qPrintable("Should show 'ptr64': " + lines[ptrLine])); + QVERIFY2(lines[ptrLine].contains("PlayerData*"), + qPrintable("Should show 'PlayerData*': " + lines[ptrLine])); // pointerTargetName metadata QCOMPARE(result.meta[ptrLine].pointerTargetName, QString("PlayerData")); @@ -1200,7 +1211,7 @@ private slots: QStringList lines = result.text.split('\n'); bool found = false; for (const QString& l : lines) { - if (l.contains("ptr64")) { found = true; break; } + if (l.contains("MyStruct*")) { found = true; break; } } QVERIFY2(found, "Should use struct name when structTypeName is empty"); } @@ -1252,22 +1263,19 @@ private slots: QString lineText = lines[ptrLine]; const LineMeta& lm = result.meta[ptrLine]; - // Kind span: covers "ptr64" + // Kind span: no longer applicable in "Type*" format ColumnSpan kindSpan = pointerKindSpanFor(lm, lineText); - QVERIFY2(kindSpan.valid, "pointerKindSpanFor must return valid span"); - QString kindText = lineText.mid(kindSpan.start, kindSpan.end - kindSpan.start); - QVERIFY2(kindText.contains("ptr64"), - qPrintable("Kind span should cover 'ptr64', got: '" + kindText + "'")); + QVERIFY2(!kindSpan.valid, "pointerKindSpanFor should return invalid in Type* format"); - // Target span: covers "VTable" + // Target span: covers "VTable" (before the '*') ColumnSpan targetSpan = pointerTargetSpanFor(lm, lineText); 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")); } 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; tree.baseAddress = 0; @@ -1302,19 +1310,19 @@ private slots: QString lineText = lines[ptrLine]; const LineMeta& lm = result.meta[ptrLine]; - // Kind span: "ptr64" + // Kind span: no longer applicable in "Type*" format 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); 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")); } void testPointerToPointerChain() { - // ptr64 → StructB { ptr64 } → StructC { field } + // StructB* → StructB { StructC* } → StructC { field } NodeTree tree; tree.baseAddress = 0; @@ -1390,18 +1398,18 @@ private slots: QVERIFY(result.meta.size() > 0); QVERIFY(result.meta.size() < 200); - // Check that ptr64 and ptr64 both appear in text + // Check that Wrapper* and InnerData* both appear in text bool foundWrapper = false, foundInner = false; QStringList lines = result.text.split('\n'); for (const QString& l : lines) { - if (l.contains("ptr64")) foundWrapper = true; - if (l.contains("ptr64")) foundInner = true; + if (l.contains("Wrapper*")) foundWrapper = true; + if (l.contains("InnerData*")) foundInner = true; } - QVERIFY2(foundWrapper, "Should display 'ptr64'"); - QVERIFY2(foundInner, "Should display 'ptr64'"); + QVERIFY2(foundWrapper, "Should display 'Wrapper*'"); + QVERIFY2(foundInner, "Should display 'InnerData*'"); - // The chain: Root → ptr64(fold head) → Wrapper expanded → - // ptr64(fold head) → InnerData expanded + // The chain: Root → Wrapper*(fold head) → Wrapper expanded → + // InnerData*(fold head) → InnerData expanded int foldHeadCount = 0; for (const LineMeta& lm : result.meta) { if (lm.foldHead && lm.nodeKind == NodeKind::Pointer64) @@ -1490,15 +1498,15 @@ private slots: qPrintable(QString("Cycle should be bounded, got %1 lines") .arg(result.meta.size()))); - // Both ptr64 and ptr64
should appear + // Both StructB* and Main* should appear bool foundToB = false, foundToMain = false; QStringList lines = result.text.split('\n'); for (const QString& l : lines) { - if (l.contains("ptr64")) foundToB = true; - if (l.contains("ptr64
")) foundToMain = true; + if (l.contains("StructB*")) foundToB = true; + if (l.contains("Main*")) foundToMain = true; } - QVERIFY2(foundToB, "Should display 'ptr64'"); - QVERIFY2(foundToMain, "Should display 'ptr64
'"); + QVERIFY2(foundToB, "Should display 'StructB*'"); + QVERIFY2(foundToMain, "Should display 'Main*'"); // The first expansion of each pointer works; // the cycle is caught on the second attempt. @@ -1555,10 +1563,10 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // Every struct name should appear in a "ptr64" format + // Every struct name should appear in a "Name*" format QStringList lines = result.text.split('\n'); for (const QString& sname : structNames) { - QString expected = QString("ptr64<%1>").arg(sname); + QString expected = QString("%1*").arg(sname); bool found = false; for (const QString& l : lines) { if (l.contains(expected)) { found = true; break; } @@ -1594,9 +1602,9 @@ private slots: QStringList lines = result.text.split('\n'); bool foundVoid = false; for (const QString& l : lines) { - if (l.contains("ptr64")) { foundVoid = true; break; } + if (l.contains("void*")) { foundVoid = true; break; } } - QVERIFY2(foundVoid, "Dangling refId should degrade to ptr64"); + QVERIFY2(foundVoid, "Dangling refId should degrade to void*"); } void testPointerCollapsedNoExpansion() { @@ -1662,7 +1670,7 @@ private slots: } void testPointerWidthComputation() { - // Type column must be wide enough for "ptr64" + // Type column must be wide enough for "LongStructName*" NodeTree tree; tree.baseAddress = 0; @@ -1698,7 +1706,7 @@ private slots: QStringList lines = result.text.split('\n'); bool foundFull = false; for (const QString& l : lines) { - if (l.contains("ptr64")) { + if (l.contains("VeryLongStructNameForTesting*")) { foundFull = true; break; } @@ -1707,9 +1715,9 @@ private slots: "Type column should be wide enough for long pointer target names"); // Layout type width should accommodate the long name - // "ptr64" = 35 chars - QVERIFY2(result.layout.typeW >= 35, - qPrintable(QString("typeW=%1, should be >= 35").arg(result.layout.typeW))); + // "VeryLongStructNameForTesting*" = 29 chars + QVERIFY2(result.layout.typeW >= 29, + qPrintable(QString("typeW=%1, should be >= 29").arg(result.layout.typeW))); } // ═════════════════════════════════════════════════════════════ @@ -1820,7 +1828,7 @@ private slots: void testCommandRow2NameSpan() { // Name span should cover the class name - QString text = "struct MyClass"; + QString text = "struct\u25BE MyClass"; ColumnSpan nameSpan = commandRow2NameSpan(text); QVERIFY(nameSpan.valid); @@ -1873,10 +1881,11 @@ private slots: QVERIFY2(result.meta.size() >= 5, 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'); QCOMPARE(lines.size(), result.meta.size()); for (int i = 0; i < lines.size(); i++) { + if (result.meta[i].lineKind == LineKind::Blank) continue; QVERIFY2(!lines[i].isEmpty(), qPrintable(QString("Line %1 is empty").arg(i))); } diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index 6ece00c..3511992 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -376,7 +376,7 @@ private slots: // Set CommandRow text with an ADDR value (simulates controller.updateCommandRow) m_editor->setCommandRowText( - QStringLiteral(" File Address: 0xD87B5E5000")); + QStringLiteral("source\u25BE \u203A 0xD87B5E5000")); // BaseAddress should be ALLOWED on CommandRow (ADDR field) bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); @@ -394,14 +394,14 @@ private slots: // ── Test: inline edit lifecycle (begin → commit → re-edit) ── void testInlineEditReEntry() { - // Move cursor to line 2 (first field; line 0=CommandRow, 1=CommandRow2, root header suppressed) - m_editor->scintilla()->setCursorPosition(2, 0); + // Move cursor to line 3 (first field; 0=CommandRow, 1=Blank, 2=CommandRow2, root header suppressed) + m_editor->scintilla()->setCursorPosition(kFirstDataLine, 0); // Should not be editing QVERIFY(!m_editor->isEditing()); // Begin edit on Name column - bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2); + bool ok = m_editor->beginInlineEdit(EditTarget::Name, kFirstDataLine); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -413,7 +413,7 @@ private slots: m_editor->applyDocument(m_result); // Should be able to edit again - ok = m_editor->beginInlineEdit(EditTarget::Name, 2); + ok = m_editor->beginInlineEdit(EditTarget::Name, kFirstDataLine); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -425,10 +425,10 @@ private slots: // ── Test: commit inline edit then re-edit same line ── void testCommitThenReEdit() { m_editor->applyDocument(m_result); - m_editor->scintilla()->setCursorPosition(2, 0); + m_editor->scintilla()->setCursorPosition(kFirstDataLine, 0); // Begin value edit - bool ok = m_editor->beginInlineEdit(EditTarget::Value, 2); + bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -445,7 +445,7 @@ private slots: m_editor->applyDocument(m_result); // 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(m_editor->isEditing()); @@ -456,7 +456,7 @@ private slots: void testMouseClickCommitsEdit() { m_editor->applyDocument(m_result); - bool ok = m_editor->beginInlineEdit(EditTarget::Name, 2); + bool ok = m_editor->beginInlineEdit(EditTarget::Name, kFirstDataLine); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -478,7 +478,7 @@ private slots: m_editor->applyDocument(m_result); // 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(m_editor->isEditing()); @@ -598,7 +598,7 @@ private slots: void testTypeAutocompleteTypingAndCommit() { m_editor->applyDocument(m_result); - bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2); + bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine); QVERIFY(ok); // Autocomplete is deferred via QTimer::singleShot(0) — poll until active @@ -635,7 +635,7 @@ private slots: void testTypeEditClickAwayNoChange() { m_editor->applyDocument(m_result); - bool ok = m_editor->beginInlineEdit(EditTarget::Type, 2); + bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine); QVERIFY(ok); // Process deferred autocomplete @@ -652,7 +652,7 @@ private slots: QCOMPARE(commitSpy.count(), 1); // 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 args = commitSpy.first(); QString committedText = args.at(3).toString(); QVERIFY2(committedText == "uint8_t", @@ -665,8 +665,8 @@ private slots: void testColumnSpanHitTest() { m_editor->applyDocument(m_result); - // Line 2 is a field line (UInt8), verify spans are valid (line 0=CommandRow, 1=CommandRow2) - const LineMeta* lm = m_editor->metaForLine(2); + // kFirstDataLine is a field line (UInt8), verify spans are valid + const LineMeta* lm = m_editor->metaForLine(kFirstDataLine); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::Field); @@ -683,7 +683,7 @@ private slots: // Value span should be valid for field lines QString lineText; int len = (int)m_editor->scintilla()->SendScintilla( - QsciScintillaBase::SCI_LINELENGTH, (unsigned long)2); + QsciScintillaBase::SCI_LINELENGTH, (unsigned long)kFirstDataLine); QVERIFY(len > 0); ColumnSpan vs = RcxEditor::valueSpan(*lm, len); QVERIFY(vs.valid); @@ -712,13 +712,13 @@ private slots: void testSelectedNodeIndices() { m_editor->applyDocument(m_result); - // Put cursor on first field line (line 2; 0=CommandRow, 1=CommandRow2, root header suppressed) - m_editor->scintilla()->setCursorPosition(2, 0); + // Put cursor on first field line (kFirstDataLine; 0=CommandRow, 1=Blank, 2=CommandRow2) + m_editor->scintilla()->setCursorPosition(kFirstDataLine, 0); QSet sel = m_editor->selectedNodeIndices(); QCOMPARE(sel.size(), 1); // 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(sel.contains(lm->nodeIdx)); } @@ -736,8 +736,8 @@ private slots: QVERIFY2(!result.text.contains("// base:"), "Composed text should not contain '// base:' (consolidated into cmd bar)"); - // Line 2 should be the first field (root header suppressed) - const LineMeta* lm = m_editor->metaForLine(2); + // kFirstDataLine should be the first field (root header suppressed) + const LineMeta* lm = m_editor->metaForLine(kFirstDataLine); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::Field); @@ -750,7 +750,7 @@ private slots: // Set CommandRow text with ADDR value (simulates controller) m_editor->setCommandRowText( - QStringLiteral(" File Address: 0xD87B5E5000")); + QStringLiteral("source\u25BE \u203A 0xD87B5E5000")); // Line 0 is CommandRow const LineMeta* lm = m_editor->metaForLine(0); @@ -852,14 +852,14 @@ private slots: void testValueEditCommitUpdatesSignal() { m_editor->applyDocument(m_result); - // Line 2 = first UInt8 field (InheritedAddressSpace, root header suppressed) - const LineMeta* lm = m_editor->metaForLine(2); + // kFirstDataLine = first UInt8 field (InheritedAddressSpace, root header suppressed) + const LineMeta* lm = m_editor->metaForLine(kFirstDataLine); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::Field); QVERIFY(lm->nodeKind != NodeKind::Padding); // Begin value edit - bool ok = m_editor->beginInlineEdit(EditTarget::Value, 2); + bool ok = m_editor->beginInlineEdit(EditTarget::Value, kFirstDataLine); QVERIFY(ok); QVERIFY(m_editor->isEditing()); @@ -902,7 +902,7 @@ private slots: // Set CommandRow text with ADDR value (simulates controller) m_editor->setCommandRowText( - QStringLiteral(" File Address: 0xD87B5E5000")); + QStringLiteral("source\u25BE \u203A 0xD87B5E5000")); // Begin base address edit on line 0 (CommandRow ADDR field) bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); @@ -919,7 +919,7 @@ private slots: m_editor->applyDocument(m_result); // 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); QApplication::processEvents(); @@ -932,8 +932,8 @@ private slots: void testCursorShapeOverText() { m_editor->applyDocument(m_result); - // Line 2 is a field (UInt8 InheritedAddressSpace) - const LineMeta* lm = m_editor->metaForLine(2); + // kFirstDataLine is a field (UInt8 InheritedAddressSpace) + const LineMeta* lm = m_editor->metaForLine(kFirstDataLine); QVERIFY(lm); // Get the name span (padded to kColName width) @@ -941,14 +941,14 @@ private slots: QVERIFY(ns.valid); // 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); QApplication::processEvents(); QCOMPARE(viewportCursor(m_editor), Qt::IBeamCursor); // 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 - 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); QApplication::processEvents(); // Should be Arrow (padding whitespace, not actual text) @@ -959,7 +959,7 @@ private slots: void testCursorShapeOverType() { m_editor->applyDocument(m_result); - const LineMeta* lm = m_editor->metaForLine(2); + const LineMeta* lm = m_editor->metaForLine(kFirstDataLine); QVERIFY(lm); // Type span starts after the fold column + indent @@ -967,7 +967,7 @@ private slots: QVERIFY(ts.valid); // 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); QApplication::processEvents(); QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor); @@ -1013,18 +1013,18 @@ private slots: m_editor->applyDocument(m_result); // Click on a field to select the node - const LineMeta* lm = m_editor->metaForLine(2); + const LineMeta* lm = m_editor->metaForLine(kFirstDataLine); QVERIFY(lm); ColumnSpan ns = RcxEditor::nameSpan(*lm, lm->effectiveTypeW, lm->effectiveNameW); QVERIFY(ns.valid); // 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); QApplication::processEvents(); // 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); QApplication::processEvents(); @@ -1033,26 +1033,26 @@ private slots: QVERIFY(!m_editor->isEditing()); } - // ── Test: CommandRow2 exists at line 1 ── + // ── Test: CommandRow2 exists at kCommandRow2Line ── void testCommandRow2Exists() { m_editor->applyDocument(m_result); - // Line 1 should be CommandRow2 - const LineMeta* lm = m_editor->metaForLine(1); + // kCommandRow2Line should be CommandRow2 + const LineMeta* lm = m_editor->metaForLine(kCommandRow2Line); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::CommandRow2); QCOMPARE(lm->nodeId, kCommandRow2Id); QCOMPARE(lm->nodeIdx, -1); // Type/Name/Value should be rejected on CommandRow2 - QVERIFY(!m_editor->beginInlineEdit(EditTarget::Type, 1)); - QVERIFY(!m_editor->beginInlineEdit(EditTarget::Name, 1)); - QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, 1)); + QVERIFY(!m_editor->beginInlineEdit(EditTarget::Type, kCommandRow2Line)); + QVERIFY(!m_editor->beginInlineEdit(EditTarget::Name, kCommandRow2Line)); + QVERIFY(!m_editor->beginInlineEdit(EditTarget::Value, kCommandRow2Line)); QVERIFY(!m_editor->isEditing()); // RootClassName should be allowed on CommandRow2 - m_editor->setCommandRow2Text(QStringLiteral("struct _PEB64")); - bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 1); + m_editor->setCommandRow2Text(QStringLiteral("struct\u25BE _PEB64")); + bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, kCommandRow2Line); QVERIFY2(ok, "RootClassName edit should be allowed on CommandRow2"); QVERIFY(m_editor->isEditing()); m_editor->cancelInlineEdit(); @@ -1063,15 +1063,15 @@ private slots: m_editor->applyDocument(m_result); // Set CommandRow2 without alignas - m_editor->setCommandRow2Text(QStringLiteral("struct _PEB64")); + m_editor->setCommandRow2Text(QStringLiteral("struct\u25BE _PEB64")); - // Line 1 is CommandRow2 - const LineMeta* lm = m_editor->metaForLine(1); + // kCommandRow2Line is CommandRow2 + const LineMeta* lm = m_editor->metaForLine(kCommandRow2Line); QVERIFY(lm); QCOMPARE(lm->lineKind, LineKind::CommandRow2); // RootClassName should work - QVERIFY(m_editor->beginInlineEdit(EditTarget::RootClassName, 1)); + QVERIFY(m_editor->beginInlineEdit(EditTarget::RootClassName, kCommandRow2Line)); QVERIFY(m_editor->isEditing()); m_editor->cancelInlineEdit(); @@ -1083,15 +1083,15 @@ private slots: m_editor->applyDocument(m_result); // Root struct header is completely suppressed from output. - // Line 0 = CommandRow, Line 1 = CommandRow2, Line 2 = first field. - const LineMeta* lm2 = m_editor->metaForLine(2); + // Line 0 = CommandRow, Line 1 = Blank, Line 2 = CommandRow2, Line 3 = first field. + const LineMeta* lm2 = m_editor->metaForLine(kFirstDataLine); QVERIFY(lm2); 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; 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; break; } diff --git a/tests/test_new_features.cpp b/tests/test_new_features.cpp index 5ba71c2..c8cf1bc 100644 --- a/tests/test_new_features.cpp +++ b/tests/test_new_features.cpp @@ -185,7 +185,7 @@ private slots: QCOMPARE(name, QString("float")); name = doc.resolveTypeName(NodeKind::Hex64); - QCOMPARE(name, QString("Hex64")); + QCOMPARE(name, QString("hex64")); } void testResolveTypeName_withAlias() { @@ -441,10 +441,11 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov, 99999); - // Only command rows - QCOMPARE(result.meta.size(), 2); + // Only command rows + blank + QCOMPARE(result.meta.size(), 3); 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() { @@ -881,23 +882,23 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + 1 Vec4 line + footer = 4 - QCOMPARE(result.meta.size(), 4); + // CommandRow + Blank + CommandRow2 + 1 Vec4 line + footer = 5 + QCOMPARE(result.meta.size(), 5); - // The Vec4 line (index 2) is a single field line, not continuation - QCOMPARE(result.meta[2].lineKind, LineKind::Field); - QCOMPARE(result.meta[2].nodeKind, NodeKind::Vec4); - QVERIFY(!result.meta[2].isContinuation); + // The Vec4 line (index 3) is a single field line, not continuation + QCOMPARE(result.meta[3].lineKind, LineKind::Field); + QCOMPARE(result.meta[3].nodeKind, NodeKind::Vec4); + QVERIFY(!result.meta[3].isContinuation); // Copy text (equivalent to editor's "Copy All as Text") QString text = result.text; - // NullProvider reads 0 for all floats, so values are "0, 0, 0, 0" - QVERIFY(text.contains("0, 0, 0, 0")); + // NullProvider reads 0 for all floats, values are "0.f, 0.f, 0.f, 0.f" + QVERIFY(text.contains("0.f, 0.f, 0.f, 0.f")); // Confirm type, name, and values all on the same line QStringList lines = text.split('\n'); - QVERIFY(lines[2].contains("Vec4")); - QVERIFY(lines[2].contains("position")); - QVERIFY(lines[2].contains("0, 0, 0, 0")); + QVERIFY(lines[3].contains("vec4")); + QVERIFY(lines[3].contains("position")); + QVERIFY(lines[3].contains("0.f, 0.f, 0.f, 0.f")); } };