mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
6 Commits
snapshot-0
...
snapshot-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
483f87cfbd | ||
|
|
a21e5a07a8 | ||
|
|
25afbe373b | ||
|
|
6a4cb47ed4 | ||
|
|
431e2b90c9 | ||
|
|
43365c1aff |
@@ -314,6 +314,11 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
|
||||
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)
|
||||
target_include_directories(test_format PRIVATE src)
|
||||
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
|
||||
@@ -423,20 +428,6 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
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
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
@@ -483,20 +474,6 @@ if(BUILD_TESTING)
|
||||
QScintilla::QScintilla)
|
||||
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
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "core.h"
|
||||
#include "typeinfer.h"
|
||||
#include "addressparser.h"
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
@@ -7,6 +8,49 @@ namespace rcx {
|
||||
|
||||
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)
|
||||
constexpr int SC_FOLDLEVELBASE = 0x400;
|
||||
constexpr int SC_FOLDLEVELHEADERFLAG = 0x2000;
|
||||
@@ -26,6 +70,7 @@ struct ComposeState {
|
||||
bool compactColumns = false; // compact column mode: cap type width, overflow long types
|
||||
bool treeLines = false; // draw Unicode tree connectors in indentation
|
||||
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
|
||||
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,
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -469,7 +537,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
int eNW = state.effectiveNameW(node.id);
|
||||
for (int i = 0; i < node.arrayLen; i++) {
|
||||
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.
|
||||
QString elemTypeStr = fmt::typeNameRaw(node.elementKind)
|
||||
@@ -478,7 +546,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
Node elem;
|
||||
elem.kind = node.elementKind;
|
||||
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.id = 0;
|
||||
|
||||
@@ -971,11 +1039,13 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
} // anonymous namespace
|
||||
|
||||
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;
|
||||
state.compactColumns = compactColumns;
|
||||
state.treeLines = treeLines;
|
||||
state.braceWrap = braceWrap;
|
||||
state.typeHints = typeHints;
|
||||
|
||||
// Precompute parent→children map
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
|
||||
@@ -73,8 +73,8 @@ RcxDocument::RcxDocument(QObject* parent)
|
||||
}
|
||||
|
||||
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
|
||||
bool treeLines, bool braceWrap) const {
|
||||
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap);
|
||||
bool treeLines, bool braceWrap, bool typeHints) const {
|
||||
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap, typeHints);
|
||||
}
|
||||
|
||||
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).
|
||||
if (!m_cachedPopup) {
|
||||
QTimer::singleShot(0, this, [this, editor]() {
|
||||
if (!m_cachedPopup && !m_editors.isEmpty())
|
||||
ensurePopup(editor);
|
||||
QPointer<RcxEditor> safeEditor = editor;
|
||||
QTimer::singleShot(0, this, [this, safeEditor]() {
|
||||
if (!m_cachedPopup && !m_editors.isEmpty() && safeEditor)
|
||||
ensurePopup(safeEditor);
|
||||
});
|
||||
}
|
||||
return editor;
|
||||
@@ -200,7 +201,7 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
|
||||
|
||||
void RcxController::removeSplitEditor(RcxEditor* editor) {
|
||||
m_editors.removeOne(editor);
|
||||
// Caller (MainWindow) owns the parent QTabWidget and handles widget destruction.
|
||||
editor->disconnect(this);
|
||||
}
|
||||
|
||||
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
|
||||
connect(editor, &RcxEditor::inlineEditCommitted,
|
||||
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
|
||||
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
|
||||
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;
|
||||
|
||||
@@ -1850,6 +1912,40 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
// Fall through to always-available actions
|
||||
} 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) ──
|
||||
bool addedQuickConvert = false;
|
||||
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
|
||||
m_doc->tree.baseAddressFormula = entry.baseAddressFormula;
|
||||
attachViaPlugin(entry.kind, entry.providerTarget);
|
||||
// Restore saved base address (user may have navigated away from provider default)
|
||||
if (entry.baseAddress != 0 && entry.baseAddressFormula.isEmpty())
|
||||
// Restore saved base address — always override with saved value on source switch
|
||||
if (entry.baseAddressFormula.isEmpty())
|
||||
m_doc->tree.baseAddress = entry.baseAddress;
|
||||
}
|
||||
}
|
||||
@@ -3313,6 +3409,11 @@ void RcxController::setBraceWrap(bool v) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
void RcxController::setTypeHints(bool v) {
|
||||
m_typeHints = v;
|
||||
refresh();
|
||||
}
|
||||
|
||||
void RcxController::setupAutoRefresh() {
|
||||
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||
m_refreshTimer = new QTimer(this);
|
||||
|
||||
@@ -41,7 +41,8 @@ public:
|
||||
}
|
||||
|
||||
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 load(const QString& path);
|
||||
void loadData(const QString& binaryPath);
|
||||
@@ -131,6 +132,8 @@ public:
|
||||
void setCompactColumns(bool v);
|
||||
void setTreeLines(bool v);
|
||||
void setBraceWrap(bool v);
|
||||
void setTypeHints(bool v);
|
||||
bool typeHints() const { return m_typeHints; }
|
||||
void resetProvider();
|
||||
|
||||
// MCP bridge accessors
|
||||
@@ -171,6 +174,7 @@ private:
|
||||
bool m_compactColumns = false;
|
||||
bool m_treeLines = false;
|
||||
bool m_braceWrap = false;
|
||||
bool m_typeHints = false;
|
||||
uint64_t m_viewRootId = 0;
|
||||
|
||||
// ── Saved sources for quick-switch ──
|
||||
|
||||
23
src/core.h
23
src/core.h
@@ -450,8 +450,8 @@ struct NodeTree {
|
||||
if (c.isStatic) continue; // static fields don't affect struct size
|
||||
int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array)
|
||||
? structSpan(c.id, childMap, visited) : c.byteSize();
|
||||
int end = c.offset + sz;
|
||||
if (end > maxEnd) maxEnd = end;
|
||||
int64_t end = (int64_t)c.offset + sz;
|
||||
if (end > maxEnd) maxEnd = (int)qMin(end, (int64_t)INT_MAX);
|
||||
}
|
||||
|
||||
// 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 uint64_t kFooterIdBit = 0x8000000000000000ULL;
|
||||
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 kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements
|
||||
static constexpr uint64_t kArrayElemShift = 42; // bits 42-61 hold element index
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
static constexpr uint64_t kMemberBit = 0x2000000000000000ULL;
|
||||
static constexpr uint64_t kMemberSubShift = 48;
|
||||
static constexpr uint64_t kMemberSubMask = 0x3FFF000000000000ULL;
|
||||
static constexpr uint64_t kMemberSubShift = 42;
|
||||
static constexpr uint64_t kMemberSubMask = 0x3FFFFC0000000000ULL;
|
||||
|
||||
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) {
|
||||
return (int)((selId & kMemberSubMask) >> kMemberSubShift);
|
||||
@@ -625,6 +625,9 @@ struct LineMeta {
|
||||
bool isArrayElement = false; // true for synthesized primitive array element lines
|
||||
bool isMemberLine = false; // true for enum member / bitfield member 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) {
|
||||
@@ -1037,6 +1040,6 @@ namespace fmt {
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
||||
bool compactColumns = false, bool treeLines = false,
|
||||
bool braceWrap = false);
|
||||
bool braceWrap = false, bool typeHints = false);
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
681
src/editor.cpp
681
src/editor.cpp
@@ -32,17 +32,14 @@ namespace rcx {
|
||||
// Forward declaration (defined below, after RcxEditor constructor)
|
||||
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;
|
||||
bool m_hasButtons = false;
|
||||
QStringList m_values;
|
||||
QVector<QLabel*> m_labels;
|
||||
std::function<void(const QString&)> m_onSet;
|
||||
std::function<void(QMouseEvent*)> m_onMouseMove;
|
||||
public:
|
||||
explicit ValueHistoryPopup(QWidget* parent)
|
||||
explicit HoverPopup(QWidget* parent)
|
||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
@@ -53,9 +50,129 @@ public:
|
||||
}
|
||||
|
||||
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; }
|
||||
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:
|
||||
void mouseMoveEvent(QMouseEvent* e) override {
|
||||
if (!m_hasButtons && m_onMouseMove)
|
||||
@@ -63,8 +180,8 @@ protected:
|
||||
else
|
||||
QFrame::mouseMoveEvent(e);
|
||||
}
|
||||
public:
|
||||
|
||||
public:
|
||||
void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font,
|
||||
bool showButtons = false) {
|
||||
QStringList vals;
|
||||
@@ -93,10 +210,7 @@ public:
|
||||
qDeleteAll(findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly));
|
||||
|
||||
const auto& theme = ThemeManager::instance().current();
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
setPalette(pal);
|
||||
applyThemePalette(theme);
|
||||
|
||||
auto* vbox = new QVBoxLayout(this);
|
||||
vbox->setContentsMargins(8, 6, 8, 6);
|
||||
@@ -169,240 +283,13 @@ public:
|
||||
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;
|
||||
void dismiss() override {
|
||||
HoverPopup::dismiss();
|
||||
m_values.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_HEX_DIM = 9;
|
||||
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_HOT = 18; // Heatmap level 3 (frequent changes)
|
||||
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";
|
||||
|
||||
@@ -724,6 +612,10 @@ void RcxEditor::setupScintilla() {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
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)
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_FIND, 14 /*INDIC_COMPOSITIONTHICK*/);
|
||||
@@ -869,6 +761,8 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
||||
IND_HINT_GREEN, theme.indHintGreen);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_LOCAL_OFF, theme.textFaint);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_TYPE_HINT, theme.indHintGreen);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_FIND, theme.borderFocused);
|
||||
|
||||
@@ -973,15 +867,22 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
m_sci->setText(result.text);
|
||||
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;
|
||||
const QStringList lines = result.text.split(QChar('\n'));
|
||||
for (const auto& line : lines) {
|
||||
int len = (int)line.size();
|
||||
while (len > 0 && line[len - 1] == QChar(' ')) --len;
|
||||
maxLen = std::max(len, maxLen);
|
||||
int maxLen = 0, curLen = 0, lastNonSpace = 0;
|
||||
for (int i = 0; i < result.text.size(); i++) {
|
||||
QChar ch = result.text[i];
|
||||
if (ch == '\n') {
|
||||
maxLen = qMax(maxLen, lastNonSpace);
|
||||
curLen = 0;
|
||||
lastNonSpace = 0;
|
||||
} else {
|
||||
++curLen;
|
||||
if (ch != ' ') lastNonSpace = curLen;
|
||||
}
|
||||
}
|
||||
maxLen = qMax(maxLen, lastNonSpace);
|
||||
QFontMetrics fm(editorFont());
|
||||
int pixelWidth = fm.horizontalAdvance(QString(maxLen, QChar('0')));
|
||||
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
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_COLOURISE, (uintptr_t)0, (long)-1);
|
||||
|
||||
applyMarginText(result.meta);
|
||||
applyMarkers(result.meta);
|
||||
applyFoldLevels(result.meta);
|
||||
applyLineAttributes(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();
|
||||
|
||||
// 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
|
||||
m_hintLine = -1;
|
||||
|
||||
@@ -1015,9 +958,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
if (m_hoveredNodeId != 0 && !m_nodeLineIndex.contains(m_hoveredNodeId)) {
|
||||
m_hoveredNodeId = 0;
|
||||
m_hoveredLine = -1;
|
||||
dismissHistoryPopup();
|
||||
if (m_disasmPopup) m_disasmPopup->hide();
|
||||
if (m_structPreviewPopup) m_structPreviewPopup->hide();
|
||||
dismissAllPopups();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (m_relativeOffsets)
|
||||
return reformatMargins();
|
||||
void RcxEditor::applyLineAttributes(const QVector<LineMeta>& meta) {
|
||||
// Margin text
|
||||
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++) {
|
||||
const auto& lm = meta[i];
|
||||
if (lm.offsetText.isEmpty()) continue;
|
||||
|
||||
QByteArray text = lm.offsetText.toUtf8();
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETTEXT,
|
||||
(uintptr_t)i, text.constData());
|
||||
QByteArray styles(text.size(), '\0'); // style 0 = dim
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETSTYLES,
|
||||
(uintptr_t)i, styles.constData());
|
||||
// Margin text (only in absolute offset mode; reformatMargins handles relative)
|
||||
if (!m_relativeOffsets && !lm.offsetText.isEmpty()) {
|
||||
QByteArray text = lm.offsetText.toUtf8();
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_MARGINSETTEXT,
|
||||
(uintptr_t)i, text.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);
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
@@ -1448,7 +1390,13 @@ void RcxEditor::showFindBar() {
|
||||
|
||||
void RcxEditor::dismissHistoryPopup() {
|
||||
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() {
|
||||
@@ -1524,7 +1472,8 @@ static QString getLineText(QsciScintilla* sci, int line) {
|
||||
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 };
|
||||
|
||||
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)];
|
||||
|
||||
// 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,
|
||||
valueSpan(lm, lineText.size(), typeW, nameW), lineText);
|
||||
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++) {
|
||||
const LineMeta& lm = meta[i];
|
||||
if (!isFuncPtr(lm.nodeKind)
|
||||
&& lm.nodeKind != NodeKind::Pointer32
|
||||
&& lm.nodeKind != NodeKind::Pointer64)
|
||||
continue;
|
||||
QString lineText = getLineText(m_sci, i);
|
||||
const QString& lineText = lineTexts[i];
|
||||
// Find " // " within the value region and color "// sym" portion green
|
||||
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
||||
if (!vs.valid) continue;
|
||||
@@ -2111,6 +2061,42 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
emit marginClicked(0, h.line, me->modifiers());
|
||||
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
|
||||
if (h.nodeId == kCommandRowId) {
|
||||
int tLine, tCol; EditTarget t;
|
||||
@@ -2486,10 +2472,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
||||
m_hoveredLine = -1;
|
||||
applyHoverHighlight();
|
||||
// Dismiss hover popups so they get recreated with Set buttons once edit starts
|
||||
if (m_historyPopup)
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
if (m_structPreviewPopup)
|
||||
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||
dismissAllPopups();
|
||||
// Clear editable-token color hints (de-emphasize non-active tokens)
|
||||
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
||||
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);
|
||||
m_editState.commentCol = cs.valid ? cs.start : -1;
|
||||
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 {
|
||||
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
|
||||
// (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 neededLen = commentStart + kColComment;
|
||||
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
|
||||
if (target == EditTarget::Value)
|
||||
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
|
||||
// and exit early above (never reach here).
|
||||
@@ -3092,25 +3080,19 @@ void RcxEditor::applyHoverCursor() {
|
||||
}
|
||||
}
|
||||
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
|
||||
if (m_disasmPopup && m_disasmPopup->isVisible())
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||
if (m_structPreviewPopup && m_structPreviewPopup->isVisible())
|
||||
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||
if (m_disasmPopup) static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
|
||||
if (m_structPreviewPopup) static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// Mouse left viewport - set Arrow, dismiss popups
|
||||
// (but not during applyDocument — the Leave is synthetic from setText)
|
||||
if (!m_hoverInside) {
|
||||
if (m_historyPopup && !m_applyingDocument)
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
if (m_disasmPopup && !m_applyingDocument)
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||
if (m_structPreviewPopup && !m_applyingDocument)
|
||||
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||
if (!m_applyingDocument)
|
||||
dismissAllPopups();
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
@@ -3211,6 +3193,30 @@ void RcxEditor::applyHoverCursor() {
|
||||
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)
|
||||
// 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())
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
static_cast<HoverPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
|
||||
// Disasm / hex-dump popup on hover for FuncPtr and void Pointer nodes
|
||||
@@ -3331,8 +3337,8 @@ void RcxEditor::applyHoverCursor() {
|
||||
}
|
||||
if (!body.isEmpty()) {
|
||||
if (!m_disasmPopup) {
|
||||
m_disasmPopup = new DisasmPopup(this);
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
||||
m_disasmPopup = new TitleBodyPopup(this);
|
||||
static_cast<TitleBodyPopup*>(m_disasmPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
||||
QPoint gp = e->globalPosition().toPoint();
|
||||
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
|
||||
m_lastHoverPos = vp;
|
||||
@@ -3350,10 +3356,11 @@ void RcxEditor::applyHoverCursor() {
|
||||
applyHoverCursor();
|
||||
});
|
||||
}
|
||||
auto* popup = static_cast<DisasmPopup*>(
|
||||
auto* popup = static_cast<TitleBodyPopup*>(
|
||||
m_disasmPopup);
|
||||
popup->populate(lm.nodeId, title, body,
|
||||
editorFont());
|
||||
editorFont(),
|
||||
ThemeManager::instance().current().syntaxNumber);
|
||||
long linePos = m_sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||
(unsigned long)h.line);
|
||||
@@ -3374,7 +3381,7 @@ void RcxEditor::applyHoverCursor() {
|
||||
showDisasm = true;
|
||||
// Dismiss value history popup to avoid fighting
|
||||
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())
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||
static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
|
||||
}
|
||||
|
||||
// Struct preview popup for collapsed typed pointers
|
||||
@@ -3418,8 +3425,8 @@ void RcxEditor::applyHoverCursor() {
|
||||
}
|
||||
if (!body.isEmpty()) {
|
||||
if (!m_structPreviewPopup) {
|
||||
m_structPreviewPopup = new StructPreviewPopup(this);
|
||||
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
||||
m_structPreviewPopup = new TitleBodyPopup(this);
|
||||
static_cast<TitleBodyPopup*>(m_structPreviewPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
||||
QPoint gp = e->globalPosition().toPoint();
|
||||
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
|
||||
m_lastHoverPos = vp;
|
||||
@@ -3437,9 +3444,10 @@ void RcxEditor::applyHoverCursor() {
|
||||
applyHoverCursor();
|
||||
});
|
||||
}
|
||||
auto* popup = static_cast<StructPreviewPopup*>(m_structPreviewPopup);
|
||||
auto* popup = static_cast<TitleBodyPopup*>(m_structPreviewPopup);
|
||||
popup->populate(lm.nodeId,
|
||||
lm.pointerTargetName, body, editorFont());
|
||||
lm.pointerTargetName, body, editorFont(),
|
||||
ThemeManager::instance().current().text);
|
||||
long linePos = m_sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||
(unsigned long)h.line);
|
||||
@@ -3458,14 +3466,14 @@ void RcxEditor::applyHoverCursor() {
|
||||
popup->showAt(anchor, lh);
|
||||
showPreview = true;
|
||||
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())
|
||||
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||
static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
|
||||
}
|
||||
|
||||
// Determine cursor shape based on interaction type
|
||||
@@ -3473,6 +3481,25 @@ void RcxEditor::applyHoverCursor() {
|
||||
|
||||
if (h.inFoldCol) {
|
||||
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) {
|
||||
// Check if mouse is actually over trimmed text content (not column padding)
|
||||
NormalizedSpan trimmed;
|
||||
|
||||
16
src/editor.h
16
src/editor.h
@@ -37,6 +37,7 @@ public:
|
||||
void scrollToNodeId(uint64_t nodeId);
|
||||
void showFindBar();
|
||||
void dismissHistoryPopup();
|
||||
void dismissAllPopups();
|
||||
|
||||
// ── Column span computation ──
|
||||
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
|
||||
@@ -85,6 +86,9 @@ signals:
|
||||
void typePickerRequested(EditTarget target, int nodeIdx, QPoint globalPos);
|
||||
void insertAboveRequested(int nodeIdx, NodeKind kind);
|
||||
void relativeOffsetsChanged(bool relative);
|
||||
void appendBytesRequested(uint64_t structId, int byteCount);
|
||||
void trimHexRequested(uint64_t structId);
|
||||
void appendEnumMembersRequested(uint64_t enumId, int count);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
@@ -155,8 +159,8 @@ private:
|
||||
// ── Value history ref (owned by controller) ──
|
||||
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
|
||||
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
|
||||
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
|
||||
QWidget* m_structPreviewPopup = nullptr; // StructPreviewPopup (file-local class in editor.cpp)
|
||||
QWidget* m_disasmPopup = nullptr; // TitleBodyPopup (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_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||
const NodeTree* m_disasmTree = nullptr;
|
||||
@@ -179,13 +183,11 @@ private:
|
||||
void setupMarkers();
|
||||
void allocateMarginStyles();
|
||||
|
||||
void applyMarginText(const QVector<LineMeta>& meta);
|
||||
void applyLineAttributes(const QVector<LineMeta>& meta);
|
||||
void reformatMargins();
|
||||
void applyMarkers(const QVector<LineMeta>& meta);
|
||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||
void applyHeatmapHighlight(const QVector<LineMeta>& meta);
|
||||
void applySymbolColoring(const QVector<LineMeta>& meta);
|
||||
void applyHeatmapHighlight(const QVector<LineMeta>& meta, const QVector<QString>& lineTexts);
|
||||
void applySymbolColoring(const QVector<LineMeta>& meta, const QVector<QString>& lineTexts);
|
||||
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||
void applyCommandRowPills();
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
244420
src/examples/WinSDK.rcx
244420
src/examples/WinSDK.rcx
File diff suppressed because one or more lines are too long
@@ -163,8 +163,13 @@ QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType
|
||||
return ind + type + SEP + node.name + SEP + suffix;
|
||||
}
|
||||
|
||||
QString fmtStructFooter(const Node& /*node*/, int depth, int /*totalSize*/) {
|
||||
return indent(depth) + QStringLiteral("};");
|
||||
QString fmtStructFooter(const Node& node, int depth, int /*totalSize*/) {
|
||||
QString footer = indent(depth) + QStringLiteral("};");
|
||||
if (node.resolvedClassKeyword() == QStringLiteral("enum"))
|
||||
footer += QStringLiteral(" +10");
|
||||
else
|
||||
footer += QStringLiteral(" +10h +100h +1000h Trim");
|
||||
return footer;
|
||||
}
|
||||
|
||||
// ── Array header ──
|
||||
@@ -656,8 +661,10 @@ QString validateValue(NodeKind kind, const QString& text) {
|
||||
QString digits = hasHexPrefix ? s.mid(2) : s;
|
||||
|
||||
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) {
|
||||
if (c == ' ' && isMultiByteHex) continue;
|
||||
if (!c.isDigit() && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
|
||||
return QStringLiteral("invalid hex '%1'").arg(c);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ static QHash<QString, TypeInfo> buildTypeTable(int ptrSize = 8) {
|
||||
t[QStringLiteral("USHORT")] = {NodeKind::UInt16, 2};
|
||||
t[QStringLiteral("SHORT")] = {NodeKind::Int16, 2};
|
||||
t[QStringLiteral("WCHAR")] = {NodeKind::UInt16, 2};
|
||||
t[QStringLiteral("TCHAR")] = {NodeKind::UInt16, 2};
|
||||
t[QStringLiteral("DWORD")] = {NodeKind::UInt32, 4};
|
||||
t[QStringLiteral("ULONG")] = {NodeKind::UInt32, 4};
|
||||
t[QStringLiteral("UINT")] = {NodeKind::UInt32, 4};
|
||||
@@ -1187,6 +1188,16 @@ static int structTypeSize(const QString& typeName, const BuildContext& ctx) {
|
||||
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,
|
||||
const QVector<ParsedField>& fields) {
|
||||
int computedOffset = 0;
|
||||
@@ -1275,8 +1286,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
|
||||
// Array of pointers: PVOID arr[N]
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
int totalElements = clampedArrayElements(field.arraySizes);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
@@ -1314,8 +1324,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int elemSize = 4;
|
||||
NodeKind elemKind = NodeKind::UInt32;
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
int totalElements = clampedArrayElements(field.arraySizes);
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
n.name = field.name;
|
||||
@@ -1366,7 +1375,8 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
if (firstDim <= 0) firstDim = 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;
|
||||
n.kind = NodeKind::UTF8;
|
||||
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 &&
|
||||
(field.typeName == QStringLiteral("wchar_t") || field.typeName == QStringLiteral("WCHAR")) &&
|
||||
firstDim <= 128) {
|
||||
(field.typeName == QStringLiteral("wchar_t") ||
|
||||
field.typeName == QStringLiteral("WCHAR") ||
|
||||
field.typeName == QStringLiteral("TCHAR"))) {
|
||||
Node n;
|
||||
n.kind = NodeKind::UTF16;
|
||||
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;
|
||||
}
|
||||
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
int totalElements = clampedArrayElements(field.arraySizes);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
@@ -1437,8 +1447,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||
int elemSize = structTypeSize(field.typeName, ctx);
|
||||
|
||||
if (!field.arraySizes.isEmpty()) {
|
||||
int totalElements = 1;
|
||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
||||
int totalElements = clampedArrayElements(field.arraySizes);
|
||||
|
||||
Node n;
|
||||
n.kind = NodeKind::Array;
|
||||
@@ -1575,6 +1584,13 @@ NodeTree importFromSource(const QString& sourceCode, QString* errorMsg, int poin
|
||||
|
||||
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
|
||||
auto sizeIt = parser.sizeAsserts.find(ps.name);
|
||||
if (sizeIt != parser.sizeAsserts.end()) {
|
||||
|
||||
808
src/main.cpp
808
src/main.cpp
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
#include "pluginmanager.h"
|
||||
#include "scannerpanel.h"
|
||||
#include "startpage.h"
|
||||
#include "workspace_model.h"
|
||||
#include <QMainWindow>
|
||||
#include <QLabel>
|
||||
#include <QSplitter>
|
||||
@@ -68,6 +69,7 @@ private slots:
|
||||
public:
|
||||
// Status bar helpers — separate app / MCP channels
|
||||
void setAppStatus(const QString& text);
|
||||
void setAppStatus(const QString& text, const QString& dimSuffix);
|
||||
void setMcpStatus(const QString& text);
|
||||
void clearMcpStatus();
|
||||
|
||||
@@ -83,6 +85,7 @@ private:
|
||||
QWidget* m_centralPlaceholder;
|
||||
ShimmerLabel* m_statusLabel;
|
||||
QString m_appStatus;
|
||||
QString m_appStatusDim;
|
||||
bool m_mcpBusy = false;
|
||||
QTimer* m_mcpClearTimer = nullptr;
|
||||
TitleBarWidget* m_titleBar = nullptr;
|
||||
@@ -117,7 +120,15 @@ private:
|
||||
QMap<QDockWidget*, TabState> m_tabs;
|
||||
QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex
|
||||
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
|
||||
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 createMenus();
|
||||
@@ -134,6 +145,7 @@ private:
|
||||
TabState* tabByIndex(int index);
|
||||
int tabCount() const { return m_tabs.size(); }
|
||||
QDockWidget* createTab(RcxDocument* doc);
|
||||
QString tabTitle(const TabState& tab) const;
|
||||
void setupDockTabBars();
|
||||
void updateWindowTitle();
|
||||
void closeAllDocDocks();
|
||||
@@ -161,10 +173,12 @@ private:
|
||||
QLabel* m_dockTitleLabel = nullptr;
|
||||
QToolButton* m_dockCloseBtn = nullptr;
|
||||
DockGripWidget* m_dockGrip = nullptr;
|
||||
QSet<uint64_t> m_pinnedIds;
|
||||
void createWorkspaceDock();
|
||||
void rebuildWorkspaceModel(); // debounced — safe to call frequently
|
||||
void rebuildWorkspaceModelNow(); // immediate rebuild
|
||||
QTimer* m_workspaceRebuildTimer = nullptr;
|
||||
QTimer* m_workspaceSearchTimer = nullptr;
|
||||
void updateBorderColor(const QColor& color);
|
||||
|
||||
// Scanner dock
|
||||
|
||||
@@ -10,13 +10,24 @@
|
||||
|
||||
namespace rcx {
|
||||
|
||||
static constexpr int kMaxReadBuffer = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Construction / lifecycle
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
McpBridge::McpBridge(MainWindow* mainWindow, QObject* parent)
|
||||
: 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() {
|
||||
stop();
|
||||
@@ -84,15 +95,24 @@ void McpBridge::onNewConnection() {
|
||||
void McpBridge::onReadyRead() {
|
||||
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) {
|
||||
int idx = m_readBuffer.indexOf('\n');
|
||||
int idx = m_readBuffer.indexOf('\n', consumed);
|
||||
if (idx < 0) break;
|
||||
QByteArray line = m_readBuffer.left(idx).trimmed();
|
||||
m_readBuffer.remove(0, idx + 1);
|
||||
QByteArray line = m_readBuffer.mid(consumed, idx - consumed).trimmed();
|
||||
consumed = idx + 1;
|
||||
if (!line.isEmpty())
|
||||
processLine(line);
|
||||
}
|
||||
if (consumed > 0)
|
||||
m_readBuffer.remove(0, consumed);
|
||||
}
|
||||
|
||||
void McpBridge::onDisconnected() {
|
||||
@@ -153,6 +173,7 @@ QJsonObject McpBridge::makeTextResult(const QString& text, bool isError) {
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
void McpBridge::processLine(const QByteArray& line) {
|
||||
try {
|
||||
qDebug() << "[MCP] <<" << line.trimmed().left(200);
|
||||
auto doc = QJsonDocument::fromJson(line);
|
||||
if (!doc.isObject()) {
|
||||
@@ -172,12 +193,10 @@ void McpBridge::processLine(const QByteArray& line) {
|
||||
|
||||
if (method == "initialize") {
|
||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: client connected"));
|
||||
QCoreApplication::processEvents();
|
||||
sendJson(handleInitialize(id, req.value("params").toObject()));
|
||||
m_mainWindow->clearMcpStatus();
|
||||
} else if (method == "tools/list") {
|
||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: tools/list"));
|
||||
QCoreApplication::processEvents();
|
||||
sendJson(handleToolsList(id));
|
||||
m_mainWindow->clearMcpStatus();
|
||||
} else if (method == "tools/call") {
|
||||
@@ -185,6 +204,14 @@ void McpBridge::processLine(const QByteArray& line) {
|
||||
} else {
|
||||
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)
|
||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: %1").arg(toolName));
|
||||
QCoreApplication::processEvents(); // paint immediately
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
QJsonObject result;
|
||||
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,
|
||||
const QHash<QString, uint64_t>& placeholderMap) {
|
||||
const QHash<QString, uint64_t>& placeholderMap,
|
||||
bool* ok) {
|
||||
if (ok) *ok = true;
|
||||
if (ref.startsWith('$')) {
|
||||
auto it = placeholderMap.find(ref);
|
||||
if (it != placeholderMap.end())
|
||||
return QString::number(it.value());
|
||||
if (ok) *ok = false;
|
||||
return ref; // unresolved placeholder
|
||||
}
|
||||
return ref; // not a placeholder — return as-is
|
||||
}
|
||||
@@ -514,26 +545,36 @@ QString McpBridge::resolvePlaceholder(const QString& ref,
|
||||
// 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
|
||||
if (args.contains("tabIndex")) {
|
||||
int idx = args.value("tabIndex").toInt();
|
||||
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)
|
||||
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
|
||||
if (m_mainWindow->tabCount() > 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
|
||||
m_mainWindow->project_new();
|
||||
if (resolvedIndex) *resolvedIndex = 0;
|
||||
return m_mainWindow->tabByIndex(0);
|
||||
}
|
||||
|
||||
@@ -725,8 +766,11 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
||||
QStringList skippedOps;
|
||||
for (int i = 0; i < ops.size(); i++) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
QJsonObject op = ops[i].toObject();
|
||||
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.kind = kindFromString(op.value("kind").toString("Hex64"));
|
||||
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();
|
||||
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.structTypeName = op.value("structTypeName").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.arrayLen = op.value("arrayLen").toInt(1);
|
||||
QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders);
|
||||
n.arrayLen = qBound(1, op.value("arrayLen").toInt(1), 1000000);
|
||||
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();
|
||||
|
||||
// Auto-place: offset -1 means "after last sibling"
|
||||
@@ -870,7 +928,7 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
||||
int idx = tree.indexOfId(nid.toULongLong());
|
||||
if (idx >= 0) {
|
||||
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,
|
||||
cmd::ChangeArrayMeta{tree.nodes[idx].id,
|
||||
tree.nodes[idx].elementKind, newElemKind,
|
||||
@@ -1383,8 +1441,7 @@ QJsonObject McpBridge::toolProcessInfo(const QJsonObject& args) {
|
||||
|
||||
void McpBridge::notifyTreeChanged() {
|
||||
if (!m_client || !m_initialized) return;
|
||||
sendNotification("notifications/resources/updated",
|
||||
QJsonObject{{"uri", "project://tree"}});
|
||||
m_notifyTimer->start(); // debounce 100ms
|
||||
}
|
||||
|
||||
void McpBridge::notifyDataChanged() {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QByteArray>
|
||||
#include <QTimer>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
@@ -34,6 +35,7 @@ private:
|
||||
QByteArray m_readBuffer;
|
||||
bool m_initialized = false;
|
||||
bool m_slowMode = false;
|
||||
QTimer* m_notifyTimer = nullptr;
|
||||
|
||||
// JSON-RPC plumbing
|
||||
void onNewConnection();
|
||||
@@ -65,10 +67,11 @@ private:
|
||||
// Helpers
|
||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||
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
|
||||
MainWindow::TabState* resolveTab(const QJsonObject& args);
|
||||
MainWindow::TabState* resolveTab(const QJsonObject& args, int* resolvedIndex = nullptr);
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -53,6 +53,7 @@ public:
|
||||
bool isReadable(uint64_t addr, int len) const override {
|
||||
if (len <= 0) return (len == 0);
|
||||
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) {
|
||||
if (!m_pages.contains(p)) return false;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<file alias="settings-gear.svg">vsicons/settings-gear.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-left.svg">vsicons/chevron-left.svg</file>
|
||||
<file alias="folder.svg">vsicons/folder.svg</file>
|
||||
<file alias="symbol-enum.svg">vsicons/symbol-enum.svg</file>
|
||||
<file alias="symbol-class.svg">vsicons/symbol-class.svg</file>
|
||||
|
||||
@@ -702,7 +702,7 @@ void ScannerPanel::onCellEdited(int row, int col) {
|
||||
m_statusLabel->setText(QStringLiteral("Wrote %1 byte%2 to 0x%3")
|
||||
.arg(bytes.size())
|
||||
.arg(bytes.size() == 1 ? "" : "s")
|
||||
.arg(addr, 0, 16, QLatin1Char('0')).toUpper());
|
||||
.arg(QString::number(addr, 16).toUpper()));
|
||||
// Re-read and update cache
|
||||
m_resultTable->blockSignals(true);
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
|
||||
505
src/typeinfer.h
Normal file
505
src/typeinfer.h
Normal 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
|
||||
@@ -57,51 +57,73 @@ TypeSpec parseTypeSpec(const QString& text) {
|
||||
}
|
||||
|
||||
// ── 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,
|
||||
QVector<int>* outPositions = nullptr) {
|
||||
int pLen = pattern.size(), tLen = text.size();
|
||||
if (pLen == 0) return 1;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 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 (score > best) { best = score; bestPos = cur; }
|
||||
if (score > best) {
|
||||
best = score;
|
||||
bestLen = curLen;
|
||||
memcpy(bestPos, curPos, curLen * sizeof(int));
|
||||
}
|
||||
return;
|
||||
}
|
||||
int maxTi = tLen - (pLen - pi);
|
||||
int branches = 0;
|
||||
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;
|
||||
if (i == 0) bonus = 10;
|
||||
else if (text[i - 1] == '_' || text[i - 1] == ' ') bonus = 8;
|
||||
else if (text[i].isUpper() && text[i - 1].isLower()) bonus = 8;
|
||||
if (!cur.isEmpty() && i == cur.last() + 1) bonus += 5;
|
||||
cur.append(i);
|
||||
self(self, pi + 1, i + 1, cur, score + bonus);
|
||||
cur.removeLast();
|
||||
if (curLen > 0 && i == curPos[curLen - 1] + 1) bonus += 5;
|
||||
curPos[curLen] = i;
|
||||
self(self, pi + 1, i + 1, curLen + 1, score + bonus);
|
||||
branches++;
|
||||
}
|
||||
};
|
||||
|
||||
QVector<int> cur;
|
||||
solve(solve, 0, 0, cur, 0);
|
||||
solve(solve, 0, 0, 0, 0);
|
||||
if (best > 0) {
|
||||
best += qMax(0, 20 - (tLen - pLen)); // tightness 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;
|
||||
}
|
||||
@@ -113,7 +135,7 @@ public:
|
||||
explicit TypeSelectorDelegate(TypeSelectorPopup* popup, QObject* parent = nullptr)
|
||||
: 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 setFilteredTypes(const QVector<TypeEntry>* filtered) {
|
||||
m_filtered = filtered;
|
||||
@@ -287,13 +309,13 @@ public:
|
||||
}
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem& /*option*/,
|
||||
const QModelIndex& index) const override {
|
||||
const QModelIndex& /*index*/) const override {
|
||||
return m_cachedSizeHint;
|
||||
}
|
||||
|
||||
void updateCachedSizeHint() {
|
||||
QFontMetrics fm(m_font);
|
||||
int row = index.row();
|
||||
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);
|
||||
m_cachedSizeHint = QSize(200, fm.height() + 8);
|
||||
}
|
||||
|
||||
bool helpEvent(QHelpEvent* event, QAbstractItemView* view,
|
||||
@@ -304,8 +326,9 @@ public:
|
||||
if (row >= 0 && row < m_filtered->size()) {
|
||||
const auto& e = (*m_filtered)[row];
|
||||
if (e.entryKind == TypeEntry::Composite && !e.fieldSummary.isEmpty()) {
|
||||
QString tip = QStringLiteral("%1 (%2 B, %3 fields)\n")
|
||||
.arg(e.displayName).arg(e.sizeBytes).arg(e.fieldCount);
|
||||
QString tip = QStringLiteral("%1 (0x%2 bytes, %3 fields)\n")
|
||||
.arg(e.displayName, QString::number(e.sizeBytes, 16).toUpper())
|
||||
.arg(e.fieldCount);
|
||||
tip += e.fieldSummary.join(QChar('\n'));
|
||||
if (e.fieldCount > e.fieldSummary.size())
|
||||
tip += QStringLiteral("\n...");
|
||||
@@ -322,6 +345,7 @@ public:
|
||||
private:
|
||||
TypeSelectorPopup* m_popup = nullptr;
|
||||
QFont m_font;
|
||||
QSize m_cachedSizeHint{200, 20};
|
||||
bool m_isLoading = false;
|
||||
const QVector<TypeEntry>* m_filtered = nullptr;
|
||||
const QVector<QVector<int>>* m_matchPositions = nullptr;
|
||||
@@ -448,6 +472,9 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
|
||||
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_listView->viewport()->setAttribute(Qt::WA_Hover, true);
|
||||
m_listView->setAccessibleName(QStringLiteral("Type list"));
|
||||
m_listView->setUniformItemSizes(true);
|
||||
m_listView->setLayoutMode(QListView::Batched);
|
||||
m_listView->setBatchSize(50);
|
||||
m_listView->installEventFilter(this);
|
||||
|
||||
auto* delegate = new TypeSelectorDelegate(this, m_listView);
|
||||
@@ -714,6 +741,7 @@ void TypeSelectorPopup::applyTheme(const Theme& theme) {
|
||||
m_titleLabel->setPalette(pal);
|
||||
m_filterEdit->setPalette(pal);
|
||||
m_listView->setPalette(pal);
|
||||
m_listView->viewport()->setPalette(pal);
|
||||
m_arrayCountEdit->setPalette(pal);
|
||||
|
||||
// Esc button (snapped to corner)
|
||||
@@ -826,6 +854,12 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
|
||||
if (delegate) delegate->setLoading(false);
|
||||
|
||||
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) {
|
||||
m_currentEntry = *current;
|
||||
m_hasCurrent = true;
|
||||
@@ -858,13 +892,12 @@ void TypeSelectorPopup::setTypes(const QVector<TypeEntry>& types, const TypeEntr
|
||||
|
||||
void TypeSelectorPopup::popup(const QPoint& globalPos) {
|
||||
QFontMetrics fm(m_font);
|
||||
int maxTextW = fm.horizontalAdvance(QStringLiteral("Choose element type "));
|
||||
for (const auto& t : m_allTypes) {
|
||||
int iconColW = fm.height() + 4;
|
||||
int w = iconColW + fm.horizontalAdvance(t.displayName) + 16;
|
||||
if (w > maxTextW) maxTextW = w;
|
||||
}
|
||||
int popupW = qBound(480, maxTextW + 24, 560);
|
||||
constexpr int kMaxPopupW = 560;
|
||||
// Estimate max width from cached max name length (avoids iterating all types)
|
||||
int iconColW = fm.height() + 4;
|
||||
int estMaxW = iconColW + fm.horizontalAdvance(QChar('W')) * m_cachedMaxNameLen + 16;
|
||||
int maxTextW = qMax(fm.horizontalAdvance(QStringLiteral("Choose element type ")), estMaxW);
|
||||
int popupW = qBound(480, maxTextW + 24, kMaxPopupW);
|
||||
int rowH = fm.height() + 8;
|
||||
int headerH = rowH * 2 + 10; // filter + chips + separator
|
||||
int footerH = rowH + 6; // separator + action row
|
||||
@@ -968,18 +1001,27 @@ void TypeSelectorPopup::applyFilter(const QString& text) {
|
||||
|
||||
auto makeLabel = [](const TypeEntry& e) {
|
||||
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;
|
||||
};
|
||||
|
||||
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()) {
|
||||
// ── 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;
|
||||
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;
|
||||
QVector<int> 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 typeCount++;
|
||||
if (catAllowed(t))
|
||||
scored.append({t, sc, pos});
|
||||
scored.append({i, sc, std::move(pos)});
|
||||
}
|
||||
std::sort(scored.begin(), scored.end(),
|
||||
[](const Scored& a, const Scored& b) { return a.score > b.score; });
|
||||
|
||||
for (const auto& s : scored) {
|
||||
m_filteredTypes.append(s.entry);
|
||||
m_filteredTypes.append(m_allTypes[s.idx]);
|
||||
m_matchPositions.append(s.pos);
|
||||
displayStrings << makeLabel(s.entry);
|
||||
displayStrings << makeLabel(m_allTypes[s.idx]);
|
||||
}
|
||||
} else {
|
||||
// ── No filter: grouped sections, alphabetical ──
|
||||
|
||||
@@ -120,6 +120,7 @@ private:
|
||||
int m_pointerSize = 8;
|
||||
bool m_loading = false;
|
||||
QFont m_font;
|
||||
int m_cachedMaxNameLen = 0; // longest displayName length (chars)
|
||||
|
||||
void applyFilter(const QString& text);
|
||||
void updateModifierPreview();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QStandardItemModel>
|
||||
#include <QStandardItem>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QPainter>
|
||||
#include <QApplication>
|
||||
#include <algorithm>
|
||||
@@ -43,6 +44,7 @@ inline void buildStructChildren(QStandardItem* item,
|
||||
};
|
||||
|
||||
for (int mi : members) {
|
||||
if (mi < 0 || mi >= tree->nodes.size()) continue;
|
||||
const Node& m = tree->nodes[mi];
|
||||
if (isHexPad(m.kind)) continue;
|
||||
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.
|
||||
inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree,
|
||||
void* subPtr) {
|
||||
static const QIcon enumIcon(":/vsicons/symbol-enum.svg");
|
||||
static const QIcon structIcon(":/vsicons/symbol-structure.svg");
|
||||
bool isEnum = node->resolvedClassKeyword() == QStringLiteral("enum");
|
||||
auto* item = new QStandardItem(
|
||||
QIcon(isEnum ? ":/vsicons/symbol-enum.svg"
|
||||
: ":/vsicons/symbol-structure.svg"),
|
||||
isEnum ? enumIcon : structIcon,
|
||||
typeDisplayString(node, tree));
|
||||
item->setData(QVariant::fromValue(subPtr), Qt::UserRole);
|
||||
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.
|
||||
inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
const QVector<TabInfo>& tabs) {
|
||||
const QVector<TabInfo>& tabs,
|
||||
const QSet<uint64_t>& pinnedIds = {}) {
|
||||
model->clear();
|
||||
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));
|
||||
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));
|
||||
}
|
||||
|
||||
// Incremental sync — preserves tree expansion/scroll state.
|
||||
inline void syncProjectExplorer(QStandardItemModel* model,
|
||||
const QVector<TabInfo>& tabs) {
|
||||
const QVector<TabInfo>& tabs,
|
||||
const QSet<uint64_t>& pinnedIds = {}) {
|
||||
// First call — full build
|
||||
if (model->rowCount() == 0 && !tabs.isEmpty()) {
|
||||
buildProjectExplorer(model, tabs);
|
||||
buildProjectExplorer(model, tabs, pinnedIds);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,7 +163,9 @@ inline void syncProjectExplorer(QStandardItemModel* model,
|
||||
|
||||
// Remove stale items (backwards)
|
||||
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))
|
||||
model->removeRow(i);
|
||||
}
|
||||
@@ -201,6 +221,8 @@ public:
|
||||
m_selected = t.selected;
|
||||
m_accent = t.borderFocused; // left accent bar
|
||||
m_bg = t.background;
|
||||
m_badgeBg = t.backgroundAlt;
|
||||
m_badgeText = t.textDim;
|
||||
}
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem& option,
|
||||
@@ -234,38 +256,74 @@ public:
|
||||
QString fullText = index.data(Qt::DisplayRole).toString();
|
||||
QRect textRect = opt.rect.adjusted(4, 0, -4, 0);
|
||||
|
||||
// Draw icon for top-level items
|
||||
if (!isChild) {
|
||||
QVariant iconVar = index.data(Qt::DecorationRole);
|
||||
if (iconVar.isValid()) {
|
||||
QIcon icon = iconVar.value<QIcon>();
|
||||
int iconSz = opt.fontMetrics.height();
|
||||
int iconY = textRect.y() + (textRect.height() - iconSz) / 2;
|
||||
icon.paint(painter, textRect.x(), iconY, iconSz, iconSz);
|
||||
textRect.setLeft(textRect.left() + iconSz + 4);
|
||||
// Letter badge (S/E for top-level, F for children)
|
||||
{
|
||||
QChar letter = 'F';
|
||||
if (!isChild) {
|
||||
bool isEnum = index.data(Qt::UserRole + 2).toBool();
|
||||
letter = isEnum ? 'E' : 'S';
|
||||
}
|
||||
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);
|
||||
|
||||
if (!isChild) {
|
||||
// Top-level: "StructName — 3"
|
||||
// Top-level: "StructName — 3" → name left, count pill right
|
||||
int dashPos = fullText.indexOf(QChar(0x2014));
|
||||
if (dashPos > 1) {
|
||||
QString name = fullText.left(dashPos - 1);
|
||||
QString meta = fullText.mid(dashPos - 1);
|
||||
QString name = (dashPos > 1) ? fullText.left(dashPos - 1) : fullText;
|
||||
QString count = (dashPos > 1) ? fullText.mid(dashPos + 2).trimmed() : QString();
|
||||
|
||||
painter->setPen(m_text);
|
||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, name);
|
||||
int nameW = opt.fontMetrics.horizontalAdvance(name);
|
||||
bool pinned = index.data(Qt::UserRole + 4).toBool();
|
||||
|
||||
QRect metaRect = textRect;
|
||||
metaRect.setLeft(textRect.left() + nameW);
|
||||
// Reserve right side for pin icon + count pill
|
||||
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->drawText(metaRect, Qt::AlignLeft | Qt::AlignVCenter, meta);
|
||||
} else {
|
||||
painter->drawText(pill, Qt::AlignCenter, count);
|
||||
}
|
||||
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->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, fullText);
|
||||
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, elided);
|
||||
}
|
||||
} else {
|
||||
// Child: "TypeName fieldName"
|
||||
@@ -294,6 +352,7 @@ public:
|
||||
private:
|
||||
QColor m_text, m_textDim, m_textMuted, m_syntaxType;
|
||||
QColor m_hover, m_selected, m_accent, m_bg;
|
||||
QColor m_badgeBg, m_badgeText;
|
||||
};
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
@@ -38,6 +38,7 @@ static void buildSmallTree(NodeTree& tree) {
|
||||
root.name = "root";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
root.collapsed = false;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
|
||||
@@ -1964,6 +1964,7 @@ private slots:
|
||||
root.structTypeName = "Chain";
|
||||
root.name = "chain";
|
||||
root.parentId = 0;
|
||||
root.collapsed = false;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
@@ -1974,6 +1975,7 @@ private slots:
|
||||
inner.name = "Inner";
|
||||
inner.parentId = 0;
|
||||
inner.offset = 300;
|
||||
inner.collapsed = false;
|
||||
int ii = tree.addNode(inner);
|
||||
uint64_t innerId = tree.nodes[ii].id;
|
||||
{
|
||||
@@ -1990,6 +1992,7 @@ private slots:
|
||||
outer.name = "Outer";
|
||||
outer.parentId = 0;
|
||||
outer.offset = 200;
|
||||
outer.collapsed = false;
|
||||
int oi = tree.addNode(outer);
|
||||
uint64_t outerId = tree.nodes[oi].id;
|
||||
{
|
||||
@@ -2002,6 +2005,7 @@ private slots:
|
||||
p.kind = NodeKind::Pointer64; p.name = "pInner";
|
||||
p.parentId = outerId; p.offset = 8;
|
||||
p.refId = innerId;
|
||||
p.collapsed = false;
|
||||
tree.addNode(p);
|
||||
}
|
||||
|
||||
@@ -2011,6 +2015,7 @@ private slots:
|
||||
p.kind = NodeKind::Pointer64; p.name = "pOuter";
|
||||
p.parentId = rootId; p.offset = 0;
|
||||
p.refId = outerId;
|
||||
p.collapsed = false;
|
||||
tree.addNode(p);
|
||||
}
|
||||
|
||||
@@ -2706,6 +2711,7 @@ private slots:
|
||||
sf.offset = 0;
|
||||
sf.isStatic = true;
|
||||
sf.offsetExpr = QStringLiteral("base + 0x10");
|
||||
sf.collapsed = false;
|
||||
tree.addNode(sf);
|
||||
|
||||
NullProvider prov;
|
||||
|
||||
@@ -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
92
tests/test_pixels.py
Normal 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()
|
||||
@@ -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 ──
|
||||
|
||||
void testPopupListsRootStructs() {
|
||||
|
||||
186
tests/test_typeinfer.cpp
Normal file
186
tests/test_typeinfer.cpp
Normal 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
Reference in New Issue
Block a user