Compare commits

...

6 Commits

Author SHA1 Message Date
IChooseYou
483f87cfbd feat: type hints green [bracketed] notation, workspace cleanup, unique naming
- Type inference hints now show value-first with bracketed type in comment
  green: "0x7ff718570000 [ptr64]", "6, 16 [int32_t×2]"
- Raise hint threshold to strong-only (score >= 75%)
- Remove Bool inference, widen Int16 range to ±16384
- Workspace: remove dead WorkspaceProxy, fix null deref, debounce search,
  cache icons, add pinning support
- Unique naming: UnnamedClass0/UnnamedEnum1 with global counter
- Footer buttons: +10h +100h +1000h replacing +1024
- MCP: project lifecycle API, snapshot provider fix
2026-03-09 10:39:22 -06:00
IChooseYou
a21e5a07a8 feat: replace +1024 footer button with +10h +100h +1000h granular grow
- Three hex-sized grow buttons: +10h (16B), +100h (256B), +1000h (4096B)
- Single-space gaps between buttons for tighter layout
- All click, hover, cursor, and pill styling updated
- Enum +10 button unchanged and correctly disambiguated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 16:38:03 -06:00
IChooseYou
25afbe373b feat: status bar format, tab titles with source, taller tabs, pill hover, source switch base fix
- Status bar: show StructName.field +0xOFFSET with dimmed offset suffix
- Status bar: sync font to global editor font (JetBrains Mono 10pt)
- Dock tab title: include active source name (StructName — source.exe)
- Dock tabs +10% height (28→31), pane tabs (24→26), workspace title (26→29)
- Footer pills (+1024, Trim, +10): add visual hover highlight via IND_HOVER_SPAN
- Fix source switch keeping old base address for plugin providers
2026-03-08 16:29:12 -06:00
IChooseYou
6a4cb47ed4 fix: kill Fusion outline on QScintilla, type inference hints, workspace styling
- Suppress PE_Frame on QsciScintilla in MenuBarStyle to eliminate the
  1px dark (#171717) Fusion outline around the editor area
- Add --screenshot flag for automated pixel regression testing
- Add type inference engine (typeinfer.h) with hex pattern analysis
- Show inferred type hints on hex nodes in compose output
- Style workspace tree corner/header widgets to match theme
- Fix integer overflow in compose.cpp array element addressing
- Fix integer overflow in core.h structSpan calculation
- Add bounds check on activePaneIdx in controller
- Use QPointer for deferred dock lambda safety
- Workspace delegate uses icon Normal/Disabled for viewed state
2026-03-08 10:26:12 -06:00
IChooseYou
431e2b90c9 perf: TypeSelector — zero-alloc fuzzy scorer, warm popup 75% faster
Stack arrays + pre-lowered QChars in fuzzyScore eliminate all heap
allocations in the hot path. applyFilter uses indices instead of
deep-copying TypeEntry. popup() width estimated from cached max name
length. QListView: uniform sizes, batched layout, cached sizeHint.

Benchmark (5000 structs): warm popup 27ms→7ms, filter 5ms→1.7ms.
2026-03-08 08:33:21 -06:00
IChooseYou
43365c1aff fix: close project actually destroys dock, editor perf single-pass line attributes
- Set WA_DeleteOnClose on doc docks so all close paths trigger cleanup
- Create fresh empty class when last project closes
- Add splitDockWidget/resizeDocks to project_new() so workspace doesn't eat editor space
- Merge applyMarginText, applyMarkers, applyFoldLevels into single-pass applyLineAttributes
- Cache line texts for heatmap/symbol coloring passes (avoid redundant Scintilla IPC)
- Zero-alloc scroll width scan replaces QString::split
2026-03-08 08:13:36 -06:00
30 changed files with 2428 additions and 406894 deletions

View File

@@ -314,6 +314,11 @@ if(BUILD_TESTING)
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test) target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_core COMMAND test_core) add_test(NAME test_core COMMAND test_core)
add_executable(test_typeinfer tests/test_typeinfer.cpp)
target_include_directories(test_typeinfer PRIVATE src)
target_link_libraries(test_typeinfer PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_typeinfer COMMAND test_typeinfer)
add_executable(test_format tests/test_format.cpp src/format.cpp src/addressparser.cpp) add_executable(test_format tests/test_format.cpp src/format.cpp src/addressparser.cpp)
target_include_directories(test_format PRIVATE src) target_include_directories(test_format PRIVATE src)
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test) target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
@@ -423,20 +428,6 @@ if(BUILD_TESTING)
endif() endif()
add_test(NAME test_controller COMMAND test_controller) add_test(NAME test_controller COMMAND test_controller)
add_executable(test_validation tests/test_validation.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_validation PRIVATE src third_party/fadec)
target_link_libraries(test_validation PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_validation COMMAND test_validation)
add_executable(test_context_menu tests/test_context_menu.cpp add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
@@ -483,20 +474,6 @@ if(BUILD_TESTING)
QScintilla::QScintilla) QScintilla::QScintilla)
add_test(NAME test_rendered_view COMMAND test_rendered_view) add_test(NAME test_rendered_view COMMAND test_rendered_view)
add_executable(test_new_features tests/test_new_features.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_new_features PRIVATE src third_party/fadec)
target_link_libraries(test_new_features PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_new_features COMMAND test_new_features)
add_executable(test_type_selector tests/test_type_selector.cpp add_executable(test_type_selector tests/test_type_selector.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp

View File

@@ -1,4 +1,5 @@
#include "core.h" #include "core.h"
#include "typeinfer.h"
#include "addressparser.h" #include "addressparser.h"
#include <algorithm> #include <algorithm>
#include <numeric> #include <numeric>
@@ -7,6 +8,49 @@ namespace rcx {
namespace { namespace {
// ── Value preview for type hints ──
// Formats raw bytes as the suggested type using existing fmt:: functions.
static QString formatPreview(const uint8_t* data, int len, const TypeSuggestion& s) {
using namespace detail;
if (s.kinds.isEmpty()) return {};
NodeKind k = s.kinds[0];
if (s.kinds.size() == 1) {
switch (k) {
case NodeKind::Float: return fmt::fmtFloat(loadF32(data));
case NodeKind::Double: return fmt::fmtDouble(loadF64(data));
case NodeKind::Int32: return fmt::fmtInt32((int32_t)loadU32(data));
case NodeKind::UInt32: return fmt::fmtUInt32(loadU32(data));
case NodeKind::Int16: return fmt::fmtInt16((int16_t)loadU16(data));
case NodeKind::UInt16: return fmt::fmtUInt16(loadU16(data));
case NodeKind::Int64: return fmt::fmtInt64((int64_t)loadU64(data));
case NodeKind::UInt64: return fmt::fmtUInt64(loadU64(data));
case NodeKind::Pointer64: return fmt::fmtPointer64(loadU64(data));
case NodeKind::Pointer32: return fmt::fmtPointer32(loadU32(data));
case NodeKind::Bool: return fmt::fmtBool(data[0]);
case NodeKind::UTF8: {
int n = std::min(len, 8);
QString s;
for (int i = 0; i < n && data[i] >= 0x20 && data[i] <= 0x7E; ++i)
s += QLatin1Char(data[i]);
return s.isEmpty() ? QString() : (QStringLiteral("\"") + s + QStringLiteral("\""));
}
default: return {};
}
}
// Split: show each part
int partSz = len / s.kinds.size();
QStringList parts;
for (int i = 0; i < s.kinds.size(); ++i) {
TypeSuggestion sub;
sub.kinds = {s.kinds[i]};
sub.score = s.score;
sub.strength = s.strength;
parts << formatPreview(data + i * partSz, partSz, sub);
}
return parts.join(QStringLiteral(", "));
}
// Scintilla fold constants (avoid including Scintilla headers in core) // Scintilla fold constants (avoid including Scintilla headers in core)
constexpr int SC_FOLDLEVELBASE = 0x400; constexpr int SC_FOLDLEVELBASE = 0x400;
constexpr int SC_FOLDLEVELHEADERFLAG = 0x2000; constexpr int SC_FOLDLEVELHEADERFLAG = 0x2000;
@@ -26,6 +70,7 @@ struct ComposeState {
bool compactColumns = false; // compact column mode: cap type width, overflow long types bool compactColumns = false; // compact column mode: cap type width, overflow long types
bool treeLines = false; // draw Unicode tree connectors in indentation bool treeLines = false; // draw Unicode tree connectors in indentation
bool braceWrap = false; // opening brace on its own line bool braceWrap = false; // opening brace on its own line
bool typeHints = false; // show type inference hints on hex nodes
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
@@ -208,6 +253,29 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub, QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
/*comment=*/{}, typeW, nameW, ptrTypeOverride, /*comment=*/{}, typeW, nameW, ptrTypeOverride,
state.compactColumns); state.compactColumns);
// Type inference hint for hex nodes (when enabled)
if (state.typeHints && isHexNode(node.kind) && sub == 0) {
const int sz = sizeForKind(node.kind);
QByteArray b = prov.isReadable(absAddr, sz)
? prov.readBytes(absAddr, sz) : QByteArray(sz, '\0');
auto suggestions = inferTypes(
reinterpret_cast<const uint8_t*>(b.constData()), sz);
if (!suggestions.isEmpty() && suggestions[0].strength >= 3) {
lm.typeHintStart = lineText.size() + 2; // after " " gap
lm.typeHintKinds = suggestions[0].kinds;
QString typeName = formatHint(suggestions[0]);
QString preview = formatPreview(
reinterpret_cast<const uint8_t*>(b.constData()), sz, suggestions[0]);
// Value-first with bracketed type: "0x7ff718570000 [ptr64]"
if (!preview.isEmpty())
lm.typeHint = preview + QStringLiteral(" [") + typeName + QStringLiteral("]");
else
lm.typeHint = QStringLiteral("[") + typeName + QStringLiteral("]");
lineText += QStringLiteral(" ") + lm.typeHint;
}
}
state.emitLine(lineText, std::move(lm)); state.emitLine(lineText, std::move(lm));
} }
} }
@@ -469,7 +537,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
int eNW = state.effectiveNameW(node.id); int eNW = state.effectiveNameW(node.id);
for (int i = 0; i < node.arrayLen; i++) { for (int i = 0; i < node.arrayLen; i++) {
state.setTreeSibling(childDepth, i < node.arrayLen - 1); state.setTreeSibling(childDepth, i < node.arrayLen - 1);
uint64_t elemAddr = absAddr + i * elemSize; uint64_t elemAddr = absAddr + (uint64_t)i * elemSize;
// Type override: "float[0]", "uint32_t[1]", etc. // Type override: "float[0]", "uint32_t[1]", etc.
QString elemTypeStr = fmt::typeNameRaw(node.elementKind) QString elemTypeStr = fmt::typeNameRaw(node.elementKind)
@@ -478,7 +546,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
Node elem; Node elem;
elem.kind = node.elementKind; elem.kind = node.elementKind;
elem.name = QString(); // no name for array elements elem.name = QString(); // no name for array elements
elem.offset = node.offset + i * elemSize; elem.offset = node.offset + (int)((uint64_t)i * elemSize);
elem.parentId = node.id; elem.parentId = node.id;
elem.id = 0; elem.id = 0;
@@ -971,11 +1039,13 @@ void composeNode(ComposeState& state, const NodeTree& tree,
} // anonymous namespace } // anonymous namespace
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId, ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
bool compactColumns, bool treeLines, bool braceWrap) { bool compactColumns, bool treeLines, bool braceWrap,
bool typeHints) {
ComposeState state; ComposeState state;
state.compactColumns = compactColumns; state.compactColumns = compactColumns;
state.treeLines = treeLines; state.treeLines = treeLines;
state.braceWrap = braceWrap; state.braceWrap = braceWrap;
state.typeHints = typeHints;
// Precompute parent→children map // Precompute parent→children map
for (int i = 0; i < tree.nodes.size(); i++) for (int i = 0; i < tree.nodes.size(); i++)

View File

@@ -73,8 +73,8 @@ RcxDocument::RcxDocument(QObject* parent)
} }
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns, ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
bool treeLines, bool braceWrap) const { bool treeLines, bool braceWrap, bool typeHints) const {
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap); return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap, typeHints);
} }
bool RcxDocument::save(const QString& path) { bool RcxDocument::save(const QString& path) {
@@ -190,9 +190,10 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
// Eagerly pre-warm the type popup so first click isn't slow (~350ms cold start). // Eagerly pre-warm the type popup so first click isn't slow (~350ms cold start).
if (!m_cachedPopup) { if (!m_cachedPopup) {
QTimer::singleShot(0, this, [this, editor]() { QPointer<RcxEditor> safeEditor = editor;
if (!m_cachedPopup && !m_editors.isEmpty()) QTimer::singleShot(0, this, [this, safeEditor]() {
ensurePopup(editor); if (!m_cachedPopup && !m_editors.isEmpty() && safeEditor)
ensurePopup(safeEditor);
}); });
} }
return editor; return editor;
@@ -200,7 +201,7 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
void RcxController::removeSplitEditor(RcxEditor* editor) { void RcxController::removeSplitEditor(RcxEditor* editor) {
m_editors.removeOne(editor); m_editors.removeOne(editor);
// Caller (MainWindow) owns the parent QTabWidget and handles widget destruction. editor->disconnect(this);
} }
void RcxController::connectEditor(RcxEditor* editor) { void RcxController::connectEditor(RcxEditor* editor) {
@@ -246,6 +247,67 @@ void RcxController::connectEditor(RcxEditor* editor) {
} }
}); });
// Footer "+1024" button
connect(editor, &RcxEditor::appendBytesRequested,
this, [this](uint64_t structId, int byteCount) {
int hex64Count = byteCount / 8;
int remainBytes = byteCount % 8;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
for (int i = 0; i < hex64Count; i++)
insertNode(structId, -1, NodeKind::Hex64,
QStringLiteral("field_%1").arg(i));
for (int i = 0; i < remainBytes; i++)
insertNode(structId, -1, NodeKind::Hex8,
QStringLiteral("field_%1").arg(hex64Count + i));
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
});
// Footer "Trim" button — remove trailing hex nodes from end of struct
connect(editor, &RcxEditor::trimHexRequested,
this, [this](uint64_t structId) {
QVector<int> children = m_doc->tree.childrenOf(structId);
if (children.isEmpty()) return;
// Sort by offset descending to find trailing hex nodes
std::sort(children.begin(), children.end(), [&](int a, int b) {
return m_doc->tree.nodes[a].offset > m_doc->tree.nodes[b].offset;
});
// Collect trailing hex nodes to remove
QVector<int> toRemove;
for (int ci : children) {
const Node& n = m_doc->tree.nodes[ci];
if (!isHexNode(n.kind)) break;
toRemove.append(ci);
}
if (toRemove.isEmpty()) return;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Trim %1 trailing hex nodes").arg(toRemove.size()));
for (int ni : toRemove)
removeNode(ni);
m_doc->undoStack.endMacro();
m_suppressRefresh = false;
refresh();
});
// Footer "+10" button — append enum members sequentially from highest value
connect(editor, &RcxEditor::appendEnumMembersRequested,
this, [this](uint64_t enumId, int count) {
int ni = m_doc->tree.indexOfId(enumId);
if (ni < 0) return;
auto members = m_doc->tree.nodes[ni].enumMembers;
int64_t nextVal = members.isEmpty() ? 0 : members.last().second + 1;
auto oldMembers = members;
for (int i = 0; i < count; i++)
members.append({QStringLiteral("Member%1").arg(nextVal + i), nextVal + i});
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeEnumMembers{enumId, oldMembers, members}));
});
// Inline editing signals // Inline editing signals
connect(editor, &RcxEditor::inlineEditCommitted, connect(editor, &RcxEditor::inlineEditCommitted,
this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text, this, [this](int nodeIdx, int subLine, EditTarget target, const QString& text,
@@ -548,9 +610,9 @@ void RcxController::refresh() {
// Compose against snapshot provider if active, otherwise real provider // Compose against snapshot provider if active, otherwise real provider
if (m_snapshotProv) if (m_snapshotProv)
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap); m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap, m_typeHints);
else else
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap); m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap, m_typeHints);
s_composeDoc = nullptr; s_composeDoc = nullptr;
@@ -1850,6 +1912,40 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
// Fall through to always-available actions // Fall through to always-available actions
} else { } else {
// ── Inference-based quick convert (from type hints) ──
if (isHexNode(node.kind) && line >= 0 && line < m_lastResult.meta.size()) {
const auto& lm = m_lastResult.meta[line];
if (!lm.typeHintKinds.isEmpty()) {
NodeKind suggested = lm.typeHintKinds[0];
if (lm.typeHintKinds.size() == 1) {
auto* m = kindMeta(suggested);
QString label = QStringLiteral("Convert to %1").arg(QString::fromLatin1(m->typeName));
menu.addAction(label, [this, nodeId, suggested]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) changeNodeKind(ni, suggested);
});
} else {
auto* m = kindMeta(lm.typeHintKinds[0]);
QString label = QStringLiteral("Split into %1\u00D7%2")
.arg(QString::fromLatin1(m->typeName))
.arg(lm.typeHintKinds.size());
menu.addAction(label, [this, nodeId, kinds = lm.typeHintKinds]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) return;
changeNodeKind(ni, kinds[0]);
for (int k = 1; k < kinds.size(); ++k) {
ni = m_doc->tree.indexOfId(nodeId);
if (ni < 0) break;
int next = ni + 1;
if (next < m_doc->tree.nodes.size() && isHexNode(m_doc->tree.nodes[next].kind))
changeNodeKind(next, kinds[k]);
}
});
}
menu.addSeparator();
}
}
// ── Quick-convert suggestions (top-level for fast access) ── // ── Quick-convert suggestions (top-level for fast access) ──
bool addedQuickConvert = false; bool addedQuickConvert = false;
if (node.kind == NodeKind::Hex64) { if (node.kind == NodeKind::Hex64) {
@@ -3130,8 +3226,8 @@ void RcxController::switchToSavedSource(int idx) {
// Restore formula before attach so it can be re-evaluated against the new provider // Restore formula before attach so it can be re-evaluated against the new provider
m_doc->tree.baseAddressFormula = entry.baseAddressFormula; m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
attachViaPlugin(entry.kind, entry.providerTarget); attachViaPlugin(entry.kind, entry.providerTarget);
// Restore saved base address (user may have navigated away from provider default) // Restore saved base address — always override with saved value on source switch
if (entry.baseAddress != 0 && entry.baseAddressFormula.isEmpty()) if (entry.baseAddressFormula.isEmpty())
m_doc->tree.baseAddress = entry.baseAddress; m_doc->tree.baseAddress = entry.baseAddress;
} }
} }
@@ -3313,6 +3409,11 @@ void RcxController::setBraceWrap(bool v) {
refresh(); refresh();
} }
void RcxController::setTypeHints(bool v) {
m_typeHints = v;
refresh();
}
void RcxController::setupAutoRefresh() { void RcxController::setupAutoRefresh() {
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt(); int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
m_refreshTimer = new QTimer(this); m_refreshTimer = new QTimer(this);

View File

@@ -41,7 +41,8 @@ public:
} }
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false, ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
bool treeLines = false, bool braceWrap = false) const; bool treeLines = false, bool braceWrap = false,
bool typeHints = false) const;
bool save(const QString& path); bool save(const QString& path);
bool load(const QString& path); bool load(const QString& path);
void loadData(const QString& binaryPath); void loadData(const QString& binaryPath);
@@ -131,6 +132,8 @@ public:
void setCompactColumns(bool v); void setCompactColumns(bool v);
void setTreeLines(bool v); void setTreeLines(bool v);
void setBraceWrap(bool v); void setBraceWrap(bool v);
void setTypeHints(bool v);
bool typeHints() const { return m_typeHints; }
void resetProvider(); void resetProvider();
// MCP bridge accessors // MCP bridge accessors
@@ -171,6 +174,7 @@ private:
bool m_compactColumns = false; bool m_compactColumns = false;
bool m_treeLines = false; bool m_treeLines = false;
bool m_braceWrap = false; bool m_braceWrap = false;
bool m_typeHints = false;
uint64_t m_viewRootId = 0; uint64_t m_viewRootId = 0;
// ── Saved sources for quick-switch ── // ── Saved sources for quick-switch ──

View File

@@ -450,8 +450,8 @@ struct NodeTree {
if (c.isStatic) continue; // static fields don't affect struct size if (c.isStatic) continue; // static fields don't affect struct size
int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array)
? structSpan(c.id, childMap, visited) : c.byteSize(); ? structSpan(c.id, childMap, visited) : c.byteSize();
int end = c.offset + sz; int64_t end = (int64_t)c.offset + sz;
if (end > maxEnd) maxEnd = end; if (end > maxEnd) maxEnd = (int)qMin(end, (int64_t)INT_MAX);
} }
// Embedded struct reference: no own children but refId points to a struct definition // Embedded struct reference: no own children but refId points to a struct definition
@@ -570,13 +570,13 @@ static constexpr int kCommandRowLine = 0;
static constexpr int kFirstDataLine = 1; static constexpr int kFirstDataLine = 1;
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL; static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
static constexpr uint64_t kArrayElemShift = 48; // bits 48-61 hold element index static constexpr uint64_t kArrayElemShift = 42; // bits 42-61 hold element index
static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements static constexpr uint64_t kArrayElemMask = 0x3FFFFC0000000000ULL; // 20 bits → max 1048575 elements
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48) // Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 42)
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) { inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
Q_ASSERT(elemIdx >= 0); Q_ASSERT(elemIdx >= 0);
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift); return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0xFFFFF) << kArrayElemShift);
} }
inline int arrayElemIdxFromSelId(uint64_t selId) { inline int arrayElemIdxFromSelId(uint64_t selId) {
return (int)((selId & kArrayElemMask) >> kArrayElemShift); return (int)((selId & kArrayElemMask) >> kArrayElemShift);
@@ -584,11 +584,11 @@ inline int arrayElemIdxFromSelId(uint64_t selId) {
// Member selection encoding (enum/bitfield members) — mirrors array element pattern // Member selection encoding (enum/bitfield members) — mirrors array element pattern
static constexpr uint64_t kMemberBit = 0x2000000000000000ULL; static constexpr uint64_t kMemberBit = 0x2000000000000000ULL;
static constexpr uint64_t kMemberSubShift = 48; static constexpr uint64_t kMemberSubShift = 42;
static constexpr uint64_t kMemberSubMask = 0x3FFF000000000000ULL; static constexpr uint64_t kMemberSubMask = 0x3FFFFC0000000000ULL;
inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) { inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) {
return nodeId | kMemberBit | ((uint64_t)(subLine & 0x3FFF) << kMemberSubShift); return nodeId | kMemberBit | ((uint64_t)(subLine & 0xFFFFF) << kMemberSubShift);
} }
inline int memberSubFromSelId(uint64_t selId) { inline int memberSubFromSelId(uint64_t selId) {
return (int)((selId & kMemberSubMask) >> kMemberSubShift); return (int)((selId & kMemberSubMask) >> kMemberSubShift);
@@ -625,6 +625,9 @@ struct LineMeta {
bool isArrayElement = false; // true for synthesized primitive array element lines bool isArrayElement = false; // true for synthesized primitive array element lines
bool isMemberLine = false; // true for enum member / bitfield member lines bool isMemberLine = false; // true for enum member / bitfield member lines
bool isStaticLine = false; // true for static field node lines bool isStaticLine = false; // true for static field node lines
QString typeHint; // Type inference hint text (e.g. "Float×2") — only set for hex nodes when hints enabled
int typeHintStart = -1; // Character offset where hint text starts in line text (-1 = none)
QVector<NodeKind> typeHintKinds; // Suggested kinds from inference (empty = no hint)
}; };
inline bool isSyntheticLine(const LineMeta& lm) { inline bool isSyntheticLine(const LineMeta& lm) {
@@ -1037,6 +1040,6 @@ namespace fmt {
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0, ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
bool compactColumns = false, bool treeLines = false, bool compactColumns = false, bool treeLines = false,
bool braceWrap = false); bool braceWrap = false, bool typeHints = false);
} // namespace rcx } // namespace rcx

View File

@@ -32,17 +32,14 @@ namespace rcx {
// Forward declaration (defined below, after RcxEditor constructor) // Forward declaration (defined below, after RcxEditor constructor)
static QString getLineText(QsciScintilla* sci, int line); static QString getLineText(QsciScintilla* sci, int line);
// ── Value history popup (styled like TypeSelectorPopup) ── // ── Base class for all hover popups ──
class ValueHistoryPopup : public QFrame { class HoverPopup : public QFrame {
protected:
uint64_t m_nodeId = 0; uint64_t m_nodeId = 0;
bool m_hasButtons = false;
QStringList m_values;
QVector<QLabel*> m_labels;
std::function<void(const QString&)> m_onSet;
std::function<void(QMouseEvent*)> m_onMouseMove; std::function<void(QMouseEvent*)> m_onMouseMove;
public: public:
explicit ValueHistoryPopup(QWidget* parent) explicit HoverPopup(QWidget* parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint) : QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{ {
setAttribute(Qt::WA_DeleteOnClose, false); setAttribute(Qt::WA_DeleteOnClose, false);
@@ -53,9 +50,129 @@ public:
} }
uint64_t nodeId() const { return m_nodeId; } uint64_t nodeId() const { return m_nodeId; }
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
void showAt(const QPoint& globalPos, int lineHeight = 0) {
QSize sz = sizeHint();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
: QRect(0, 0, 1920, 1080);
int x = qMin(globalPos.x(), screen.right() - sz.width());
int y = globalPos.y();
if (y + sz.height() > screen.bottom())
y = globalPos.y() - sz.height() - lineHeight - 4;
move(x, y);
if (!isVisible()) show();
}
virtual void dismiss() {
if (isVisible()) hide();
m_nodeId = 0;
}
protected:
void mouseMoveEvent(QMouseEvent* e) override {
if (m_onMouseMove) m_onMouseMove(e);
else QFrame::mouseMoveEvent(e);
}
void applyThemePalette(const Theme& t) {
QPalette pal;
pal.setColor(QPalette::Window, t.backgroundAlt);
pal.setColor(QPalette::WindowText, t.text);
setPalette(pal);
}
void styleSeparator(const Theme& t) {
for (auto* child : findChildren<QFrame*>()) {
if (child->frameShape() == QFrame::HLine) {
QPalette sp;
sp.setColor(QPalette::WindowText, t.border);
child->setPalette(sp);
break;
}
}
}
};
// ── Title + body popup (used for disasm/hex-dump and struct preview) ──
class TitleBodyPopup : public HoverPopup {
QString m_body;
QLabel* m_titleLabel = nullptr;
QLabel* m_bodyLabel = nullptr;
public:
explicit TitleBodyPopup(QWidget* parent) : HoverPopup(parent) {
auto* vbox = new QVBoxLayout(this);
vbox->setContentsMargins(8, 6, 8, 6);
vbox->setSpacing(2);
m_titleLabel = new QLabel;
QFont bold = m_titleLabel->font();
bold.setBold(true);
m_titleLabel->setFont(bold);
vbox->addWidget(m_titleLabel);
auto* sep = new QFrame;
sep->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain);
sep->setFixedHeight(1);
vbox->addWidget(sep);
m_bodyLabel = new QLabel;
m_bodyLabel->setTextFormat(Qt::PlainText);
m_bodyLabel->setWordWrap(false);
vbox->addWidget(m_bodyLabel);
}
void populate(uint64_t nodeId, const QString& title, const QString& body,
const QFont& font, const QColor& bodyColor) {
if (nodeId == m_nodeId && body == m_body && isVisible())
return;
m_nodeId = nodeId;
m_body = body;
const auto& theme = ThemeManager::instance().current();
applyThemePalette(theme);
QFont bold = font;
bold.setBold(true);
m_titleLabel->setFont(bold);
m_titleLabel->setText(title);
m_titleLabel->setStyleSheet(
QStringLiteral("color: %1;").arg(theme.text.name()));
styleSeparator(theme);
m_bodyLabel->setFont(font);
m_bodyLabel->setText(body);
m_bodyLabel->setStyleSheet(
QStringLiteral("color: %1;").arg(bodyColor.name()));
setMaximumWidth(600);
adjustSize();
}
void dismiss() override {
HoverPopup::dismiss();
m_body.clear();
}
};
// ── Value history popup ──
class ValueHistoryPopup : public HoverPopup {
bool m_hasButtons = false;
QStringList m_values;
QVector<QLabel*> m_labels;
std::function<void(const QString&)> m_onSet;
public:
explicit ValueHistoryPopup(QWidget* parent) : HoverPopup(parent) {}
bool hasButtons() const { return m_hasButtons; } bool hasButtons() const { return m_hasButtons; }
void setOnSet(std::function<void(const QString&)> fn) { m_onSet = std::move(fn); } void setOnSet(std::function<void(const QString&)> fn) { m_onSet = std::move(fn); }
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
protected: protected:
void mouseMoveEvent(QMouseEvent* e) override { void mouseMoveEvent(QMouseEvent* e) override {
if (!m_hasButtons && m_onMouseMove) if (!m_hasButtons && m_onMouseMove)
@@ -63,8 +180,8 @@ protected:
else else
QFrame::mouseMoveEvent(e); QFrame::mouseMoveEvent(e);
} }
public:
public:
void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font, void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font,
bool showButtons = false) { bool showButtons = false) {
QStringList vals; QStringList vals;
@@ -93,10 +210,7 @@ public:
qDeleteAll(findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly)); qDeleteAll(findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly));
const auto& theme = ThemeManager::instance().current(); const auto& theme = ThemeManager::instance().current();
QPalette pal; applyThemePalette(theme);
pal.setColor(QPalette::Window, theme.backgroundAlt);
pal.setColor(QPalette::WindowText, theme.text);
setPalette(pal);
auto* vbox = new QVBoxLayout(this); auto* vbox = new QVBoxLayout(this);
vbox->setContentsMargins(8, 6, 8, 6); vbox->setContentsMargins(8, 6, 8, 6);
@@ -169,240 +283,13 @@ public:
adjustSize(); adjustSize();
} }
void showAt(const QPoint& globalPos, int lineHeight = 0) { void dismiss() override {
QSize sz = sizeHint(); HoverPopup::dismiss();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
: QRect(0, 0, 1920, 1080);
int x = qMin(globalPos.x(), screen.right() - sz.width());
int y = globalPos.y();
if (y + sz.height() > screen.bottom())
y = globalPos.y() - sz.height() - lineHeight - 4;
move(x, y);
if (!isVisible()) show();
}
void dismiss() {
if (isVisible()) hide();
m_nodeId = 0;
m_values.clear(); m_values.clear();
m_labels.clear(); m_labels.clear();
} }
}; };
// ── Disassembly / hex-dump hover popup ──
class DisasmPopup : public QFrame {
uint64_t m_nodeId = 0;
QString m_body;
QLabel* m_titleLabel = nullptr;
QLabel* m_bodyLabel = nullptr;
std::function<void(QMouseEvent*)> m_onMouseMove;
public:
explicit DisasmPopup(QWidget* parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_DeleteOnClose, false);
setAttribute(Qt::WA_ShowWithoutActivating, true);
setMouseTracking(true);
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
auto* vbox = new QVBoxLayout(this);
vbox->setContentsMargins(8, 6, 8, 6);
vbox->setSpacing(2);
m_titleLabel = new QLabel;
QFont bold = m_titleLabel->font();
bold.setBold(true);
m_titleLabel->setFont(bold);
vbox->addWidget(m_titleLabel);
auto* sep = new QFrame;
sep->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain);
sep->setFixedHeight(1);
vbox->addWidget(sep);
m_bodyLabel = new QLabel;
m_bodyLabel->setTextFormat(Qt::PlainText);
m_bodyLabel->setWordWrap(false);
vbox->addWidget(m_bodyLabel);
}
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
uint64_t nodeId() const { return m_nodeId; }
protected:
void mouseMoveEvent(QMouseEvent* e) override {
if (m_onMouseMove) m_onMouseMove(e);
else QFrame::mouseMoveEvent(e);
}
public:
void populate(uint64_t nodeId, const QString& title, const QString& body,
const QFont& font) {
if (nodeId == m_nodeId && body == m_body && isVisible())
return;
m_nodeId = nodeId;
m_body = body;
const auto& theme = ThemeManager::instance().current();
QPalette pal;
pal.setColor(QPalette::Window, theme.backgroundAlt);
pal.setColor(QPalette::WindowText, theme.text);
setPalette(pal);
QFont bold = font;
bold.setBold(true);
m_titleLabel->setFont(bold);
m_titleLabel->setText(title);
m_titleLabel->setStyleSheet(
QStringLiteral("color: %1;").arg(theme.text.name()));
// Find and style the separator
for (auto* child : findChildren<QFrame*>()) {
if (child->frameShape() == QFrame::HLine) {
QPalette sp;
sp.setColor(QPalette::WindowText, theme.border);
child->setPalette(sp);
break;
}
}
m_bodyLabel->setFont(font);
m_bodyLabel->setText(body);
m_bodyLabel->setStyleSheet(
QStringLiteral("color: %1;").arg(theme.syntaxNumber.name()));
setMaximumWidth(600);
adjustSize();
}
void showAt(const QPoint& globalPos, int lineHeight = 0) {
QSize sz = sizeHint();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
: QRect(0, 0, 1920, 1080);
int x = qMin(globalPos.x(), screen.right() - sz.width());
int y = globalPos.y();
if (y + sz.height() > screen.bottom())
y = globalPos.y() - sz.height() - lineHeight - 4;
move(x, y);
if (!isVisible()) show();
}
void dismiss() {
if (isVisible()) hide();
m_nodeId = 0;
m_body.clear();
}
};
class StructPreviewPopup : public QFrame {
uint64_t m_nodeId = 0;
QString m_body;
QLabel* m_titleLabel = nullptr;
QLabel* m_bodyLabel = nullptr;
std::function<void(QMouseEvent*)> m_onMouseMove;
public:
explicit StructPreviewPopup(QWidget* parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_DeleteOnClose, false);
setAttribute(Qt::WA_ShowWithoutActivating, true);
setMouseTracking(true);
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
auto* vbox = new QVBoxLayout(this);
vbox->setContentsMargins(8, 6, 8, 6);
vbox->setSpacing(2);
m_titleLabel = new QLabel;
QFont bold = m_titleLabel->font();
bold.setBold(true);
m_titleLabel->setFont(bold);
vbox->addWidget(m_titleLabel);
auto* sep = new QFrame;
sep->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain);
sep->setFixedHeight(1);
vbox->addWidget(sep);
m_bodyLabel = new QLabel;
m_bodyLabel->setTextFormat(Qt::PlainText);
m_bodyLabel->setWordWrap(false);
vbox->addWidget(m_bodyLabel);
}
uint64_t nodeId() const { return m_nodeId; }
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
protected:
void mouseMoveEvent(QMouseEvent* e) override {
if (m_onMouseMove) m_onMouseMove(e);
else QFrame::mouseMoveEvent(e);
}
public:
void populate(uint64_t nodeId, const QString& title, const QString& body,
const QFont& font) {
if (nodeId == m_nodeId && body == m_body && isVisible())
return;
m_nodeId = nodeId;
m_body = body;
const auto& theme = ThemeManager::instance().current();
QPalette pal;
pal.setColor(QPalette::Window, theme.backgroundAlt);
pal.setColor(QPalette::WindowText, theme.text);
setPalette(pal);
QFont bold = font;
bold.setBold(true);
m_titleLabel->setFont(bold);
m_titleLabel->setText(title);
m_titleLabel->setStyleSheet(
QStringLiteral("color: %1;").arg(theme.text.name()));
for (auto* child : findChildren<QFrame*>()) {
if (child->frameShape() == QFrame::HLine) {
QPalette sp;
sp.setColor(QPalette::WindowText, theme.border);
child->setPalette(sp);
break;
}
}
m_bodyLabel->setFont(font);
m_bodyLabel->setText(body);
m_bodyLabel->setStyleSheet(
QStringLiteral("color: %1;").arg(theme.text.name()));
setMaximumWidth(600);
adjustSize();
}
void showAt(const QPoint& globalPos, int lineHeight = 0) {
QSize sz = sizeHint();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
: QRect(0, 0, 1920, 1080);
int x = qMin(globalPos.x(), screen.right() - sz.width());
int y = globalPos.y();
if (y + sz.height() > screen.bottom())
y = globalPos.y() - sz.height() - lineHeight - 4;
move(x, y);
if (!isVisible()) show();
}
void dismiss() {
if (isVisible()) hide();
m_nodeId = 0;
m_body.clear();
}
};
static constexpr int IND_EDITABLE = 8; static constexpr int IND_EDITABLE = 8;
static constexpr int IND_HEX_DIM = 9; static constexpr int IND_HEX_DIM = 9;
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
@@ -415,6 +302,7 @@ static constexpr int IND_LOCAL_OFF = 16; // Dim text for inline local offset
static constexpr int IND_HEAT_WARM = 17; // Heatmap level 2 (moderate changes) static constexpr int IND_HEAT_WARM = 17; // Heatmap level 2 (moderate changes)
static constexpr int IND_HEAT_HOT = 18; // Heatmap level 3 (frequent changes) static constexpr int IND_HEAT_HOT = 18; // Heatmap level 3 (frequent changes)
static constexpr int IND_FIND = 19; // Search match highlight static constexpr int IND_FIND = 19; // Search match highlight
static constexpr int IND_TYPE_HINT = 20; // Dimmed type inference hint text on hex nodes
static QString g_fontName = "JetBrains Mono"; static QString g_fontName = "JetBrains Mono";
@@ -724,6 +612,10 @@ void RcxEditor::setupScintilla() {
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_LOCAL_OFF, 17 /*INDIC_TEXTFORE*/); IND_LOCAL_OFF, 17 /*INDIC_TEXTFORE*/);
// Type inference hint — dimmed text appended to hex lines
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_TYPE_HINT, 17 /*INDIC_TEXTFORE*/);
// Find match highlight — thick underline (avoids box rendering artifacts) // Find match highlight — thick underline (avoids box rendering artifacts)
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
IND_FIND, 14 /*INDIC_COMPOSITIONTHICK*/); IND_FIND, 14 /*INDIC_COMPOSITIONTHICK*/);
@@ -869,6 +761,8 @@ void RcxEditor::applyTheme(const Theme& theme) {
IND_HINT_GREEN, theme.indHintGreen); IND_HINT_GREEN, theme.indHintGreen);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_LOCAL_OFF, theme.textFaint); IND_LOCAL_OFF, theme.textFaint);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_TYPE_HINT, theme.indHintGreen);
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
IND_FIND, theme.borderFocused); IND_FIND, theme.borderFocused);
@@ -973,15 +867,22 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
m_sci->setText(result.text); m_sci->setText(result.text);
m_sci->setReadOnly(true); m_sci->setReadOnly(true);
// Set horizontal scroll width to match the longest line (ignoring trailing spaces) // Set horizontal scroll width to match the longest line (ignoring trailing spaces).
// Single-pass scan avoids QString::split() allocation of entire QStringList.
{ {
int maxLen = 0; int maxLen = 0, curLen = 0, lastNonSpace = 0;
const QStringList lines = result.text.split(QChar('\n')); for (int i = 0; i < result.text.size(); i++) {
for (const auto& line : lines) { QChar ch = result.text[i];
int len = (int)line.size(); if (ch == '\n') {
while (len > 0 && line[len - 1] == QChar(' ')) --len; maxLen = qMax(maxLen, lastNonSpace);
maxLen = std::max(len, maxLen); curLen = 0;
lastNonSpace = 0;
} else {
++curLen;
if (ch != ' ') lastNonSpace = curLen;
}
} }
maxLen = qMax(maxLen, lastNonSpace);
QFontMetrics fm(editorFont()); QFontMetrics fm(editorFont());
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0'))); int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH, m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH,
@@ -995,14 +896,56 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
// Force full re-lex to fix stale syntax coloring after edits // Force full re-lex to fix stale syntax coloring after edits
m_sci->SendScintilla(QsciScintillaBase::SCI_COLOURISE, (uintptr_t)0, (long)-1); m_sci->SendScintilla(QsciScintillaBase::SCI_COLOURISE, (uintptr_t)0, (long)-1);
applyMarginText(result.meta); applyLineAttributes(result.meta);
applyMarkers(result.meta);
applyFoldLevels(result.meta);
applyHexDimming(result.meta); applyHexDimming(result.meta);
applyHeatmapHighlight(result.meta);
applySymbolColoring(result.meta); // Build line-text cache for indicator passes (avoids redundant Scintilla IPC)
QVector<QString> lineTexts(result.meta.size());
for (int i = 0; i < result.meta.size(); i++) {
const auto& lm = result.meta[i];
if (lm.heatLevel > 0 || isFuncPtr(lm.nodeKind) ||
lm.nodeKind == NodeKind::Pointer32 ||
lm.nodeKind == NodeKind::Pointer64 ||
lm.lineKind == LineKind::Footer ||
lm.typeHintStart >= 0)
lineTexts[i] = getLineText(m_sci, i);
}
applyHeatmapHighlight(result.meta, lineTexts);
applySymbolColoring(result.meta, lineTexts);
applyCommandRowPills(); applyCommandRowPills();
// Footer buttons — pill styling
for (int i = 0; i < result.meta.size(); i++) {
if (result.meta[i].lineKind != LineKind::Footer) continue;
const QString& ft = lineTexts[i];
// Struct footer: +10h +100h +1000h Trim (search longest first)
int p1000 = ft.indexOf(QStringLiteral("+1000h"));
if (p1000 >= 0)
fillIndicatorCols(IND_CMD_PILL, i, p1000, p1000 + 6);
int p100 = ft.indexOf(QStringLiteral("+100h"));
if (p100 >= 0 && p100 != p1000 + 1)
fillIndicatorCols(IND_CMD_PILL, i, p100, p100 + 5);
int p10 = ft.indexOf(QStringLiteral("+10h"));
if (p10 >= 0 && p10 != p100 && p10 != p1000)
fillIndicatorCols(IND_CMD_PILL, i, p10, p10 + 4);
// Enum footer: +10 (no 'h')
int add10Start = ft.indexOf(QStringLiteral("+10"));
if (add10Start >= 0 && add10Start != p10 && add10Start != p100 && add10Start != p1000)
fillIndicatorCols(IND_CMD_PILL, i, add10Start, add10Start + 3);
int trimStart = ft.indexOf(QStringLiteral("Trim"));
if (trimStart >= 0)
fillIndicatorCols(IND_CMD_PILL, i, trimStart, trimStart + 4);
}
// Apply type inference hint coloring (green, same as comment annotations)
for (int i = 0; i < result.meta.size(); i++) {
const auto& lm = result.meta[i];
if (lm.typeHintStart < 0) continue;
const QString& ft = lineTexts[i];
if (lm.typeHintStart < ft.size())
fillIndicatorCols(IND_TYPE_HINT, i, lm.typeHintStart, ft.size());
}
// Reset hint line - applySelectionOverlay will repaint indicators // Reset hint line - applySelectionOverlay will repaint indicators
m_hintLine = -1; m_hintLine = -1;
@@ -1015,9 +958,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
if (m_hoveredNodeId != 0 && !m_nodeLineIndex.contains(m_hoveredNodeId)) { if (m_hoveredNodeId != 0 && !m_nodeLineIndex.contains(m_hoveredNodeId)) {
m_hoveredNodeId = 0; m_hoveredNodeId = 0;
m_hoveredLine = -1; m_hoveredLine = -1;
dismissHistoryPopup(); dismissAllPopups();
if (m_disasmPopup) m_disasmPopup->hide();
if (m_structPreviewPopup) m_structPreviewPopup->hide();
} }
// Re-apply hover markers (setText() clears all Scintilla markers). // Re-apply hover markers (setText() clears all Scintilla markers).
@@ -1051,22 +992,47 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
} }
} }
void RcxEditor::applyMarginText(const QVector<LineMeta>& meta) { void RcxEditor::applyLineAttributes(const QVector<LineMeta>& meta) {
if (m_relativeOffsets) // Margin text
return reformatMargins(); if (m_relativeOffsets) {
reformatMargins();
} else {
m_sci->clearMarginText(-1);
}
m_sci->clearMarginText(-1); // Clear markers
for (int m = M_CONT; m <= M_STRUCT_BG; m++)
m_sci->markerDeleteAll(m);
m_sci->markerDeleteAll(M_CMD_ROW);
// Single pass: margin text (absolute mode), markers, fold levels
for (int i = 0; i < meta.size(); i++) { for (int i = 0; i < meta.size(); i++) {
const auto& lm = meta[i]; const auto& lm = meta[i];
if (lm.offsetText.isEmpty()) continue;
QByteArray text = lm.offsetText.toUtf8(); // Margin text (only in absolute offset mode; reformatMargins handles relative)
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETTEXT, if (!m_relativeOffsets && !lm.offsetText.isEmpty()) {
(uintptr_t)i, text.constData()); QByteArray text = lm.offsetText.toUtf8();
QByteArray styles(text.size(), '\0'); // style 0 = dim m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETTEXT,
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLES, (uintptr_t)i, text.constData());
(uintptr_t)i, styles.constData()); QByteArray styles(text.size(), '\0');
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLES,
(uintptr_t)i, styles.constData());
}
// Markers
if (lm.lineKind == LineKind::CommandRow) {
m_sci->markerAdd(i, M_CMD_ROW);
} else {
uint32_t mask = lm.markerMask;
for (int m = M_CONT; m <= M_STRUCT_BG; m++) {
if (mask & (1u << m))
m_sci->markerAdd(i, m);
}
}
// Fold level
m_sci->SendScintilla(QsciScintillaBase::SCI_SETFOLDLEVEL,
(unsigned long)i, (long)lm.foldLevel);
} }
} }
@@ -1187,31 +1153,6 @@ void RcxEditor::reformatMargins() {
m_sci->setReadOnly(true); m_sci->setReadOnly(true);
} }
void RcxEditor::applyMarkers(const QVector<LineMeta>& meta) {
for (int m = M_CONT; m <= M_STRUCT_BG; m++) {
m_sci->markerDeleteAll(m);
}
m_sci->markerDeleteAll(M_CMD_ROW);
for (int i = 0; i < meta.size(); i++) {
if (meta[i].lineKind == LineKind::CommandRow) {
m_sci->markerAdd(i, M_CMD_ROW);
continue;
}
uint32_t mask = meta[i].markerMask;
for (int m = M_CONT; m <= M_STRUCT_BG; m++) {
if (mask & (1u << m)) {
m_sci->markerAdd(i, m);
}
}
}
}
void RcxEditor::applyFoldLevels(const QVector<LineMeta>& meta) {
for (int i = 0; i < meta.size(); i++) {
m_sci->SendScintilla(QsciScintillaBase::SCI_SETFOLDLEVEL,
(unsigned long)i, (long)meta[i].foldLevel);
}
}
static inline void lineRangeNoEol(QsciScintilla* sci, int line, long& start, long& len) { static inline void lineRangeNoEol(QsciScintilla* sci, int line, long& start, long& len) {
start = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line); start = sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)line);
@@ -1272,6 +1213,7 @@ void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
} }
} }
} }
} }
void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) { void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
@@ -1448,7 +1390,13 @@ void RcxEditor::showFindBar() {
void RcxEditor::dismissHistoryPopup() { void RcxEditor::dismissHistoryPopup() {
if (m_historyPopup) if (m_historyPopup)
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss(); static_cast<HoverPopup*>(m_historyPopup)->dismiss();
}
void RcxEditor::dismissAllPopups() {
if (m_historyPopup) static_cast<HoverPopup*>(m_historyPopup)->dismiss();
if (m_disasmPopup) static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
if (m_structPreviewPopup) static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
} }
void RcxEditor::hideFindBar() { void RcxEditor::hideFindBar() {
@@ -1524,7 +1472,8 @@ static QString getLineText(QsciScintilla* sci, int line) {
return text; return text;
} }
void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) { void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta,
const QVector<QString>& lineTexts) {
static constexpr int heatIndicators[] = { IND_HEAT_COLD, IND_HEAT_WARM, IND_HEAT_HOT }; static constexpr int heatIndicators[] = { IND_HEAT_COLD, IND_HEAT_WARM, IND_HEAT_HOT };
for (int i = 0; i < meta.size(); i++) { for (int i = 0; i < meta.size(); i++) {
@@ -1546,7 +1495,7 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
int activeInd = heatIndicators[qBound(0, heat - 1, 2)]; int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
// Apply heat-level indicator to value span (narrowed for pointer-like nodes) // Apply heat-level indicator to value span (narrowed for pointer-like nodes)
QString lineText = getLineText(m_sci, i); const QString& lineText = lineTexts[i];
ColumnSpan vs = narrowPtrValueSpan(lm, ColumnSpan vs = narrowPtrValueSpan(lm,
valueSpan(lm, lineText.size(), typeW, nameW), lineText); valueSpan(lm, lineText.size(), typeW, nameW), lineText);
if (!vs.valid) continue; if (!vs.valid) continue;
@@ -1561,14 +1510,15 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
} }
} }
void RcxEditor::applySymbolColoring(const QVector<LineMeta>& meta) { void RcxEditor::applySymbolColoring(const QVector<LineMeta>& meta,
const QVector<QString>& lineTexts) {
for (int i = 0; i < meta.size(); i++) { for (int i = 0; i < meta.size(); i++) {
const LineMeta& lm = meta[i]; const LineMeta& lm = meta[i];
if (!isFuncPtr(lm.nodeKind) if (!isFuncPtr(lm.nodeKind)
&& lm.nodeKind != NodeKind::Pointer32 && lm.nodeKind != NodeKind::Pointer32
&& lm.nodeKind != NodeKind::Pointer64) && lm.nodeKind != NodeKind::Pointer64)
continue; continue;
QString lineText = getLineText(m_sci, i); const QString& lineText = lineTexts[i];
// Find " // " within the value region and color "// sym" portion green // Find " // " within the value region and color "// sym" portion green
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW); ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
if (!vs.valid) continue; if (!vs.valid) continue;
@@ -2111,6 +2061,42 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
emit marginClicked(0, h.line, me->modifiers()); emit marginClicked(0, h.line, me->modifiers());
return true; return true;
} }
// Footer buttons: +10h/+100h/+1000h, +10 (enum), Trim
if (h.line >= 0 && h.line < m_meta.size()
&& m_meta[h.line].lineKind == LineKind::Footer) {
QString ft = getLineText(m_sci, h.line);
uint64_t nid = m_meta[h.line].nodeId;
// Struct: +1000h (0x1000 = 4096 bytes)
int p1000 = ft.indexOf(QStringLiteral("+1000h"));
if (p1000 >= 0 && h.col >= p1000 && h.col < p1000 + 6) {
emit appendBytesRequested(nid, 0x1000);
return true;
}
// Struct: +100h (0x100 = 256 bytes)
int p100 = ft.indexOf(QStringLiteral("+100h"));
if (p100 >= 0 && p100 != p1000 + 1 && h.col >= p100 && h.col < p100 + 5) {
emit appendBytesRequested(nid, 0x100);
return true;
}
// Struct: +10h (0x10 = 16 bytes)
int p10 = ft.indexOf(QStringLiteral("+10h"));
if (p10 >= 0 && p10 != p100 && p10 != p1000 && h.col >= p10 && h.col < p10 + 4) {
emit appendBytesRequested(nid, 0x10);
return true;
}
// Enum: +10 (10 members)
int add10Start = ft.indexOf(QStringLiteral("+10"));
if (add10Start >= 0 && add10Start != p10 && add10Start != p100 && add10Start != p1000
&& h.col >= add10Start && h.col < add10Start + 3) {
emit appendEnumMembersRequested(nid, 10);
return true;
}
int trimStart = ft.indexOf(QStringLiteral("Trim"));
if (trimStart >= 0 && h.col >= trimStart && h.col < trimStart + 4) {
emit trimHexRequested(nid);
return true;
}
}
// CommandRow: try chevron/ADDR edit or consume // CommandRow: try chevron/ADDR edit or consume
if (h.nodeId == kCommandRowId) { if (h.nodeId == kCommandRowId) {
int tLine, tCol; EditTarget t; int tLine, tCol; EditTarget t;
@@ -2486,10 +2472,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
m_hoveredLine = -1; m_hoveredLine = -1;
applyHoverHighlight(); applyHoverHighlight();
// Dismiss hover popups so they get recreated with Set buttons once edit starts // Dismiss hover popups so they get recreated with Set buttons once edit starts
if (m_historyPopup) dismissAllPopups();
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
if (m_structPreviewPopup)
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
// Clear editable-token color hints (de-emphasize non-active tokens) // Clear editable-token color hints (de-emphasize non-active tokens)
clearIndicatorLine(IND_EDITABLE, m_hintLine); clearIndicatorLine(IND_EDITABLE, m_hintLine);
m_hintLine = -1; m_hintLine = -1;
@@ -2590,6 +2573,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
ColumnSpan cs = commentSpanFor(*lm, 9999, lm->effectiveTypeW, lm->effectiveNameW); ColumnSpan cs = commentSpanFor(*lm, 9999, lm->effectiveTypeW, lm->effectiveNameW);
m_editState.commentCol = cs.valid ? cs.start : -1; m_editState.commentCol = cs.valid ? cs.start : -1;
m_editState.lastValidationOk = true; // original value is always valid m_editState.lastValidationOk = true; // original value is always valid
} else if (target == EditTarget::BaseAddress) {
m_editState.commentCol = norm.end + 2; // command row has no column layout
} else { } else {
m_editState.commentCol = -1; m_editState.commentCol = -1;
} }
@@ -2603,7 +2588,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
// For value editing: extend line with trailing spaces for the edit comment area // For value editing: extend line with trailing spaces for the edit comment area
// (comment padding is no longer baked into every line to avoid unnecessary scroll width) // (comment padding is no longer baked into every line to avoid unnecessary scroll width)
if (target == EditTarget::Value && m_editState.commentCol >= 0) { if ((target == EditTarget::Value || target == EditTarget::BaseAddress)
&& m_editState.commentCol >= 0) {
int commentStart = norm.end + 2; int commentStart = norm.end + 2;
int neededLen = commentStart + kColComment; int neededLen = commentStart + kColComment;
int currentLen = (int)lineText.size(); int currentLen = (int)lineText.size();
@@ -2652,6 +2638,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
// Show initial edit hint in comment column // Show initial edit hint in comment column
if (target == EditTarget::Value) if (target == EditTarget::Value)
setEditComment(QStringLiteral("Enter=Save Esc=Cancel")); setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
else if (target == EditTarget::BaseAddress)
setEditComment(QStringLiteral("e.g. <mod.exe> + 0xFF | [0x1000 + 0x10] | 7ff6`1234ABCD"));
// Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup // Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup
// and exit early above (never reach here). // and exit early above (never reach here).
@@ -3092,25 +3080,19 @@ void RcxEditor::applyHoverCursor() {
} }
} }
if (!showPopup && m_historyPopup && m_historyPopup->isVisible()) if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss(); static_cast<HoverPopup*>(m_historyPopup)->dismiss();
} }
// Always dismiss disasm/preview popups during inline editing // Always dismiss disasm/preview popups during inline editing
if (m_disasmPopup && m_disasmPopup->isVisible()) if (m_disasmPopup) static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss(); if (m_structPreviewPopup) static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
if (m_structPreviewPopup && m_structPreviewPopup->isVisible())
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
return; return;
} }
// Mouse left viewport - set Arrow, dismiss popups // Mouse left viewport - set Arrow, dismiss popups
// (but not during applyDocument — the Leave is synthetic from setText) // (but not during applyDocument — the Leave is synthetic from setText)
if (!m_hoverInside) { if (!m_hoverInside) {
if (m_historyPopup && !m_applyingDocument) if (!m_applyingDocument)
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss(); dismissAllPopups();
if (m_disasmPopup && !m_applyingDocument)
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
if (m_structPreviewPopup && !m_applyingDocument)
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
m_sci->viewport()->setCursor(Qt::ArrowCursor); m_sci->viewport()->setCursor(Qt::ArrowCursor);
return; return;
} }
@@ -3211,6 +3193,30 @@ void RcxEditor::applyHoverCursor() {
m_hoverSpanLines.append(h.line); m_hoverSpanLines.append(h.line);
} }
// Apply hover span on footer pills (+10h/+100h/+1000h, +10, Trim)
if (h.line >= 0 && h.line < m_meta.size()
&& m_meta[h.line].lineKind == LineKind::Footer) {
QString ft = getLineText(m_sci, h.line);
auto tryPill = [&](const QString& text, int pos) {
if (pos >= 0 && h.col >= pos && h.col < pos + text.size()) {
fillIndicatorCols(IND_HOVER_SPAN, h.line, pos, pos + text.size());
m_hoverSpanLines.append(h.line);
}
};
int p1000 = ft.indexOf(QStringLiteral("+1000h"));
tryPill(QStringLiteral("+1000h"), p1000);
int p100 = ft.indexOf(QStringLiteral("+100h"));
if (p100 >= 0 && p100 != p1000 + 1)
tryPill(QStringLiteral("+100h"), p100);
int p10 = ft.indexOf(QStringLiteral("+10h"));
if (p10 >= 0 && p10 != p100 && p10 != p1000)
tryPill(QStringLiteral("+10h"), p10);
int add10Start = ft.indexOf(QStringLiteral("+10"));
if (add10Start >= 0 && add10Start != p10 && add10Start != p100 && add10Start != p1000)
tryPill(QStringLiteral("+10"), add10Start);
tryPill(QStringLiteral("Trim"), ft.indexOf(QStringLiteral("Trim")));
}
// Value history popup on hover (read-only, no buttons) // Value history popup on hover (read-only, no buttons)
// Skip FuncPtr and void-Pointer nodes — they use the disasm popup instead. // Skip FuncPtr and void-Pointer nodes — they use the disasm popup instead.
{ {
@@ -3266,7 +3272,7 @@ void RcxEditor::applyHoverCursor() {
} }
} }
if (!showPopup && m_historyPopup && m_historyPopup->isVisible()) if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss(); static_cast<HoverPopup*>(m_historyPopup)->dismiss();
} }
// Disasm / hex-dump popup on hover for FuncPtr and void Pointer nodes // Disasm / hex-dump popup on hover for FuncPtr and void Pointer nodes
@@ -3331,8 +3337,8 @@ void RcxEditor::applyHoverCursor() {
} }
if (!body.isEmpty()) { if (!body.isEmpty()) {
if (!m_disasmPopup) { if (!m_disasmPopup) {
m_disasmPopup = new DisasmPopup(this); m_disasmPopup = new TitleBodyPopup(this);
static_cast<DisasmPopup*>(m_disasmPopup)->setOnMouseMove([this](QMouseEvent* e) { static_cast<TitleBodyPopup*>(m_disasmPopup)->setOnMouseMove([this](QMouseEvent* e) {
QPoint gp = e->globalPosition().toPoint(); QPoint gp = e->globalPosition().toPoint();
QPoint vp = m_sci->viewport()->mapFromGlobal(gp); QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
m_lastHoverPos = vp; m_lastHoverPos = vp;
@@ -3350,10 +3356,11 @@ void RcxEditor::applyHoverCursor() {
applyHoverCursor(); applyHoverCursor();
}); });
} }
auto* popup = static_cast<DisasmPopup*>( auto* popup = static_cast<TitleBodyPopup*>(
m_disasmPopup); m_disasmPopup);
popup->populate(lm.nodeId, title, body, popup->populate(lm.nodeId, title, body,
editorFont()); editorFont(),
ThemeManager::instance().current().syntaxNumber);
long linePos = m_sci->SendScintilla( long linePos = m_sci->SendScintilla(
QsciScintillaBase::SCI_POSITIONFROMLINE, QsciScintillaBase::SCI_POSITIONFROMLINE,
(unsigned long)h.line); (unsigned long)h.line);
@@ -3374,7 +3381,7 @@ void RcxEditor::applyHoverCursor() {
showDisasm = true; showDisasm = true;
// Dismiss value history popup to avoid fighting // Dismiss value history popup to avoid fighting
if (m_historyPopup && m_historyPopup->isVisible()) if (m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss(); static_cast<HoverPopup*>(m_historyPopup)->dismiss();
} }
} }
} }
@@ -3383,7 +3390,7 @@ void RcxEditor::applyHoverCursor() {
} }
} }
if (!showDisasm && m_disasmPopup && m_disasmPopup->isVisible()) if (!showDisasm && m_disasmPopup && m_disasmPopup->isVisible())
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss(); static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
} }
// Struct preview popup for collapsed typed pointers // Struct preview popup for collapsed typed pointers
@@ -3418,8 +3425,8 @@ void RcxEditor::applyHoverCursor() {
} }
if (!body.isEmpty()) { if (!body.isEmpty()) {
if (!m_structPreviewPopup) { if (!m_structPreviewPopup) {
m_structPreviewPopup = new StructPreviewPopup(this); m_structPreviewPopup = new TitleBodyPopup(this);
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->setOnMouseMove([this](QMouseEvent* e) { static_cast<TitleBodyPopup*>(m_structPreviewPopup)->setOnMouseMove([this](QMouseEvent* e) {
QPoint gp = e->globalPosition().toPoint(); QPoint gp = e->globalPosition().toPoint();
QPoint vp = m_sci->viewport()->mapFromGlobal(gp); QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
m_lastHoverPos = vp; m_lastHoverPos = vp;
@@ -3437,9 +3444,10 @@ void RcxEditor::applyHoverCursor() {
applyHoverCursor(); applyHoverCursor();
}); });
} }
auto* popup = static_cast<StructPreviewPopup*>(m_structPreviewPopup); auto* popup = static_cast<TitleBodyPopup*>(m_structPreviewPopup);
popup->populate(lm.nodeId, popup->populate(lm.nodeId,
lm.pointerTargetName, body, editorFont()); lm.pointerTargetName, body, editorFont(),
ThemeManager::instance().current().text);
long linePos = m_sci->SendScintilla( long linePos = m_sci->SendScintilla(
QsciScintillaBase::SCI_POSITIONFROMLINE, QsciScintillaBase::SCI_POSITIONFROMLINE,
(unsigned long)h.line); (unsigned long)h.line);
@@ -3458,14 +3466,14 @@ void RcxEditor::applyHoverCursor() {
popup->showAt(anchor, lh); popup->showAt(anchor, lh);
showPreview = true; showPreview = true;
if (m_historyPopup && m_historyPopup->isVisible()) if (m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss(); static_cast<HoverPopup*>(m_historyPopup)->dismiss();
} }
} }
} }
} }
} }
if (!showPreview && m_structPreviewPopup && m_structPreviewPopup->isVisible()) if (!showPreview && m_structPreviewPopup && m_structPreviewPopup->isVisible())
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss(); static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
} }
// Determine cursor shape based on interaction type // Determine cursor shape based on interaction type
@@ -3473,6 +3481,25 @@ void RcxEditor::applyHoverCursor() {
if (h.inFoldCol) { if (h.inFoldCol) {
desired = Qt::PointingHandCursor; // fold toggle = button desired = Qt::PointingHandCursor; // fold toggle = button
} else if (h.line >= 0 && h.line < m_meta.size()
&& m_meta[h.line].lineKind == LineKind::Footer) {
QString ft = getLineText(m_sci, h.line);
int p1000 = ft.indexOf(QStringLiteral("+1000h"));
if (p1000 >= 0 && h.col >= p1000 && h.col < p1000 + 6)
desired = Qt::PointingHandCursor;
int p100 = ft.indexOf(QStringLiteral("+100h"));
if (p100 >= 0 && p100 != p1000 + 1 && h.col >= p100 && h.col < p100 + 5)
desired = Qt::PointingHandCursor;
int p10 = ft.indexOf(QStringLiteral("+10h"));
if (p10 >= 0 && p10 != p100 && p10 != p1000 && h.col >= p10 && h.col < p10 + 4)
desired = Qt::PointingHandCursor;
int add10Start = ft.indexOf(QStringLiteral("+10"));
if (add10Start >= 0 && add10Start != p10 && add10Start != p100 && add10Start != p1000
&& h.col >= add10Start && h.col < add10Start + 3)
desired = Qt::PointingHandCursor;
int trimStart = ft.indexOf(QStringLiteral("Trim"));
if (trimStart >= 0 && h.col >= trimStart && h.col < trimStart + 4)
desired = Qt::PointingHandCursor;
} else if (tokenHit) { } else if (tokenHit) {
// Check if mouse is actually over trimmed text content (not column padding) // Check if mouse is actually over trimmed text content (not column padding)
NormalizedSpan trimmed; NormalizedSpan trimmed;

View File

@@ -37,6 +37,7 @@ public:
void scrollToNodeId(uint64_t nodeId); void scrollToNodeId(uint64_t nodeId);
void showFindBar(); void showFindBar();
void dismissHistoryPopup(); void dismissHistoryPopup();
void dismissAllPopups();
// ── Column span computation ── // ── Column span computation ──
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType); static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
@@ -85,6 +86,9 @@ signals:
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos); void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
void insertAboveRequested(int nodeIdx, NodeKind kind); void insertAboveRequested(int nodeIdx, NodeKind kind);
void relativeOffsetsChanged(bool relative); void relativeOffsetsChanged(bool relative);
void appendBytesRequested(uint64_t structId, int byteCount);
void trimHexRequested(uint64_t structId);
void appendEnumMembersRequested(uint64_t enumId, int count);
protected: protected:
bool eventFilter(QObject* obj, QEvent* event) override; bool eventFilter(QObject* obj, QEvent* event) override;
@@ -155,8 +159,8 @@ private:
// ── Value history ref (owned by controller) ── // ── Value history ref (owned by controller) ──
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr; const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp) QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp) QWidget* m_disasmPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
QWidget* m_structPreviewPopup = nullptr; // StructPreviewPopup (file-local class in editor.cpp) QWidget* m_structPreviewPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
const NodeTree* m_disasmTree = nullptr; const NodeTree* m_disasmTree = nullptr;
@@ -179,13 +183,11 @@ private:
void setupMarkers(); void setupMarkers();
void allocateMarginStyles(); void allocateMarginStyles();
void applyMarginText(const QVector<LineMeta>& meta); void applyLineAttributes(const QVector<LineMeta>& meta);
void reformatMargins(); void reformatMargins();
void applyMarkers(const QVector<LineMeta>& meta);
void applyFoldLevels(const QVector<LineMeta>& meta);
void applyHexDimming(const QVector<LineMeta>& meta); void applyHexDimming(const QVector<LineMeta>& meta);
void applyHeatmapHighlight(const QVector<LineMeta>& meta); void applyHeatmapHighlight(const QVector<LineMeta>& meta, const QVector<QString>& lineTexts);
void applySymbolColoring(const QVector<LineMeta>& meta); void applySymbolColoring(const QVector<LineMeta>& meta, const QVector<QString>& lineTexts);
void applyBaseAddressColoring(const QVector<LineMeta>& meta); void applyBaseAddressColoring(const QVector<LineMeta>& meta);
void applyCommandRowPills(); void applyCommandRowPills();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -163,8 +163,13 @@ QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType
return ind + type + SEP + node.name + SEP + suffix; return ind + type + SEP + node.name + SEP + suffix;
} }
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) { QString fmtStructFooter(const Node& node, int depth, int /*totalSize*/) {
return indent(depth) + QStringLiteral("};"); QString footer = indent(depth) + QStringLiteral("};");
if (node.resolvedClassKeyword() == QStringLiteral("enum"))
footer += QStringLiteral(" +10");
else
footer += QStringLiteral(" +10h +100h +1000h Trim");
return footer;
} }
// ── Array header ── // ── Array header ──
@@ -656,8 +661,10 @@ QString validateValue(NodeKind kind, const QString& text) {
QString digits = hasHexPrefix ? s.mid(2) : s; QString digits = hasHexPrefix ? s.mid(2) : s;
if (hasHexPrefix || isHexKind) { if (hasHexPrefix || isHexKind) {
// Hex mode: only 0-9, a-f, A-F // Hex mode: only 0-9, a-f, A-F (spaces allowed for multi-byte hex kinds)
bool isMultiByteHex = (kind >= NodeKind::Hex16 && kind <= NodeKind::Hex64);
for (QChar c : digits) { for (QChar c : digits) {
if (c == ' ' && isMultiByteHex) continue;
if (!c.isDigit() && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F')) if (!c.isDigit() && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
return QStringLiteral("invalid hex '%1'").arg(c); return QStringLiteral("invalid hex '%1'").arg(c);
} }

View File

@@ -72,6 +72,7 @@ static QHash<QString, TypeInfo> buildTypeTable(int ptrSize = 8) {
t[QStringLiteral("USHORT")] = {NodeKind::UInt16, 2}; t[QStringLiteral("USHORT")] = {NodeKind::UInt16, 2};
t[QStringLiteral("SHORT")] = {NodeKind::Int16, 2}; t[QStringLiteral("SHORT")] = {NodeKind::Int16, 2};
t[QStringLiteral("WCHAR")] = {NodeKind::UInt16, 2}; t[QStringLiteral("WCHAR")] = {NodeKind::UInt16, 2};
t[QStringLiteral("TCHAR")] = {NodeKind::UInt16, 2};
t[QStringLiteral("DWORD")] = {NodeKind::UInt32, 4}; t[QStringLiteral("DWORD")] = {NodeKind::UInt32, 4};
t[QStringLiteral("ULONG")] = {NodeKind::UInt32, 4}; t[QStringLiteral("ULONG")] = {NodeKind::UInt32, 4};
t[QStringLiteral("UINT")] = {NodeKind::UInt32, 4}; t[QStringLiteral("UINT")] = {NodeKind::UInt32, 4};
@@ -1187,6 +1188,16 @@ static int structTypeSize(const QString& typeName, const BuildContext& ctx) {
return 0; return 0;
} }
// Compute total array elements from multi-dimensional sizes, capped to prevent overflow.
static int clampedArrayElements(const QVector<int>& dims, int maxElements = 1000000) {
int64_t total = 1;
for (int dim : dims) {
total *= (dim > 0 ? dim : 1);
if (total > maxElements) return maxElements;
}
return (int)total;
}
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset, static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
const QVector<ParsedField>& fields) { const QVector<ParsedField>& fields) {
int computedOffset = 0; int computedOffset = 0;
@@ -1275,8 +1286,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
// Array of pointers: PVOID arr[N] // Array of pointers: PVOID arr[N]
if (!field.arraySizes.isEmpty()) { if (!field.arraySizes.isEmpty()) {
int totalElements = 1; int totalElements = clampedArrayElements(field.arraySizes);
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
Node n; Node n;
n.kind = NodeKind::Array; n.kind = NodeKind::Array;
@@ -1314,8 +1324,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
int elemSize = 4; int elemSize = 4;
NodeKind elemKind = NodeKind::UInt32; NodeKind elemKind = NodeKind::UInt32;
if (!field.arraySizes.isEmpty()) { if (!field.arraySizes.isEmpty()) {
int totalElements = 1; int totalElements = clampedArrayElements(field.arraySizes);
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
Node n; Node n;
n.kind = NodeKind::Array; n.kind = NodeKind::Array;
n.name = field.name; n.name = field.name;
@@ -1366,7 +1375,8 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
if (firstDim <= 0) firstDim = 1; if (firstDim <= 0) firstDim = 1;
if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 && if (baseKind == NodeKind::Int8 && field.arraySizes.size() == 1 &&
field.typeName == QStringLiteral("char") && firstDim <= 128) { (field.typeName == QStringLiteral("char") ||
field.typeName == QStringLiteral("CHAR"))) {
Node n; Node n;
n.kind = NodeKind::UTF8; n.kind = NodeKind::UTF8;
n.name = field.name; n.name = field.name;
@@ -1379,8 +1389,9 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
} }
if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 && if (baseKind == NodeKind::UInt16 && field.arraySizes.size() == 1 &&
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR")) && (field.typeName == QStringLiteral("wchar_t") ||
firstDim <= 128) { field.typeName == QStringLiteral("WCHAR") ||
field.typeName == QStringLiteral("TCHAR"))) {
Node n; Node n;
n.kind = NodeKind::UTF16; n.kind = NodeKind::UTF16;
n.name = field.name; n.name = field.name;
@@ -1417,8 +1428,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
ctx.tree.addNode(n); computedOffset = fieldOffset + 64; continue; ctx.tree.addNode(n); computedOffset = fieldOffset + 64; continue;
} }
int totalElements = 1; int totalElements = clampedArrayElements(field.arraySizes);
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
Node n; Node n;
n.kind = NodeKind::Array; n.kind = NodeKind::Array;
@@ -1437,8 +1447,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
int elemSize = structTypeSize(field.typeName, ctx); int elemSize = structTypeSize(field.typeName, ctx);
if (!field.arraySizes.isEmpty()) { if (!field.arraySizes.isEmpty()) {
int totalElements = 1; int totalElements = clampedArrayElements(field.arraySizes);
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
Node n; Node n;
n.kind = NodeKind::Array; n.kind = NodeKind::Array;
@@ -1575,6 +1584,13 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int poin
buildFields(ctx, structId, 0, ps.fields); buildFields(ctx, structId, 0, ps.fields);
// Union: all direct children overlap at offset 0
if (ps.keyword == QStringLiteral("union")) {
QVector<int> children = tree.childrenOf(structId);
for (int ci : children)
tree.nodes[ci].offset = 0;
}
// Apply static_assert size: add tail padding if needed // Apply static_assert size: add tail padding if needed
auto sizeIt = parser.sizeAsserts.find(ps.name); auto sizeIt = parser.sizeAsserts.find(ps.name);
if (sizeIt != parser.sizeAsserts.end()) { if (sizeIt != parser.sizeAsserts.end()) {

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
#include "pluginmanager.h" #include "pluginmanager.h"
#include "scannerpanel.h" #include "scannerpanel.h"
#include "startpage.h" #include "startpage.h"
#include "workspace_model.h"
#include <QMainWindow> #include <QMainWindow>
#include <QLabel> #include <QLabel>
#include <QSplitter> #include <QSplitter>
@@ -68,6 +69,7 @@ private slots:
public: public:
// Status bar helpers — separate app / MCP channels // Status bar helpers — separate app / MCP channels
void setAppStatus(const QString& text); void setAppStatus(const QString& text);
void setAppStatus(const QString& text, const QString& dimSuffix);
void setMcpStatus(const QString& text); void setMcpStatus(const QString& text);
void clearMcpStatus(); void clearMcpStatus();
@@ -83,6 +85,7 @@ private:
QWidget* m_centralPlaceholder; QWidget* m_centralPlaceholder;
ShimmerLabel* m_statusLabel; ShimmerLabel* m_statusLabel;
QString m_appStatus; QString m_appStatus;
QString m_appStatusDim;
bool m_mcpBusy = false; bool m_mcpBusy = false;
QTimer* m_mcpClearTimer = nullptr; QTimer* m_mcpClearTimer = nullptr;
TitleBarWidget* m_titleBar = nullptr; TitleBarWidget* m_titleBar = nullptr;
@@ -117,7 +120,15 @@ private:
QMap<QDockWidget*, TabState> m_tabs; QMap<QDockWidget*, TabState> m_tabs;
QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex
QDockWidget* m_activeDocDock = nullptr; // tracks active document dock QDockWidget* m_activeDocDock = nullptr; // tracks active document dock
QDockWidget* m_sentinelDock = nullptr; // hidden dock to bootstrap tab bar creation
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
bool m_closingAll = false; // guards spurious project_new during batch close
bool m_tabBarShowGuard = false; // prevents recursion in event filter re-show
struct ClosingGuard {
bool& flag;
ClosingGuard(bool& f) : flag(f) { flag = true; }
~ClosingGuard() { flag = false; }
};
void rebuildAllDocs(); void rebuildAllDocs();
void createMenus(); void createMenus();
@@ -134,6 +145,7 @@ private:
TabState* tabByIndex(int index); TabState* tabByIndex(int index);
int tabCount() const { return m_tabs.size(); } int tabCount() const { return m_tabs.size(); }
QDockWidget* createTab(RcxDocument* doc); QDockWidget* createTab(RcxDocument* doc);
QString tabTitle(const TabState& tab) const;
void setupDockTabBars(); void setupDockTabBars();
void updateWindowTitle(); void updateWindowTitle();
void closeAllDocDocks(); void closeAllDocDocks();
@@ -161,10 +173,12 @@ private:
QLabel* m_dockTitleLabel = nullptr; QLabel* m_dockTitleLabel = nullptr;
QToolButton* m_dockCloseBtn = nullptr; QToolButton* m_dockCloseBtn = nullptr;
DockGripWidget* m_dockGrip = nullptr; DockGripWidget* m_dockGrip = nullptr;
QSet<uint64_t> m_pinnedIds;
void createWorkspaceDock(); void createWorkspaceDock();
void rebuildWorkspaceModel(); // debounced — safe to call frequently void rebuildWorkspaceModel(); // debounced — safe to call frequently
void rebuildWorkspaceModelNow(); // immediate rebuild void rebuildWorkspaceModelNow(); // immediate rebuild
QTimer* m_workspaceRebuildTimer = nullptr; QTimer* m_workspaceRebuildTimer = nullptr;
QTimer* m_workspaceSearchTimer = nullptr;
void updateBorderColor(const QColor& color); void updateBorderColor(const QColor& color);
// Scanner dock // Scanner dock

View File

@@ -10,13 +10,24 @@
namespace rcx { namespace rcx {
static constexpr int kMaxReadBuffer = 10 * 1024 * 1024; // 10 MB
// ════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════
// Construction / lifecycle // Construction / lifecycle
// ════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════
McpBridge::McpBridge(MainWindow* mainWindow, QObject* parent) McpBridge::McpBridge(MainWindow* mainWindow, QObject* parent)
: QObject(parent), m_mainWindow(mainWindow) : QObject(parent), m_mainWindow(mainWindow)
{} {
m_notifyTimer = new QTimer(this);
m_notifyTimer->setSingleShot(true);
m_notifyTimer->setInterval(100);
connect(m_notifyTimer, &QTimer::timeout, this, [this]() {
if (m_client && m_initialized)
sendNotification("notifications/resources/updated",
QJsonObject{{"uri", "project://tree"}});
});
}
McpBridge::~McpBridge() { McpBridge::~McpBridge() {
stop(); stop();
@@ -84,15 +95,24 @@ void McpBridge::onNewConnection() {
void McpBridge::onReadyRead() { void McpBridge::onReadyRead() {
m_readBuffer.append(m_client->readAll()); m_readBuffer.append(m_client->readAll());
// Newline-delimited JSON framing if (m_readBuffer.size() > kMaxReadBuffer) {
qWarning() << "[MCP] Read buffer exceeded 10MB, disconnecting client";
m_client->disconnectFromServer();
return;
}
// Newline-delimited JSON framing (cursor approach avoids quadratic shifting)
int consumed = 0;
while (true) { while (true) {
int idx = m_readBuffer.indexOf('\n'); int idx = m_readBuffer.indexOf('\n', consumed);
if (idx < 0) break; if (idx < 0) break;
QByteArray line = m_readBuffer.left(idx).trimmed(); QByteArray line = m_readBuffer.mid(consumed, idx - consumed).trimmed();
m_readBuffer.remove(0, idx + 1); consumed = idx + 1;
if (!line.isEmpty()) if (!line.isEmpty())
processLine(line); processLine(line);
} }
if (consumed > 0)
m_readBuffer.remove(0, consumed);
} }
void McpBridge::onDisconnected() { void McpBridge::onDisconnected() {
@@ -153,6 +173,7 @@ QJsonObject McpBridge::makeTextResult(const QString& text, bool isError) {
// ════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════
void McpBridge::processLine(const QByteArray& line) { void McpBridge::processLine(const QByteArray& line) {
try {
qDebug() << "[MCP] <<" << line.trimmed().left(200); qDebug() << "[MCP] <<" << line.trimmed().left(200);
auto doc = QJsonDocument::fromJson(line); auto doc = QJsonDocument::fromJson(line);
if (!doc.isObject()) { if (!doc.isObject()) {
@@ -172,12 +193,10 @@ void McpBridge::processLine(const QByteArray& line) {
if (method == "initialize") { if (method == "initialize") {
m_mainWindow->setMcpStatus(QStringLiteral("MCP: client connected")); m_mainWindow->setMcpStatus(QStringLiteral("MCP: client connected"));
QCoreApplication::processEvents();
sendJson(handleInitialize(id, req.value("params").toObject())); sendJson(handleInitialize(id, req.value("params").toObject()));
m_mainWindow->clearMcpStatus(); m_mainWindow->clearMcpStatus();
} else if (method == "tools/list") { } else if (method == "tools/list") {
m_mainWindow->setMcpStatus(QStringLiteral("MCP: tools/list")); m_mainWindow->setMcpStatus(QStringLiteral("MCP: tools/list"));
QCoreApplication::processEvents();
sendJson(handleToolsList(id)); sendJson(handleToolsList(id));
m_mainWindow->clearMcpStatus(); m_mainWindow->clearMcpStatus();
} else if (method == "tools/call") { } else if (method == "tools/call") {
@@ -185,6 +204,14 @@ void McpBridge::processLine(const QByteArray& line) {
} else { } else {
sendJson(errReply(id, -32601, "Method not found: " + method)); sendJson(errReply(id, -32601, "Method not found: " + method));
} }
} catch (const std::exception& e) {
qWarning() << "[MCP] Exception:" << e.what();
sendJson(errReply(QJsonValue(), -32603,
QStringLiteral("Internal error: %1").arg(e.what())));
} catch (...) {
qWarning() << "[MCP] Unknown exception";
sendJson(errReply(QJsonValue(), -32603, "Internal error"));
}
} }
// ════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════
@@ -476,7 +503,7 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
// Show tool activity in status bar (with shimmer) // Show tool activity in status bar (with shimmer)
m_mainWindow->setMcpStatus(QStringLiteral("MCP: %1").arg(toolName)); m_mainWindow->setMcpStatus(QStringLiteral("MCP: %1").arg(toolName));
QCoreApplication::processEvents(); // paint immediately QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QJsonObject result; QJsonObject result;
if (toolName == "project.state") result = toolProjectState(args); if (toolName == "project.state") result = toolProjectState(args);
@@ -501,11 +528,15 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
// ════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════
QString McpBridge::resolvePlaceholder(const QString& ref, QString McpBridge::resolvePlaceholder(const QString& ref,
const QHash<QString, uint64_t>& placeholderMap) { const QHash<QString, uint64_t>& placeholderMap,
bool* ok) {
if (ok) *ok = true;
if (ref.startsWith('$')) { if (ref.startsWith('$')) {
auto it = placeholderMap.find(ref); auto it = placeholderMap.find(ref);
if (it != placeholderMap.end()) if (it != placeholderMap.end())
return QString::number(it.value()); return QString::number(it.value());
if (ok) *ok = false;
return ref; // unresolved placeholder
} }
return ref; // not a placeholder — return as-is return ref; // not a placeholder — return as-is
} }
@@ -514,26 +545,36 @@ QString McpBridge::resolvePlaceholder(const QString& ref,
// Smart tab resolution // Smart tab resolution
// ════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════
MainWindow::TabState* McpBridge::resolveTab(const QJsonObject& args) { MainWindow::TabState* McpBridge::resolveTab(const QJsonObject& args, int* resolvedIndex) {
if (resolvedIndex) *resolvedIndex = -1;
// 1) Explicit tab index from args // 1) Explicit tab index from args
if (args.contains("tabIndex")) { if (args.contains("tabIndex")) {
int idx = args.value("tabIndex").toInt(); int idx = args.value("tabIndex").toInt();
auto* t = m_mainWindow->tabByIndex(idx); auto* t = m_mainWindow->tabByIndex(idx);
if (t) return t; if (t) { if (resolvedIndex) *resolvedIndex = idx; return t; }
} }
// 2) Active sub-window (user clicked on it) // 2) Active sub-window (user clicked on it)
auto* t = m_mainWindow->activeTab(); auto* t = m_mainWindow->activeTab();
if (t) return t; if (t) {
if (resolvedIndex) {
for (int i = 0; i < m_mainWindow->tabCount(); i++) {
if (m_mainWindow->tabByIndex(i) == t) { *resolvedIndex = i; break; }
}
}
return t;
}
// 3) Fall back to first available tab // 3) Fall back to first available tab
if (m_mainWindow->tabCount() > 0) { if (m_mainWindow->tabCount() > 0) {
t = m_mainWindow->tabByIndex(0); t = m_mainWindow->tabByIndex(0);
if (t) return t; if (t) { if (resolvedIndex) *resolvedIndex = 0; return t; }
} }
// 4) No tabs at all — auto-create a project // 4) No tabs at all — auto-create a project
m_mainWindow->project_new(); m_mainWindow->project_new();
if (resolvedIndex) *resolvedIndex = 0;
return m_mainWindow->tabByIndex(0); return m_mainWindow->tabByIndex(0);
} }
@@ -725,8 +766,11 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
QStringList skippedOps; QStringList skippedOps;
for (int i = 0; i < ops.size(); i++) { for (int i = 0; i < ops.size(); i++) {
// Safety valve: keep paint events flowing for large batches // Safety valve: keep paint events flowing for large batches
if (i % 100 == 0 && ops.size() > 200) if (i % 100 == 0 && ops.size() > 200) {
m_mainWindow->setMcpStatus(
QStringLiteral("MCP: tree.apply %1/%2").arg(i).arg(ops.size()));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 5); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 5);
}
QJsonObject op = ops[i].toObject(); QJsonObject op = ops[i].toObject();
QString opType = op.value("op").toString(); QString opType = op.value("op").toString();
@@ -736,15 +780,29 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
n.id = placeholders.value(QStringLiteral("$%1").arg(i), tree.reserveId()); n.id = placeholders.value(QStringLiteral("$%1").arg(i), tree.reserveId());
n.kind = kindFromString(op.value("kind").toString("Hex64")); n.kind = kindFromString(op.value("kind").toString("Hex64"));
n.name = op.value("name").toString(); n.name = op.value("name").toString();
QString pid = resolvePlaceholder(op.value("parentId").toString("0"), placeholders); bool pidOk;
QString pid = resolvePlaceholder(op.value("parentId").toString("0"), placeholders, &pidOk);
if (!pidOk) {
skippedOps.append(QStringLiteral("op[%1]: unresolved placeholder for parentId").arg(i));
continue;
}
n.parentId = pid.toULongLong(); n.parentId = pid.toULongLong();
if (n.parentId != 0 && tree.indexOfId(n.parentId) < 0) {
skippedOps.append(QStringLiteral("op[%1]: parentId '%2' not found").arg(i).arg(pid));
continue;
}
n.offset = op.value("offset").toInt(0); n.offset = op.value("offset").toInt(0);
n.structTypeName = op.value("structTypeName").toString(); n.structTypeName = op.value("structTypeName").toString();
n.classKeyword = op.value("classKeyword").toString(); n.classKeyword = op.value("classKeyword").toString();
n.strLen = op.value("strLen").toInt(64); n.strLen = qBound(1, op.value("strLen").toInt(64), 1000000);
n.elementKind = kindFromString(op.value("elementKind").toString("UInt8")); n.elementKind = kindFromString(op.value("elementKind").toString("UInt8"));
n.arrayLen = op.value("arrayLen").toInt(1); n.arrayLen = qBound(1, op.value("arrayLen").toInt(1), 1000000);
QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders); bool refOk;
QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders, &refOk);
if (!refOk) {
skippedOps.append(QStringLiteral("op[%1]: unresolved placeholder for refId").arg(i));
continue;
}
n.refId = refStr.toULongLong(); n.refId = refStr.toULongLong();
// Auto-place: offset -1 means "after last sibling" // Auto-place: offset -1 means "after last sibling"
@@ -870,7 +928,7 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
int idx = tree.indexOfId(nid.toULongLong()); int idx = tree.indexOfId(nid.toULongLong());
if (idx >= 0) { if (idx >= 0) {
NodeKind newElemKind = kindFromString(op.value("elementKind").toString()); NodeKind newElemKind = kindFromString(op.value("elementKind").toString());
int newLen = op.value("arrayLen").toInt(1); int newLen = qBound(1, op.value("arrayLen").toInt(1), 1000000);
doc->undoStack.push(new RcxCommand(ctrl, doc->undoStack.push(new RcxCommand(ctrl,
cmd::ChangeArrayMeta{tree.nodes[idx].id, cmd::ChangeArrayMeta{tree.nodes[idx].id,
tree.nodes[idx].elementKind, newElemKind, tree.nodes[idx].elementKind, newElemKind,
@@ -1383,8 +1441,7 @@ QJsonObject McpBridge::toolProcessInfo(const QJsonObject& args) {
void McpBridge::notifyTreeChanged() { void McpBridge::notifyTreeChanged() {
if (!m_client || !m_initialized) return; if (!m_client || !m_initialized) return;
sendNotification("notifications/resources/updated", m_notifyTimer->start(); // debounce 100ms
QJsonObject{{"uri", "project://tree"}});
} }
void McpBridge::notifyDataChanged() { void McpBridge::notifyDataChanged() {

View File

@@ -7,6 +7,7 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QByteArray> #include <QByteArray>
#include <QTimer>
namespace rcx { namespace rcx {
@@ -34,6 +35,7 @@ private:
QByteArray m_readBuffer; QByteArray m_readBuffer;
bool m_initialized = false; bool m_initialized = false;
bool m_slowMode = false; bool m_slowMode = false;
QTimer* m_notifyTimer = nullptr;
// JSON-RPC plumbing // JSON-RPC plumbing
void onNewConnection(); void onNewConnection();
@@ -65,10 +67,11 @@ private:
// Helpers // Helpers
QJsonObject makeTextResult(const QString& text, bool isError = false); QJsonObject makeTextResult(const QString& text, bool isError = false);
QString resolvePlaceholder(const QString& ref, QString resolvePlaceholder(const QString& ref,
const QHash<QString, uint64_t>& placeholderMap); const QHash<QString, uint64_t>& placeholderMap,
bool* ok = nullptr);
// Smart tab resolution: tabIndex arg → activeTab → first tab → auto-create // Smart tab resolution: tabIndex arg → activeTab → first tab → auto-create
MainWindow::TabState* resolveTab(const QJsonObject& args); MainWindow::TabState* resolveTab(const QJsonObject& args, int* resolvedIndex = nullptr);
}; };
} // namespace rcx } // namespace rcx

View File

@@ -53,6 +53,7 @@ public:
bool isReadable(uint64_t addr, int len) const override { bool isReadable(uint64_t addr, int len) const override {
if (len <= 0) return (len == 0); if (len <= 0) return (len == 0);
uint64_t end = addr + static_cast<uint64_t>(len); uint64_t end = addr + static_cast<uint64_t>(len);
if (end < addr) return false; // overflow
for (uint64_t p = addr & kPageMask; p < end; p += kPageSize) { for (uint64_t p = addr & kPageMask; p < end; p += kPageSize) {
if (!m_pages.contains(p)) return false; if (!m_pages.contains(p)) return false;
} }

View File

@@ -50,6 +50,7 @@
<file alias="settings-gear.svg">vsicons/settings-gear.svg</file> <file alias="settings-gear.svg">vsicons/settings-gear.svg</file>
<file alias="chevron-down.svg">vsicons/chevron-down.svg</file> <file alias="chevron-down.svg">vsicons/chevron-down.svg</file>
<file alias="chevron-right.svg">vsicons/chevron-right.svg</file> <file alias="chevron-right.svg">vsicons/chevron-right.svg</file>
<file alias="chevron-left.svg">vsicons/chevron-left.svg</file>
<file alias="folder.svg">vsicons/folder.svg</file> <file alias="folder.svg">vsicons/folder.svg</file>
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file> <file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
<file alias="symbol-class.svg">vsicons/symbol-class.svg</file> <file alias="symbol-class.svg">vsicons/symbol-class.svg</file>

View File

@@ -702,7 +702,7 @@ void ScannerPanel::onCellEdited(int row, int col) {
m_statusLabel->setText(QStringLiteral("Wrote %1 byte%2 to 0x%3") m_statusLabel->setText(QStringLiteral("Wrote %1 byte%2 to 0x%3")
.arg(bytes.size()) .arg(bytes.size())
.arg(bytes.size() == 1 ? "" : "s") .arg(bytes.size() == 1 ? "" : "s")
.arg(addr, 0, 16, QLatin1Char('0')).toUpper()); .arg(QString::number(addr, 16).toUpper()));
// Re-read and update cache // Re-read and update cache
m_resultTable->blockSignals(true); m_resultTable->blockSignals(true);
int readSize = (m_lastScanMode == 1) ? valueSize() : 16; int readSize = (m_lastScanMode == 1) ? valueSize() : 16;

505
src/typeinfer.h Normal file
View File

@@ -0,0 +1,505 @@
#pragma once
#include <QVector>
#include <cmath>
#include <cstdint>
#include <cstring>
#include "core.h"
namespace rcx {
// ── Hints from value history (optional, improves accuracy) ──
struct InferHints {
const uint8_t* minObserved = nullptr; // raw bytes, same len as data
const uint8_t* maxObserved = nullptr;
bool monotonic = false; // value only increases or only decreases
bool neverChanged = false; // identical across all samples
int sampleCount = 0; // 0 = no history
int ptrSize = 8;
};
// ── Suggestion result ──
struct TypeSuggestion {
QVector<NodeKind> kinds; // size==1: convert, size>1: uniform split
int score = 0; // 0-100 feature ratio (passed / checked × 100)
int strength = 0; // 0=hidden, 1=weak, 2=moderate, 3=strong
};
// ── Public API ──
QVector<TypeSuggestion> inferTypes(
const uint8_t* data, int len,
const InferHints& hints = {},
int maxResults = 3);
// Format top suggestion as short type label (e.g. "ptr64", "int32_t×2")
inline QString formatHint(const TypeSuggestion& s) {
if (s.kinds.isEmpty()) return {};
const char* name = kindMeta(s.kinds[0])->typeName;
return (s.kinds.size() == 1)
? QString::fromLatin1(name)
: QStringLiteral("%1\u00D7%2").arg(QString::fromLatin1(name)).arg(s.kinds.size());
}
// ── Implementation (header-only) ──
namespace detail {
inline uint32_t loadU32(const uint8_t* p) {
uint32_t v; std::memcpy(&v, p, 4); return v;
}
inline uint64_t loadU64(const uint8_t* p) {
uint64_t v; std::memcpy(&v, p, 8); return v;
}
inline uint16_t loadU16(const uint8_t* p) {
uint16_t v; std::memcpy(&v, p, 2); return v;
}
inline float loadF32(const uint8_t* p) {
float v; std::memcpy(&v, p, 4); return v;
}
inline double loadF64(const uint8_t* p) {
double v; std::memcpy(&v, p, 8); return v;
}
inline bool allZero(const uint8_t* p, int n) {
for (int i = 0; i < n; ++i) if (p[i]) return false;
return true;
}
inline int popcount32(uint32_t v) {
#if defined(__GNUC__) || defined(__clang__)
return __builtin_popcount(v);
#else
int c = 0; while (v) { v &= v - 1; ++c; } return c;
#endif
}
inline bool isPrintable(uint8_t c) {
return c >= 0x20 && c <= 0x7E;
}
// ── Float feature checker ──
// Returns features passed out of features checked (as pair)
struct FeatureResult { int passed; int checked; };
inline bool isGoodFloat(uint32_t bits) {
uint32_t exp = (bits >> 23) & 0xFF;
if (exp == 0xFF) return false; // inf/nan
if (exp == 0 && (bits & 0x7FFFFF)) return false; // denormal
float f; std::memcpy(&f, &bits, 4);
double af = std::fabs((double)f);
return f == 0.0f || (af >= 1e-6 && af <= 1e7);
}
inline FeatureResult countFloatFeatures(uint32_t cur,
const uint8_t* minP, const uint8_t* maxP,
const InferHints& h) {
int passed = 0, checked = 4;
float f; std::memcpy(&f, &cur, 4);
// Feature 1: finite
passed += std::isfinite((double)f) ? 1 : 0;
// Feature 2: non-denormal (exponent > 0 or value is ±0)
uint32_t exp = (cur >> 23) & 0xFF;
passed += (exp > 0 || (cur & 0x7FFFFFFF) == 0) ? 1 : 0;
// Feature 3: reasonable range
double af = std::fabs((double)f);
passed += (f == 0.0f || (af >= 1e-6 && af <= 1e7)) ? 1 : 0;
// Feature 4: has fractional part (not just a reinterpreted integer)
float ip; double frac = std::fabs((double)std::modf(f, &ip));
passed += (frac > 0.0001) ? 1 : 0;
if (h.sampleCount > 0 && minP && maxP) {
checked += 4;
uint32_t minBits = loadU32(minP), maxBits = loadU32(maxP);
// Feature 5-6: min/max are also valid floats
passed += isGoodFloat(minBits) ? 1 : 0;
passed += isGoodFloat(maxBits) ? 1 : 0;
// Feature 7: field changes
passed += (minBits != maxBits) ? 1 : 0;
// Feature 8: range is game-plausible
float fmin, fmax;
std::memcpy(&fmin, &minBits, 4);
std::memcpy(&fmax, &maxBits, 4);
double range = std::fabs((double)fmax - (double)fmin);
passed += (range < 1e6) ? 1 : 0;
}
return {passed, checked};
}
// ── Integer feature checker ──
inline FeatureResult countIntFeatures(uint32_t val,
const uint8_t* minP, const uint8_t* maxP,
const InferHints& h) {
// Hard reject: zero and sentinel are never useful integers
if (val == 0 || val == 0xFFFFFFFF)
return {0, 3};
int passed = 0, checked = 3;
int32_t sv = (int32_t)val;
// Feature 1: non-zero and not sentinel (always passes after hard reject)
passed += 1;
// Feature 2: small absolute value
passed += (val <= 1000000u || (uint32_t)(sv + 1000000) <= 2000000u) ? 1 : 0;
// Feature 3: fits int16 range
passed += (sv >= -32768 && sv <= 32767) ? 1 : 0;
if (h.sampleCount > 0 && minP && maxP) {
checked += 3;
uint32_t minV = loadU32(minP), maxV = loadU32(maxP);
// Feature 4: min/max in reasonable range
passed += (minV <= 1000000u && maxV <= 1000000u) ? 1 : 0;
// Feature 5: monotonic (counter/timer)
passed += h.monotonic ? 1 : 0;
// Feature 6: field varies
passed += (minV != maxV) ? 1 : 0;
}
return {passed, checked};
}
// ── Flags feature checker ──
inline FeatureResult countFlagFeatures(uint32_t val,
const uint8_t* minP, const uint8_t* maxP,
const InferHints& h) {
int passed = 0, checked = 2;
int pc = popcount32(val);
// Feature 1: sparse bits (1-3 set)
passed += (pc >= 1 && pc <= 3) ? 1 : 0;
// Feature 2: not a small sequential integer (flags are usually not 1,2,3...)
passed += (val > 256 || (val & (val - 1)) != 0) ? 1 : 0;
if (h.sampleCount > 0 && minP && maxP) {
checked += 3;
uint32_t minV = loadU32(minP), maxV = loadU32(maxP);
// Feature 3: XOR of min/max has low popcount (specific bits toggle)
passed += (popcount32(minV ^ maxV) <= 4) ? 1 : 0;
// Feature 4: field varies
passed += (minV != maxV) ? 1 : 0;
// Feature 5: max is superset of min bits
passed += ((minV & maxV) == minV) ? 1 : 0;
}
return {passed, checked};
}
// ── Pointer feature checker ──
inline FeatureResult countPtrFeatures64(uint64_t val) {
// Hard reject: common sentinel values are never pointers
if (val == 0 || val == 0xFFFFFFFFFFFFFFFFULL || val == 0x00000000FFFFFFFFULL)
return {0, 6};
int passed = 0, checked = 6;
// Feature 1: canonical 48-bit address (sign-extended from bit 47)
passed += (val <= 0x00007FFFFFFFFFFFULL
|| val >= 0xFFFF800000000000ULL) ? 1 : 0;
// Feature 2: aligned to 8 (heap/vtable allocations)
passed += ((val & 7) == 0) ? 1 : 0;
// Feature 3: above null guard pages (real addresses >= 64KB)
passed += (val >= 0x10000) ? 1 : 0;
// Feature 4: has upper 32 bits (real 64-bit address, not a small constant)
passed += ((val >> 32) != 0) ? 1 : 0;
// Feature 5: above 4GB (in real 64-bit address space, not a 32-bit value)
passed += (val > 0x100000000ULL) ? 1 : 0;
// Feature 6: user-mode address range (not kernel 0xFFFF800000000000+)
passed += (val < 0xFFFF800000000000ULL) ? 1 : 0;
return {passed, checked};
}
inline FeatureResult countPtrFeatures32(uint32_t val) {
int passed = 0, checked = 3;
// Feature 1: non-zero and not sentinel
passed += (val != 0 && val != 0xFFFFFFFF) ? 1 : 0;
// Feature 2: aligned to 4
passed += ((val & 3) == 0) ? 1 : 0;
// Feature 3: above null guard pages (>= 64KB)
passed += (val >= 0x10000) ? 1 : 0;
return {passed, checked};
}
// ── String feature checker ──
inline FeatureResult countStringFeatures(const uint8_t* data, int len) {
if (len < 2) return {0, 4};
int printable = 0, letters = 0, consecutive = 0, maxConsec = 0;
for (int i = 0; i < len; ++i) {
if (isPrintable(data[i])) {
printable++;
consecutive++;
maxConsec = std::max(maxConsec, consecutive);
if ((data[i] >= 'A' && data[i] <= 'Z') || (data[i] >= 'a' && data[i] <= 'z'))
letters++;
} else {
consecutive = 0;
}
}
double ratio = (double)printable / len;
int passed = 0, checked = 4;
passed += (maxConsec >= 4) ? 1 : 0;
passed += (ratio > 0.75) ? 1 : 0;
passed += (letters >= 1) ? 1 : 0;
passed += (ratio > 0.90) ? 1 : 0;
return {passed, checked};
}
// ── Int16 feature checker ──
inline FeatureResult countInt16Features(uint16_t val,
const uint8_t* minP, const uint8_t* maxP,
const InferHints& h) {
int passed = 0, checked = 2;
int16_t sv = (int16_t)val;
passed += (val != 0) ? 1 : 0;
passed += (sv >= -16384 && sv <= 16384) ? 1 : 0;
if (h.sampleCount > 0 && minP && maxP) {
checked += 2;
uint16_t minV = loadU16(minP), maxV = loadU16(maxP);
passed += (minV <= 4096 && maxV <= 4096) ? 1 : 0;
passed += (minV != maxV) ? 1 : 0;
}
return {passed, checked};
}
// ── Score from feature result ──
inline int featureScore(FeatureResult r) {
if (r.checked == 0) return 0;
return (r.passed * 100) / r.checked;
}
inline int strengthFromScore(int score) {
if (score >= 75) return 3;
if (score >= 50) return 2;
if (score >= 25) return 1;
return 0;
}
// ── Candidate accumulator ──
struct Candidate {
QVector<NodeKind> kinds;
int score;
};
inline void addCandidate(QVector<Candidate>& out, NodeKind k, int score) {
if (score >= 25) out.append({{k}, score});
}
inline void addSplitCandidate(QVector<Candidate>& out, NodeKind k, int count, int score) {
if (score >= 25) {
QVector<NodeKind> kinds(count, k);
out.append({std::move(kinds), score});
}
}
// ── Try whole-width interpretations ──
inline void tryWhole8(const uint8_t* data, const InferHints& h, QVector<Candidate>& out) {
uint64_t u64 = loadU64(data);
// Pointer64
if (h.ptrSize == 8)
addCandidate(out, NodeKind::Pointer64, featureScore(countPtrFeatures64(u64)));
// Double
{
double d; std::memcpy(&d, data, 8);
uint64_t exp = (u64 >> 52) & 0x7FF;
int passed = 0, checked = 3;
passed += std::isfinite(d) ? 1 : 0;
passed += (exp > 0 || (u64 & 0x7FFFFFFFFFFFFFFFull) == 0) ? 1 : 0;
double ad = std::fabs(d);
passed += (d == 0.0 || (ad >= 1e-6 && ad <= 1e12)) ? 1 : 0;
addCandidate(out, NodeKind::Double, featureScore({passed, checked}));
}
// UTF8
addCandidate(out, NodeKind::UTF8, featureScore(countStringFeatures(data, 8)));
// UInt64 / Int64
{
int passed = 0, checked = 4;
// Feature 1: fits in 32 bits (small constant, not an address)
passed += (u64 <= 0xFFFFFFFFull) ? 1 : 0;
// Feature 2: upper 32 bits are zero (confirms it's a small value, not a pointer)
passed += ((u64 >> 32) == 0) ? 1 : 0;
// Feature 3: non-zero
passed += (u64 != 0) ? 1 : 0;
// Feature 4: monotonic or very small (< 0x10000)
passed += (h.monotonic || u64 < 0x10000) ? 1 : 0;
addCandidate(out, NodeKind::UInt64, featureScore({passed, checked}));
}
}
inline void tryWhole4(const uint8_t* data, const uint8_t* minP, const uint8_t* maxP,
const InferHints& h, QVector<Candidate>& out) {
uint32_t u32 = loadU32(data);
// Float
addCandidate(out, NodeKind::Float, featureScore(countFloatFeatures(u32, minP, maxP, h)));
// Int32
addCandidate(out, NodeKind::Int32, featureScore(countIntFeatures(u32, minP, maxP, h)));
// UInt32
addCandidate(out, NodeKind::UInt32, featureScore(countIntFeatures(u32, minP, maxP, h)));
// Flags (only if sparse bits)
addCandidate(out, NodeKind::UInt32, featureScore(countFlagFeatures(u32, minP, maxP, h)));
// Pointer32
if (h.ptrSize == 4)
addCandidate(out, NodeKind::Pointer32, featureScore(countPtrFeatures32(u32)));
}
inline void tryWhole2(const uint8_t* data, const uint8_t* minP, const uint8_t* maxP,
const InferHints& h, QVector<Candidate>& out) {
uint16_t u16 = loadU16(data);
int scoreI = featureScore(countInt16Features(u16, minP, maxP, h));
addCandidate(out, NodeKind::Int16, scoreI);
addCandidate(out, NodeKind::UInt16, scoreI);
}
inline void tryWhole1(const uint8_t* data, QVector<Candidate>& out) {
uint8_t v = data[0];
int score = (v == 0 || v == 1) ? 50 : 25;
addCandidate(out, NodeKind::UInt8, score);
}
// ── Try uniform splits ──
inline void trySplitUniform(const uint8_t* data, int len,
const InferHints& h,
QVector<Candidate>& out) {
// 8 → 2×4
if (len == 8) {
const uint8_t* minA = h.minObserved;
const uint8_t* minB = h.minObserved ? h.minObserved + 4 : nullptr;
const uint8_t* maxA = h.maxObserved;
const uint8_t* maxB = h.maxObserved ? h.maxObserved + 4 : nullptr;
bool zA = allZero(data, 4), zB = allZero(data + 4, 4);
// Float×2: both halves must be good floats and at least one non-zero
if (!zA || !zB) {
uint32_t bitsA = loadU32(data), bitsB = loadU32(data + 4);
bool fA = zA || isGoodFloat(bitsA);
bool fB = zB || isGoodFloat(bitsB);
if (fA && fB) {
auto rA = zA ? FeatureResult{2, 4} : countFloatFeatures(bitsA, minA, maxA, h);
auto rB = zB ? FeatureResult{2, 4} : countFloatFeatures(bitsB, minB, maxB, h);
int score = std::min(featureScore(rA), featureScore(rB));
addSplitCandidate(out, NodeKind::Float, 2, score);
}
}
// Int32×2: both halves, at least one non-zero
if (!zA || !zB) {
auto rA = zA ? FeatureResult{1, 3} : countIntFeatures(loadU32(data), minA, maxA, h);
auto rB = zB ? FeatureResult{1, 3} : countIntFeatures(loadU32(data + 4), minB, maxB, h);
int score = std::min(featureScore(rA), featureScore(rB));
addSplitCandidate(out, NodeKind::Int32, 2, score);
}
// UInt32×2
if (!zA || !zB) {
auto rA = zA ? FeatureResult{1, 3} : countIntFeatures(loadU32(data), minA, maxA, h);
auto rB = zB ? FeatureResult{1, 3} : countIntFeatures(loadU32(data + 4), minB, maxB, h);
int score = std::min(featureScore(rA), featureScore(rB));
addSplitCandidate(out, NodeKind::UInt32, 2, score);
}
}
// 8 → 4×2 or 4 → 2×2
int halfLen = len / 2;
if (halfLen == 2) {
int minScore = 100;
int count = len / 2;
bool anyNonZero = false;
for (int i = 0; i < count; ++i) {
const uint8_t* part = data + i * 2;
if (!allZero(part, 2)) anyNonZero = true;
const uint8_t* mp = h.minObserved ? h.minObserved + i * 2 : nullptr;
const uint8_t* xp = h.maxObserved ? h.maxObserved + i * 2 : nullptr;
int s = featureScore(countInt16Features(loadU16(part), mp, xp, h));
minScore = std::min(minScore, s);
}
if (anyNonZero) {
addSplitCandidate(out, NodeKind::Int16, count, minScore);
addSplitCandidate(out, NodeKind::UInt16, count, minScore);
}
}
}
// ── Prune and rank ──
inline QVector<TypeSuggestion> pruneAndRank(QVector<Candidate>& cands, int maxResults) {
// Sort descending by score
std::sort(cands.begin(), cands.end(), [](const Candidate& a, const Candidate& b) {
return a.score > b.score;
});
// Dedup: keep highest-scoring per unique kinds vector
QVector<Candidate> deduped;
for (const auto& c : cands) {
bool dup = false;
for (const auto& d : deduped) {
if (d.kinds == c.kinds) { dup = true; break; }
}
if (!dup) deduped.append(c);
}
// Dominance: if top >= 1.5× second, keep only top
if (deduped.size() >= 2 && deduped[0].score >= deduped[1].score * 3 / 2)
deduped.resize(1);
else if (deduped.size() > maxResults)
deduped.resize(maxResults);
QVector<TypeSuggestion> result;
result.reserve(deduped.size());
for (const auto& c : deduped) {
int str = strengthFromScore(c.score);
if (str > 0)
result.append({c.kinds, c.score, str});
}
return result;
}
} // namespace detail
// ── Entry point ──
inline QVector<TypeSuggestion> inferTypes(
const uint8_t* data, int len,
const InferHints& hints,
int maxResults)
{
using namespace detail;
if (!data || len <= 0) return {};
if (allZero(data, len)) return {}; // NULL → skip entirely
QVector<Candidate> cands;
cands.reserve(12);
// Whole-width candidates
if (len >= 8) tryWhole8(data, hints, cands);
if (len == 4) tryWhole4(data, hints.minObserved, hints.maxObserved, hints, cands);
if (len == 2) tryWhole2(data, hints.minObserved, hints.maxObserved, hints, cands);
if (len == 1) tryWhole1(data, cands);
// Uniform splits (compete directly with whole-width candidates)
if (len >= 4)
trySplitUniform(data, len, hints, cands);
return pruneAndRank(cands, maxResults);
}
} // namespace rcx

View File

@@ -57,51 +57,73 @@ TypeSpec parseTypeSpec(const QString& text) {
} }
// ── Fuzzy scorer: subsequence match with word-boundary bonuses ── // ── Fuzzy scorer: subsequence match with word-boundary bonuses ──
// Hot path — uses stack arrays and pre-lowered QChars to avoid heap allocs.
static constexpr int kMaxFuzzyLen = 64;
static int fuzzyScore(const QString& pattern, const QString& text, static int fuzzyScore(const QString& pattern, const QString& text,
QVector<int>* outPositions = nullptr) { QVector<int>* outPositions = nullptr) {
int pLen = pattern.size(), tLen = text.size(); int pLen = pattern.size(), tLen = text.size();
if (pLen == 0) return 1; if (pLen == 0) return 1;
if (pLen > tLen) return 0; if (pLen > tLen) return 0;
if (pLen > kMaxFuzzyLen || tLen > 256) {
// Fallback: prefix match only for very long names
if (text.startsWith(pattern, Qt::CaseInsensitive)) return 1;
return 0;
}
// Quick subsequence reject // Pre-compute lowercase chars on the stack
QChar pLow[kMaxFuzzyLen];
for (int i = 0; i < pLen; i++) pLow[i] = pattern[i].toLower();
QChar tLow[256];
for (int i = 0; i < tLen; i++) tLow[i] = text[i].toLower();
// Quick subsequence reject using pre-lowered arrays
{ int pi = 0; { int pi = 0;
for (int ti = 0; ti < tLen && pi < pLen; ti++) for (int ti = 0; ti < tLen && pi < pLen; ti++)
if (pattern[pi].toLower() == text[ti].toLower()) pi++; if (pLow[pi] == tLow[ti]) pi++;
if (pi < pLen) return 0; if (pi < pLen) return 0;
} }
// Recursive best-match (bounded: max 4 branches per pattern char) // Recursive best-match (bounded: max 4 branches per pattern char)
QVector<int> bestPos; // Stack arrays instead of QVector to avoid heap allocation
int bestPos[kMaxFuzzyLen];
int curPos[kMaxFuzzyLen];
int best = 0; int best = 0;
int bestLen = 0;
auto solve = [&](auto& self, int pi, int ti, QVector<int>& cur, int score) -> void { auto solve = [&](auto& self, int pi, int ti, int curLen, int score) -> void {
if (pi == pLen) { if (pi == pLen) {
if (score > best) { best = score; bestPos = cur; } if (score > best) {
best = score;
bestLen = curLen;
memcpy(bestPos, curPos, curLen * sizeof(int));
}
return; return;
} }
int maxTi = tLen - (pLen - pi); int maxTi = tLen - (pLen - pi);
int branches = 0; int branches = 0;
for (int i = ti; i <= maxTi && branches < 4; i++) { for (int i = ti; i <= maxTi && branches < 4; i++) {
if (pattern[pi].toLower() != text[i].toLower()) continue; if (pLow[pi] != tLow[i]) continue;
int bonus = 1; int bonus = 1;
if (i == 0) bonus = 10; if (i == 0) bonus = 10;
else if (text[i - 1] == '_' || text[i - 1] == ' ') bonus = 8; else if (text[i - 1] == '_' || text[i - 1] == ' ') bonus = 8;
else if (text[i].isUpper() && text[i - 1].isLower()) bonus = 8; else if (text[i].isUpper() && text[i - 1].isLower()) bonus = 8;
if (!cur.isEmpty() && i == cur.last() + 1) bonus += 5; if (curLen > 0 && i == curPos[curLen - 1] + 1) bonus += 5;
cur.append(i); curPos[curLen] = i;
self(self, pi + 1, i + 1, cur, score + bonus); self(self, pi + 1, i + 1, curLen + 1, score + bonus);
cur.removeLast();
branches++; branches++;
} }
}; };
QVector<int> cur; solve(solve, 0, 0, 0, 0);
solve(solve, 0, 0, cur, 0);
if (best > 0) { if (best > 0) {
best += qMax(0, 20 - (tLen - pLen)); // tightness bonus best += qMax(0, 20 - (tLen - pLen)); // tightness bonus
if (pLen == tLen) best += 20; // exact match bonus if (pLen == tLen) best += 20; // exact match bonus
if (outPositions) *outPositions = bestPos; if (outPositions) {
outPositions->resize(bestLen);
memcpy(outPositions->data(), bestPos, bestLen * sizeof(int));
}
} }
return best; return best;
} }
@@ -113,7 +135,7 @@ public:
explicit TypeSelectorDelegate(TypeSelectorPopup* popup, QObject* parent = nullptr) explicit TypeSelectorDelegate(TypeSelectorPopup* popup, QObject* parent = nullptr)
: QStyledItemDelegate(parent), m_popup(popup) {} : QStyledItemDelegate(parent), m_popup(popup) {}
void setFont(const QFont& f) { m_font = f; } void setFont(const QFont& f) { m_font = f; updateCachedSizeHint(); }
void setLoading(bool v) { m_isLoading = v; } void setLoading(bool v) { m_isLoading = v; }
void setFilteredTypes(const QVector<TypeEntry>* filtered) { void setFilteredTypes(const QVector<TypeEntry>* filtered) {
m_filtered = filtered; m_filtered = filtered;
@@ -287,13 +309,13 @@ public:
} }
QSize sizeHint(const QStyleOptionViewItem& /*option*/, QSize sizeHint(const QStyleOptionViewItem& /*option*/,
const QModelIndex& index) const override { const QModelIndex& /*index*/) const override {
return m_cachedSizeHint;
}
void updateCachedSizeHint() {
QFontMetrics fm(m_font); QFontMetrics fm(m_font);
int row = index.row(); m_cachedSizeHint = QSize(200, fm.height() + 8);
bool isSection = (m_filtered && row >= 0 && row < m_filtered->size()
&& (*m_filtered)[row].entryKind == TypeEntry::Section);
int h = isSection ? fm.height() + 2 : fm.height() + 8;
return QSize(200, h);
} }
bool helpEvent(QHelpEvent* event, QAbstractItemView* view, bool helpEvent(QHelpEvent* event, QAbstractItemView* view,
@@ -304,8 +326,9 @@ public:
if (row >= 0 && row < m_filtered->size()) { if (row >= 0 && row < m_filtered->size()) {
const auto& e = (*m_filtered)[row]; const auto& e = (*m_filtered)[row];
if (e.entryKind == TypeEntry::Composite && !e.fieldSummary.isEmpty()) { if (e.entryKind == TypeEntry::Composite && !e.fieldSummary.isEmpty()) {
QString tip = QStringLiteral("%1 (%2 B, %3 fields)\n") QString tip = QStringLiteral("%1 (0x%2 bytes, %3 fields)\n")
.arg(e.displayName).arg(e.sizeBytes).arg(e.fieldCount); .arg(e.displayName, QString::number(e.sizeBytes, 16).toUpper())
.arg(e.fieldCount);
tip += e.fieldSummary.join(QChar('\n')); tip += e.fieldSummary.join(QChar('\n'));
if (e.fieldCount > e.fieldSummary.size()) if (e.fieldCount > e.fieldSummary.size())
tip += QStringLiteral("\n..."); tip += QStringLiteral("\n...");
@@ -322,6 +345,7 @@ public:
private: private:
TypeSelectorPopup* m_popup = nullptr; TypeSelectorPopup* m_popup = nullptr;
QFont m_font; QFont m_font;
QSize m_cachedSizeHint{200, 20};
bool m_isLoading = false; bool m_isLoading = false;
const QVector<TypeEntry>* m_filtered = nullptr; const QVector<TypeEntry>* m_filtered = nullptr;
const QVector<QVector<int>>* m_matchPositions = nullptr; const QVector<QVector<int>>* m_matchPositions = nullptr;
@@ -448,6 +472,9 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers); m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_listView->viewport()->setAttribute(Qt::WA_Hover, true); m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
m_listView->setAccessibleName(QStringLiteral("Type list")); m_listView->setAccessibleName(QStringLiteral("Type list"));
m_listView->setUniformItemSizes(true);
m_listView->setLayoutMode(QListView::Batched);
m_listView->setBatchSize(50);
m_listView->installEventFilter(this); m_listView->installEventFilter(this);
auto* delegate = new TypeSelectorDelegate(this, m_listView); auto* delegate = new TypeSelectorDelegate(this, m_listView);
@@ -714,6 +741,7 @@ void TypeSelectorPopup::applyTheme(const Theme& theme) {
m_titleLabel->setPalette(pal); m_titleLabel->setPalette(pal);
m_filterEdit->setPalette(pal); m_filterEdit->setPalette(pal);
m_listView->setPalette(pal); m_listView->setPalette(pal);
m_listView->viewport()->setPalette(pal);
m_arrayCountEdit->setPalette(pal); m_arrayCountEdit->setPalette(pal);
// Esc button (snapped to corner) // Esc button (snapped to corner)
@@ -826,6 +854,12 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
if (delegate) delegate->setLoading(false); if (delegate) delegate->setLoading(false);
m_allTypes = types; m_allTypes = types;
// Cache max display name length for popup width calculation
m_cachedMaxNameLen = 0;
for (const auto& t : m_allTypes) {
if (t.entryKind != TypeEntry::Section)
m_cachedMaxNameLen = qMax(m_cachedMaxNameLen, (int)t.displayName.size());
}
if (current) { if (current) {
m_currentEntry = *current; m_currentEntry = *current;
m_hasCurrent = true; m_hasCurrent = true;
@@ -858,13 +892,12 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
void TypeSelectorPopup::popup(const QPoint& globalPos) { void TypeSelectorPopup::popup(const QPoint& globalPos) {
QFontMetrics fm(m_font); QFontMetrics fm(m_font);
int maxTextW = fm.horizontalAdvance(QStringLiteral("Choose element type ")); constexpr int kMaxPopupW = 560;
for (const auto& t : m_allTypes) { // Estimate max width from cached max name length (avoids iterating all types)
int iconColW = fm.height() + 4; int iconColW = fm.height() + 4;
int w = iconColW + fm.horizontalAdvance(t.displayName) + 16; int estMaxW = iconColW + fm.horizontalAdvance(QChar('W')) * m_cachedMaxNameLen + 16;
if (w > maxTextW) maxTextW = w; int maxTextW = qMax(fm.horizontalAdvance(QStringLiteral("Choose element type ")), estMaxW);
} int popupW = qBound(480, maxTextW + 24, kMaxPopupW);
int popupW = qBound(480, maxTextW + 24, 560);
int rowH = fm.height() + 8; int rowH = fm.height() + 8;
int headerH = rowH * 2 + 10; // filter + chips + separator int headerH = rowH * 2 + 10; // filter + chips + separator
int footerH = rowH + 6; // separator + action row int footerH = rowH + 6; // separator + action row
@@ -968,18 +1001,27 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
auto makeLabel = [](const TypeEntry& e) { auto makeLabel = [](const TypeEntry& e) {
QString label = e.displayName; QString label = e.displayName;
if (e.sizeBytes > 0) label += QStringLiteral(" - %1").arg(e.sizeBytes); if (e.sizeBytes > 0) label += QStringLiteral(" - 0x%1 bytes").arg(QString::number(e.sizeBytes, 16).toUpper());
return label; return label;
}; };
int primCount = 0, typeCount = 0, enumCount = 0; int primCount = 0, typeCount = 0, enumCount = 0;
const int totalTypes = m_allTypes.size();
// Pre-reserve to avoid realloc churn
m_filteredTypes.reserve(totalTypes);
m_matchPositions.reserve(totalTypes);
displayStrings.reserve(totalTypes);
if (!filterBase.isEmpty()) { if (!filterBase.isEmpty()) {
// ── Fuzzy search: flat ranked list, no section headers ── // ── Fuzzy search: flat ranked list, no section headers ──
struct Scored { TypeEntry entry; int score; QVector<int> pos; }; // Use index + score to avoid deep-copying TypeEntry structs
struct Scored { int idx; int score; QVector<int> pos; };
QVector<Scored> scored; QVector<Scored> scored;
scored.reserve(totalTypes);
for (const auto& t : m_allTypes) { for (int i = 0; i < totalTypes; i++) {
const auto& t = m_allTypes[i];
if (t.entryKind == TypeEntry::Section) continue; if (t.entryKind == TypeEntry::Section) continue;
QVector<int> pos; QVector<int> pos;
int sc = fuzzyScore(filterBase, t.displayName, &pos); int sc = fuzzyScore(filterBase, t.displayName, &pos);
@@ -988,15 +1030,15 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
else if (t.category == TypeEntry::CatEnum) enumCount++; else if (t.category == TypeEntry::CatEnum) enumCount++;
else typeCount++; else typeCount++;
if (catAllowed(t)) if (catAllowed(t))
scored.append({t, sc, pos}); scored.append({i, sc, std::move(pos)});
} }
std::sort(scored.begin(), scored.end(), std::sort(scored.begin(), scored.end(),
[](const Scored& a, const Scored& b) { return a.score > b.score; }); [](const Scored& a, const Scored& b) { return a.score > b.score; });
for (const auto& s : scored) { for (const auto& s : scored) {
m_filteredTypes.append(s.entry); m_filteredTypes.append(m_allTypes[s.idx]);
m_matchPositions.append(s.pos); m_matchPositions.append(s.pos);
displayStrings << makeLabel(s.entry); displayStrings << makeLabel(m_allTypes[s.idx]);
} }
} else { } else {
// ── No filter: grouped sections, alphabetical ── // ── No filter: grouped sections, alphabetical ──

View File

@@ -120,6 +120,7 @@ private:
int m_pointerSize = 8; int m_pointerSize = 8;
bool m_loading = false; bool m_loading = false;
QFont m_font; QFont m_font;
int m_cachedMaxNameLen = 0; // longest displayName length (chars)
void applyFilter(const QString& text); void applyFilter(const QString& text);
void updateModifierPreview(); void updateModifierPreview();

View File

@@ -5,6 +5,7 @@
#include <QStandardItemModel> #include <QStandardItemModel>
#include <QStandardItem> #include <QStandardItem>
#include <QStyledItemDelegate> #include <QStyledItemDelegate>
#include <QSortFilterProxyModel>
#include <QPainter> #include <QPainter>
#include <QApplication> #include <QApplication>
#include <algorithm> #include <algorithm>
@@ -43,6 +44,7 @@ inline void buildStructChildren(QStandardItem* item,
}; };
for (int mi : members) { for (int mi : members) {
if (mi < 0 || mi >= tree->nodes.size()) continue;
const Node& m = tree->nodes[mi]; const Node& m = tree->nodes[mi];
if (isHexPad(m.kind)) continue; if (isHexPad(m.kind)) continue;
QString childDisplay = QStringLiteral("%1 %2") QString childDisplay = QStringLiteral("%1 %2")
@@ -75,10 +77,11 @@ inline QString typeDisplayString(const Node* node, const NodeTree* tree) {
// Build a new item for a type entry. // Build a new item for a type entry.
inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree, inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree,
void* subPtr) { void* subPtr) {
static const QIcon enumIcon(":/vsicons/symbol-enum.svg");
static const QIcon structIcon(":/vsicons/symbol-structure.svg");
bool isEnum = node->resolvedClassKeyword() == QStringLiteral("enum"); bool isEnum = node->resolvedClassKeyword() == QStringLiteral("enum");
auto* item = new QStandardItem( auto* item = new QStandardItem(
QIcon(isEnum ? ":/vsicons/symbol-enum.svg" isEnum ? enumIcon : structIcon,
: ":/vsicons/symbol-structure.svg"),
typeDisplayString(node, tree)); typeDisplayString(node, tree));
item->setData(QVariant::fromValue(subPtr), Qt::UserRole); item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
item->setData(QVariant::fromValue(node->id), Qt::UserRole + 1); item->setData(QVariant::fromValue(node->id), Qt::UserRole + 1);
@@ -92,7 +95,8 @@ inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree,
// Full rebuild — used by benchmarks and first build. // Full rebuild — used by benchmarks and first build.
inline void buildProjectExplorer(QStandardItemModel* model, inline void buildProjectExplorer(QStandardItemModel* model,
const QVector<TabInfo>& tabs) { const QVector<TabInfo>& tabs,
const QSet<uint64_t>& pinnedIds = {}) {
model->clear(); model->clear();
model->setHorizontalHeaderLabels({QStringLiteral("Name")}); model->setHorizontalHeaderLabels({QStringLiteral("Name")});
@@ -110,18 +114,32 @@ inline void buildProjectExplorer(QStandardItemModel* model,
} }
} }
for (const auto& e : types) // Pinned items at the very top, then structs, then enums
QVector<Entry> pinned;
QVector<Entry> unpinnedTypes, unpinnedEnums;
for (const auto& e : types) {
if (pinnedIds.contains(e.node->id)) pinned.append(e);
else unpinnedTypes.append(e);
}
for (const auto& e : enums) {
if (pinnedIds.contains(e.node->id)) pinned.append(e);
else unpinnedEnums.append(e);
}
for (const auto& e : pinned)
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr)); model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
for (const auto& e : enums) for (const auto& e : unpinnedTypes)
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
for (const auto& e : unpinnedEnums)
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr)); model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
} }
// Incremental sync — preserves tree expansion/scroll state. // Incremental sync — preserves tree expansion/scroll state.
inline void syncProjectExplorer(QStandardItemModel* model, inline void syncProjectExplorer(QStandardItemModel* model,
const QVector<TabInfo>& tabs) { const QVector<TabInfo>& tabs,
const QSet<uint64_t>& pinnedIds = {}) {
// First call — full build // First call — full build
if (model->rowCount() == 0 && !tabs.isEmpty()) { if (model->rowCount() == 0 && !tabs.isEmpty()) {
buildProjectExplorer(model, tabs); buildProjectExplorer(model, tabs, pinnedIds);
return; return;
} }
@@ -145,7 +163,9 @@ inline void syncProjectExplorer(QStandardItemModel* model,
// Remove stale items (backwards) // Remove stale items (backwards)
for (int i = model->rowCount() - 1; i >= 0; --i) { for (int i = model->rowCount() - 1; i >= 0; --i) {
uint64_t id = model->item(i)->data(Qt::UserRole + 1).toULongLong(); auto* item = model->item(i);
if (!item) continue;
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
if (!desiredMap.contains(id)) if (!desiredMap.contains(id))
model->removeRow(i); model->removeRow(i);
} }
@@ -201,6 +221,8 @@ public:
m_selected = t.selected; m_selected = t.selected;
m_accent = t.borderFocused; // left accent bar m_accent = t.borderFocused; // left accent bar
m_bg = t.background; m_bg = t.background;
m_badgeBg = t.backgroundAlt;
m_badgeText = t.textDim;
} }
QSize sizeHint(const QStyleOptionViewItem& option, QSize sizeHint(const QStyleOptionViewItem& option,
@@ -234,38 +256,74 @@ public:
QString fullText = index.data(Qt::DisplayRole).toString(); QString fullText = index.data(Qt::DisplayRole).toString();
QRect textRect = opt.rect.adjusted(4, 0, -4, 0); QRect textRect = opt.rect.adjusted(4, 0, -4, 0);
// Draw icon for top-level items // Letter badge (S/E for top-level, F for children)
if (!isChild) { {
QVariant iconVar = index.data(Qt::DecorationRole); QChar letter = 'F';
if (iconVar.isValid()) { if (!isChild) {
QIcon icon = iconVar.value<QIcon>(); bool isEnum = index.data(Qt::UserRole + 2).toBool();
int iconSz = opt.fontMetrics.height(); letter = isEnum ? 'E' : 'S';
int iconY = textRect.y() + (textRect.height() - iconSz) / 2;
icon.paint(painter, textRect.x(), iconY, iconSz, iconSz);
textRect.setLeft(textRect.left() + iconSz + 4);
} }
int sz = opt.fontMetrics.height();
int y = textRect.y() + (textRect.height() - sz) / 2;
QRect badge(textRect.x(), y, sz, sz);
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setRenderHint(QPainter::TextAntialiasing, true);
painter->setPen(Qt::NoPen);
painter->setBrush(m_badgeBg);
painter->drawRoundedRect(badge, 3, 3);
QColor letterCol = m_badgeText;
if (!isChild && !index.data(Qt::UserRole + 3).toBool())
letterCol.setAlpha(100);
painter->setPen(letterCol);
QFont bf = opt.font;
bf.setBold(true);
painter->setFont(bf);
painter->drawText(badge, Qt::AlignCenter, letter);
painter->setRenderHint(QPainter::Antialiasing, false);
textRect.setLeft(textRect.left() + sz + 4);
} }
painter->setFont(opt.font); painter->setFont(opt.font);
if (!isChild) { if (!isChild) {
// Top-level: "StructName — 3" // Top-level: "StructName — 3" → name left, count pill right
int dashPos = fullText.indexOf(QChar(0x2014)); int dashPos = fullText.indexOf(QChar(0x2014));
if (dashPos > 1) { QString name = (dashPos > 1) ? fullText.left(dashPos - 1) : fullText;
QString name = fullText.left(dashPos - 1); QString count = (dashPos > 1) ? fullText.mid(dashPos + 2).trimmed() : QString();
QString meta = fullText.mid(dashPos - 1);
painter->setPen(m_text); bool pinned = index.data(Qt::UserRole + 4).toBool();
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, name);
int nameW = opt.fontMetrics.horizontalAdvance(name);
QRect metaRect = textRect; // Reserve right side for pin icon + count pill
metaRect.setLeft(textRect.left() + nameW); int rightEdge = textRect.right();
if (!count.isEmpty()) {
int cw = opt.fontMetrics.horizontalAdvance(count) + 10;
int ch = opt.fontMetrics.height();
int cy = textRect.y() + (textRect.height() - ch) / 2;
QRect pill(rightEdge - cw, cy, cw, ch);
rightEdge = pill.left() - 2;
painter->setPen(Qt::NoPen);
painter->setBrush(m_badgeBg);
painter->drawRect(pill);
painter->setPen(m_textMuted); painter->setPen(m_textMuted);
painter->drawText(metaRect, Qt::AlignLeft | Qt::AlignVCenter, meta); painter->drawText(pill, Qt::AlignCenter, count);
} else { }
if (pinned) {
static const QIcon pinIcon(":/vsicons/pin.svg");
int isz = opt.fontMetrics.height() - 2;
int iy = textRect.y() + (textRect.height() - isz) / 2;
QRect pinRect(rightEdge - isz, iy, isz, isz);
pinIcon.paint(painter, pinRect);
rightEdge = pinRect.left() - 2;
}
// Draw name clipped before right-side elements
if (rightEdge > textRect.left() + 4) {
QRect nameRect = textRect;
nameRect.setRight(rightEdge);
QString elided = opt.fontMetrics.elidedText(name, Qt::ElideRight, nameRect.width());
painter->setPen(m_text); painter->setPen(m_text);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, fullText); painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, elided);
} }
} else { } else {
// Child: "TypeName fieldName" // Child: "TypeName fieldName"
@@ -294,6 +352,7 @@ public:
private: private:
QColor m_text, m_textDim, m_textMuted, m_syntaxType; QColor m_text, m_textDim, m_textMuted, m_syntaxType;
QColor m_hover, m_selected, m_accent, m_bg; QColor m_hover, m_selected, m_accent, m_bg;
QColor m_badgeBg, m_badgeText;
}; };
} // namespace rcx } // namespace rcx

View File

@@ -38,6 +38,7 @@ static void buildSmallTree(NodeTree& tree) {
root.name = "root"; root.name = "root";
root.parentId = 0; root.parentId = 0;
root.offset = 0; root.offset = 0;
root.collapsed = false;
int ri = tree.addNode(root); int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id; uint64_t rootId = tree.nodes[ri].id;

View File

@@ -1964,6 +1964,7 @@ private slots:
root.structTypeName = "Chain"; root.structTypeName = "Chain";
root.name = "chain"; root.name = "chain";
root.parentId = 0; root.parentId = 0;
root.collapsed = false;
int ri = tree.addNode(root); int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id; uint64_t rootId = tree.nodes[ri].id;
@@ -1974,6 +1975,7 @@ private slots:
inner.name = "Inner"; inner.name = "Inner";
inner.parentId = 0; inner.parentId = 0;
inner.offset = 300; inner.offset = 300;
inner.collapsed = false;
int ii = tree.addNode(inner); int ii = tree.addNode(inner);
uint64_t innerId = tree.nodes[ii].id; uint64_t innerId = tree.nodes[ii].id;
{ {
@@ -1990,6 +1992,7 @@ private slots:
outer.name = "Outer"; outer.name = "Outer";
outer.parentId = 0; outer.parentId = 0;
outer.offset = 200; outer.offset = 200;
outer.collapsed = false;
int oi = tree.addNode(outer); int oi = tree.addNode(outer);
uint64_t outerId = tree.nodes[oi].id; uint64_t outerId = tree.nodes[oi].id;
{ {
@@ -2002,6 +2005,7 @@ private slots:
p.kind = NodeKind::Pointer64; p.name = "pInner"; p.kind = NodeKind::Pointer64; p.name = "pInner";
p.parentId = outerId; p.offset = 8; p.parentId = outerId; p.offset = 8;
p.refId = innerId; p.refId = innerId;
p.collapsed = false;
tree.addNode(p); tree.addNode(p);
} }
@@ -2011,6 +2015,7 @@ private slots:
p.kind = NodeKind::Pointer64; p.name = "pOuter"; p.kind = NodeKind::Pointer64; p.name = "pOuter";
p.parentId = rootId; p.offset = 0; p.parentId = rootId; p.offset = 0;
p.refId = outerId; p.refId = outerId;
p.collapsed = false;
tree.addNode(p); tree.addNode(p);
} }
@@ -2706,6 +2711,7 @@ private slots:
sf.offset = 0; sf.offset = 0;
sf.isStatic = true; sf.isStatic = true;
sf.offsetExpr = QStringLiteral("base + 0x10"); sf.offsetExpr = QStringLiteral("base + 0x10");
sf.collapsed = false;
tree.addNode(sf); tree.addNode(sf);
NullProvider prov; NullProvider prov;

View File

@@ -1,807 +0,0 @@
#include <QtTest/QTest>
#include <QJsonDocument>
#include <QJsonObject>
#include <QTemporaryFile>
#include <QStandardItemModel>
#include "core.h"
#include "generator.h"
#include "controller.h"
#include "workspace_model.h"
using namespace rcx;
class TestNewFeatures : public QObject {
Q_OBJECT
private:
NodeTree makeSimpleTree() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Player";
root.structTypeName = "Player";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node f1;
f1.kind = NodeKind::Int32;
f1.name = "health";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
Node f2;
f2.kind = NodeKind::Float;
f2.name = "speed";
f2.parentId = rootId;
f2.offset = 4;
tree.addNode(f2);
return tree;
}
NodeTree makeTwoRootTree() {
NodeTree tree;
tree.baseAddress = 0;
// Root struct A
Node a;
a.kind = NodeKind::Struct;
a.name = "Alpha";
a.structTypeName = "Alpha";
a.parentId = 0;
a.offset = 0;
int ai = tree.addNode(a);
uint64_t aId = tree.nodes[ai].id;
Node af;
af.kind = NodeKind::UInt32;
af.name = "flagsA";
af.parentId = aId;
af.offset = 0;
tree.addNode(af);
// Root struct B
Node b;
b.kind = NodeKind::Struct;
b.name = "Bravo";
b.structTypeName = "Bravo";
b.parentId = 0;
b.offset = 0x100;
int bi = tree.addNode(b);
uint64_t bId = tree.nodes[bi].id;
Node bf;
bf.kind = NodeKind::UInt64;
bf.name = "flagsB";
bf.parentId = bId;
bf.offset = 0;
tree.addNode(bf);
return tree;
}
NodeTree makeRichTree() {
NodeTree tree;
tree.baseAddress = 0x00400000;
// ── Pet (root struct) ──
Node pet;
pet.kind = NodeKind::Struct;
pet.name = "Pet";
pet.structTypeName = "Pet";
pet.parentId = 0;
pet.offset = 0;
int pi = tree.addNode(pet);
uint64_t petId = tree.nodes[pi].id;
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_00"; n.parentId = petId; n.offset = 0; tree.addNode(n); }
{ Node n; n.kind = NodeKind::UTF8; n.name = "name"; n.parentId = petId; n.offset = 8; n.strLen = 16; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_18"; n.parentId = petId; n.offset = 24; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_20"; n.parentId = petId; n.offset = 32; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_24"; n.parentId = petId; n.offset = 36; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Pointer64; n.name = "owner"; n.parentId = petId; n.offset = 40; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_30"; n.parentId = petId; n.offset = 48; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_38"; n.parentId = petId; n.offset = 56; tree.addNode(n); }
// ── Cat (root struct, "inherits" Pet via nested struct) ──
Node cat;
cat.kind = NodeKind::Struct;
cat.name = "Cat";
cat.structTypeName = "Cat";
cat.parentId = 0;
cat.offset = 0;
int ci = tree.addNode(cat);
uint64_t catId = tree.nodes[ci].id;
// base = embedded Pet (nested struct child at offset 0)
Node base;
base.kind = NodeKind::Struct;
base.name = "base";
base.structTypeName = "Pet";
base.parentId = catId;
base.offset = 0;
int bi = tree.addNode(base);
uint64_t baseId = tree.nodes[bi].id;
// Children inside the nested Pet base
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_00"; n.parentId = baseId; n.offset = 0; tree.addNode(n); }
{ Node n; n.kind = NodeKind::UTF8; n.name = "name"; n.parentId = baseId; n.offset = 8; n.strLen = 16; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_18"; n.parentId = baseId; n.offset = 24; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Pointer64; n.name = "owner"; n.parentId = baseId; n.offset = 32; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_28"; n.parentId = baseId; n.offset = 40; tree.addNode(n); }
// Cat's own fields after base
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_30"; n.parentId = catId; n.offset = 48; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_38"; n.parentId = catId; n.offset = 56; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "whiskerLen"; n.parentId = catId; n.offset = 64; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_44"; n.parentId = catId; n.offset = 68; tree.addNode(n); }
{ Node n; n.kind = NodeKind::UInt8; n.name = "lives"; n.parentId = catId; n.offset = 72; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex8; n.name = "hex_49"; n.parentId = catId; n.offset = 73; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex16; n.name = "hex_4A"; n.parentId = catId; n.offset = 74; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_4C"; n.parentId = catId; n.offset = 76; tree.addNode(n); }
// ── Ball (independent root struct) ──
Node ball;
ball.kind = NodeKind::Struct;
ball.name = "Ball";
ball.structTypeName = "Ball";
ball.parentId = 0;
ball.offset = 0;
int bli = tree.addNode(ball);
uint64_t ballId = tree.nodes[bli].id;
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_00"; n.parentId = ballId; n.offset = 0; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_08"; n.parentId = ballId; n.offset = 8; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = ballId; n.offset = 16; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_14"; n.parentId = ballId; n.offset = 20; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_18"; n.parentId = ballId; n.offset = 24; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Vec4; n.name = "position"; n.parentId = ballId; n.offset = 32; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_30"; n.parentId = ballId; n.offset = 48; tree.addNode(n); }
{ Node n; n.kind = NodeKind::UInt32; n.name = "color"; n.parentId = ballId; n.offset = 56; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex32; n.name = "hex_3C"; n.parentId = ballId; n.offset = 60; tree.addNode(n); }
{ Node n; n.kind = NodeKind::Hex64; n.name = "hex_40"; n.parentId = ballId; n.offset = 64; tree.addNode(n); }
return tree;
}
private slots:
// ═══════════════════════════════════════════════════
// Feature 1: Type Aliases
// ═══════════════════════════════════════════════════
void testResolveTypeName_noAlias() {
RcxDocument doc;
// No aliases set — should return default type name
QString name = doc.resolveTypeName(NodeKind::Int32);
QCOMPARE(name, QString("int32_t"));
name = doc.resolveTypeName(NodeKind::Float);
QCOMPARE(name, QString("float"));
name = doc.resolveTypeName(NodeKind::Hex64);
QCOMPARE(name, QString("hex64"));
}
void testResolveTypeName_withAlias() {
RcxDocument doc;
doc.typeAliases[NodeKind::Int32] = "DWORD";
doc.typeAliases[NodeKind::Float] = "FLOAT";
QCOMPARE(doc.resolveTypeName(NodeKind::Int32), QString("DWORD"));
QCOMPARE(doc.resolveTypeName(NodeKind::Float), QString("FLOAT"));
// Non-aliased types still return default
QCOMPARE(doc.resolveTypeName(NodeKind::UInt64), QString("uint64_t"));
}
void testResolveTypeName_emptyAlias() {
RcxDocument doc;
doc.typeAliases[NodeKind::Int32] = ""; // empty alias should be ignored
QCOMPARE(doc.resolveTypeName(NodeKind::Int32), QString("int32_t"));
}
void testTypeAliases_saveLoad() {
// Save a document with type aliases, reload, verify aliases persist
QTemporaryFile tmpFile;
tmpFile.setAutoRemove(true);
QVERIFY(tmpFile.open());
QString path = tmpFile.fileName();
tmpFile.close();
// Create document with aliases and save
{
RcxDocument doc;
doc.tree = makeSimpleTree();
doc.typeAliases[NodeKind::Int32] = "DWORD";
doc.typeAliases[NodeKind::Float] = "FLOAT";
QVERIFY(doc.save(path));
}
// Reload and check aliases
{
RcxDocument doc;
QVERIFY(doc.load(path));
QCOMPARE(doc.typeAliases.size(), 2);
QCOMPARE(doc.typeAliases.value(NodeKind::Int32), QString("DWORD"));
QCOMPARE(doc.typeAliases.value(NodeKind::Float), QString("FLOAT"));
}
}
void testTypeAliases_saveLoadEmpty() {
// Save without aliases, reload, verify no aliases
QTemporaryFile tmpFile;
tmpFile.setAutoRemove(true);
QVERIFY(tmpFile.open());
QString path = tmpFile.fileName();
tmpFile.close();
{
RcxDocument doc;
doc.tree = makeSimpleTree();
QVERIFY(doc.save(path));
}
{
RcxDocument doc;
QVERIFY(doc.load(path));
QVERIFY(doc.typeAliases.isEmpty());
}
}
void testTypeAliases_jsonFormat() {
// Verify the JSON format of saved aliases
QTemporaryFile tmpFile;
tmpFile.setAutoRemove(true);
QVERIFY(tmpFile.open());
QString path = tmpFile.fileName();
tmpFile.close();
RcxDocument doc;
doc.tree = makeSimpleTree();
doc.typeAliases[NodeKind::UInt32] = "UINT";
QVERIFY(doc.save(path));
// Read raw JSON
QFile file(path);
QVERIFY(file.open(QIODevice::ReadOnly));
QJsonDocument jdoc = QJsonDocument::fromJson(file.readAll());
QJsonObject root = jdoc.object();
QVERIFY(root.contains("typeAliases"));
QJsonObject aliases = root["typeAliases"].toObject();
QCOMPARE(aliases["UInt32"].toString(), QString("UINT"));
}
void testGenerator_typeAliases() {
// Generator should use aliases for field types
auto tree = makeSimpleTree();
uint64_t rootId = tree.nodes[0].id;
QHash<NodeKind, QString> aliases;
aliases[NodeKind::Int32] = "LONG";
aliases[NodeKind::Float] = "FLOAT";
QString result = renderCpp(tree, rootId, &aliases);
QVERIFY(result.contains("LONG health;"));
QVERIFY(result.contains("FLOAT speed;"));
// struct keyword itself should not be aliased
QVERIFY(result.contains("struct Player {"));
}
void testGenerator_typeAliases_null() {
// With nullptr aliases, should behave like before
auto tree = makeSimpleTree();
uint64_t rootId = tree.nodes[0].id;
QString result = renderCpp(tree, rootId, nullptr);
QVERIFY(result.contains("int32_t health;"));
QVERIFY(result.contains("float speed;"));
}
void testGenerator_typeAliases_array() {
// Array element type should use alias
NodeTree tree;
Node root;
root.kind = NodeKind::Struct;
root.name = "ArrTest";
root.structTypeName = "ArrTest";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node arr;
arr.kind = NodeKind::Array;
arr.name = "data";
arr.parentId = rootId;
arr.offset = 0;
arr.arrayLen = 16;
arr.elementKind = NodeKind::UInt32;
tree.addNode(arr);
QHash<NodeKind, QString> aliases;
aliases[NodeKind::UInt32] = "DWORD";
QString result = renderCpp(tree, rootId, &aliases);
QVERIFY(result.contains("DWORD data[16];"));
}
void testGenerator_renderCppAll_typeAliases() {
auto tree = makeTwoRootTree();
QHash<NodeKind, QString> aliases;
aliases[NodeKind::UInt32] = "DWORD";
aliases[NodeKind::UInt64] = "QWORD";
QString result = renderCppAll(tree, &aliases);
QVERIFY(result.contains("DWORD flagsA;"));
QVERIFY(result.contains("QWORD flagsB;"));
}
// ═══════════════════════════════════════════════════
// Feature 3: Per-Window View Root Class
// ═══════════════════════════════════════════════════
void testCompose_viewRootId_zero() {
// viewRootId=0 should show all roots (same as default)
auto tree = makeTwoRootTree();
NullProvider prov;
ComposeResult result = compose(tree, prov, 0);
// Should have content from both structs
QStringList lines = result.text.split('\n');
bool foundFlagsA = false, foundFlagsB = false;
for (const QString& l : lines) {
if (l.contains("flagsA")) foundFlagsA = true;
if (l.contains("flagsB")) foundFlagsB = true;
}
QVERIFY2(foundFlagsA, "viewRootId=0 should include Alpha struct");
QVERIFY2(foundFlagsB, "viewRootId=0 should include Bravo struct");
}
void testCompose_viewRootId_filter() {
// viewRootId set to Alpha's id should only show Alpha's fields
auto tree = makeTwoRootTree();
uint64_t alphaId = tree.nodes[0].id;
NullProvider prov;
ComposeResult result = compose(tree, prov, alphaId);
QStringList lines = result.text.split('\n');
bool foundFlagsA = false, foundFlagsB = false;
for (const QString& l : lines) {
if (l.contains("flagsA")) foundFlagsA = true;
if (l.contains("flagsB")) foundFlagsB = true;
}
QVERIFY2(foundFlagsA, "viewRootId=Alpha should include Alpha's fields");
QVERIFY2(!foundFlagsB, "viewRootId=Alpha should NOT include Bravo's fields");
}
void testCompose_viewRootId_otherRoot() {
// viewRootId set to Bravo's id should only show Bravo's fields
auto tree = makeTwoRootTree();
uint64_t bravoId = tree.nodes[2].id; // Bravo is the 3rd node (index 2)
NullProvider prov;
ComposeResult result = compose(tree, prov, bravoId);
QStringList lines = result.text.split('\n');
bool foundFlagsA = false, foundFlagsB = false;
for (const QString& l : lines) {
if (l.contains("flagsA")) foundFlagsA = true;
if (l.contains("flagsB")) foundFlagsB = true;
}
QVERIFY2(!foundFlagsA, "viewRootId=Bravo should NOT include Alpha's fields");
QVERIFY2(foundFlagsB, "viewRootId=Bravo should include Bravo's fields");
}
void testCompose_viewRootId_invalid() {
// viewRootId pointing to non-existent node: should show nothing (only command rows)
auto tree = makeTwoRootTree();
NullProvider prov;
ComposeResult result = compose(tree, prov, 99999);
// Only command row
QCOMPARE(result.meta.size(), 1);
QCOMPARE(result.meta[0].lineKind, LineKind::CommandRow);
}
void testCompose_viewRootId_singleRoot() {
// Single root tree with viewRootId set to that root — should work normally
auto tree = makeSimpleTree();
uint64_t rootId = tree.nodes[0].id;
NullProvider prov;
ComposeResult full = compose(tree, prov, 0);
ComposeResult filtered = compose(tree, prov, rootId);
// Both should have same number of lines (only one root anyway)
QCOMPARE(full.meta.size(), filtered.meta.size());
}
void testDocument_compose_viewRootId() {
// Test RcxDocument::compose passes viewRootId through
RcxDocument doc;
doc.tree = makeTwoRootTree();
uint64_t alphaId = doc.tree.nodes[0].id;
ComposeResult fullResult = doc.compose(0);
ComposeResult filtered = doc.compose(alphaId);
// Filtered should have fewer lines than full
QVERIFY(filtered.meta.size() < fullResult.meta.size());
// Filtered should have Alpha's fields
bool foundFlagsA = false;
for (const QString& l : filtered.text.split('\n')) {
if (l.contains("flagsA")) foundFlagsA = true;
}
QVERIFY(foundFlagsA);
}
// ═══════════════════════════════════════════════════
// Feature 2: Project Lifecycle API (document-level)
// ═══════════════════════════════════════════════════
void testDocument_saveLoadPreservesData() {
// Verify save/load round-trip preserves tree + aliases + baseAddress
QTemporaryFile tmpFile;
tmpFile.setAutoRemove(true);
QVERIFY(tmpFile.open());
QString path = tmpFile.fileName();
tmpFile.close();
{
RcxDocument doc;
doc.tree = makeTwoRootTree();
doc.tree.baseAddress = 0xDEADBEEF;
doc.typeAliases[NodeKind::Int32] = "INT";
QVERIFY(doc.save(path));
}
{
RcxDocument doc;
QVERIFY(doc.load(path));
QCOMPARE(doc.tree.baseAddress, (uint64_t)0xDEADBEEF);
QCOMPARE(doc.tree.nodes.size(), 4); // 2 roots + 2 fields
QCOMPARE(doc.typeAliases.value(NodeKind::Int32), QString("INT"));
QCOMPARE(doc.filePath, path);
QVERIFY(!doc.modified);
}
}
void testDocument_saveCreatesFile() {
QTemporaryFile tmpFile;
tmpFile.setAutoRemove(true);
QVERIFY(tmpFile.open());
QString path = tmpFile.fileName();
tmpFile.close();
RcxDocument doc;
doc.tree = makeSimpleTree();
QVERIFY(doc.save(path));
QCOMPARE(doc.filePath, path);
QVERIFY(!doc.modified);
// Verify file exists and is valid JSON
QFile file(path);
QVERIFY(file.open(QIODevice::ReadOnly));
QJsonDocument jdoc = QJsonDocument::fromJson(file.readAll());
QVERIFY(!jdoc.isNull());
QVERIFY(jdoc.object().contains("nodes"));
}
void testDocument_loadInvalidPath() {
RcxDocument doc;
QVERIFY(!doc.load("/nonexistent/path/file.rcx"));
}
// ═══════════════════════════════════════════════════
// Integration: Type aliases + compose + generator
// ═══════════════════════════════════════════════════
// ═══════════════════════════════════════════════════
// Feature 4: Workspace Model
// ═══════════════════════════════════════════════════
void testWorkspace_simpleTree() {
auto tree = makeSimpleTree();
QStandardItemModel model;
QVector<TabInfo> tabs = {{ &tree, "TestProject.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
// Flat model: Player at root (has 2 non-hex members → lazy placeholder)
QCOMPARE(model.rowCount(), 1);
QVERIFY(model.item(0)->text().contains("Player"));
QVERIFY(model.item(0)->text().contains("struct"));
QVERIFY(model.item(0)->rowCount() > 0); // children populated directly
}
void testWorkspace_twoRootTree() {
auto tree = makeTwoRootTree();
QStandardItemModel model;
QVector<TabInfo> tabs = {{ &tree, "TwoRoot.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
// Flat model: 2 types at root
QCOMPARE(model.rowCount(), 2);
QVERIFY(model.item(0)->text().contains("Alpha"));
QVERIFY(model.item(1)->text().contains("Bravo"));
}
void testWorkspace_richTree_rootCount() {
auto tree = makeRichTree();
QStandardItemModel model;
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
QCOMPARE(model.rowCount(), 3); // Ball, Cat, Pet
}
void testWorkspace_richTree_insertionOrder() {
auto tree = makeRichTree();
QStandardItemModel model;
QVector<TabInfo> tabs = {{ &tree, "Rich.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
// Types at root in insertion order: Pet, Cat, Ball
QVERIFY(model.item(0)->text().contains("Pet"));
QVERIFY(model.item(1)->text().contains("Cat"));
QVERIFY(model.item(2)->text().contains("Ball"));
}
void testWorkspace_emptyTree() {
NodeTree tree;
QStandardItemModel model;
QVector<TabInfo> tabs = {{ &tree, "Empty.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
// Flat model: no types means no rows
QCOMPARE(model.rowCount(), 0);
}
void testWorkspace_structIdRole() {
auto tree = makeSimpleTree();
QStandardItemModel model;
QVector<TabInfo> tabs = {{ &tree, "Test.rcx", nullptr }};
buildProjectExplorer(&model, tabs);
// Flat model: first item is the Player type with its structId
QVERIFY(model.rowCount() > 0);
QStandardItem* player = model.item(0);
QVERIFY(player->data(Qt::UserRole + 1).isValid());
QVERIFY(player->data(Qt::UserRole + 1).toULongLong() > 0);
}
// ═══════════════════════════════════════════════════
// Feature: Double-click navigation (viewRootId + scroll)
// ═══════════════════════════════════════════════════
void testDoubleClick_switchToCollapsedClass() {
// Simulates: Ball is collapsed (hidden). Double-click Ball in workspace
// → uncollapse, set viewRootId, compose shows only Ball with children.
RcxDocument doc;
doc.tree = makeRichTree();
// Collapse Ball (3rd root struct)
uint64_t ballId = 0;
for (auto& node : doc.tree.nodes) {
if (node.parentId == 0 && node.kind == NodeKind::Struct
&& node.structTypeName == "Ball") {
node.collapsed = true;
ballId = node.id;
break;
}
}
QVERIFY(ballId != 0);
// Compose with viewRootId=0 should skip collapsed Ball
{
NullProvider prov;
ComposeResult result = compose(doc.tree, prov, 0);
bool foundSpeed = false;
for (const auto& lm : result.meta) {
int ni = lm.nodeIdx;
if (ni >= 0 && ni < doc.tree.nodes.size()
&& doc.tree.nodes[ni].name == "speed")
foundSpeed = true;
}
QVERIFY2(!foundSpeed, "Collapsed Ball's children should not appear with viewRootId=0");
}
// Simulate double-click: uncollapse Ball + set viewRootId
int bi = doc.tree.indexOfId(ballId);
QVERIFY(bi >= 0);
doc.tree.nodes[bi].collapsed = false;
// Compose with viewRootId=Ball should show Ball and its children
{
NullProvider prov;
ComposeResult result = compose(doc.tree, prov, ballId);
bool foundSpeed = false, foundPosition = false, foundColor = false;
for (const auto& lm : result.meta) {
int ni = lm.nodeIdx;
if (ni < 0 || ni >= doc.tree.nodes.size()) continue;
const QString& name = doc.tree.nodes[ni].name;
if (name == "speed") foundSpeed = true;
if (name == "position") foundPosition = true;
if (name == "color") foundColor = true;
}
QVERIFY2(foundSpeed, "Ball's speed field should appear");
QVERIFY2(foundPosition, "Ball's position field should appear");
QVERIFY2(foundColor, "Ball's color field should appear");
}
// Pet/Cat fields should NOT be in the Ball-filtered result
{
NullProvider prov;
ComposeResult result = compose(doc.tree, prov, ballId);
bool foundPetField = false;
for (const auto& lm : result.meta) {
int ni = lm.nodeIdx;
if (ni < 0 || ni >= doc.tree.nodes.size()) continue;
if (doc.tree.nodes[ni].name == "owner") foundPetField = true;
}
QVERIFY2(!foundPetField, "Pet's owner should not appear when viewing Ball");
}
}
void testDoubleClick_fieldNavigatesToParentRoot() {
// Simulates: double-click a field inside Ball → walk up to Ball root,
// set viewRootId to Ball, and the field should be in the compose output.
RcxDocument doc;
doc.tree = makeRichTree();
// Find Ball's "speed" child
uint64_t ballId = 0, speedId = 0;
for (auto& node : doc.tree.nodes) {
if (node.parentId == 0 && node.structTypeName == "Ball")
ballId = node.id;
}
QVERIFY(ballId != 0);
for (auto& node : doc.tree.nodes) {
if (node.parentId == ballId && node.name == "speed")
speedId = node.id;
}
QVERIFY(speedId != 0);
// Walk up from speed to find root struct (simulating handler logic)
uint64_t rootId = 0;
uint64_t cur = speedId;
while (cur != 0) {
int idx = doc.tree.indexOfId(cur);
if (idx < 0) break;
if (doc.tree.nodes[idx].parentId == 0) { rootId = cur; break; }
cur = doc.tree.nodes[idx].parentId;
}
QCOMPARE(rootId, ballId);
// Compose with viewRootId=Ball should contain speed
NullProvider prov;
ComposeResult result = compose(doc.tree, prov, ballId);
bool foundSpeed = false;
for (const auto& lm : result.meta) {
if (lm.nodeId == speedId) { foundSpeed = true; break; }
}
QVERIFY2(foundSpeed, "speed field should be in compose output when viewing its root");
}
void testDoubleClick_projectRootShowsAll() {
// Double-click project root clears viewRootId → all non-collapsed roots shown
RcxDocument doc;
doc.tree = makeRichTree();
// Collapse Ball
for (auto& node : doc.tree.nodes) {
if (node.parentId == 0 && node.structTypeName == "Ball")
node.collapsed = true;
}
// viewRootId=0 → Pet and Cat visible, Ball hidden
NullProvider prov;
ComposeResult result = compose(doc.tree, prov, 0);
bool foundOwner = false, foundWhiskerLen = false, foundSpeed = false;
for (const auto& lm : result.meta) {
int ni = lm.nodeIdx;
if (ni < 0 || ni >= doc.tree.nodes.size()) continue;
const QString& name = doc.tree.nodes[ni].name;
if (name == "owner") foundOwner = true;
if (name == "whiskerLen") foundWhiskerLen = true;
if (name == "speed") foundSpeed = true;
}
QVERIFY2(foundOwner, "Pet's owner should appear with viewRootId=0");
QVERIFY2(foundWhiskerLen, "Cat's whiskerLen should appear with viewRootId=0");
QVERIFY2(!foundSpeed, "Collapsed Ball's speed should not appear with viewRootId=0");
}
// ═══════════════════════════════════════════════════
// Integration: Type aliases + compose + generator
// ═══════════════════════════════════════════════════
void testAliasesPreservedThroughSaveReloadCompose() {
// Full workflow: set aliases, save, reload, compose + generate
QTemporaryFile tmpFile;
tmpFile.setAutoRemove(true);
QVERIFY(tmpFile.open());
QString path = tmpFile.fileName();
tmpFile.close();
auto tree = makeSimpleTree();
// Save with aliases
{
RcxDocument doc;
doc.tree = tree;
doc.typeAliases[NodeKind::Int32] = "my_int32";
doc.typeAliases[NodeKind::Float] = "my_float";
QVERIFY(doc.save(path));
}
// Reload and verify compose + generate work
{
RcxDocument doc;
QVERIFY(doc.load(path));
// Compose should succeed
ComposeResult result = doc.compose();
QVERIFY(result.meta.size() > 0);
// Generator should use aliases
uint64_t rootId = doc.tree.nodes[0].id;
const QHash<NodeKind, QString>* aliases =
doc.typeAliases.isEmpty() ? nullptr : &doc.typeAliases;
QString cpp = renderCpp(doc.tree, rootId, aliases);
QVERIFY(cpp.contains("my_int32 health;"));
QVERIFY(cpp.contains("my_float speed;"));
}
}
void testVec4SingleLineValue() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Obj";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node v;
v.kind = NodeKind::Vec4;
v.name = "position";
v.parentId = rootId;
v.offset = 0;
tree.addNode(v);
NullProvider prov;
ComposeResult result = compose(tree, prov);
// CommandRow + 1 Vec4 line + footer = 3
QCOMPARE(result.meta.size(), 3);
// The Vec4 line (index 1) is a single field line, not continuation
QCOMPARE(result.meta[1].lineKind, LineKind::Field);
QCOMPARE(result.meta[1].nodeKind, NodeKind::Vec4);
QVERIFY(!result.meta[1].isContinuation);
// Copy text (equivalent to editor's "Copy All as Text")
QString text = result.text;
// NullProvider reads 0 for all floats, values are "0.f, 0.f, 0.f, 0.f"
QVERIFY(text.contains("0.f, 0.f, 0.f, 0.f"));
// Confirm type, name, and values all on the same line
QStringList lines = text.split('\n');
QVERIFY(lines[1].contains("vec4"));
QVERIFY(lines[1].contains("position"));
QVERIFY(lines[1].contains("0.f, 0.f, 0.f, 0.f"));
}
};
QTEST_MAIN(TestNewFeatures)
#include "test_new_features.moc"

92
tests/test_pixels.py Normal file
View File

@@ -0,0 +1,92 @@
"""
Pixel boundary test: validates no Fusion outline leak at the workspace→editor seam.
Usage:
python tests/test_pixels.py [screenshot.png]
If no screenshot given, launches Reclass.exe --screenshot to grab one.
Scans for the specific Fusion outline artifact: color (23,23,23) which is
window.darker(140) for the VS2022 Dark theme background #1e1e1e.
"""
import sys, os, subprocess
from PIL import Image
from collections import defaultdict
GRAB_PATH = os.path.join("build", "test_grab.png")
def get_screenshot(path):
if not os.path.exists(path):
print(f"Launching Reclass.exe --screenshot {path}")
subprocess.run(["./build/Reclass.exe", "--screenshot", path],
timeout=15, check=True)
return Image.open(path)
def scan_for_artifact(img):
"""Scan entire image for the Fusion outline color (23,23,23).
Also find all near-black pixels (< 28,28,28) that aren't the
theme background (30,30,30)."""
w, h = img.size
px = img.load()
target = (23, 23, 23)
bg = (30, 30, 30)
target_hits = []
dark_hits = defaultdict(list) # color → [(x,y), ...]
for y in range(h):
for x in range(w):
r, g, b = px[x, y][:3]
if r == target[0] and g == target[1] and b == target[2]:
target_hits.append((x, y))
elif r < 28 and g < 28 and b < 28 and (r, g, b) != (0, 0, 0):
# Near-black but not pure black (text anti-aliasing) and not bg
dark_hits[(r, g, b)].append((x, y))
return target_hits, dark_hits
def summarize_region(hits):
"""Summarize a list of (x,y) hits."""
if not hits:
return "none"
xs = [p[0] for p in hits]
ys = [p[1] for p in hits]
return (f"{len(hits)}px x=[{min(xs)}..{max(xs)}] y=[{min(ys)}..{max(ys)}] "
f"size={max(xs)-min(xs)+1}x{max(ys)-min(ys)+1}")
def main():
path = sys.argv[1] if len(sys.argv) > 1 else GRAB_PATH
img = get_screenshot(path)
w, h = img.size
print(f"Image: {w}x{h}")
target_hits, dark_hits = scan_for_artifact(img)
print(f"\n(23,23,23) Fusion outline pixels: {summarize_region(target_hits)}")
if dark_hits:
print(f"\nOther near-black pixels (< 28,28,28):")
for c, positions in sorted(dark_hits.items(), key=lambda t: -len(t[1])):
print(f" ({c[0]:3},{c[1]:3},{c[2]:3}): {summarize_region(positions)}")
if target_hits:
# Show row distribution (condensed)
rows = defaultdict(list)
for x, y in target_hits:
rows[y].append(x)
print(f"\n(23,23,23) row detail:")
for y in sorted(rows.keys()):
xs = sorted(rows[y])
if len(xs) > 5:
print(f" y={y}: {len(xs)}px x=[{xs[0]}..{xs[-1]}]")
else:
print(f" y={y}: {len(xs)}px x={xs}")
print(f"\nFAIL: Found {len(target_hits)} Fusion outline pixels (23,23,23)")
sys.exit(1)
else:
print("\nPASS: No Fusion outline artifact found")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -322,6 +322,104 @@ private slots:
} }
} }
// ── Benchmark: large SDK (5000 structs) ──
void benchmarkLargeSDK() {
auto ms = [](qint64 ns) { return QString::number(ns / 1000000.0, 'f', 2); };
// Build 5000 composite types with field summaries (simulates WinSDK)
QVector<TypeEntry> types;
types.reserve(5000);
for (int i = 0; i < 5000; i++) {
TypeEntry e;
e.entryKind = TypeEntry::Composite;
e.structId = (uint64_t)(i + 1);
e.displayName = QStringLiteral("_STRUCT_%1").arg(i, 4, 10, QChar('0'));
e.classKeyword = QStringLiteral("struct");
e.sizeBytes = 64 + (i % 256) * 8;
e.alignment = 8;
e.fieldCount = 5 + (i % 20);
for (int f = 0; f < qMin(6, e.fieldCount); f++)
e.fieldSummary << QStringLiteral("0x%1: int32_t field_%2")
.arg(f * 4, 2, 16, QChar('0')).arg(f);
types.append(e);
}
QFont font("Consolas", 12);
font.setFixedPitch(true);
auto* popup = new TypeSelectorPopup();
popup->warmUp();
popup->setFont(font);
// Measure setTypes (data loading)
QElapsedTimer t;
t.start();
popup->setTypes(types, nullptr);
qint64 tSetTypes = t.nsecsElapsed();
// Measure popup show (broken down)
t.restart();
popup->popup(QPoint(100, 100));
qint64 tPopupCall = t.nsecsElapsed();
t.restart();
QApplication::processEvents();
qint64 tProcessEvents = t.nsecsElapsed();
qint64 tShow = tPopupCall + tProcessEvents;
// Second popup show (warm)
popup->hide();
QApplication::processEvents();
t.restart();
popup->popup(QPoint(100, 100));
qint64 tPopup2 = t.nsecsElapsed();
t.restart();
QApplication::processEvents();
qint64 tProcess2 = t.nsecsElapsed();
// Measure filter with 1-char (worst case: most matches)
t.restart();
auto* filterEdit = popup->findChild<QLineEdit*>();
QVERIFY(filterEdit);
filterEdit->setText(QStringLiteral("S"));
qint64 tFilter1 = t.nsecsElapsed();
// Measure filter with 3-char (moderate filtering)
t.restart();
filterEdit->setText(QStringLiteral("STR"));
qint64 tFilter3 = t.nsecsElapsed();
// Measure filter with 6-char (narrow results)
t.restart();
filterEdit->setText(QStringLiteral("STRUCT"));
qint64 tFilter6 = t.nsecsElapsed();
// Measure clear filter (back to grouped view)
t.restart();
filterEdit->setText(QString());
qint64 tClear = t.nsecsElapsed();
popup->hide();
QApplication::processEvents();
qDebug() << "";
qDebug().noquote() << "=== Large SDK Benchmark (5000 structs) ===";
qDebug().noquote() << QString(" setTypes: %1 ms").arg(ms(tSetTypes));
qDebug().noquote() << QString(" popup() call: %1 ms").arg(ms(tPopupCall));
qDebug().noquote() << QString(" processEvents: %1 ms").arg(ms(tProcessEvents));
qDebug().noquote() << QString(" popup total: %1 ms").arg(ms(tShow));
qDebug().noquote() << QString(" popup2() call: %1 ms (warm)").arg(ms(tPopup2));
qDebug().noquote() << QString(" processEvents2: %1 ms (warm)").arg(ms(tProcess2));
qDebug().noquote() << QString(" popup2 total: %1 ms (warm)").arg(ms(tPopup2 + tProcess2));
qDebug().noquote() << QString(" filter 'S': %1 ms").arg(ms(tFilter1));
qDebug().noquote() << QString(" filter 'STR': %1 ms").arg(ms(tFilter3));
qDebug().noquote() << QString(" filter 'STRUCT': %1 ms").arg(ms(tFilter6));
qDebug().noquote() << QString(" clear filter: %1 ms").arg(ms(tClear));
QVERIFY(tSetTypes > 0);
delete popup;
}
// ── Popup data model ── // ── Popup data model ──
void testPopupListsRootStructs() { void testPopupListsRootStructs() {

186
tests/test_typeinfer.cpp Normal file
View File

@@ -0,0 +1,186 @@
#include <QtTest/QTest>
#include <cstring>
#include "typeinfer.h"
using namespace rcx;
class TestTypeInfer : public QObject {
Q_OBJECT
private slots:
// ── NULL / zero → empty ──
void nullPtr() {
QVERIFY(inferTypes(nullptr, 8).isEmpty());
}
void zeroLen() {
uint8_t d[4] = {};
QVERIFY(inferTypes(d, 0).isEmpty());
}
void allZeros8() {
uint8_t d[8] = {};
QVERIFY(inferTypes(d, 8).isEmpty());
}
void allZeros4() {
uint8_t d[4] = {};
QVERIFY(inferTypes(d, 4).isEmpty());
}
void allZeros2() {
uint8_t d[2] = {};
QVERIFY(inferTypes(d, 2).isEmpty());
}
// ── Hex64: float pair ──
// {21.0488f, 547.3f} — two clear floats with fractional parts;
// whole-width Double/Ptr64 score poorly → Float×2 dominates
void hex64_floatPair() {
float a = 21.0488f, b = 547.3f;
uint8_t d[8];
std::memcpy(d, &a, 4);
std::memcpy(d + 4, &b, 4);
auto r = inferTypes(d, 8);
QVERIFY(!r.isEmpty());
auto& top = r[0];
QCOMPARE(top.kinds.size(), 2);
QCOMPARE(top.kinds[0], NodeKind::Float);
QVERIFY(top.strength >= 3); // strong
}
// ── Hex64: int32 pair ──
// {42, 99} — two small integers
void hex64_intPair() {
int32_t a = 42, b = 99;
uint8_t d[8];
std::memcpy(d, &a, 4);
std::memcpy(d + 4, &b, 4);
auto r = inferTypes(d, 8);
QVERIFY(!r.isEmpty());
auto& top = r[0];
QVERIFY(top.kinds.size() == 2);
QVERIFY(top.kinds[0] == NodeKind::Int32 || top.kinds[0] == NodeKind::UInt32);
}
// ── Hex64: UTF-8 string ──
void hex64_utf8() {
uint8_t d[8] = {'I', 'C', 'h', 'o', 'o', 's', 'e', 'Y'};
auto r = inferTypes(d, 8);
QVERIFY(!r.isEmpty());
// Top should be UTF8 (strong)
bool foundUtf8 = false;
for (const auto& s : r) {
if (s.kinds.size() == 1 && s.kinds[0] == NodeKind::UTF8) {
foundUtf8 = true;
QVERIFY(s.strength >= 3); // strong
}
}
QVERIFY(foundUtf8);
}
// ── Hex64: pointer-like value ──
void hex64_pointer() {
// 0x00007FF6A0B01000 — typical Windows user-mode address
uint8_t d[8] = {0x00, 0x10, 0xB0, 0xA0, 0xF6, 0x7F, 0x00, 0x00};
auto r = inferTypes(d, 8);
QVERIFY(!r.isEmpty());
bool foundPtr = false;
for (const auto& s : r)
if (s.kinds.size() == 1 && s.kinds[0] == NodeKind::Pointer64)
foundPtr = true;
QVERIFY(foundPtr);
}
// ── Hex32: clear float ──
void hex32_float() {
// 21.0488f = 0x41A86600
uint8_t d[4] = {0x00, 0x66, 0xA8, 0x41};
auto r = inferTypes(d, 4);
QVERIFY(!r.isEmpty());
QCOMPARE(r[0].kinds.size(), 1);
QCOMPARE(r[0].kinds[0], NodeKind::Float);
QVERIFY(r[0].strength >= 2);
}
// ── Hex32: small integer with monotonic history ──
void hex32_int_monotonic() {
// Value: 0x0000BFFC = 49148 (signed: 49148)
uint8_t d[4] = {0xFC, 0xBF, 0x00, 0x00};
InferHints h;
h.monotonic = true;
h.sampleCount = 10;
uint8_t minB[4] = {0x10, 0x00, 0x00, 0x00}; // 16
uint8_t maxB[4] = {0xFC, 0xBF, 0x00, 0x00}; // 49148
h.minObserved = minB;
h.maxObserved = maxB;
auto r = inferTypes(d, 4, h);
QVERIFY(!r.isEmpty());
QVERIFY(r[0].kinds[0] == NodeKind::Int32 || r[0].kinds[0] == NodeKind::UInt32);
QVERIFY(r[0].strength >= 2);
}
// ── Hex16: small unsigned ──
void hex16_uint() {
uint8_t d[2] = {0x5F, 0x00}; // 95
auto r = inferTypes(d, 2);
QVERIFY(!r.isEmpty());
QVERIFY(r[0].kinds[0] == NodeKind::Int16 || r[0].kinds[0] == NodeKind::UInt16);
}
// ── Hex8: uint8 ──
void hex8_uint() {
uint8_t d[1] = {1};
auto r = inferTypes(d, 1);
QVERIFY(!r.isEmpty());
QCOMPARE(r[0].kinds[0], NodeKind::UInt8);
}
// ── formatHint ──
void formatHint_single() {
TypeSuggestion s;
s.kinds = {NodeKind::Float};
s.strength = 3;
QCOMPARE(formatHint(s), QStringLiteral("float"));
}
void formatHint_split() {
TypeSuggestion s;
s.kinds = {NodeKind::Float, NodeKind::Float};
s.strength = 3;
QString h = formatHint(s);
QCOMPARE(h, QStringLiteral("float\u00D72"));
}
// ── Denormal rejection ──
void denormalRejected() {
// Denormal float: exp=0, mantissa non-zero → 0x00000001
uint8_t d[4] = {0x01, 0x00, 0x00, 0x00};
auto r = inferTypes(d, 4);
// Should NOT suggest Float as top pick
if (!r.isEmpty() && r[0].kinds.size() == 1)
QVERIFY(r[0].kinds[0] != NodeKind::Float);
}
// ── Benchmark: single call ──
void bench_singleCall() {
uint8_t d[8] = {0x00, 0x00, 0x80, 0x3F, 0xCD, 0xCC, 0x4C, 0x3E};
QBENCHMARK {
inferTypes(d, 8);
}
}
// ── Benchmark: 200-node batch (simulates one refresh) ──
void bench_batchRefresh() {
// Prepare 200 varied byte patterns
QVector<std::array<uint8_t, 8>> data(200);
for (int i = 0; i < 200; ++i) {
uint32_t seed = (uint32_t)(i * 7919 + 1);
for (int j = 0; j < 8; ++j)
data[i][j] = (uint8_t)((seed >> (j * 3)) ^ (i + j));
}
QBENCHMARK {
for (int i = 0; i < 200; ++i)
inferTypes(data[i].data(), 8);
}
}
};
QTEST_MAIN(TestTypeInfer)
#include "test_typeinfer.moc"

File diff suppressed because it is too large Load Diff