mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: enum/bitfield editing, MCP guard rails, PDB anonymous type inlining
- Enum inline editing: name/value commit handling, auto-sort by value - Bitfield support in PDB import with proper container nodes - Per-member hover/selection highlighting (kMemberBit encoding) - Context menu fixes for enum/bitfield member lines - MCP pagination (limit/offset), includeMembers param, tree.search tool - MCP status bar activity indicator for tool calls - PDB anonymous type inlining: inline <unnamed-tag> types as children - Skip anonymous pointer targets to prevent root orphans - Enum import diagnostics for debugging missing enums
This commit is contained in:
@@ -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<int> 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<int>& 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;
|
||||
|
||||
@@ -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<uint64_t> 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<T, cmd::ChangeEnumMembers>) {
|
||||
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);
|
||||
|
||||
@@ -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<int>& nodeIndices);
|
||||
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
|
||||
|
||||
132
src/core.h
132
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<QPair<QString, int64_t>> enumMembers; // Enum: name→value pairs
|
||||
QVector<BitfieldMember> 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<QPair<QString, int64_t>> 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 ──
|
||||
|
||||
@@ -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<uint64_t>& 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<uint64_t>& 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);
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QHash>
|
||||
#include <QPair>
|
||||
#include <QSet>
|
||||
#include <QDebug>
|
||||
|
||||
// ── 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<QPair<int,int>> bitfieldSlots;
|
||||
QHash<QPair<int,int>, uint64_t> bitfieldNodeIds;
|
||||
|
||||
for (size_t i = 0; i < maximumSize; ) {
|
||||
auto* field = reinterpret_cast<const PDB::CodeView::TPI::FieldList*>(
|
||||
@@ -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<PdbTypeInfo> 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");
|
||||
}
|
||||
|
||||
@@ -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<ParsedField>& 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;
|
||||
|
||||
@@ -1417,7 +1417,9 @@ void MainWindow::removeNode() {
|
||||
QSet<uint64_t> ids = ctrl->selectedIds();
|
||||
QVector<int> 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<uint64_t> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<QueueEntry> 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<NodeKind, QString>* 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<uint64_t, int> 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)
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user