From 18475d057041927d4a4060c4835c819af5c6d458 Mon Sep 17 00:00:00 2001 From: batallion caputa Date: Sat, 7 Feb 2026 16:41:01 -0700 Subject: [PATCH] minorfixes --- src/compose.cpp | 10 +-- src/controller.cpp | 4 +- src/core.h | 5 +- src/editor.cpp | 2 + src/main.cpp | 45 +++++++++++- tests/test_compose.cpp | 148 +++++++++++++++++++++++--------------- tests/test_validation.cpp | 4 +- 7 files changed, 146 insertions(+), 72 deletions(-) diff --git a/src/compose.cpp b/src/compose.cpp index 3444f01..d4104d1 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -222,6 +222,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, state.baseEmitted = true; // Header line (skip for array element structs and root struct) + // Root struct header is on CommandRow2 (type + name + {) if (!isArrayChild && !isRootHeader) { // Get per-scope widths for this header's parent scope int typeW = state.effectiveTypeW(scopeId); @@ -263,8 +264,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, return tree.nodes[a].offset < tree.nodes[b].offset; }); - // Root struct children compose at same depth (no header to indent from) - int childDepth = isRootHeader ? depth : depth + 1; + int childDepth = depth + 1; // For arrays, render children as condensed (no header/footer for struct elements) bool childrenAreArrayElements = (node.kind == NodeKind::Array); @@ -278,8 +278,8 @@ void composeParent(ComposeState& state, const NodeTree& tree, } } - // Footer line: skip when collapsed, for array element structs, or for root struct - if (!isArrayChild && !isRootHeader && !node.collapsed) { + // Footer line: skip when collapsed or for array element structs + if (!isArrayChild && (!node.collapsed || isRootHeader)) { LineMeta lm; lm.nodeIdx = nodeIdx; lm.nodeId = node.id; @@ -501,7 +501,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 {"), lm); } QVector roots = state.childMap.value(0); diff --git a/src/controller.cpp b/src/controller.cpp index 904fba3..269ec91 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1408,13 +1408,13 @@ void RcxController::updateCommandRow() { if (n.parentId == 0 && n.kind == NodeKind::Struct) { QString keyword = n.resolvedClassKeyword(); QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName; - row2 = QStringLiteral("%1 %2") + row2 = QStringLiteral("%1 %2 {") .arg(keyword, className); break; } } if (row2.isEmpty()) - row2 = QStringLiteral("struct "); + row2 = QStringLiteral("struct {"); for (auto* ed : m_editors) { ed->setCommandRowText(row); diff --git a/src/core.h b/src/core.h index 12e47aa..2f10a1a 100644 --- a/src/core.h +++ b/src/core.h @@ -603,6 +603,7 @@ inline ColumnSpan commandRow2TypeSpan(const QString& lineText) { } inline ColumnSpan commandRow2NameSpan(const QString& lineText) { + // Format: "keyword name {" — extract just the name part (before " {") int start = 0; while (start < lineText.size() && lineText[start].isSpace()) start++; int space = lineText.indexOf(' ', start); @@ -610,7 +611,9 @@ inline ColumnSpan commandRow2NameSpan(const QString& lineText) { int nameStart = space + 1; while (nameStart < lineText.size() && lineText[nameStart].isSpace()) nameStart++; if (nameStart >= lineText.size()) return {}; - int nameEnd = lineText.size(); + // Stop before trailing " {" + int nameEnd = lineText.indexOf(QStringLiteral(" {"), nameStart); + if (nameEnd < 0) nameEnd = lineText.size(); while (nameEnd > nameStart && lineText[nameEnd - 1].isSpace()) nameEnd--; if (nameEnd <= nameStart) return {}; return {nameStart, nameEnd, true}; diff --git a/src/editor.cpp b/src/editor.cpp index e2e5c76..a0acbde 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -677,6 +677,8 @@ void RcxEditor::applyCommandRowPills() { // ── Shared inline-edit shutdown ── RcxEditor::EndEditInfo RcxEditor::endInlineEdit() { + // Dismiss any open user list / autocomplete popup + m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL); // Clear edit comment and error marker before deactivating if (m_editState.target == EditTarget::Value) { setEditComment({}); // Clear to spaces diff --git a/src/main.cpp b/src/main.cpp index ef7aea8..a476a4a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -201,9 +201,9 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { m_mdiArea->setTabsMovable(true); setCentralWidget(m_mdiArea); + createWorkspaceDock(); createMenus(); createStatusBar(); - createWorkspaceDock(); connect(m_mdiArea, &QMdiArea::subWindowActivated, this, [this](QMdiSubWindow*) { @@ -279,6 +279,8 @@ void MainWindow::createMenus() { view->addSeparator(); m_actViewRendered = view->addAction(makeIcon(":/vsicons/code.svg"), "&C/C++", this, [this]() { setViewMode(VM_Rendered); }); m_actViewReclass = view->addAction(makeIcon(":/vsicons/eye.svg"), "&Reclass View", this, [this]() { setViewMode(VM_Reclass); }); + view->addSeparator(); + view->addAction(m_workspaceDock->toggleViewAction()); // Node auto* node = menuBar()->addMenu("&Node"); @@ -395,12 +397,48 @@ void MainWindow::newFile() { } void MainWindow::selfTest() { - // Load demo.rcx if it exists, otherwise create a blank project QString demoPath = QCoreApplication::applicationDirPath() + "/demo.rcx"; if (QFile::exists(demoPath)) { project_open(demoPath); } else { - project_new(); + // Create default demo with a single Ball struct + auto* doc = new RcxDocument(this); + doc->tree.baseAddress = 0x00400000; + + Node ball; + ball.kind = NodeKind::Struct; + ball.name = "aBall"; + ball.structTypeName = "Ball"; + ball.parentId = 0; + ball.offset = 0; + int bi = doc->tree.addNode(ball); + uint64_t ballId = doc->tree.nodes[bi].id; + + { Node n; n.kind = NodeKind::Hex64; n.name = "field_00"; n.parentId = ballId; n.offset = 0; doc->tree.addNode(n); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_08"; n.parentId = ballId; n.offset = 8; doc->tree.addNode(n); } + { Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 16; 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::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 = 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::Float; n.name = "radius"; 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::Double; n.name = "mass"; n.parentId = ballId; n.offset = 72; 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::Bool; n.name = "bouncy"; n.parentId = ballId; n.offset = 88; 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::Hex16; n.name = "field_5A"; n.parentId = ballId; n.offset = 90; 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::Hex32; 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); } + { Node n; n.kind = NodeKind::Hex64; n.name = "field_78"; n.parentId = ballId; n.offset = 116; doc->tree.addNode(n); } + + doc->save(demoPath); + doc->load(demoPath); + createTab(doc); } } @@ -886,6 +924,7 @@ void MainWindow::createWorkspaceDock() { m_workspaceDock->setWidget(m_workspaceTree); addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock); + m_workspaceDock->hide(); connect(m_workspaceTree, &QTreeView::doubleClicked, this, [this](const QModelIndex& index) { // Data roles: UserRole=QMdiSubWindow*, UserRole+1=structId, UserRole+2=nodeId diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index 1729fd6..2bfc67c 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -35,8 +35,8 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + 2 fields = 4 lines (root header/footer suppressed) - QCOMPARE(result.meta.size(), 4); + // CommandRow + CommandRow2 + 2 fields + root footer = 5 + QCOMPARE(result.meta.size(), 5); // Line 0 is CommandRow QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow); @@ -44,13 +44,18 @@ private slots: // Line 1 is CommandRow2 QCOMPARE(result.meta[1].lineKind, LineKind::CommandRow2); - // Fields at depth 0 (root struct suppressed) + // 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); // Offset text QCOMPARE(result.meta[2].offsetText, QString("0")); QCOMPARE(result.meta[3].offsetText, QString("4")); + + // Line 4 is root footer + QCOMPARE(result.meta[4].lineKind, LineKind::Footer); } void testVec3Continuation() { @@ -74,22 +79,28 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + 3 Vec3 lines = 5 lines (root header/footer suppressed) - QCOMPARE(result.meta.size(), 5); + // CommandRow + CommandRow2 + 3 Vec3 lines + root footer = 6 + QCOMPARE(result.meta.size(), 6); - // Line 2 (first Vec3 component): not continuation + // Line 2 (first Vec3 component): not continuation, depth 1 QVERIFY(!result.meta[2].isContinuation); QCOMPARE(result.meta[2].offsetText, QString("0")); + QCOMPARE(result.meta[2].depth, 1); - // Lines 3-4: continuation + // Lines 3-4: continuation, depth 1 QVERIFY(result.meta[3].isContinuation); QCOMPARE(result.meta[3].offsetText, QString(" \u00B7")); + QCOMPARE(result.meta[3].depth, 1); QVERIFY(result.meta[4].isContinuation); QCOMPARE(result.meta[4].offsetText, QString(" \u00B7")); + QCOMPARE(result.meta[4].depth, 1); // Continuation marker QVERIFY(result.meta[3].markerMask & (1u << M_CONT)); QVERIFY(result.meta[4].markerMask & (1u << M_CONT)); + + // Line 5 is root footer + QCOMPARE(result.meta[5].lineKind, LineKind::Footer); } void testPaddingMarker() { @@ -113,9 +124,13 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + padding = 3 (root header/footer suppressed) - QCOMPARE(result.meta.size(), 3); + // 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); + + // Line 3 is root footer + QCOMPARE(result.meta[3].lineKind, LineKind::Footer); } void testNullPointerMarker() { @@ -141,10 +156,14 @@ private slots: BufferProvider prov(data); ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + ptr = 3 (root header/footer suppressed) - QCOMPARE(result.meta.size(), 3); + // CommandRow + CommandRow2 + ptr + root footer = 4 + QCOMPARE(result.meta.size(), 4); // No ambient validation markers — M_PTR0 is no longer set QVERIFY(!(result.meta[2].markerMask & (1u << M_PTR0))); + QCOMPARE(result.meta[2].depth, 1); + + // Line 3 is root footer + QCOMPARE(result.meta[3].lineKind, LineKind::Footer); } void testCollapsedStruct() { @@ -169,10 +188,12 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // Collapsed: CommandRow + CommandRow2 + header only (no children, no footer) - QCOMPARE(result.meta.size(), 3); - QVERIFY(!result.meta[2].foldHead); // root fold suppressed - QVERIFY(!result.meta[2].foldCollapsed); // root fold suppressed + // 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); } void testUnreadablePointerNoRead() { @@ -199,11 +220,15 @@ private slots: BufferProvider prov(data); ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + ptr = 3 (root header/footer suppressed) - QCOMPARE(result.meta.size(), 3); + // CommandRow + CommandRow2 + ptr + root footer = 4 + QCOMPARE(result.meta.size(), 4); // 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); + + // Line 3 is root footer + QCOMPARE(result.meta[3].lineKind, LineKind::Footer); } void testFoldLevels() { @@ -235,14 +260,14 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // Child header (depth 0, fold head) — root suppressed, children at depth 0 - QCOMPARE(result.meta[2].foldLevel, 0x400 | 0x2000); - QCOMPARE(result.meta[2].depth, 0); + // 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); - // Leaf (depth 1, not head) - QCOMPARE(result.meta[3].foldLevel, 0x401); - QCOMPARE(result.meta[3].depth, 1); + // Leaf (depth 2, not head) + QCOMPARE(result.meta[3].foldLevel, 0x402); + QCOMPARE(result.meta[3].depth, 2); } void testNestedStruct() { @@ -289,28 +314,31 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + flags + Inner header + x + y + Inner footer = 7 - // (root header/footer suppressed, children at depth 0) - QCOMPARE(result.meta.size(), 7); + // CommandRow + CommandRow2 + flags + Inner header + x + y + Inner footer + root footer = 8 + QCOMPARE(result.meta.size(), 8); - // flags field (depth 0, root children at depth 0) + // flags field (depth 1) QCOMPARE(result.meta[2].lineKind, LineKind::Field); - QCOMPARE(result.meta[2].depth, 0); + QCOMPARE(result.meta[2].depth, 1); - // Inner header (depth 0, fold head) + // Inner header (depth 1, fold head) QCOMPARE(result.meta[3].lineKind, LineKind::Header); - QCOMPARE(result.meta[3].depth, 0); + QCOMPARE(result.meta[3].depth, 1); QVERIFY(result.meta[3].foldHead); - QCOMPARE(result.meta[3].foldLevel, 0x400 | 0x2000); + QCOMPARE(result.meta[3].foldLevel, 0x401 | 0x2000); - // Inner fields at depth 1 - QCOMPARE(result.meta[4].depth, 1); - QCOMPARE(result.meta[4].foldLevel, 0x401); - QCOMPARE(result.meta[5].depth, 1); + // Inner fields at depth 2 + QCOMPARE(result.meta[4].depth, 2); + QCOMPARE(result.meta[4].foldLevel, 0x402); + QCOMPARE(result.meta[5].depth, 2); // Inner footer QCOMPARE(result.meta[6].lineKind, LineKind::Footer); - QCOMPARE(result.meta[6].depth, 0); + QCOMPARE(result.meta[6].depth, 1); + + // Root footer + QCOMPARE(result.meta[7].lineKind, LineKind::Footer); + QCOMPARE(result.meta[7].depth, 0); } void testPointerDerefExpansion() { @@ -378,28 +406,28 @@ private slots: ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + magic + ptr(merged fold header) + fn1 + fn2 + ptr footer = 7 + // CommandRow + CommandRow2 + magic + ptr(merged fold header) + fn1 + fn2 + ptr footer + Main footer = 8 // VTable standalone: header + fn1 + fn2 + footer = 4 - // Total = 11 (root header/footer suppressed) - QCOMPARE(result.meta.size(), 11); + // Total = 12 + QCOMPARE(result.meta.size(), 12); - // magic field (depth 0, root children at depth 0) + // magic field (depth 1) QCOMPARE(result.meta[2].lineKind, LineKind::Field); - QCOMPARE(result.meta[2].depth, 0); + QCOMPARE(result.meta[2].depth, 1); // Pointer as merged fold header: "ptr64 ptr {" QCOMPARE(result.meta[3].lineKind, LineKind::Header); - QCOMPARE(result.meta[3].depth, 0); + QCOMPARE(result.meta[3].depth, 1); QVERIFY(result.meta[3].foldHead); QCOMPARE(result.meta[3].nodeKind, NodeKind::Pointer64); - // Expanded fields at depth 1 (struct header merged into pointer) - QCOMPARE(result.meta[4].depth, 1); - QCOMPARE(result.meta[5].depth, 1); + // Expanded fields at depth 2 (struct header merged into pointer) + QCOMPARE(result.meta[4].depth, 2); + QCOMPARE(result.meta[5].depth, 2); // Pointer fold footer QCOMPARE(result.meta[6].lineKind, LineKind::Footer); - QCOMPARE(result.meta[6].depth, 0); + QCOMPARE(result.meta[6].depth, 1); } void testPointerDerefNull() { @@ -443,13 +471,14 @@ private slots: ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + ptr(merged fold header) + ptr footer = 4 + // CommandRow + CommandRow2 + ptr(merged fold header) + ptr footer + Main footer = 5 // Target standalone: header + field + footer = 3 - // Total = 7 (root header/footer suppressed) - QCOMPARE(result.meta.size(), 7); + // Total = 8 + QCOMPARE(result.meta.size(), 8); // 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 fold footer (empty expansion) @@ -500,14 +529,14 @@ private slots: ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + ptr(fold head, collapsed) = 3 + // CommandRow + CommandRow2 + ptr(fold head, collapsed) + Main footer = 4 // Target standalone: header + field + footer = 3 - // Total = 6 (root header/footer suppressed) - QCOMPARE(result.meta.size(), 6); + // Total = 7 + QCOMPARE(result.meta.size(), 7); - // Pointer is fold head (depth 0, root children at depth 0) + // Pointer is fold head (depth 1) QVERIFY(result.meta[2].foldHead); - QCOMPARE(result.meta[2].depth, 0); + QCOMPARE(result.meta[2].depth, 1); } void testPointerDerefCycle() { @@ -570,9 +599,9 @@ private slots: QVERIFY(result.meta.size() > 0); QVERIFY(result.meta.size() < 100); // sanity: bounded output - // Root suppressed: CommandRow + CommandRow2 + ptr merged header + data + self merged header + // CommandRow + CommandRow2 + ptr merged header + data + self merged header // Second expansion blocked by cycle guard: no children under self - // Then: self footer + ptr footer + standalone Recursive rendering + // 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) @@ -926,15 +955,16 @@ private slots: NullProvider prov; ComposeResult result = compose(tree, prov); - // CommandRow + CommandRow2 + Array header(collapsed) = 3 (root header/footer suppressed) - QCOMPARE(result.meta.size(), 3); + // CommandRow + CommandRow2 + Array header(collapsed) + root footer = 4 + QCOMPARE(result.meta.size(), 4); - // Array header is collapsed + // Array header is collapsed (at index 2) 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); QVERIFY(result.meta[arrLine].foldCollapsed); // Header text should NOT contain "{" diff --git a/tests/test_validation.cpp b/tests/test_validation.cpp index 951db2f..9236741 100644 --- a/tests/test_validation.cpp +++ b/tests/test_validation.cpp @@ -1026,10 +1026,10 @@ private slots: m_editor->applyDocument(result); QApplication::processEvents(); - // Find header line + // Find a non-root header line (root header has no editable name/type spans) int headerLine = -1; for (int i = 0; i < result.meta.size(); i++) { - if (result.meta[i].lineKind == LineKind::Header) { + if (result.meta[i].lineKind == LineKind::Header && !result.meta[i].isRootHeader) { headerLine = i; break; }