diff --git a/src/compose.cpp b/src/compose.cpp index 568cde0..2d3b048 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -296,7 +296,15 @@ void composeParent(ComposeState& state, const NodeTree& tree, for (const auto& m : node.enumMembers) maxNameLen = qMax(maxNameLen, (int)m.first.size()); - for (int mi = 0; mi < node.enumMembers.size(); mi++) { + // Build display order sorted by value + QVector order(node.enumMembers.size()); + std::iota(order.begin(), order.end(), 0); + std::sort(order.begin(), order.end(), [&](int a, int b) { + return node.enumMembers[a].second < node.enumMembers[b].second; + }); + + for (int oi = 0; oi < order.size(); oi++) { + int mi = order[oi]; const auto& m = node.enumMembers[mi]; LineMeta lm; lm.nodeIdx = nodeIdx; @@ -304,6 +312,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.subLine = mi; lm.depth = childDepth; lm.lineKind = LineKind::Field; + lm.isMemberLine = true; lm.nodeKind = NodeKind::UInt32; lm.foldLevel = computeFoldLevel(childDepth, false); lm.markerMask = 0; @@ -334,6 +343,57 @@ void composeParent(ComposeState& state, const NodeTree& tree, return; } + // Bitfield with members: render name : width = value lines + if (node.resolvedClassKeyword() == QStringLiteral("bitfield") + && !node.bitfieldMembers.isEmpty()) { + int childDepth = depth + 1; + int maxNameLen = 4; + for (const auto& m : node.bitfieldMembers) + maxNameLen = qMax(maxNameLen, (int)m.name.size()); + + for (int mi = 0; mi < node.bitfieldMembers.size(); mi++) { + const auto& m = node.bitfieldMembers[mi]; + uint64_t bitVal = fmt::extractBits(prov, absAddr, node.elementKind, + m.bitOffset, m.bitWidth); + LineMeta lm; + lm.nodeIdx = nodeIdx; + lm.nodeId = node.id; + lm.subLine = mi; + lm.depth = childDepth; + lm.lineKind = LineKind::Field; + lm.isMemberLine = true; + lm.nodeKind = node.elementKind; + lm.foldLevel = computeFoldLevel(childDepth, false); + lm.markerMask = 0; + lm.offsetText = fmt::fmtOffsetMargin(absAddr, true, state.offsetHexDigits); + lm.offsetAddr = absAddr; + lm.ptrBase = state.currentPtrBase; + state.emitLine(fmt::fmtBitfieldMember(m.name, m.bitWidth, bitVal, + childDepth, maxNameLen), lm); + } + + // Footer + if (!isArrayChild) { + LineMeta lm; + lm.nodeIdx = nodeIdx; + lm.nodeId = node.id; + lm.depth = depth; + lm.lineKind = LineKind::Footer; + lm.nodeKind = node.kind; + lm.isRootHeader = isRootHeader; + lm.foldLevel = computeFoldLevel(depth, false); + lm.markerMask = 0; + int sz = sizeForKind(node.elementKind); + lm.offsetText = fmt::fmtOffsetMargin(absAddr + sz, false, state.offsetHexDigits); + lm.offsetAddr = absAddr + sz; + lm.ptrBase = state.currentPtrBase; + state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm); + } + + state.visiting.remove(node.id); + return; + } + const QVector& children = childIndices(state, node.id); int childDepth = depth + 1; @@ -741,7 +801,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR } // Emit CommandRow as line 0 (combined: source + address + root class type + name) - const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct NoName {"); + const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE 0x0 struct NoName {"); { LineMeta lm; lm.nodeIdx = -1; diff --git a/src/controller.cpp b/src/controller.cpp index 0198da3..44485be 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -250,6 +250,15 @@ void RcxController::connectEditor(RcxEditor* editor) { if (text.isEmpty()) break; if (nodeIdx >= m_doc->tree.nodes.size()) break; const Node& node = m_doc->tree.nodes[nodeIdx]; + // Enum member name edit + if (node.resolvedClassKeyword() == QStringLiteral("enum") + && subLine >= 0 && subLine < node.enumMembers.size()) { + auto members = node.enumMembers; + members[subLine].first = text; + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangeEnumMembers{node.id, node.enumMembers, members})); + break; + } // ASCII edit on Hex nodes if (isHexPreview(node.kind)) { setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true, resolvedAddr); @@ -321,9 +330,27 @@ void RcxController::connectEditor(RcxEditor* editor) { } break; } - case EditTarget::Value: + case EditTarget::Value: { + // Enum member value edit + if (nodeIdx >= 0 && nodeIdx < m_doc->tree.nodes.size()) { + const Node& node = m_doc->tree.nodes[nodeIdx]; + if (node.resolvedClassKeyword() == QStringLiteral("enum") + && subLine >= 0 && subLine < node.enumMembers.size()) { + bool ok; + int64_t val = text.toLongLong(&ok); + if (!ok) val = text.toLongLong(&ok, 16); + if (ok) { + auto members = node.enumMembers; + members[subLine].second = val; + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangeEnumMembers{node.id, node.enumMembers, members})); + } + break; + } + } setNodeValue(nodeIdx, subLine, text, /*isAscii=*/false, resolvedAddr); break; + } case EditTarget::BaseAddress: { QString s = text.trimmed(); s.remove('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000) @@ -569,9 +596,10 @@ void RcxController::refresh() { // Prune stale selections (nodes removed by undo/redo/delete) QSet valid; for (uint64_t id : m_selIds) { - uint64_t nodeId = id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask); + uint64_t nodeId = id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask + | kMemberBit | kMemberSubMask); if (m_doc->tree.indexOfId(nodeId) >= 0) - valid.insert(id); // Keep original ID (with footer/array bits if present) + valid.insert(id); // Keep original ID (with footer/array/member bits if present) } m_selIds = valid; @@ -1145,6 +1173,10 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { m_valueHistory.remove(c.nodeId); for (int ci : tree.subtreeIndices(c.nodeId)) m_valueHistory.remove(tree.nodes[ci].id); + } else if constexpr (std::is_same_v) { + int idx = tree.indexOfId(c.nodeId); + if (idx >= 0) + tree.nodes[idx].enumMembers = isUndo ? c.oldMembers : c.newMembers; } }, command); @@ -1379,6 +1411,86 @@ void RcxController::splitHexNode(uint64_t nodeId) { refresh(); } +void RcxController::toggleBitfieldBit(uint64_t nodeId, int memberIdx) { + int ni = m_doc->tree.indexOfId(nodeId); + if (ni < 0) return; + const Node& node = m_doc->tree.nodes[ni]; + if (node.resolvedClassKeyword() != QStringLiteral("bitfield")) return; + if (memberIdx < 0 || memberIdx >= node.bitfieldMembers.size()) return; + if (!m_doc->provider || !m_doc->provider->isWritable()) return; + + const auto& bm = node.bitfieldMembers[memberIdx]; + uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni); + int containerSize = sizeForKind(node.elementKind); + if (containerSize <= 0) containerSize = 4; + + QByteArray oldBytes(containerSize, 0); + m_doc->provider->read(addr, oldBytes.data(), containerSize); + + QByteArray newBytes = oldBytes; + // Toggle the bit + int byteIdx = bm.bitOffset / 8; + int bitInByte = bm.bitOffset % 8; + if (byteIdx < containerSize) + newBytes[byteIdx] = newBytes[byteIdx] ^ (1 << bitInByte); + + m_doc->undoStack.push(new RcxCommand(this, + cmd::WriteBytes{addr, oldBytes, newBytes})); + refresh(); +} + +void RcxController::editBitfieldValue(uint64_t nodeId, int memberIdx) { + int ni = m_doc->tree.indexOfId(nodeId); + if (ni < 0) return; + const Node& node = m_doc->tree.nodes[ni]; + if (node.resolvedClassKeyword() != QStringLiteral("bitfield")) return; + if (memberIdx < 0 || memberIdx >= node.bitfieldMembers.size()) return; + if (!m_doc->provider || !m_doc->provider->isWritable()) return; + + const auto& bm = node.bitfieldMembers[memberIdx]; + uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni); + int containerSize = sizeForKind(node.elementKind); + if (containerSize <= 0) containerSize = 4; + + // Read current value + uint64_t curVal = fmt::extractBits(*m_doc->provider, addr, node.elementKind, + bm.bitOffset, bm.bitWidth); + uint64_t maxVal = (bm.bitWidth >= 64) ? UINT64_MAX : ((1ULL << bm.bitWidth) - 1); + + bool ok = false; + QString input = QInputDialog::getText(nullptr, + QStringLiteral("Edit Bitfield Value"), + QStringLiteral("%1 (%2 bits, max %3):") + .arg(bm.name).arg(bm.bitWidth).arg(maxVal), + QLineEdit::Normal, + QString::number(curVal), &ok); + if (!ok || input.isEmpty()) return; + + // Parse value (support hex with 0x prefix) + uint64_t newVal; + if (input.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) + newVal = input.mid(2).toULongLong(&ok, 16); + else + newVal = input.toULongLong(&ok, 10); + if (!ok) return; + newVal &= maxVal; + + QByteArray oldBytes(containerSize, 0); + m_doc->provider->read(addr, oldBytes.data(), containerSize); + + // Read-modify-write: clear target bits and set new value + QByteArray newBytes = oldBytes; + uint64_t container = 0; + memcpy(&container, newBytes.constData(), qMin(containerSize, (int)sizeof(container))); + uint64_t mask = maxVal << bm.bitOffset; + container = (container & ~mask) | ((newVal & maxVal) << bm.bitOffset); + memcpy(newBytes.data(), &container, qMin(containerSize, (int)sizeof(container))); + + m_doc->undoStack.push(new RcxCommand(this, + cmd::WriteBytes{addr, oldBytes, newBytes})); + refresh(); +} + void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos) { auto icon = [](const char* name) { return QIcon(QStringLiteral(":/vsicons/%1").arg(name)); }; @@ -1535,6 +1647,31 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, uint64_t nodeId = node.id; uint64_t parentId = node.parentId; + // ── Member line: enum or bitfield member ── + bool isEnumMember = node.resolvedClassKeyword() == QStringLiteral("enum") + && !node.enumMembers.isEmpty() + && subLine >= 0 && subLine < node.enumMembers.size(); + bool isBitfieldMember = node.resolvedClassKeyword() == QStringLiteral("bitfield") + && !node.bitfieldMembers.isEmpty() + && subLine >= 0 && subLine < node.bitfieldMembers.size(); + + if (isEnumMember || isBitfieldMember) { + if (isBitfieldMember) { + const auto& bm = node.bitfieldMembers[subLine]; + if (bm.bitWidth == 1) { + menu.addAction("Toggle Bit", [this, nodeId, subLine]() { + toggleBitfieldBit(nodeId, subLine); + }); + } else { + menu.addAction("Edit Value...", [this, nodeId, subLine]() { + editBitfieldValue(nodeId, subLine); + }); + } + menu.addSeparator(); + } + // Fall through to always-available actions + } else { + // Quick-convert suggestions for Hex nodes bool addedQuickConvert = false; if (node.kind == NodeKind::Hex64) { @@ -1756,6 +1893,7 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, }); menu.addSeparator(); + } // else (non-member node actions) } // ── Always-available actions ── @@ -1885,6 +2023,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line, return nid | kFooterIdBit; if (lm.isArrayElement && lm.arrayElementIdx >= 0) return makeArrayElemSelId(nid, lm.arrayElementIdx); + if (lm.isMemberLine && lm.subLine >= 0) + return makeMemberSelId(nid, lm.subLine); return nid; }; @@ -1933,8 +2073,9 @@ void RcxController::handleNodeClick(RcxEditor* source, int line, if (m_selIds.size() == 1) { uint64_t sid = *m_selIds.begin(); - // Strip footer/array bits for node lookup - int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask)); + // Strip footer/array/member bits for node lookup + int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask + | kMemberBit | kMemberSubMask)); if (idx >= 0) emit nodeSelected(idx); } } @@ -1970,7 +2111,7 @@ void RcxController::updateCommandRow() { addr = QStringLiteral("0x") + QString::number(m_doc->tree.baseAddress, 16).toUpper(); - QString row = QStringLiteral("%1 \u00B7 %2") + QString row = QStringLiteral("%1 %2") .arg(elide(src, 40), elide(addr, 24)); // Build row 2: root class type + name (uses current view root) @@ -2001,7 +2142,7 @@ void RcxController::updateCommandRow() { if (row2.isEmpty()) row2 = QStringLiteral("struct NoName {"); - QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" \u00B7 ") + row2; + QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" ") + row2; for (auto* ed : m_editors) { ed->setCommandRowText(combined); diff --git a/src/controller.h b/src/controller.h index f00cf6e..2a9892c 100644 --- a/src/controller.h +++ b/src/controller.h @@ -98,6 +98,8 @@ public: void duplicateNode(int nodeIdx); void convertToTypedPointer(uint64_t nodeId); void splitHexNode(uint64_t nodeId); + void toggleBitfieldBit(uint64_t nodeId, int memberIdx); + void editBitfieldValue(uint64_t nodeId, int memberIdx); void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos); void batchRemoveNodes(const QVector& nodeIndices); void batchChangeKind(const QVector& nodeIndices, NodeKind newKind); diff --git a/src/core.h b/src/core.h index 54be174..d55c50e 100644 --- a/src/core.h +++ b/src/core.h @@ -179,6 +179,14 @@ enum Marker : int { M_ACCENT = 9, }; +// ── Bitfield member (name + bit position + width within a container) ── + +struct BitfieldMember { + QString name; + uint8_t bitOffset = 0; // position from LSB within the container + uint8_t bitWidth = 1; // number of bits (1..64) +}; + // ── Node ── struct Node { @@ -197,6 +205,7 @@ struct Node { int ptrDepth = 0; // Pointer: 0=struct/void ptr, 1=primitive*, 2=primitive** int viewIndex = 0; // Array: current view offset (transient) QVector> enumMembers; // Enum: name→value pairs + QVector bitfieldMembers; // Bitfield: per-bit member definitions // Note: Returns 0 for Array-of-Struct/Array. Use tree.structSpan() for accurate size. int byteSize() const { @@ -208,6 +217,12 @@ struct Node { if (elemSz <= 0) return 0; return qMin(arrayLen, INT_MAX / elemSz) * elemSz; } + case NodeKind::Struct: + if (classKeyword == QStringLiteral("bitfield")) { + int sz = sizeForKind(elementKind); + return sz > 0 ? sz : 4; + } + return 0; default: return sizeForKind(kind); } } @@ -240,6 +255,17 @@ struct Node { } o["enumMembers"] = arr; } + if (!bitfieldMembers.isEmpty()) { + QJsonArray arr; + for (const auto& m : bitfieldMembers) { + QJsonObject bm; + bm["name"] = m.name; + bm["bitOffset"] = m.bitOffset; + bm["bitWidth"] = m.bitWidth; + arr.append(bm); + } + o["bitfieldMembers"] = arr; + } return o; } static Node fromJson(const QJsonObject& o) { @@ -265,6 +291,17 @@ struct Node { em["value"].toString("0").toLongLong()}); } } + if (o.contains("bitfieldMembers")) { + QJsonArray arr = o["bitfieldMembers"].toArray(); + for (const auto& v : arr) { + QJsonObject bm = v.toObject(); + BitfieldMember m; + m.name = bm["name"].toString(); + m.bitOffset = (uint8_t)bm["bitOffset"].toInt(0); + m.bitWidth = (uint8_t)qBound(1, bm["bitWidth"].toInt(1), 64); + n.bitfieldMembers.append(m); + } + } return n; } @@ -512,6 +549,18 @@ inline int arrayElemIdxFromSelId(uint64_t selId) { return (int)((selId & kArrayElemMask) >> kArrayElemShift); } +// Member selection encoding (enum/bitfield members) — mirrors array element pattern +static constexpr uint64_t kMemberBit = 0x2000000000000000ULL; +static constexpr uint64_t kMemberSubShift = 48; +static constexpr uint64_t kMemberSubMask = 0x3FFF000000000000ULL; + +inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) { + return nodeId | kMemberBit | ((uint64_t)(subLine & 0x3FFF) << kMemberSubShift); +} +inline int memberSubFromSelId(uint64_t selId) { + return (int)((selId & kMemberSubMask) >> kMemberSubShift); +} + struct LineMeta { int nodeIdx = -1; uint64_t nodeId = 0; @@ -541,6 +590,7 @@ struct LineMeta { int effectiveNameW = 22; // Per-line name column width used for rendering QString pointerTargetName; // Resolved target type name for Pointer32/64 (empty = "void") bool isArrayElement = false; // true for synthesized primitive array element lines + bool isMemberLine = false; // true for enum member / bitfield member lines }; inline bool isSyntheticLine(const LineMeta& lm) { @@ -585,13 +635,15 @@ namespace cmd { struct ChangeStructTypeName { uint64_t nodeId; QString oldName, newName; }; struct ChangeClassKeyword { uint64_t nodeId; QString oldKeyword, newKeyword; }; struct ChangeOffset { uint64_t nodeId; int oldOffset, newOffset; }; + struct ChangeEnumMembers { uint64_t nodeId; + QVector> oldMembers, newMembers; }; } using Command = std::variant< cmd::ChangeKind, cmd::Rename, cmd::Collapse, cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes, cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName, - cmd::ChangeClassKeyword, cmd::ChangeOffset + cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers >; // ── Column spans (for inline editing) ── @@ -621,13 +673,13 @@ inline constexpr int kMaxNameW = 128; // Maximum name column width inline constexpr int kCompactTypeW = 20; // Type column cap for compact column mode inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) { - if (lm.lineKind != LineKind::Field || lm.isContinuation) return {}; + if (lm.lineKind != LineKind::Field || lm.isContinuation || lm.isMemberLine) return {}; int ind = kFoldCol + lm.depth * 3; return {ind, ind + typeW, true}; } inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int nameW = kColName) { - if (lm.isContinuation || lm.lineKind != LineKind::Field) return {}; + if (lm.isContinuation || lm.lineKind != LineKind::Field || lm.isMemberLine) return {}; int ind = kFoldCol + lm.depth * 3; int start = ind + typeW + kSepWidth; @@ -642,6 +694,7 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) { if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer || lm.lineKind == LineKind::ArrayElementSeparator) return {}; + if (lm.isMemberLine) return {}; int ind = kFoldCol + lm.depth * 3; // Hex uses nameW for ASCII column (same as regular name column) @@ -660,6 +713,27 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW return {start, start + valWidth, true}; } +// Member line spans (enum "name = value", bitfield "name : N = value") +inline ColumnSpan memberNameSpanFor(const LineMeta& lm, const QString& lineText) { + if (!lm.isMemberLine) return {}; + int ind = kFoldCol + lm.depth * 3; + int eq = lineText.indexOf(QLatin1String(" = "), ind); + if (eq < 0) return {}; + int nameEnd = eq; + while (nameEnd > ind && lineText[nameEnd - 1] == ' ') nameEnd--; + return {ind, nameEnd, true}; +} + +inline ColumnSpan memberValueSpanFor(const LineMeta& lm, const QString& lineText) { + if (!lm.isMemberLine) return {}; + int eq = lineText.indexOf(QLatin1String(" = ")); + if (eq < 0) return {}; + int valStart = eq + 3; + int valEnd = lineText.size(); + while (valEnd > valStart && lineText[valEnd - 1] == ' ') valEnd--; + return {valStart, valEnd, true}; +} + inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName) { if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {}; int ind = kFoldCol + lm.depth * 3; @@ -681,30 +755,14 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = // Line format: "source▾ · 0x140000000" inline ColumnSpan commandRowSrcSpan(const QString& lineText) { - int idx = lineText.indexOf(QStringLiteral(" \u00B7")); - if (idx < 0) return {}; + // Source label ends at the ▾ dropdown arrow + int arrow = lineText.indexOf(QChar(0x25BE)); + if (arrow < 0) return {}; int start = 0; - while (start < idx && !lineText[start].isLetterOrNumber() + while (start < arrow && !lineText[start].isLetterOrNumber() && lineText[start] != '<' && lineText[start] != '\'') start++; - if (start >= idx) return {}; - // 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) { - int tag = lineText.indexOf(QStringLiteral(" \u00B7")); - if (tag < 0) return {}; - int start = tag + 3; // after " · " - // Scan to next " · " separator (or end of line) to support formulas with spaces - int nextSep = lineText.indexOf(QStringLiteral(" \u00B7"), start); - int end = (nextSep >= 0) ? nextSep : lineText.size(); - // Trim trailing whitespace - while (end > start && lineText[end - 1].isSpace()) end--; - if (end <= start) return {}; - return {start, end, true}; + if (start >= arrow) return {}; + return {start, arrow, true}; } // ── CommandRow root-class spans ── @@ -723,6 +781,25 @@ inline int commandRowRootStart(const QString& lineText) { return best; } +inline ColumnSpan commandRowAddrSpan(const QString& lineText) { + // Address starts at "0x" after the source dropdown arrow + int arrow = lineText.indexOf(QChar(0x25BE)); + if (arrow < 0) return {}; + int start = lineText.indexOf(QStringLiteral("0x"), arrow); + if (start < 0) { + // Formula mode: address is between arrow and root keyword + start = arrow + 1; + while (start < lineText.size() && lineText[start].isSpace()) start++; + } + // End at root keyword (struct/class/enum) or end of line + int rootStart = commandRowRootStart(lineText); + int end = (rootStart > start) ? rootStart : lineText.size(); + // Trim trailing whitespace + while (end > start && lineText[end - 1].isSpace()) end--; + if (end <= start) return {}; + return {start, end, true}; +} + inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) { int start = commandRowRootStart(lineText); if (start < 0) return {}; @@ -893,6 +970,11 @@ namespace fmt { QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok); QString validateValue(NodeKind kind, const QString& text); QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW); + QString fmtBitfieldMember(const QString& name, uint8_t bitWidth, + uint64_t value, int depth, int nameW); + uint64_t extractBits(const Provider& prov, uint64_t addr, + NodeKind containerKind, + uint8_t bitOffset, uint8_t bitWidth); } // namespace fmt // ── Compose function forward declaration ── diff --git a/src/editor.cpp b/src/editor.cpp index 8409ae3..af17c48 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -880,7 +880,7 @@ void RcxEditor::reformatMargins() { for (int i = 0; i < m_meta.size(); i++) { auto& lm = m_meta[i]; - if (lm.isContinuation) { + if (lm.isContinuation || lm.isMemberLine) { lm.offsetText = QStringLiteral(" \u00B7 "); } else if (lm.offsetText.isEmpty()) { continue; @@ -1079,8 +1079,11 @@ void RcxEditor::applySelectionOverlay(const QSet& selIds) { for (uint64_t selId : selIds) { bool isFooterSel = (selId & kFooterIdBit) != 0; bool isArrayElemSel = (selId & kArrayElemBit) != 0; + bool isMemberSel = (selId & kMemberBit) != 0; int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1; - uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask); + int memberSubLine = isMemberSel ? memberSubFromSelId(selId) : -1; + uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask + | kMemberBit | kMemberSubMask); auto it = m_nodeLineIndex.constFind(nodeId); if (it == m_nodeLineIndex.constEnd()) continue; for (int ln : *it) { @@ -1094,8 +1097,13 @@ void RcxEditor::applySelectionOverlay(const QSet& selIds) { if (!m_meta[ln].isArrayElement || m_meta[ln].arrayElementIdx != arrayElemIdx) continue; } else if (m_meta[ln].isArrayElement) { - // Plain nodeId selection shouldn't highlight individual array elements - // (the header line is enough) + continue; + } + // Member line: match by subLine index + if (isMemberSel) { + if (!m_meta[ln].isMemberLine || m_meta[ln].subLine != memberSubLine) + continue; + } else if (m_meta[ln].isMemberLine) { continue; } m_sci->markerAdd(ln, M_SELECTED); @@ -1127,7 +1135,8 @@ void RcxEditor::applyHoverHighlight() { if (prevId != 0) { // Check if old hovered line was a single-line highlight (footer or array element) bool prevSingleLine = (prevLine >= 0 && prevLine < m_meta.size() && - (m_meta[prevLine].lineKind == LineKind::Footer || m_meta[prevLine].isArrayElement)); + (m_meta[prevLine].lineKind == LineKind::Footer || m_meta[prevLine].isArrayElement + || m_meta[prevLine].isMemberLine)); if (prevSingleLine) { m_sci->markerDelete(prevLine, M_HOVER); } else { @@ -1143,11 +1152,13 @@ void RcxEditor::applyHoverHighlight() { if (!m_hoverInside) return; if (m_hoveredNodeId == 0) return; - // Footer and array elements highlight only the specific line + // Footer, array elements, and member lines highlight only the specific line bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() && m_meta[m_hoveredLine].lineKind == LineKind::Footer); bool hoveringArrayElem = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() && m_meta[m_hoveredLine].isArrayElement); + bool hoveringMember = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() && + m_meta[m_hoveredLine].isMemberLine); // Check if the hovered item is already selected (using appropriate ID) uint64_t checkId; @@ -1155,12 +1166,14 @@ void RcxEditor::applyHoverHighlight() { checkId = m_hoveredNodeId | kFooterIdBit; else if (hoveringArrayElem) checkId = makeArrayElemSelId(m_hoveredNodeId, m_meta[m_hoveredLine].arrayElementIdx); + else if (hoveringMember) + checkId = makeMemberSelId(m_hoveredNodeId, m_meta[m_hoveredLine].subLine); else checkId = m_hoveredNodeId; if (m_currentSelIds.contains(checkId)) return; - if (hoveringFooter || hoveringArrayElem) { - // Single-line highlight for footers and array elements + if (hoveringFooter || hoveringArrayElem || hoveringMember) { + // Single-line highlight for footers, array elements, and member lines m_sci->markerAdd(m_hoveredLine, M_HOVER); } else { // Non-footer, non-array-element: highlight all lines for this node @@ -1374,15 +1387,6 @@ void RcxEditor::applyCommandRowPills() { if (srcDrop >= 0 && (rootStart < 0 || srcDrop < rootStart)) fillIndicatorCols(IND_HEX_DIM, line, srcDrop, srcDrop + 1); } - // Dim all " · " separators - int searchFrom = 0; - while (true) { - int tag = t.indexOf(QStringLiteral(" \u00B7"), searchFrom); - if (tag < 0) break; - fillIndicatorCols(IND_HEX_DIM, line, tag, tag + 3); - searchFrom = tag + 3; - } - // Dim base address to match source/struct grey ColumnSpan addrSpan = commandRowAddrSpan(t); if (addrSpan.valid) @@ -1615,6 +1619,12 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, if (!s.valid && t == EditTarget::Name) s = headerNameSpan(*lm, lineText); + // Member lines: override Name/Value spans + if (!s.valid && lm->isMemberLine) { + if (t == EditTarget::Name) s = memberNameSpanFor(*lm, lineText); + if (t == EditTarget::Value) s = memberValueSpanFor(*lm, lineText); + } + out = normalizeSpan(s, lineText, t, /*skipPrefixes=*/true); if (lineTextOut) *lineTextOut = lineText; return out.valid; @@ -1728,6 +1738,12 @@ static bool hitTestTarget(QsciScintilla* sci, if (!ns.valid) ns = headerNameSpan(lm, lineText); + // Member lines: use name/value spans from line text (no type span) + if (lm.isMemberLine) { + ns = memberNameSpanFor(lm, lineText); + vs = memberValueSpanFor(lm, lineText); + } + if (inSpan(ts)) outTarget = EditTarget::Type; else if (inSpan(ns)) outTarget = EditTarget::Name; else if (inSpan(vs)) outTarget = EditTarget::Value; @@ -2686,6 +2702,8 @@ void RcxEditor::updateEditableIndicators(int line) { checkId = lm->nodeId | kFooterIdBit; else if (lm->isArrayElement && lm->arrayElementIdx >= 0) checkId = makeArrayElemSelId(lm->nodeId, lm->arrayElementIdx); + else if (lm->isMemberLine && lm->subLine >= 0) + checkId = makeMemberSelId(lm->nodeId, lm->subLine); else checkId = lm->nodeId; return m_currentSelIds.contains(checkId); diff --git a/src/examples/EPROCESS.rcx b/src/examples/EPROCESS.rcx index 8ed4f59..30b1d0a 100644 --- a/src/examples/EPROCESS.rcx +++ b/src/examples/EPROCESS.rcx @@ -41,7 +41,10 @@ {"id":"182","kind":"Hex32","name":"State:3 StackCount:29","offset":0,"parentId":"180"}, {"id":"190","kind":"Struct","name":"kexecute_options","structTypeName":"_KEXECUTE_OPTIONS","classKeyword":"union","offset":0,"parentId":"0","refId":"0","collapsed":true}, - {"id":"191","kind":"Hex8","name":"ExecuteOptions","offset":0,"parentId":"190"}, + {"id":"191","kind":"Struct","name":"","offset":0,"parentId":"190","refId":"0","collapsed":false}, + {"id":"192","kind":"UInt8","name":"ExecuteDisable","offset":0,"parentId":"191"}, + {"id":"193","kind":"Hex8","name":"ExecuteDisable:1 ExecuteEnable:1 DisableThunkEmulation:1 Permanent:1 ExecuteDispatchEnable:1 ImageDispatchEnable:1 DisableExceptionChainValidation:1 Spare:1","offset":0,"parentId":"191"}, + {"id":"194","kind":"UInt8","name":"ExecuteOptions","offset":0,"parentId":"190"}, {"id":"200","kind":"Struct","name":"se_audit_info","structTypeName":"_SE_AUDIT_PROCESS_CREATION_INFO","offset":0,"parentId":"0","refId":"0","collapsed":true}, {"id":"201","kind":"Pointer64","name":"ImageFileName","offset":0,"parentId":"200"}, diff --git a/src/format.cpp b/src/format.cpp index 9f6cd45..8cae184 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -121,15 +121,8 @@ QString fmtDouble(double v) { } QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); } -QString fmtPointer32(uint32_t v) { - if (v == 0) return QStringLiteral("-> NULL"); - return QStringLiteral("-> ") + hexVal(v); -} - -QString fmtPointer64(uint64_t v) { - if (v == 0) return QStringLiteral("-> NULL"); - return QStringLiteral("-> ") + hexVal(v); -} +QString fmtPointer32(uint32_t v) { return hexVal(v); } +QString fmtPointer64(uint64_t v) { return hexVal(v); } // ── Indentation ── @@ -148,11 +141,11 @@ QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDig // ── Struct type name (for width calculation) ── QString structTypeName(const Node& node) { - // Full type string: "struct TypeName", "union TypeName", "class TypeName", etc. - QString base = node.resolvedClassKeyword(); + // Named types: just the type name (e.g. "_LIST_ENTRY") + // Anonymous: just the keyword (e.g. "union", "struct") if (!node.structTypeName.isEmpty()) - return base + QStringLiteral(" ") + node.structTypeName; - return base; + return node.structTypeName; + return node.resolvedClassKeyword(); } // ── Struct header / footer ── @@ -710,4 +703,27 @@ QString fmtEnumMember(const QString& name, int64_t value, int depth, int nameW) return ind + name.leftJustified(nameW) + QStringLiteral(" = ") + QString::number(value); } +// ── Bitfield member formatting ── + +uint64_t extractBits(const Provider& prov, uint64_t addr, + NodeKind containerKind, + uint8_t bitOffset, uint8_t bitWidth) { + uint64_t container = 0; + switch (containerKind) { + case NodeKind::Hex8: container = prov.readU8(addr); break; + case NodeKind::Hex16: container = prov.readU16(addr); break; + case NodeKind::Hex32: container = prov.readU32(addr); break; + default: container = prov.readU64(addr); break; + } + if (bitWidth >= 64) return container >> bitOffset; + return (container >> bitOffset) & ((1ULL << bitWidth) - 1); +} + +QString fmtBitfieldMember(const QString& name, uint8_t bitWidth, + uint64_t value, int depth, int nameW) { + QString ind = indent(depth); + return ind + name.leftJustified(nameW) + + QStringLiteral(" : %1 = %2").arg(bitWidth).arg(value); +} + } // namespace rcx::fmt diff --git a/src/imports/export_reclass_xml.cpp b/src/imports/export_reclass_xml.cpp index db270ca..2e539f4 100644 --- a/src/imports/export_reclass_xml.cpp +++ b/src/imports/export_reclass_xml.cpp @@ -115,6 +115,24 @@ bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* er while (i < children.size()) { const Node& child = tree.nodes[children[i]]; + // Bitfield container: export as hex node (ReClassEx has no bitfield concept) + if (child.kind == NodeKind::Struct + && child.resolvedClassKeyword() == QStringLiteral("bitfield")) { + int sz = child.byteSize(); + if (sz <= 0) sz = 4; + xml.writeStartElement(QStringLiteral("Node")); + xml.writeAttribute(QStringLiteral("Name"), child.name); + NodeKind hexKind = (sz <= 1) ? NodeKind::Hex8 : (sz <= 2) ? NodeKind::Hex16 + : (sz <= 4) ? NodeKind::Hex32 : NodeKind::Hex64; + xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(hexKind))); + xml.writeAttribute(QStringLiteral("Size"), QString::number(sz)); + xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false")); + xml.writeAttribute(QStringLiteral("Comment"), QStringLiteral("bitfield")); + xml.writeEndElement(); + i++; + continue; + } + // Collapse consecutive hex nodes into a single Custom node (Type=21) if (isHexNode(child.kind)) { int runStart = child.offset; diff --git a/src/imports/import_pdb.cpp b/src/imports/import_pdb.cpp index fbb91b4..0bb8672 100644 --- a/src/imports/import_pdb.cpp +++ b/src/imports/import_pdb.cpp @@ -7,6 +7,7 @@ #include #include #include +#include // ── RawPDB headers ── #include "PDB.h" @@ -415,6 +416,7 @@ void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) { auto maximumSize = rec->header.size - sizeof(uint16_t); QSet> bitfieldSlots; + QHash, uint64_t> bitfieldNodeIds; for (size_t i = 0; i < maximumSize; ) { auto* field = reinterpret_cast( @@ -440,7 +442,7 @@ void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) { if (typeRec && typeRec->header.kind == TRK::LF_BITFIELD) { uint32_t underlying = typeRec->data.LF_BITFIELD.type; uint8_t bitLen = typeRec->data.LF_BITFIELD.length; - (void)bitLen; + uint8_t bitPos = typeRec->data.LF_BITFIELD.position; // Determine slot size from underlying type uint64_t slotSize = 4; @@ -452,12 +454,26 @@ void PdbCtx::importFieldList(uint32_t fieldListIndex, uint64_t parentId) { auto key = qMakePair((int)offset, (int)slotSize); if (!bitfieldSlots.contains(key)) { bitfieldSlots.insert(key); + // Create bitfield container node Node n; - n.kind = hexForSize(slotSize); - n.name = qname; + n.kind = NodeKind::Struct; + n.classKeyword = QStringLiteral("bitfield"); + n.elementKind = hexForSize(slotSize); n.parentId = parentId; n.offset = offset; - tree.addNode(n); + n.collapsed = false; + int idx = tree.addNode(n); + bitfieldNodeIds[key] = tree.nodes[idx].id; + } + // Add this member to the bitfield container + uint64_t bfNodeId = bitfieldNodeIds[key]; + int bfIdx = tree.indexOfId(bfNodeId); + if (bfIdx >= 0) { + BitfieldMember bm; + bm.name = qname; + bm.bitOffset = bitPos; + bm.bitWidth = bitLen; + tree.nodes[bfIdx].bitfieldMembers.append(bm); } } else { importMemberType(memberType, offset, qname, parentId); @@ -641,7 +657,21 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam uint32_t resolved = findUdtDefinitionIndex(pointeeRec->header.kind, typeName); if (resolved != 0) defIndex = resolved; } - n.refId = importUDT(defIndex); + // Skip anonymous pointer targets — they'd create root orphans + const char* ptName = nullptr; + const auto* defRec2 = tt->get(defIndex); + if (defRec2) { + if (defRec2->header.kind == TRK::LF_UNION) + ptName = leafName(defRec2->data.LF_UNION.data, + unionLeafKind(defRec2->data.LF_UNION.data)); + else if (defRec2->header.kind == TRK::LF_STRUCTURE || + defRec2->header.kind == TRK::LF_CLASS) + ptName = leafName(defRec2->data.LF_CLASS.data, + defRec2->data.LF_CLASS.lfEasy.kind); + } + bool isAnonTarget = !ptName || ptName[0] == '<' || ptName[0] == '\0'; + if (!isAnonTarget) + n.refId = importUDT(defIndex); } else if (pointeeRec->header.kind == TRK::LF_PROCEDURE || pointeeRec->header.kind == TRK::LF_MFUNCTION) { n.kind = (ptrSize <= 4) ? NodeKind::FuncPtr32 : NodeKind::FuncPtr64; @@ -676,8 +706,6 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam if (resolved != 0) defIndex = resolved; } - uint64_t refId = importUDT(defIndex); - const char* typeName = nullptr; bool isUnion = (rec->header.kind == TRK::LF_UNION); if (isUnion) @@ -685,6 +713,38 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam else typeName = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind); + // Anonymous types: inline fields directly instead of creating root orphan + bool isAnonymous = !typeName || typeName[0] == '<' || typeName[0] == '\0'; + if (isAnonymous) { + // Resolve to definition if needed + const auto* defRec = tt->get(defIndex); + uint32_t fieldListIdx = 0; + if (defRec) { + if (defRec->header.kind == TRK::LF_UNION) + fieldListIdx = defRec->data.LF_UNION.field; + else if (defRec->header.kind == TRK::LF_STRUCTURE || + defRec->header.kind == TRK::LF_CLASS) + fieldListIdx = defRec->data.LF_CLASS.field; + } + if (fieldListIdx != 0) { + // Create inline container (no refId, no root orphan) + Node n; + n.kind = NodeKind::Struct; + n.name = name; + n.classKeyword = isUnion ? QStringLiteral("union") : QStringLiteral("struct"); + n.parentId = parentId; + n.offset = offset; + n.collapsed = true; + int idx = tree.addNode(n); + uint64_t inlineId = tree.nodes[idx].id; + importFieldList(fieldListIdx, inlineId); + break; + } + // Fallthrough if no field list + } + + uint64_t refId = importUDT(defIndex); + Node n; n.kind = NodeKind::Struct; n.name = name; @@ -806,16 +866,21 @@ void PdbCtx::importMemberType(uint32_t typeIndex, int offset, const QString& nam case TRK::LF_BITFIELD: { uint32_t underlying = rec->data.LF_BITFIELD.type; + uint8_t bitLen = rec->data.LF_BITFIELD.length; + uint8_t bitPos = rec->data.LF_BITFIELD.position; uint64_t slotSize = 4; if (underlying < tt->firstIndex()) { NodeKind k = mapPrimitiveType(underlying); slotSize = sizeForKind(k); } Node n; - n.kind = hexForSize(slotSize); + n.kind = NodeKind::Struct; + n.classKeyword = QStringLiteral("bitfield"); + n.elementKind = hexForSize(slotSize); n.name = name; n.parentId = parentId; n.offset = offset; + n.bitfieldMembers.append({name, bitPos, bitLen}); tree.addNode(n); break; } @@ -944,6 +1009,12 @@ QVector enumeratePdbTypes(const QString& pdbPath, QString* errorMsg result.append(info); } + int enumCount = 0; + for (const auto& r : result) + if (r.isEnum) enumCount++; + qDebug() << "[PDB] enumeratePdbTypes:" << result.size() << "types," + << enumCount << "enums"; + return result; } @@ -960,19 +1031,34 @@ NodeTree importPdbSelected(const QString& pdbPath, ctx.tt = pdb.typeTable; int total = typeIndices.size(); + int enumDispatched = 0, enumCreated = 0; for (int i = 0; i < total; i++) { uint32_t ti = typeIndices[i]; const auto* rec = pdb.typeTable->get(ti); - if (rec && rec->header.kind == TRK::LF_ENUM) - ctx.importEnum(ti); - else + if (rec && rec->header.kind == TRK::LF_ENUM) { + enumDispatched++; + uint64_t id = ctx.importEnum(ti); + if (id != 0) enumCreated++; + else qDebug() << "[PDB] importEnum FAILED for typeIndex" << ti; + } else { ctx.importUDT(ti); + } if (progressCb && !progressCb(i + 1, total)) { if (errorMsg) *errorMsg = QStringLiteral("Import cancelled"); return ctx.tree; // return partial result } } + // Count enum nodes in tree + int enumNodes = 0; + for (const auto& n : ctx.tree.nodes) + if (n.classKeyword == QLatin1String("enum")) enumNodes++; + qDebug() << "[PDB] importPdbSelected:" << total << "types," + << enumDispatched << "enum dispatches," + << enumCreated << "enum created," + << enumNodes << "enum nodes in tree," + << ctx.tree.nodes.size() << "total nodes"; + if (ctx.tree.nodes.isEmpty()) { if (errorMsg) *errorMsg = QStringLiteral("No types imported"); } diff --git a/src/imports/import_source.cpp b/src/imports/import_source.cpp index 0db8d6c..680d013 100644 --- a/src/imports/import_source.cpp +++ b/src/imports/import_source.cpp @@ -894,20 +894,40 @@ static void emitHexPadding(NodeTree& tree, uint64_t parentId, int offset, int si } } -// ── Bitfield grouping: emit a single hex node covering consecutive bitfields ── +// ── Bitfield grouping: emit a bitfield container with named members ── -static void emitBitfieldGroup(NodeTree& tree, uint64_t parentId, int offset, int totalBits) { +static void emitBitfieldGroup(NodeTree& tree, uint64_t parentId, int offset, + const QVector& fields, + int startIdx, int endIdx) { + int totalBits = 0; + for (int i = startIdx; i < endIdx; i++) + totalBits += fields[i].bitfieldWidth; int bytes = (totalBits + 7) / 8; - // Round up to nearest power-of-2 hex node - NodeKind hexKind; - if (bytes <= 1) hexKind = NodeKind::Hex8; - else if (bytes <= 2) hexKind = NodeKind::Hex16; - else if (bytes <= 4) hexKind = NodeKind::Hex32; - else hexKind = NodeKind::Hex64; + NodeKind containerKind; + if (bytes <= 1) containerKind = NodeKind::Hex8; + else if (bytes <= 2) containerKind = NodeKind::Hex16; + else if (bytes <= 4) containerKind = NodeKind::Hex32; + else containerKind = NodeKind::Hex64; + Node n; - n.kind = hexKind; + n.kind = NodeKind::Struct; + n.classKeyword = QStringLiteral("bitfield"); + n.elementKind = containerKind; n.parentId = parentId; n.offset = offset; + n.collapsed = false; + + // Populate bitfield members with computed bit offsets + uint8_t bitOffset = 0; + for (int i = startIdx; i < endIdx; i++) { + BitfieldMember bm; + bm.name = fields[i].name; + bm.bitOffset = bitOffset; + bm.bitWidth = (uint8_t)fields[i].bitfieldWidth; + n.bitfieldMembers.append(bm); + bitOffset += bm.bitWidth; + } + tree.addNode(n); } @@ -929,13 +949,14 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset, for (int fi = 0; fi < fields.size(); fi++) { const auto& field = fields[fi]; - // Bitfield group: consume consecutive bitfields, emit single hex node + // Bitfield group: consume consecutive bitfields, emit bitfield container if (field.bitfieldWidth >= 0) { int groupOffset; if (ctx.useCommentOffsets && field.commentOffset >= 0) groupOffset = field.commentOffset - baseOffset; else groupOffset = computedOffset; + int startIdx = fi; int totalBits = 0; while (fi < fields.size() && fields[fi].bitfieldWidth >= 0) { totalBits += fields[fi].bitfieldWidth; @@ -943,7 +964,8 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset, } fi--; // compensate for outer loop increment if (totalBits > 0) - emitBitfieldGroup(ctx.tree, parentId, groupOffset, totalBits); + emitBitfieldGroup(ctx.tree, parentId, groupOffset, + fields, startIdx, fi + 1); int bytes = (totalBits + 7) / 8; int nodeSize = (bytes <= 1) ? 1 : (bytes <= 2) ? 2 : (bytes <= 4) ? 4 : 8; computedOffset = groupOffset + nodeSize; diff --git a/src/main.cpp b/src/main.cpp index 549da29..1c0deb3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1417,7 +1417,9 @@ void MainWindow::removeNode() { QSet ids = ctrl->selectedIds(); QVector indices; for (uint64_t id : ids) { - int idx = ctrl->document()->tree.indexOfId(id & ~kFooterIdBit); + int idx = ctrl->document()->tree.indexOfId( + id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask + | kMemberBit | kMemberSubMask)); if (idx >= 0) indices.append(idx); } if (indices.size() > 1) @@ -1878,7 +1880,8 @@ void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) { QSet selIds = tab.ctrl->selectedIds(); if (selIds.size() >= 1) { uint64_t selId = *selIds.begin(); - selId &= ~kFooterIdBit; + selId &= ~(kFooterIdBit | kArrayElemBit | kArrayElemMask + | kMemberBit | kMemberSubMask); rootId = findRootStructForNode(tab.doc->tree, selId); } diff --git a/src/mcp/mcp_bridge.cpp b/src/mcp/mcp_bridge.cpp index fc8614b..88804bf 100644 --- a/src/mcp/mcp_bridge.cpp +++ b/src/mcp/mcp_bridge.cpp @@ -170,9 +170,12 @@ void McpBridge::processLine(const QByteArray& line) { } if (method == "initialize") { + m_mainWindow->m_statusLabel->setText(QStringLiteral("MCP: client connected")); sendJson(handleInitialize(id, req.value("params").toObject())); } else if (method == "tools/list") { + m_mainWindow->m_statusLabel->setText(QStringLiteral("MCP: tools/list")); sendJson(handleToolsList(id)); + m_mainWindow->m_statusLabel->setText(QStringLiteral("Ready")); } else if (method == "tools/call") { sendJson(handleToolsCall(id, req.value("params").toObject())); } else { @@ -211,20 +214,29 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { // 1. project.state tools.append(QJsonObject{ {"name", "project.state"}, - {"description", "Returns project state: node tree, base address, sources, provider info. " - "Use depth/parentId to avoid dumping the whole tree. " - "Call with depth:1 first to see top-level structs, then drill in with parentId."}, + {"description", "Returns project state with paginated node tree. " + "Responses return max 'limit' nodes (default 50). " + "Use depth:1 first, then parentId to drill into a struct. " + "Enum/bitfield member arrays are omitted by default (counts shown instead); " + "pass includeMembers:true to get full arrays. " + "Response includes returned/total/nextOffset for paging."}, {"inputSchema", QJsonObject{ {"type", "object"}, {"properties", QJsonObject{ {"tabIndex", QJsonObject{{"type", "integer"}, {"description", "MDI tab index (0-based). Omit for active tab."}}}, {"depth", QJsonObject{{"type", "integer"}, - {"description", "Max tree depth to return (default 1 = top-level structs only)."}}}, + {"description", "Max tree depth to return (default 1)."}}}, {"parentId", QJsonObject{{"type", "string"}, {"description", "Only return children of this node."}}}, {"includeTree", QJsonObject{{"type", "boolean"}, - {"description", "If false, return only provider/source info, no tree. Default true."}}} + {"description", "If false, return only provider/source info, no tree. Default true."}}}, + {"includeMembers", QJsonObject{{"type", "boolean"}, + {"description", "If true, include full enumMembers/bitfieldMembers arrays. Default false (shows counts only)."}}}, + {"limit", QJsonObject{{"type", "integer"}, + {"description", "Max nodes to return (default 50, max 500)."}}}, + {"offset", QJsonObject{{"type", "integer"}, + {"description", "Skip this many nodes (for pagination). Use nextOffset from previous response."}}} }} }} }); @@ -343,7 +355,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { {"description", "Trigger a UI action. Fallback for operations without dedicated tools. " "Actions: undo, redo, new_file, open_file, save_file, save_file_as, " "export_cpp, set_view_root, scroll_to_node, collapse_node, expand_node, " - "select_node, refresh"}, + "select_node, refresh. " + "export_cpp accepts optional nodeId to export a single struct (recommended for large projects)."}, {"inputSchema", QJsonObject{ {"type", "object"}, {"properties", QJsonObject{ @@ -357,6 +370,28 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { }} }); + // 8. tree.search + tools.append(QJsonObject{ + {"name", "tree.search"}, + {"description", "Search for nodes by name (substring, case-insensitive). " + "Returns compact results: id, name, kind, parentId, offset, childCount. " + "Use kindFilter to narrow (e.g. 'Struct'). Max 100 results. " + "Much faster than paging through project.state to find a specific type."}, + {"inputSchema", QJsonObject{ + {"type", "object"}, + {"properties", QJsonObject{ + {"tabIndex", QJsonObject{{"type", "integer"}, + {"description", "MDI tab index (0-based). Omit for active tab."}}}, + {"query", QJsonObject{{"type", "string"}, + {"description", "Name substring to search for (case-insensitive)."}}}, + {"kindFilter", QJsonObject{{"type", "string"}, + {"description", "Filter by node kind (e.g. 'Struct', 'Hex64', 'Array')."}}}, + {"limit", QJsonObject{{"type", "integer"}, + {"description", "Max results to return (default 20, max 100)."}}} + }} + }} + }); + return okReply(id, QJsonObject{{"tools", tools}}); } @@ -368,6 +403,10 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject& QString toolName = params.value("name").toString(); QJsonObject args = params.value("arguments").toObject(); + // Show tool activity in status bar + m_mainWindow->m_statusLabel->setText(QStringLiteral("MCP: %1").arg(toolName)); + QCoreApplication::processEvents(); // paint immediately + QJsonObject result; if (toolName == "project.state") result = toolProjectState(args); else if (toolName == "tree.apply") result = toolTreeApply(args); @@ -376,8 +415,11 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject& else if (toolName == "hex.write") result = toolHexWrite(args); else if (toolName == "status.set") result = toolStatusSet(args); else if (toolName == "ui.action") result = toolUiAction(args); + else if (toolName == "tree.search") result = toolTreeSearch(args); else return errReply(id, -32601, "Unknown tool: " + toolName); + m_mainWindow->m_statusLabel->setText(QStringLiteral("Ready")); + return okReply(id, result); } @@ -436,6 +478,9 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) { int maxDepth = args.value("depth").toInt(1); bool includeTree = args.contains("includeTree") ? args.value("includeTree").toBool() : true; + bool includeMembers = args.value("includeMembers").toBool(false); + int limit = qBound(1, args.value("limit").toInt(50), 500); + int offset = qMax(0, args.value("offset").toInt(0)); QString parentIdStr = args.value("parentId").toString(); uint64_t filterParentId = parentIdStr.isEmpty() ? 0 : parentIdStr.toULongLong(); @@ -489,12 +534,15 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) { for (int i = 0; i < tree.nodes.size(); i++) childMap[tree.nodes[i].parentId].append(i); - // BFS from filterParentId, respecting maxDepth + // BFS from filterParentId, respecting maxDepth + pagination QJsonArray nodeArr; struct QueueEntry { uint64_t parentId; int depth; }; QVector queue; queue.append({filterParentId, 0}); + int totalCount = 0; // total nodes that match depth filter + int emitted = 0; + while (!queue.isEmpty()) { auto entry = queue.takeFirst(); if (entry.depth > maxDepth) continue; @@ -502,13 +550,47 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) { const auto& kids = childMap.value(entry.parentId); for (int ci : kids) { const Node& n = tree.nodes[ci]; + + // Count all matching nodes for pagination metadata + totalCount++; + + // Apply offset/limit pagination + if (totalCount <= offset) { + // Still skipping — but enqueue children for counting + if (entry.depth + 1 <= maxDepth) + queue.append({n.id, entry.depth + 1}); + continue; + } + if (emitted >= limit) { + // Past limit — just keep counting total + if (entry.depth + 1 <= maxDepth) + queue.append({n.id, entry.depth + 1}); + continue; + } + QJsonObject nj = n.toJson(); + + // Strip inline member arrays unless requested + if (!includeMembers) { + if (nj.contains("enumMembers")) { + int count = nj.value("enumMembers").toArray().size(); + nj.remove("enumMembers"); + nj["enumMemberCount"] = count; + } + if (nj.contains("bitfieldMembers")) { + int count = nj.value("bitfieldMembers").toArray().size(); + nj.remove("bitfieldMembers"); + nj["bitfieldMemberCount"] = count; + } + } + // Add computed size for containers if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array) { nj["computedSize"] = tree.structSpan(n.id, &childMap); nj["childCount"] = childMap.value(n.id).size(); } nodeArr.append(nj); + emitted++; // Enqueue children if we haven't hit depth limit if (entry.depth + 1 <= maxDepth) @@ -520,6 +602,10 @@ QJsonObject McpBridge::toolProjectState(const QJsonObject& args) { treeObj["baseAddress"] = QString::number(tree.baseAddress, 16); treeObj["nextId"] = QString::number(tree.m_nextId); treeObj["nodes"] = nodeArr; + treeObj["returned"] = emitted; + treeObj["total"] = totalCount; + if (emitted < totalCount) + treeObj["nextOffset"] = offset + emitted; state["tree"] = treeObj; } @@ -1004,7 +1090,24 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) { if (action == "export_cpp") { if (!doc) return makeTextResult("No active tab", true); const QHash* aliases = doc->typeAliases.isEmpty() ? nullptr : &doc->typeAliases; - QString code = renderCppAll(doc->tree, aliases); + QString code; + if (!nodeIdStr.isEmpty()) { + // Per-struct export + uint64_t nid = nodeIdStr.toULongLong(); + code = renderCpp(doc->tree, nid, aliases); + if (code.isEmpty()) + return makeTextResult("Node not found or not a struct: " + nodeIdStr, true); + } else { + code = renderCppAll(doc->tree, aliases); + } + // Truncate if too large (64 KB limit) + if (code.size() > 65536) { + int totalSize = code.size(); + code.truncate(65536); + code += QStringLiteral("\n\n... truncated (%1 bytes total, showing first 64KB)" + "\nUse nodeId param to export a single struct.") + .arg(totalSize); + } return makeTextResult(code); } if (action == "save_file") { @@ -1053,6 +1156,70 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) { return makeTextResult("Unknown action: " + action, true); } +// ════════════════════════════════════════════════════════════════════ +// TOOL: tree.search +// ════════════════════════════════════════════════════════════════════ + +QJsonObject McpBridge::toolTreeSearch(const QJsonObject& args) { + auto* tab = resolveTab(args); + if (!tab) return makeTextResult("No active tab", true); + + const auto& tree = tab->doc->tree; + QString query = args.value("query").toString(); + QString kindFilter = args.value("kindFilter").toString(); + int limit = qBound(1, args.value("limit").toInt(20), 100); + + if (query.isEmpty() && kindFilter.isEmpty()) + return makeTextResult("Provide 'query' (name substring) and/or 'kindFilter' (e.g. 'Struct')", true); + + // Build parent→children map for childCount + QHash childCounts; + for (const auto& n : tree.nodes) + childCounts[n.parentId]++; + + QJsonArray results; + for (const auto& n : tree.nodes) { + // Kind filter + if (!kindFilter.isEmpty()) { + if (kindToString(n.kind) != kindFilter) continue; + } + // Name substring match (case-insensitive) + if (!query.isEmpty()) { + bool nameMatch = n.name.contains(query, Qt::CaseInsensitive); + bool typeMatch = n.structTypeName.contains(query, Qt::CaseInsensitive); + if (!nameMatch && !typeMatch) continue; + } + + QJsonObject nj; + nj["id"] = QString::number(n.id); + nj["name"] = n.name; + nj["kind"] = kindToString(n.kind); + nj["parentId"] = QString::number(n.parentId); + nj["offset"] = n.offset; + if (!n.structTypeName.isEmpty()) + nj["structTypeName"] = n.structTypeName; + if (!n.classKeyword.isEmpty()) + nj["classKeyword"] = n.classKeyword; + if (n.kind == NodeKind::Struct || n.kind == NodeKind::Array) + nj["childCount"] = childCounts.value(n.id, 0); + if (!n.enumMembers.isEmpty()) + nj["enumMemberCount"] = n.enumMembers.size(); + if (!n.bitfieldMembers.isEmpty()) + nj["bitfieldMemberCount"] = n.bitfieldMembers.size(); + results.append(nj); + + if (results.size() >= limit) break; + } + + QJsonObject out; + out["results"] = results; + out["count"] = results.size(); + out["query"] = query; + if (!kindFilter.isEmpty()) out["kindFilter"] = kindFilter; + return makeTextResult(QString::fromUtf8( + QJsonDocument(out).toJson(QJsonDocument::Indented))); +} + // ════════════════════════════════════════════════════════════════════ // Notifications (call from MainWindow/Controller hooks) // ════════════════════════════════════════════════════════════════════ diff --git a/src/mcp/mcp_bridge.h b/src/mcp/mcp_bridge.h index 2f1d8ab..05458c0 100644 --- a/src/mcp/mcp_bridge.h +++ b/src/mcp/mcp_bridge.h @@ -58,6 +58,7 @@ private: QJsonObject toolHexWrite(const QJsonObject& args); QJsonObject toolStatusSet(const QJsonObject& args); QJsonObject toolUiAction(const QJsonObject& args); + QJsonObject toolTreeSearch(const QJsonObject& args); // Helpers QJsonObject makeTextResult(const QString& text, bool isError = false); diff --git a/tests/test_command_row.cpp b/tests/test_command_row.cpp index 00d11d7..9fbbf61 100644 --- a/tests/test_command_row.cpp +++ b/tests/test_command_row.cpp @@ -21,7 +21,7 @@ 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 \u00B7 %2").arg(src, addr); + return QStringLiteral(" %1 %2").arg(src, addr); } // -- Replicate commandRowSrcSpan for testing @@ -32,17 +32,13 @@ struct TestColumnSpan { }; static TestColumnSpan commandRowSrcSpan(const QString& lineText) { - int idx = lineText.indexOf(QStringLiteral(" \u00B7")); - if (idx < 0) return {}; + int arrow = lineText.indexOf(QChar(0x25BE)); + if (arrow < 0) return {}; int start = 0; - while (start < idx && !lineText[start].isLetterOrNumber() + while (start < arrow && !lineText[start].isLetterOrNumber() && lineText[start] != '<' && lineText[start] != '\'') start++; - if (start >= idx) return {}; - // 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}; + if (start >= arrow) return {}; + return {start, arrow, true}; } class TestCommandRow : public QObject { @@ -77,13 +73,13 @@ private slots: void row_nullProvider() { NullProvider p; QString row = buildCommandRow(p, 0); - QCOMPARE(row, QStringLiteral(" source\u25BE \u00B7 0x0")); + QCOMPARE(row, QStringLiteral(" source\u25BE 0x0")); } void row_fileProvider() { BufferProvider p(QByteArray(4, '\0'), "test.bin"); QString row = buildCommandRow(p, 0x140000000ULL); - QCOMPARE(row, QStringLiteral(" 'test.bin'\u25BE \u00B7 0x140000000")); + QCOMPARE(row, QStringLiteral(" 'test.bin'\u25BE 0x140000000")); } // --------------------------------------------------------------- @@ -110,7 +106,7 @@ private slots: void span_processProvider_simulated() { // Simulate a process provider without needing Windows APIs // by building the string directly - QString row = QStringLiteral(" 'notepad.exe'\u25BE \u00B7 0x7FF600000000"); + QString row = QStringLiteral(" 'notepad.exe'\u25BE 0x7FF600000000"); auto span = commandRowSrcSpan(row); QVERIFY(span.valid); QString extracted = row.mid(span.start, span.end - span.start); diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index 1baae25..3297d59 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -1924,7 +1924,7 @@ private slots: void testCommandRowRootNameSpan() { // Name span should cover the class name in the merged command row - QString text = "source\u25BE \u00B7 0x0 \u00B7 struct MyClass {"; + QString text = "source\u25BE 0x0 struct MyClass {"; ColumnSpan nameSpan = commandRowRootNameSpan(text); QVERIFY(nameSpan.valid); @@ -2173,8 +2173,8 @@ private slots: QVERIFY(result.text.contains("Blue")); QVERIFY(result.text.contains("= 0")); QVERIFY(result.text.contains("= 2")); - // Header should contain "enum" - QVERIFY(result.text.contains("enum")); + // Header should contain the type name + QVERIFY(result.text.contains("Color")); } void testEnumCollapsed() { @@ -2205,8 +2205,7 @@ private slots: // Collapsed: members should NOT appear QVERIFY(!result.text.contains("= 0")); QVERIFY(!result.text.contains("= 1")); - // But header should still show - QVERIFY(result.text.contains("enum")); + // But header should still show the type name QVERIFY(result.text.contains("Flags")); } @@ -2351,6 +2350,91 @@ private slots: } } + void testBitfieldMembers() { + NodeTree tree; + tree.baseAddress = 0; + + Node root; + root.kind = NodeKind::Struct; + root.name = QStringLiteral("Test"); + root.structTypeName = QStringLiteral("Test"); + int ri = tree.addNode(root); + uint64_t rootId = tree.nodes[ri].id; + + Node bf; + bf.kind = NodeKind::Struct; + bf.classKeyword = QStringLiteral("bitfield"); + bf.name = QStringLiteral("flags"); + bf.elementKind = NodeKind::Hex32; + bf.parentId = rootId; + bf.offset = 0; + bf.collapsed = false; + bf.bitfieldMembers = { + {QStringLiteral("Valid"), 0, 1}, + {QStringLiteral("Dirty"), 1, 1}, + {QStringLiteral("PageNum"), 2, 20} + }; + tree.addNode(bf); + + NullProvider prov; + auto result = compose(tree, prov); + + // Should contain bitfield member names + QVERIFY(result.text.contains(QStringLiteral("Valid"))); + QVERIFY(result.text.contains(QStringLiteral("Dirty"))); + QVERIFY(result.text.contains(QStringLiteral("PageNum"))); + // Should contain : width = value format + QVERIFY(result.text.contains(QStringLiteral(": 1 ="))); + QVERIFY(result.text.contains(QStringLiteral(": 20 ="))); + // Member lines should have isMemberLine set + bool foundMemberLine = false; + for (const auto& lm : result.meta) { + if (lm.isMemberLine) { + foundMemberLine = true; + break; + } + } + QVERIFY(foundMemberLine); + } + + void testBitfieldJsonRoundtrip() { + Node n; + n.id = 42; + n.kind = NodeKind::Struct; + n.classKeyword = QStringLiteral("bitfield"); + n.elementKind = NodeKind::Hex64; + n.bitfieldMembers = { + {QStringLiteral("ExecuteDisable"), 63, 1}, + {QStringLiteral("PageFrameNumber"), 12, 36} + }; + + QJsonObject json = n.toJson(); + Node restored = Node::fromJson(json); + + QCOMPARE(restored.classKeyword, QStringLiteral("bitfield")); + QCOMPARE(restored.bitfieldMembers.size(), 2); + QCOMPARE(restored.bitfieldMembers[0].name, QStringLiteral("ExecuteDisable")); + QCOMPARE(restored.bitfieldMembers[0].bitOffset, (uint8_t)63); + QCOMPARE(restored.bitfieldMembers[0].bitWidth, (uint8_t)1); + QCOMPARE(restored.bitfieldMembers[1].name, QStringLiteral("PageFrameNumber")); + QCOMPARE(restored.bitfieldMembers[1].bitOffset, (uint8_t)12); + QCOMPARE(restored.bitfieldMembers[1].bitWidth, (uint8_t)36); + } + + void testBitfieldByteSize() { + Node n; + n.kind = NodeKind::Struct; + n.classKeyword = QStringLiteral("bitfield"); + n.elementKind = NodeKind::Hex8; + QCOMPARE(n.byteSize(), 1); + n.elementKind = NodeKind::Hex16; + QCOMPARE(n.byteSize(), 2); + n.elementKind = NodeKind::Hex32; + QCOMPARE(n.byteSize(), 4); + n.elementKind = NodeKind::Hex64; + QCOMPARE(n.byteSize(), 8); + } + }; QTEST_MAIN(TestCompose) diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index 17b4c89..3ac16c7 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -481,7 +481,7 @@ private slots: // Set CommandRow text with an ADDR value (simulates controller.updateCommandRow) m_editor->setCommandRowText( - QStringLiteral("source\u25BE \u00B7 0xD87B5E5000")); + QStringLiteral("source\u25BE 0xD87B5E5000")); // BaseAddress should be ALLOWED on CommandRow (ADDR field) bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); @@ -816,7 +816,7 @@ private slots: // Set CommandRow text with ADDR value (simulates controller) m_editor->setCommandRowText( - QStringLiteral("source\u25BE \u00B7 0xD87B5E5000")); + QStringLiteral("source\u25BE 0xD87B5E5000")); // Line 0 is CommandRow const LineMeta* lm = m_editor->metaForLine(0); @@ -901,7 +901,7 @@ private slots: // Set CommandRow text with ADDR value (simulates controller) m_editor->setCommandRowText( - QStringLiteral("source\u25BE \u00B7 0xD87B5E5000")); + QStringLiteral("source\u25BE 0xD87B5E5000")); // Begin base address edit on line 0 (CommandRow ADDR field) bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); @@ -1038,7 +1038,7 @@ private slots: // Set CommandRow text with root class (simulates controller.updateCommandRow) m_editor->setCommandRowText( - QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {")); + QStringLiteral("source\u25BE 0xD87B5E5000 struct _PEB64 {")); // RootClassName should be allowed on CommandRow (line 0) bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 0); @@ -1053,7 +1053,7 @@ private slots: // Set CommandRow with root class m_editor->setCommandRowText( - QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {")); + QStringLiteral("source\u25BE 0xD87B5E5000 struct _PEB64 {")); // Line 0 is CommandRow const LineMeta* lm = m_editor->metaForLine(0); @@ -1099,7 +1099,7 @@ private slots: // Set command row text (simulates controller.updateCommandRow) QString cmdText = QStringLiteral( - "source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"); + "source\u25BE 0xD87B5E5000 struct _PEB64 {"); m_editor->setCommandRowText(cmdText); QApplication::processEvents(); @@ -1177,7 +1177,7 @@ private slots: m_editor->applyDocument(m_result); QString cmdText = QStringLiteral( - "source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"); + "source\u25BE 0xD87B5E5000 struct _PEB64 {"); m_editor->setCommandRowText(cmdText); QApplication::processEvents(); diff --git a/tests/test_format.cpp b/tests/test_format.cpp index 8690f56..6c65c46 100644 --- a/tests/test_format.cpp +++ b/tests/test_format.cpp @@ -29,12 +29,12 @@ private slots: } void testFmtPointer64_null() { - QCOMPARE(fmt::fmtPointer64(0), QString("-> NULL")); + QCOMPARE(fmt::fmtPointer64(0), QString("0x0")); } void testFmtPointer64_nonNull() { QString s = fmt::fmtPointer64(0x400000); - QVERIFY(s.startsWith("-> 0x")); + QVERIFY(s.startsWith("0x")); QVERIFY(s.contains("400000")); } diff --git a/tests/test_import_source.cpp b/tests/test_import_source.cpp index 43cc30b..0602e72 100644 --- a/tests/test_import_source.cpp +++ b/tests/test_import_source.cpp @@ -780,7 +780,7 @@ void TestImportSource::structPrefixOnType() { } void TestImportSource::bitfieldSkipped() { - // Bitfields emit a hex placeholder covering the group + // Bitfields emit a bitfield container with named members NodeTree tree = importFromSource(QStringLiteral( "struct BF {\n" " uint32_t normal;\n" @@ -790,12 +790,20 @@ void TestImportSource::bitfieldSkipped() { "};\n" )); auto kids = childrenOf(tree, tree.nodes[0].id); - // normal + Hex16 (16 bits → 2 bytes) + after + // normal + bitfield container (16 bits → 2 bytes) + after QCOMPARE(kids.size(), 3); QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal")); QCOMPARE(tree.nodes[kids[0]].offset, 0); - QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex16); + QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Struct); + QCOMPARE(tree.nodes[kids[1]].resolvedClassKeyword(), QStringLiteral("bitfield")); QCOMPARE(tree.nodes[kids[1]].offset, 4); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers.size(), 2); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].name, QStringLiteral("bitA")); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].bitWidth, (uint8_t)4); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].bitOffset, (uint8_t)0); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].name, QStringLiteral("bitB")); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].bitWidth, (uint8_t)12); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].bitOffset, (uint8_t)4); QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after")); QCOMPARE(tree.nodes[kids[2]].offset, 6); } @@ -812,13 +820,22 @@ void TestImportSource::bitfieldWithOffsetsEmitsHex() { "};\n" )); auto kids = childrenOf(tree, tree.nodes[0].id); - // normal + hex64 (bitfield group: 64 bits) + after = 3 + // normal + bitfield container (64 bits) + after = 3 QCOMPARE(kids.size(), 3); QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal")); QCOMPARE(tree.nodes[kids[0]].offset, 0); - // Bitfield group emitted as Hex64 at offset 4 - QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex64); + // Bitfield container at offset 4 + QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Struct); + QCOMPARE(tree.nodes[kids[1]].resolvedClassKeyword(), QStringLiteral("bitfield")); QCOMPARE(tree.nodes[kids[1]].offset, 4); + QCOMPARE(tree.nodes[kids[1]].elementKind, NodeKind::Hex64); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers.size(), 4); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].name, QStringLiteral("Valid")); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[0].bitWidth, (uint8_t)1); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[1].name, QStringLiteral("Dirty")); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[2].name, QStringLiteral("PageFrameNumber")); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[2].bitWidth, (uint8_t)36); + QCOMPARE(tree.nodes[kids[1]].bitfieldMembers[3].name, QStringLiteral("Reserved")); // after at 0xC QCOMPARE(tree.nodes[kids[2]].name, QStringLiteral("after")); QCOMPARE(tree.nodes[kids[2]].offset, 0xC); diff --git a/tests/test_type_selector.cpp b/tests/test_type_selector.cpp index 838e274..b6907c8 100644 --- a/tests/test_type_selector.cpp +++ b/tests/test_type_selector.cpp @@ -63,7 +63,7 @@ private slots: // ── Chevron span detection ── void testChevronSpanDetected() { - QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {"); + QString text = QStringLiteral("[\u25B8] source\u25BE 0x1000 struct Alpha {"); ColumnSpan span = commandRowChevronSpan(text); QVERIFY(span.valid); QCOMPARE(span.start, 0); @@ -80,7 +80,7 @@ private slots: // ── Existing spans unbroken by chevron prefix ── void testSpansWithPrefix() { - QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {"); + QString text = QStringLiteral("[\u25B8] source\u25BE 0x1000 struct Alpha {"); ColumnSpan src = commandRowSrcSpan(text); QVERIFY(src.valid);