mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: type hints green [bracketed] notation, workspace cleanup, unique naming
- Type inference hints now show value-first with bracketed type in comment green: "0x7ff718570000 [ptr64]", "6, 16 [int32_t×2]" - Raise hint threshold to strong-only (score >= 75%) - Remove Bool inference, widen Int16 range to ±16384 - Workspace: remove dead WorkspaceProxy, fix null deref, debounce search, cache icons, add pinning support - Unique naming: UnnamedClass0/UnnamedEnum1 with global counter - Footer buttons: +10h +100h +1000h replacing +1024 - MCP: project lifecycle API, snapshot provider fix
This commit is contained in:
@@ -428,20 +428,6 @@ if(BUILD_TESTING)
|
|||||||
endif()
|
endif()
|
||||||
add_test(NAME test_controller COMMAND test_controller)
|
add_test(NAME test_controller COMMAND test_controller)
|
||||||
|
|
||||||
add_executable(test_validation tests/test_validation.cpp
|
|
||||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
|
||||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
|
||||||
src/typeselectorpopup.cpp
|
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
|
||||||
target_include_directories(test_validation PRIVATE src third_party/fadec)
|
|
||||||
target_link_libraries(test_validation PRIVATE
|
|
||||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
|
||||||
QScintilla::QScintilla)
|
|
||||||
if(WIN32)
|
|
||||||
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
|
||||||
endif()
|
|
||||||
add_test(NAME test_validation COMMAND test_validation)
|
|
||||||
|
|
||||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
add_executable(test_context_menu tests/test_context_menu.cpp
|
||||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
@@ -488,20 +474,6 @@ if(BUILD_TESTING)
|
|||||||
QScintilla::QScintilla)
|
QScintilla::QScintilla)
|
||||||
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
add_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||||
|
|
||||||
add_executable(test_new_features tests/test_new_features.cpp
|
|
||||||
src/generator.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
|
||||||
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
|
||||||
src/typeselectorpopup.cpp
|
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
|
||||||
target_include_directories(test_new_features PRIVATE src third_party/fadec)
|
|
||||||
target_link_libraries(test_new_features PRIVATE
|
|
||||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
|
||||||
QScintilla::QScintilla)
|
|
||||||
if(WIN32)
|
|
||||||
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
|
||||||
endif()
|
|
||||||
add_test(NAME test_new_features COMMAND test_new_features)
|
|
||||||
|
|
||||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
add_executable(test_type_selector tests/test_type_selector.cpp
|
||||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp
|
||||||
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
|
|||||||
@@ -261,14 +261,17 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
|||||||
? prov.readBytes(absAddr, sz) : QByteArray(sz, '\0');
|
? prov.readBytes(absAddr, sz) : QByteArray(sz, '\0');
|
||||||
auto suggestions = inferTypes(
|
auto suggestions = inferTypes(
|
||||||
reinterpret_cast<const uint8_t*>(b.constData()), sz);
|
reinterpret_cast<const uint8_t*>(b.constData()), sz);
|
||||||
if (!suggestions.isEmpty() && suggestions[0].strength >= 2) {
|
if (!suggestions.isEmpty() && suggestions[0].strength >= 3) {
|
||||||
lm.typeHintStart = lineText.size() + 2; // after " " gap
|
lm.typeHintStart = lineText.size() + 2; // after " " gap
|
||||||
lm.typeHintKinds = suggestions[0].kinds;
|
lm.typeHintKinds = suggestions[0].kinds;
|
||||||
lm.typeHint = formatHint(suggestions[0]);
|
QString typeName = formatHint(suggestions[0]);
|
||||||
QString preview = formatPreview(
|
QString preview = formatPreview(
|
||||||
reinterpret_cast<const uint8_t*>(b.constData()), sz, suggestions[0]);
|
reinterpret_cast<const uint8_t*>(b.constData()), sz, suggestions[0]);
|
||||||
|
// Value-first with bracketed type: "0x7ff718570000 [ptr64]"
|
||||||
if (!preview.isEmpty())
|
if (!preview.isEmpty())
|
||||||
lm.typeHint += QStringLiteral(" ") + preview;
|
lm.typeHint = preview + QStringLiteral(" [") + typeName + QStringLiteral("]");
|
||||||
|
else
|
||||||
|
lm.typeHint = QStringLiteral("[") + typeName + QStringLiteral("]");
|
||||||
lineText += QStringLiteral(" ") + lm.typeHint;
|
lineText += QStringLiteral(" ") + lm.typeHint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,9 +190,10 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
|
|||||||
|
|
||||||
// Eagerly pre-warm the type popup so first click isn't slow (~350ms cold start).
|
// Eagerly pre-warm the type popup so first click isn't slow (~350ms cold start).
|
||||||
if (!m_cachedPopup) {
|
if (!m_cachedPopup) {
|
||||||
QTimer::singleShot(0, this, [this, editor]() {
|
QPointer<RcxEditor> safeEditor = editor;
|
||||||
if (!m_cachedPopup && !m_editors.isEmpty())
|
QTimer::singleShot(0, this, [this, safeEditor]() {
|
||||||
ensurePopup(editor);
|
if (!m_cachedPopup && !m_editors.isEmpty() && safeEditor)
|
||||||
|
ensurePopup(safeEditor);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return editor;
|
return editor;
|
||||||
@@ -200,7 +201,7 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
|
|||||||
|
|
||||||
void RcxController::removeSplitEditor(RcxEditor* editor) {
|
void RcxController::removeSplitEditor(RcxEditor* editor) {
|
||||||
m_editors.removeOne(editor);
|
m_editors.removeOne(editor);
|
||||||
// Caller (MainWindow) owns the parent QTabWidget and handles widget destruction.
|
editor->disconnect(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::connectEditor(RcxEditor* editor) {
|
void RcxController::connectEditor(RcxEditor* editor) {
|
||||||
|
|||||||
14
src/core.h
14
src/core.h
@@ -570,13 +570,13 @@ static constexpr int kCommandRowLine = 0;
|
|||||||
static constexpr int kFirstDataLine = 1;
|
static constexpr int kFirstDataLine = 1;
|
||||||
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
|
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
|
||||||
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
|
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
|
||||||
static constexpr uint64_t kArrayElemShift = 48; // bits 48-61 hold element index
|
static constexpr uint64_t kArrayElemShift = 42; // bits 42-61 hold element index
|
||||||
static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements
|
static constexpr uint64_t kArrayElemMask = 0x3FFFFC0000000000ULL; // 20 bits → max 1048575 elements
|
||||||
|
|
||||||
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
|
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 42)
|
||||||
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
|
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
|
||||||
Q_ASSERT(elemIdx >= 0);
|
Q_ASSERT(elemIdx >= 0);
|
||||||
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
|
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0xFFFFF) << kArrayElemShift);
|
||||||
}
|
}
|
||||||
inline int arrayElemIdxFromSelId(uint64_t selId) {
|
inline int arrayElemIdxFromSelId(uint64_t selId) {
|
||||||
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
|
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
|
||||||
@@ -584,11 +584,11 @@ inline int arrayElemIdxFromSelId(uint64_t selId) {
|
|||||||
|
|
||||||
// Member selection encoding (enum/bitfield members) — mirrors array element pattern
|
// Member selection encoding (enum/bitfield members) — mirrors array element pattern
|
||||||
static constexpr uint64_t kMemberBit = 0x2000000000000000ULL;
|
static constexpr uint64_t kMemberBit = 0x2000000000000000ULL;
|
||||||
static constexpr uint64_t kMemberSubShift = 48;
|
static constexpr uint64_t kMemberSubShift = 42;
|
||||||
static constexpr uint64_t kMemberSubMask = 0x3FFF000000000000ULL;
|
static constexpr uint64_t kMemberSubMask = 0x3FFFFC0000000000ULL;
|
||||||
|
|
||||||
inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) {
|
inline uint64_t makeMemberSelId(uint64_t nodeId, int subLine) {
|
||||||
return nodeId | kMemberBit | ((uint64_t)(subLine & 0x3FFF) << kMemberSubShift);
|
return nodeId | kMemberBit | ((uint64_t)(subLine & 0xFFFFF) << kMemberSubShift);
|
||||||
}
|
}
|
||||||
inline int memberSubFromSelId(uint64_t selId) {
|
inline int memberSubFromSelId(uint64_t selId) {
|
||||||
return (int)((selId & kMemberSubMask) >> kMemberSubShift);
|
return (int)((selId & kMemberSubMask) >> kMemberSubShift);
|
||||||
|
|||||||
@@ -762,7 +762,7 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_LOCAL_OFF, theme.textFaint);
|
IND_LOCAL_OFF, theme.textFaint);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_TYPE_HINT, theme.textFaint);
|
IND_TYPE_HINT, theme.indHintGreen);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_FIND, theme.borderFocused);
|
IND_FIND, theme.borderFocused);
|
||||||
|
|
||||||
@@ -905,7 +905,9 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
const auto& lm = result.meta[i];
|
const auto& lm = result.meta[i];
|
||||||
if (lm.heatLevel > 0 || isFuncPtr(lm.nodeKind) ||
|
if (lm.heatLevel > 0 || isFuncPtr(lm.nodeKind) ||
|
||||||
lm.nodeKind == NodeKind::Pointer32 ||
|
lm.nodeKind == NodeKind::Pointer32 ||
|
||||||
lm.nodeKind == NodeKind::Pointer64)
|
lm.nodeKind == NodeKind::Pointer64 ||
|
||||||
|
lm.lineKind == LineKind::Footer ||
|
||||||
|
lm.typeHintStart >= 0)
|
||||||
lineTexts[i] = getLineText(m_sci, i);
|
lineTexts[i] = getLineText(m_sci, i);
|
||||||
}
|
}
|
||||||
applyHeatmapHighlight(result.meta, lineTexts);
|
applyHeatmapHighlight(result.meta, lineTexts);
|
||||||
@@ -915,7 +917,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
// Footer buttons — pill styling
|
// Footer buttons — pill styling
|
||||||
for (int i = 0; i < result.meta.size(); i++) {
|
for (int i = 0; i < result.meta.size(); i++) {
|
||||||
if (result.meta[i].lineKind != LineKind::Footer) continue;
|
if (result.meta[i].lineKind != LineKind::Footer) continue;
|
||||||
QString ft = getLineText(m_sci, i);
|
const QString& ft = lineTexts[i];
|
||||||
// Struct footer: +10h +100h +1000h Trim (search longest first)
|
// Struct footer: +10h +100h +1000h Trim (search longest first)
|
||||||
int p1000 = ft.indexOf(QStringLiteral("+1000h"));
|
int p1000 = ft.indexOf(QStringLiteral("+1000h"));
|
||||||
if (p1000 >= 0)
|
if (p1000 >= 0)
|
||||||
@@ -935,6 +937,15 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
fillIndicatorCols(IND_CMD_PILL, i, trimStart, trimStart + 4);
|
fillIndicatorCols(IND_CMD_PILL, i, trimStart, trimStart + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply type inference hint coloring (green, same as comment annotations)
|
||||||
|
for (int i = 0; i < result.meta.size(); i++) {
|
||||||
|
const auto& lm = result.meta[i];
|
||||||
|
if (lm.typeHintStart < 0) continue;
|
||||||
|
const QString& ft = lineTexts[i];
|
||||||
|
if (lm.typeHintStart < ft.size())
|
||||||
|
fillIndicatorCols(IND_TYPE_HINT, i, lm.typeHintStart, ft.size());
|
||||||
|
}
|
||||||
|
|
||||||
// Reset hint line - applySelectionOverlay will repaint indicators
|
// Reset hint line - applySelectionOverlay will repaint indicators
|
||||||
m_hintLine = -1;
|
m_hintLine = -1;
|
||||||
|
|
||||||
@@ -2562,6 +2573,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
ColumnSpan cs = commentSpanFor(*lm, 9999, lm->effectiveTypeW, lm->effectiveNameW);
|
ColumnSpan cs = commentSpanFor(*lm, 9999, lm->effectiveTypeW, lm->effectiveNameW);
|
||||||
m_editState.commentCol = cs.valid ? cs.start : -1;
|
m_editState.commentCol = cs.valid ? cs.start : -1;
|
||||||
m_editState.lastValidationOk = true; // original value is always valid
|
m_editState.lastValidationOk = true; // original value is always valid
|
||||||
|
} else if (target == EditTarget::BaseAddress) {
|
||||||
|
m_editState.commentCol = norm.end + 2; // command row has no column layout
|
||||||
} else {
|
} else {
|
||||||
m_editState.commentCol = -1;
|
m_editState.commentCol = -1;
|
||||||
}
|
}
|
||||||
@@ -2575,7 +2588,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
|
|
||||||
// For value editing: extend line with trailing spaces for the edit comment area
|
// For value editing: extend line with trailing spaces for the edit comment area
|
||||||
// (comment padding is no longer baked into every line to avoid unnecessary scroll width)
|
// (comment padding is no longer baked into every line to avoid unnecessary scroll width)
|
||||||
if (target == EditTarget::Value && m_editState.commentCol >= 0) {
|
if ((target == EditTarget::Value || target == EditTarget::BaseAddress)
|
||||||
|
&& m_editState.commentCol >= 0) {
|
||||||
int commentStart = norm.end + 2;
|
int commentStart = norm.end + 2;
|
||||||
int neededLen = commentStart + kColComment;
|
int neededLen = commentStart + kColComment;
|
||||||
int currentLen = (int)lineText.size();
|
int currentLen = (int)lineText.size();
|
||||||
@@ -2624,6 +2638,8 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
|||||||
// Show initial edit hint in comment column
|
// Show initial edit hint in comment column
|
||||||
if (target == EditTarget::Value)
|
if (target == EditTarget::Value)
|
||||||
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
|
setEditComment(QStringLiteral("Enter=Save Esc=Cancel"));
|
||||||
|
else if (target == EditTarget::BaseAddress)
|
||||||
|
setEditComment(QStringLiteral("e.g. <mod.exe> + 0xFF | [0x1000 + 0x10] | 7ff6`1234ABCD"));
|
||||||
|
|
||||||
// Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup
|
// Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup
|
||||||
// and exit early above (never reach here).
|
// and exit early above (never reach here).
|
||||||
|
|||||||
@@ -661,8 +661,10 @@ QString validateValue(NodeKind kind, const QString& text) {
|
|||||||
QString digits = hasHexPrefix ? s.mid(2) : s;
|
QString digits = hasHexPrefix ? s.mid(2) : s;
|
||||||
|
|
||||||
if (hasHexPrefix || isHexKind) {
|
if (hasHexPrefix || isHexKind) {
|
||||||
// Hex mode: only 0-9, a-f, A-F
|
// Hex mode: only 0-9, a-f, A-F (spaces allowed for multi-byte hex kinds)
|
||||||
|
bool isMultiByteHex = (kind >= NodeKind::Hex16 && kind <= NodeKind::Hex64);
|
||||||
for (QChar c : digits) {
|
for (QChar c : digits) {
|
||||||
|
if (c == ' ' && isMultiByteHex) continue;
|
||||||
if (!c.isDigit() && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
|
if (!c.isDigit() && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F'))
|
||||||
return QStringLiteral("invalid hex '%1'").arg(c);
|
return QStringLiteral("invalid hex '%1'").arg(c);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1188,6 +1188,16 @@ static int structTypeSize(const QString& typeName, const BuildContext& ctx) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute total array elements from multi-dimensional sizes, capped to prevent overflow.
|
||||||
|
static int clampedArrayElements(const QVector<int>& dims, int maxElements = 1000000) {
|
||||||
|
int64_t total = 1;
|
||||||
|
for (int dim : dims) {
|
||||||
|
total *= (dim > 0 ? dim : 1);
|
||||||
|
if (total > maxElements) return maxElements;
|
||||||
|
}
|
||||||
|
return (int)total;
|
||||||
|
}
|
||||||
|
|
||||||
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
||||||
const QVector<ParsedField>& fields) {
|
const QVector<ParsedField>& fields) {
|
||||||
int computedOffset = 0;
|
int computedOffset = 0;
|
||||||
@@ -1276,8 +1286,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
|||||||
|
|
||||||
// Array of pointers: PVOID arr[N]
|
// Array of pointers: PVOID arr[N]
|
||||||
if (!field.arraySizes.isEmpty()) {
|
if (!field.arraySizes.isEmpty()) {
|
||||||
int totalElements = 1;
|
int totalElements = clampedArrayElements(field.arraySizes);
|
||||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
|
||||||
|
|
||||||
Node n;
|
Node n;
|
||||||
n.kind = NodeKind::Array;
|
n.kind = NodeKind::Array;
|
||||||
@@ -1315,8 +1324,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
|||||||
int elemSize = 4;
|
int elemSize = 4;
|
||||||
NodeKind elemKind = NodeKind::UInt32;
|
NodeKind elemKind = NodeKind::UInt32;
|
||||||
if (!field.arraySizes.isEmpty()) {
|
if (!field.arraySizes.isEmpty()) {
|
||||||
int totalElements = 1;
|
int totalElements = clampedArrayElements(field.arraySizes);
|
||||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
|
||||||
Node n;
|
Node n;
|
||||||
n.kind = NodeKind::Array;
|
n.kind = NodeKind::Array;
|
||||||
n.name = field.name;
|
n.name = field.name;
|
||||||
@@ -1420,8 +1428,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
|||||||
ctx.tree.addNode(n); computedOffset = fieldOffset + 64; continue;
|
ctx.tree.addNode(n); computedOffset = fieldOffset + 64; continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int totalElements = 1;
|
int totalElements = clampedArrayElements(field.arraySizes);
|
||||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
|
||||||
|
|
||||||
Node n;
|
Node n;
|
||||||
n.kind = NodeKind::Array;
|
n.kind = NodeKind::Array;
|
||||||
@@ -1440,8 +1447,7 @@ static void buildFields(BuildContext& ctx, uint64_t parentId, int baseOffset,
|
|||||||
int elemSize = structTypeSize(field.typeName, ctx);
|
int elemSize = structTypeSize(field.typeName, ctx);
|
||||||
|
|
||||||
if (!field.arraySizes.isEmpty()) {
|
if (!field.arraySizes.isEmpty()) {
|
||||||
int totalElements = 1;
|
int totalElements = clampedArrayElements(field.arraySizes);
|
||||||
for (int dim : field.arraySizes) totalElements *= (dim > 0 ? dim : 1);
|
|
||||||
|
|
||||||
Node n;
|
Node n;
|
||||||
n.kind = NodeKind::Array;
|
n.kind = NodeKind::Array;
|
||||||
|
|||||||
116
src/main.cpp
116
src/main.cpp
@@ -604,6 +604,25 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
|||||||
|
|
||||||
createWorkspaceDock();
|
createWorkspaceDock();
|
||||||
createScannerDock();
|
createScannerDock();
|
||||||
|
|
||||||
|
// Hidden sentinel dock — never visible, only used to force Qt to create a
|
||||||
|
// QTabBar when the first document dock is added (Qt only creates tab bars
|
||||||
|
// via tabifyDockWidget). Immediately hidden after tabification so it takes
|
||||||
|
// zero layout space. An event filter on the QTabBar keeps it visible.
|
||||||
|
{
|
||||||
|
m_sentinelDock = new QDockWidget(this);
|
||||||
|
m_sentinelDock->setObjectName(QStringLiteral("_sentinel"));
|
||||||
|
m_sentinelDock->setFeatures(QDockWidget::NoDockWidgetFeatures);
|
||||||
|
auto* sw = new QWidget(m_sentinelDock);
|
||||||
|
sw->setFixedSize(0, 0);
|
||||||
|
m_sentinelDock->setWidget(sw);
|
||||||
|
auto* stb = new QWidget(m_sentinelDock);
|
||||||
|
stb->setFixedHeight(0);
|
||||||
|
m_sentinelDock->setTitleBarWidget(stb);
|
||||||
|
addDockWidget(Qt::TopDockWidgetArea, m_sentinelDock);
|
||||||
|
m_sentinelDock->hide(); // hidden = zero layout space
|
||||||
|
}
|
||||||
|
|
||||||
createMenus();
|
createMenus();
|
||||||
createStatusBar();
|
createStatusBar();
|
||||||
|
|
||||||
@@ -1644,6 +1663,16 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
else
|
else
|
||||||
addDockWidget(Qt::TopDockWidgetArea, dock);
|
addDockWidget(Qt::TopDockWidgetArea, dock);
|
||||||
|
|
||||||
|
// Bootstrap: tabify the hidden sentinel with the first doc dock so Qt
|
||||||
|
// creates a QTabBar. Then hide sentinel (zero layout space). The event
|
||||||
|
// filter in eventFilter() keeps the tab bar visible even at count==1.
|
||||||
|
if (m_sentinelDock && m_docDocks.isEmpty()) {
|
||||||
|
m_sentinelDock->show();
|
||||||
|
tabifyDockWidget(dock, m_sentinelDock);
|
||||||
|
m_sentinelDock->hide();
|
||||||
|
dock->raise();
|
||||||
|
}
|
||||||
|
|
||||||
m_docDocks.append(dock);
|
m_docDocks.append(dock);
|
||||||
m_tabs[dock] = { doc, ctrl, splitter, {}, 0 };
|
m_tabs[dock] = { doc, ctrl, splitter, {}, 0 };
|
||||||
m_activeDocDock = dock;
|
m_activeDocDock = dock;
|
||||||
@@ -1698,7 +1727,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
m_activeDocDock = m_docDocks.isEmpty() ? nullptr : m_docDocks.last();
|
m_activeDocDock = m_docDocks.isEmpty() ? nullptr : m_docDocks.last();
|
||||||
rebuildAllDocs();
|
rebuildAllDocs();
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
if (m_tabs.isEmpty())
|
if (m_tabs.isEmpty() && !m_closingAll)
|
||||||
project_new();
|
project_new();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1780,6 +1809,10 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
|||||||
updateWindowTitle();
|
updateWindowTitle();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// Notify MCP clients of tree changes
|
||||||
|
connect(doc, &RcxDocument::documentChanged, this, [this]() {
|
||||||
|
if (m_mcp) m_mcp->notifyTreeChanged();
|
||||||
|
});
|
||||||
connect(&doc->undoStack, &QUndoStack::indexChanged,
|
connect(&doc->undoStack, &QUndoStack::indexChanged,
|
||||||
this, [this, dockGuard](int) {
|
this, [this, dockGuard](int) {
|
||||||
if (!dockGuard) return;
|
if (!dockGuard) return;
|
||||||
@@ -1875,6 +1908,9 @@ void MainWindow::setupDockTabBars() {
|
|||||||
.arg(theme.background.name(), theme.border.name(), theme.hover.name()));
|
.arg(theme.background.name(), theme.border.name(), theme.hover.name()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force tab bar visible (event filter keeps it alive, belt-and-suspenders)
|
||||||
|
tabBar->show();
|
||||||
|
|
||||||
// Install tab buttons for any tab that doesn't have them yet
|
// Install tab buttons for any tab that doesn't have them yet
|
||||||
for (int i = 0; i < tabBar->count(); ++i) {
|
for (int i = 0; i < tabBar->count(); ++i) {
|
||||||
auto* existing = qobject_cast<DockTabButtons*>(
|
auto* existing = qobject_cast<DockTabButtons*>(
|
||||||
@@ -2010,6 +2046,25 @@ void MainWindow::setupDockTabBars() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool MainWindow::eventFilter(QObject* obj, QEvent* event) {
|
bool MainWindow::eventFilter(QObject* obj, QEvent* event) {
|
||||||
|
// Keep dock tab bars visible even when Qt wants to hide them (count==1).
|
||||||
|
// Qt's QMainWindowLayout calls setVisible(false) on the QTabBar when only
|
||||||
|
// one dock remains in a tab group. We catch the resulting Hide event and
|
||||||
|
// immediately re-show the tab bar, provided at least one doc dock is docked.
|
||||||
|
if (event->type() == QEvent::Hide && !m_tabBarShowGuard) {
|
||||||
|
if (auto* tabBar = qobject_cast<QTabBar*>(obj)) {
|
||||||
|
if (tabBar->parent() == this && tabBar->count() >= 1) {
|
||||||
|
bool hasDockedDoc = false;
|
||||||
|
for (auto* d : m_docDocks)
|
||||||
|
if (!d->isFloating() && d->isVisible()) { hasDockedDoc = true; break; }
|
||||||
|
if (hasDockedDoc) {
|
||||||
|
m_tabBarShowGuard = true;
|
||||||
|
tabBar->show();
|
||||||
|
m_tabBarShowGuard = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (event->type() == QEvent::MouseButtonPress) {
|
if (event->type() == QEvent::MouseButtonPress) {
|
||||||
auto* me = static_cast<QMouseEvent*>(event);
|
auto* me = static_cast<QMouseEvent*>(event);
|
||||||
if (me->button() == Qt::MiddleButton) {
|
if (me->button() == Qt::MiddleButton) {
|
||||||
@@ -3107,8 +3162,10 @@ void MainWindow::importReclassXml() {
|
|||||||
auto* doc = new RcxDocument(this);
|
auto* doc = new RcxDocument(this);
|
||||||
doc->tree = std::move(tree);
|
doc->tree = std::move(tree);
|
||||||
|
|
||||||
closeAllDocDocks();
|
{ ClosingGuard guard(m_closingAll);
|
||||||
createTab(doc);
|
closeAllDocDocks();
|
||||||
|
createTab(doc);
|
||||||
|
}
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||||
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||||
@@ -3156,8 +3213,10 @@ void MainWindow::importFromSource() {
|
|||||||
auto* doc = new RcxDocument(this);
|
auto* doc = new RcxDocument(this);
|
||||||
doc->tree = std::move(tree);
|
doc->tree = std::move(tree);
|
||||||
|
|
||||||
closeAllDocDocks();
|
{ ClosingGuard guard(m_closingAll);
|
||||||
createTab(doc);
|
closeAllDocDocks();
|
||||||
|
createTab(doc);
|
||||||
|
}
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
if (!m_docDocks.isEmpty()) {
|
if (!m_docDocks.isEmpty()) {
|
||||||
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
||||||
@@ -3210,8 +3269,10 @@ void MainWindow::importPdb() {
|
|||||||
auto* doc = new rcx::RcxDocument(this);
|
auto* doc = new rcx::RcxDocument(this);
|
||||||
doc->tree = std::move(tree);
|
doc->tree = std::move(tree);
|
||||||
|
|
||||||
closeAllDocDocks();
|
{ ClosingGuard guard(m_closingAll);
|
||||||
createTab(doc);
|
closeAllDocDocks();
|
||||||
|
createTab(doc);
|
||||||
|
}
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
if (!m_docDocks.isEmpty()) {
|
if (!m_docDocks.isEmpty()) {
|
||||||
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
||||||
@@ -3408,8 +3469,11 @@ QDockWidget* MainWindow::project_open(const QString& path) {
|
|||||||
}
|
}
|
||||||
auto* doc = new RcxDocument(this);
|
auto* doc = new RcxDocument(this);
|
||||||
doc->tree = std::move(tree);
|
doc->tree = std::move(tree);
|
||||||
closeAllDocDocks();
|
QDockWidget* dock;
|
||||||
auto* dock = createTab(doc);
|
{ ClosingGuard guard(m_closingAll);
|
||||||
|
closeAllDocDocks();
|
||||||
|
dock = createTab(doc);
|
||||||
|
}
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
if (!m_docDocks.isEmpty()) {
|
if (!m_docDocks.isEmpty()) {
|
||||||
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
||||||
@@ -3433,9 +3497,11 @@ QDockWidget* MainWindow::project_open(const QString& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close all existing tabs so the project replaces the current state
|
// Close all existing tabs so the project replaces the current state
|
||||||
closeAllDocDocks();
|
QDockWidget* dock;
|
||||||
|
{ ClosingGuard guard(m_closingAll);
|
||||||
auto* dock = createTab(doc);
|
closeAllDocDocks();
|
||||||
|
dock = createTab(doc);
|
||||||
|
}
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
if (!m_docDocks.isEmpty()) {
|
if (!m_docDocks.isEmpty()) {
|
||||||
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
splitDockWidget(m_workspaceDock, m_docDocks.first(), Qt::Horizontal);
|
||||||
@@ -3750,6 +3816,16 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
actConvert = menu.addAction("Convert to Class");
|
actConvert = menu.addAction("Convert to Class");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pin/Unpin
|
||||||
|
bool allPinned = true;
|
||||||
|
for (const auto& item : items)
|
||||||
|
if (!m_pinnedIds.contains(item.structId)) { allPinned = false; break; }
|
||||||
|
auto* actPin = menu.addAction(
|
||||||
|
QIcon(QStringLiteral(":/vsicons/pin.svg")),
|
||||||
|
allPinned ? QStringLiteral("Unpin") : QStringLiteral("Pin"));
|
||||||
|
|
||||||
|
menu.addSeparator();
|
||||||
|
|
||||||
// Delete: works for single or multi
|
// Delete: works for single or multi
|
||||||
QString delLabel = items.size() == 1
|
QString delLabel = items.size() == 1
|
||||||
? QStringLiteral("Delete")
|
? QStringLiteral("Delete")
|
||||||
@@ -3941,6 +4017,17 @@ void MainWindow::createWorkspaceDock() {
|
|||||||
tab.doc->undoStack.push(new rcx::RcxCommand(tab.ctrl,
|
tab.doc->undoStack.push(new rcx::RcxCommand(tab.ctrl,
|
||||||
rcx::cmd::ChangeClassKeyword{item.structId, item.keyword, newKw}));
|
rcx::cmd::ChangeClassKeyword{item.structId, item.keyword, newKw}));
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
|
|
||||||
|
} else if (chosen && chosen == actPin) {
|
||||||
|
for (const auto& item : items) {
|
||||||
|
if (allPinned)
|
||||||
|
m_pinnedIds.remove(item.structId);
|
||||||
|
else
|
||||||
|
m_pinnedIds.insert(item.structId);
|
||||||
|
}
|
||||||
|
// Full rebuild to reorder pinned items to top
|
||||||
|
m_workspaceModel->removeRows(0, m_workspaceModel->rowCount());
|
||||||
|
rebuildWorkspaceModelNow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4244,9 +4331,9 @@ void MainWindow::rebuildWorkspaceModelNow() {
|
|||||||
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
|
QString name = rootName(tab.doc->tree, tab.ctrl->viewRootId());
|
||||||
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
|
tabs.append({ &tab.doc->tree, name, static_cast<void*>(it.key()) });
|
||||||
}
|
}
|
||||||
rcx::syncProjectExplorer(m_workspaceModel, tabs);
|
rcx::syncProjectExplorer(m_workspaceModel, tabs, m_pinnedIds);
|
||||||
|
|
||||||
// Mark items that are currently viewed in a tab
|
// Mark items that are currently viewed in a tab + pinned state
|
||||||
QSet<uint64_t> viewedIds;
|
QSet<uint64_t> viewedIds;
|
||||||
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it)
|
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it)
|
||||||
viewedIds.insert(it->ctrl->viewRootId());
|
viewedIds.insert(it->ctrl->viewRootId());
|
||||||
@@ -4255,6 +4342,7 @@ void MainWindow::rebuildWorkspaceModelNow() {
|
|||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
|
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
|
||||||
item->setData(viewedIds.contains(id), Qt::UserRole + 3);
|
item->setData(viewedIds.contains(id), Qt::UserRole + 3);
|
||||||
|
item->setData(m_pinnedIds.contains(id), Qt::UserRole + 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_dockTitleLabel) {
|
if (m_dockTitleLabel) {
|
||||||
|
|||||||
@@ -120,7 +120,15 @@ private:
|
|||||||
QMap<QDockWidget*, TabState> m_tabs;
|
QMap<QDockWidget*, TabState> m_tabs;
|
||||||
QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex
|
QVector<QDockWidget*> m_docDocks; // ordered list for tabByIndex
|
||||||
QDockWidget* m_activeDocDock = nullptr; // tracks active document dock
|
QDockWidget* m_activeDocDock = nullptr; // tracks active document dock
|
||||||
|
QDockWidget* m_sentinelDock = nullptr; // hidden dock to bootstrap tab bar creation
|
||||||
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
|
QVector<RcxDocument*> m_allDocs; // all open docs, shared with controllers
|
||||||
|
bool m_closingAll = false; // guards spurious project_new during batch close
|
||||||
|
bool m_tabBarShowGuard = false; // prevents recursion in event filter re-show
|
||||||
|
struct ClosingGuard {
|
||||||
|
bool& flag;
|
||||||
|
ClosingGuard(bool& f) : flag(f) { flag = true; }
|
||||||
|
~ClosingGuard() { flag = false; }
|
||||||
|
};
|
||||||
void rebuildAllDocs();
|
void rebuildAllDocs();
|
||||||
|
|
||||||
void createMenus();
|
void createMenus();
|
||||||
@@ -165,6 +173,7 @@ private:
|
|||||||
QLabel* m_dockTitleLabel = nullptr;
|
QLabel* m_dockTitleLabel = nullptr;
|
||||||
QToolButton* m_dockCloseBtn = nullptr;
|
QToolButton* m_dockCloseBtn = nullptr;
|
||||||
DockGripWidget* m_dockGrip = nullptr;
|
DockGripWidget* m_dockGrip = nullptr;
|
||||||
|
QSet<uint64_t> m_pinnedIds;
|
||||||
void createWorkspaceDock();
|
void createWorkspaceDock();
|
||||||
void rebuildWorkspaceModel(); // debounced — safe to call frequently
|
void rebuildWorkspaceModel(); // debounced — safe to call frequently
|
||||||
void rebuildWorkspaceModelNow(); // immediate rebuild
|
void rebuildWorkspaceModelNow(); // immediate rebuild
|
||||||
|
|||||||
@@ -10,13 +10,24 @@
|
|||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
|
static constexpr int kMaxReadBuffer = 10 * 1024 * 1024; // 10 MB
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
// Construction / lifecycle
|
// Construction / lifecycle
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
McpBridge::McpBridge(MainWindow* mainWindow, QObject* parent)
|
McpBridge::McpBridge(MainWindow* mainWindow, QObject* parent)
|
||||||
: QObject(parent), m_mainWindow(mainWindow)
|
: QObject(parent), m_mainWindow(mainWindow)
|
||||||
{}
|
{
|
||||||
|
m_notifyTimer = new QTimer(this);
|
||||||
|
m_notifyTimer->setSingleShot(true);
|
||||||
|
m_notifyTimer->setInterval(100);
|
||||||
|
connect(m_notifyTimer, &QTimer::timeout, this, [this]() {
|
||||||
|
if (m_client && m_initialized)
|
||||||
|
sendNotification("notifications/resources/updated",
|
||||||
|
QJsonObject{{"uri", "project://tree"}});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
McpBridge::~McpBridge() {
|
McpBridge::~McpBridge() {
|
||||||
stop();
|
stop();
|
||||||
@@ -84,15 +95,24 @@ void McpBridge::onNewConnection() {
|
|||||||
void McpBridge::onReadyRead() {
|
void McpBridge::onReadyRead() {
|
||||||
m_readBuffer.append(m_client->readAll());
|
m_readBuffer.append(m_client->readAll());
|
||||||
|
|
||||||
// Newline-delimited JSON framing
|
if (m_readBuffer.size() > kMaxReadBuffer) {
|
||||||
|
qWarning() << "[MCP] Read buffer exceeded 10MB, disconnecting client";
|
||||||
|
m_client->disconnectFromServer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Newline-delimited JSON framing (cursor approach avoids quadratic shifting)
|
||||||
|
int consumed = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
int idx = m_readBuffer.indexOf('\n');
|
int idx = m_readBuffer.indexOf('\n', consumed);
|
||||||
if (idx < 0) break;
|
if (idx < 0) break;
|
||||||
QByteArray line = m_readBuffer.left(idx).trimmed();
|
QByteArray line = m_readBuffer.mid(consumed, idx - consumed).trimmed();
|
||||||
m_readBuffer.remove(0, idx + 1);
|
consumed = idx + 1;
|
||||||
if (!line.isEmpty())
|
if (!line.isEmpty())
|
||||||
processLine(line);
|
processLine(line);
|
||||||
}
|
}
|
||||||
|
if (consumed > 0)
|
||||||
|
m_readBuffer.remove(0, consumed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void McpBridge::onDisconnected() {
|
void McpBridge::onDisconnected() {
|
||||||
@@ -153,6 +173,7 @@ QJsonObject McpBridge::makeTextResult(const QString& text, bool isError) {
|
|||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
void McpBridge::processLine(const QByteArray& line) {
|
void McpBridge::processLine(const QByteArray& line) {
|
||||||
|
try {
|
||||||
qDebug() << "[MCP] <<" << line.trimmed().left(200);
|
qDebug() << "[MCP] <<" << line.trimmed().left(200);
|
||||||
auto doc = QJsonDocument::fromJson(line);
|
auto doc = QJsonDocument::fromJson(line);
|
||||||
if (!doc.isObject()) {
|
if (!doc.isObject()) {
|
||||||
@@ -172,12 +193,10 @@ void McpBridge::processLine(const QByteArray& line) {
|
|||||||
|
|
||||||
if (method == "initialize") {
|
if (method == "initialize") {
|
||||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: client connected"));
|
m_mainWindow->setMcpStatus(QStringLiteral("MCP: client connected"));
|
||||||
QCoreApplication::processEvents();
|
|
||||||
sendJson(handleInitialize(id, req.value("params").toObject()));
|
sendJson(handleInitialize(id, req.value("params").toObject()));
|
||||||
m_mainWindow->clearMcpStatus();
|
m_mainWindow->clearMcpStatus();
|
||||||
} else if (method == "tools/list") {
|
} else if (method == "tools/list") {
|
||||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: tools/list"));
|
m_mainWindow->setMcpStatus(QStringLiteral("MCP: tools/list"));
|
||||||
QCoreApplication::processEvents();
|
|
||||||
sendJson(handleToolsList(id));
|
sendJson(handleToolsList(id));
|
||||||
m_mainWindow->clearMcpStatus();
|
m_mainWindow->clearMcpStatus();
|
||||||
} else if (method == "tools/call") {
|
} else if (method == "tools/call") {
|
||||||
@@ -185,6 +204,14 @@ void McpBridge::processLine(const QByteArray& line) {
|
|||||||
} else {
|
} else {
|
||||||
sendJson(errReply(id, -32601, "Method not found: " + method));
|
sendJson(errReply(id, -32601, "Method not found: " + method));
|
||||||
}
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
qWarning() << "[MCP] Exception:" << e.what();
|
||||||
|
sendJson(errReply(QJsonValue(), -32603,
|
||||||
|
QStringLiteral("Internal error: %1").arg(e.what())));
|
||||||
|
} catch (...) {
|
||||||
|
qWarning() << "[MCP] Unknown exception";
|
||||||
|
sendJson(errReply(QJsonValue(), -32603, "Internal error"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
@@ -476,7 +503,7 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
|||||||
|
|
||||||
// Show tool activity in status bar (with shimmer)
|
// Show tool activity in status bar (with shimmer)
|
||||||
m_mainWindow->setMcpStatus(QStringLiteral("MCP: %1").arg(toolName));
|
m_mainWindow->setMcpStatus(QStringLiteral("MCP: %1").arg(toolName));
|
||||||
QCoreApplication::processEvents(); // paint immediately
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
|
||||||
QJsonObject result;
|
QJsonObject result;
|
||||||
if (toolName == "project.state") result = toolProjectState(args);
|
if (toolName == "project.state") result = toolProjectState(args);
|
||||||
@@ -501,11 +528,15 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
|||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
QString McpBridge::resolvePlaceholder(const QString& ref,
|
QString McpBridge::resolvePlaceholder(const QString& ref,
|
||||||
const QHash<QString, uint64_t>& placeholderMap) {
|
const QHash<QString, uint64_t>& placeholderMap,
|
||||||
|
bool* ok) {
|
||||||
|
if (ok) *ok = true;
|
||||||
if (ref.startsWith('$')) {
|
if (ref.startsWith('$')) {
|
||||||
auto it = placeholderMap.find(ref);
|
auto it = placeholderMap.find(ref);
|
||||||
if (it != placeholderMap.end())
|
if (it != placeholderMap.end())
|
||||||
return QString::number(it.value());
|
return QString::number(it.value());
|
||||||
|
if (ok) *ok = false;
|
||||||
|
return ref; // unresolved placeholder
|
||||||
}
|
}
|
||||||
return ref; // not a placeholder — return as-is
|
return ref; // not a placeholder — return as-is
|
||||||
}
|
}
|
||||||
@@ -514,26 +545,36 @@ QString McpBridge::resolvePlaceholder(const QString& ref,
|
|||||||
// Smart tab resolution
|
// Smart tab resolution
|
||||||
// ════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
MainWindow::TabState* McpBridge::resolveTab(const QJsonObject& args) {
|
MainWindow::TabState* McpBridge::resolveTab(const QJsonObject& args, int* resolvedIndex) {
|
||||||
|
if (resolvedIndex) *resolvedIndex = -1;
|
||||||
|
|
||||||
// 1) Explicit tab index from args
|
// 1) Explicit tab index from args
|
||||||
if (args.contains("tabIndex")) {
|
if (args.contains("tabIndex")) {
|
||||||
int idx = args.value("tabIndex").toInt();
|
int idx = args.value("tabIndex").toInt();
|
||||||
auto* t = m_mainWindow->tabByIndex(idx);
|
auto* t = m_mainWindow->tabByIndex(idx);
|
||||||
if (t) return t;
|
if (t) { if (resolvedIndex) *resolvedIndex = idx; return t; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Active sub-window (user clicked on it)
|
// 2) Active sub-window (user clicked on it)
|
||||||
auto* t = m_mainWindow->activeTab();
|
auto* t = m_mainWindow->activeTab();
|
||||||
if (t) return t;
|
if (t) {
|
||||||
|
if (resolvedIndex) {
|
||||||
|
for (int i = 0; i < m_mainWindow->tabCount(); i++) {
|
||||||
|
if (m_mainWindow->tabByIndex(i) == t) { *resolvedIndex = i; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
// 3) Fall back to first available tab
|
// 3) Fall back to first available tab
|
||||||
if (m_mainWindow->tabCount() > 0) {
|
if (m_mainWindow->tabCount() > 0) {
|
||||||
t = m_mainWindow->tabByIndex(0);
|
t = m_mainWindow->tabByIndex(0);
|
||||||
if (t) return t;
|
if (t) { if (resolvedIndex) *resolvedIndex = 0; return t; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) No tabs at all — auto-create a project
|
// 4) No tabs at all — auto-create a project
|
||||||
m_mainWindow->project_new();
|
m_mainWindow->project_new();
|
||||||
|
if (resolvedIndex) *resolvedIndex = 0;
|
||||||
return m_mainWindow->tabByIndex(0);
|
return m_mainWindow->tabByIndex(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,8 +766,11 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
|||||||
QStringList skippedOps;
|
QStringList skippedOps;
|
||||||
for (int i = 0; i < ops.size(); i++) {
|
for (int i = 0; i < ops.size(); i++) {
|
||||||
// Safety valve: keep paint events flowing for large batches
|
// Safety valve: keep paint events flowing for large batches
|
||||||
if (i % 100 == 0 && ops.size() > 200)
|
if (i % 100 == 0 && ops.size() > 200) {
|
||||||
|
m_mainWindow->setMcpStatus(
|
||||||
|
QStringLiteral("MCP: tree.apply %1/%2").arg(i).arg(ops.size()));
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 5);
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 5);
|
||||||
|
}
|
||||||
|
|
||||||
QJsonObject op = ops[i].toObject();
|
QJsonObject op = ops[i].toObject();
|
||||||
QString opType = op.value("op").toString();
|
QString opType = op.value("op").toString();
|
||||||
@@ -736,15 +780,29 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
|||||||
n.id = placeholders.value(QStringLiteral("$%1").arg(i), tree.reserveId());
|
n.id = placeholders.value(QStringLiteral("$%1").arg(i), tree.reserveId());
|
||||||
n.kind = kindFromString(op.value("kind").toString("Hex64"));
|
n.kind = kindFromString(op.value("kind").toString("Hex64"));
|
||||||
n.name = op.value("name").toString();
|
n.name = op.value("name").toString();
|
||||||
QString pid = resolvePlaceholder(op.value("parentId").toString("0"), placeholders);
|
bool pidOk;
|
||||||
|
QString pid = resolvePlaceholder(op.value("parentId").toString("0"), placeholders, &pidOk);
|
||||||
|
if (!pidOk) {
|
||||||
|
skippedOps.append(QStringLiteral("op[%1]: unresolved placeholder for parentId").arg(i));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
n.parentId = pid.toULongLong();
|
n.parentId = pid.toULongLong();
|
||||||
|
if (n.parentId != 0 && tree.indexOfId(n.parentId) < 0) {
|
||||||
|
skippedOps.append(QStringLiteral("op[%1]: parentId '%2' not found").arg(i).arg(pid));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
n.offset = op.value("offset").toInt(0);
|
n.offset = op.value("offset").toInt(0);
|
||||||
n.structTypeName = op.value("structTypeName").toString();
|
n.structTypeName = op.value("structTypeName").toString();
|
||||||
n.classKeyword = op.value("classKeyword").toString();
|
n.classKeyword = op.value("classKeyword").toString();
|
||||||
n.strLen = op.value("strLen").toInt(64);
|
n.strLen = qBound(1, op.value("strLen").toInt(64), 1000000);
|
||||||
n.elementKind = kindFromString(op.value("elementKind").toString("UInt8"));
|
n.elementKind = kindFromString(op.value("elementKind").toString("UInt8"));
|
||||||
n.arrayLen = op.value("arrayLen").toInt(1);
|
n.arrayLen = qBound(1, op.value("arrayLen").toInt(1), 1000000);
|
||||||
QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders);
|
bool refOk;
|
||||||
|
QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders, &refOk);
|
||||||
|
if (!refOk) {
|
||||||
|
skippedOps.append(QStringLiteral("op[%1]: unresolved placeholder for refId").arg(i));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
n.refId = refStr.toULongLong();
|
n.refId = refStr.toULongLong();
|
||||||
|
|
||||||
// Auto-place: offset -1 means "after last sibling"
|
// Auto-place: offset -1 means "after last sibling"
|
||||||
@@ -870,7 +928,7 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
|||||||
int idx = tree.indexOfId(nid.toULongLong());
|
int idx = tree.indexOfId(nid.toULongLong());
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
NodeKind newElemKind = kindFromString(op.value("elementKind").toString());
|
NodeKind newElemKind = kindFromString(op.value("elementKind").toString());
|
||||||
int newLen = op.value("arrayLen").toInt(1);
|
int newLen = qBound(1, op.value("arrayLen").toInt(1), 1000000);
|
||||||
doc->undoStack.push(new RcxCommand(ctrl,
|
doc->undoStack.push(new RcxCommand(ctrl,
|
||||||
cmd::ChangeArrayMeta{tree.nodes[idx].id,
|
cmd::ChangeArrayMeta{tree.nodes[idx].id,
|
||||||
tree.nodes[idx].elementKind, newElemKind,
|
tree.nodes[idx].elementKind, newElemKind,
|
||||||
@@ -1383,8 +1441,7 @@ QJsonObject McpBridge::toolProcessInfo(const QJsonObject& args) {
|
|||||||
|
|
||||||
void McpBridge::notifyTreeChanged() {
|
void McpBridge::notifyTreeChanged() {
|
||||||
if (!m_client || !m_initialized) return;
|
if (!m_client || !m_initialized) return;
|
||||||
sendNotification("notifications/resources/updated",
|
m_notifyTimer->start(); // debounce 100ms
|
||||||
QJsonObject{{"uri", "project://tree"}});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void McpBridge::notifyDataChanged() {
|
void McpBridge::notifyDataChanged() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ private:
|
|||||||
QByteArray m_readBuffer;
|
QByteArray m_readBuffer;
|
||||||
bool m_initialized = false;
|
bool m_initialized = false;
|
||||||
bool m_slowMode = false;
|
bool m_slowMode = false;
|
||||||
|
QTimer* m_notifyTimer = nullptr;
|
||||||
|
|
||||||
// JSON-RPC plumbing
|
// JSON-RPC plumbing
|
||||||
void onNewConnection();
|
void onNewConnection();
|
||||||
@@ -65,10 +67,11 @@ private:
|
|||||||
// Helpers
|
// Helpers
|
||||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||||
QString resolvePlaceholder(const QString& ref,
|
QString resolvePlaceholder(const QString& ref,
|
||||||
const QHash<QString, uint64_t>& placeholderMap);
|
const QHash<QString, uint64_t>& placeholderMap,
|
||||||
|
bool* ok = nullptr);
|
||||||
|
|
||||||
// Smart tab resolution: tabIndex arg → activeTab → first tab → auto-create
|
// Smart tab resolution: tabIndex arg → activeTab → first tab → auto-create
|
||||||
MainWindow::TabState* resolveTab(const QJsonObject& args);
|
MainWindow::TabState* resolveTab(const QJsonObject& args, int* resolvedIndex = nullptr);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rcx
|
} // namespace rcx
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ public:
|
|||||||
bool isReadable(uint64_t addr, int len) const override {
|
bool isReadable(uint64_t addr, int len) const override {
|
||||||
if (len <= 0) return (len == 0);
|
if (len <= 0) return (len == 0);
|
||||||
uint64_t end = addr + static_cast<uint64_t>(len);
|
uint64_t end = addr + static_cast<uint64_t>(len);
|
||||||
|
if (end < addr) return false; // overflow
|
||||||
for (uint64_t p = addr & kPageMask; p < end; p += kPageSize) {
|
for (uint64_t p = addr & kPageMask; p < end; p += kPageSize) {
|
||||||
if (!m_pages.contains(p)) return false;
|
if (!m_pages.contains(p)) return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -702,7 +702,7 @@ void ScannerPanel::onCellEdited(int row, int col) {
|
|||||||
m_statusLabel->setText(QStringLiteral("Wrote %1 byte%2 to 0x%3")
|
m_statusLabel->setText(QStringLiteral("Wrote %1 byte%2 to 0x%3")
|
||||||
.arg(bytes.size())
|
.arg(bytes.size())
|
||||||
.arg(bytes.size() == 1 ? "" : "s")
|
.arg(bytes.size() == 1 ? "" : "s")
|
||||||
.arg(addr, 0, 16, QLatin1Char('0')).toUpper());
|
.arg(QString::number(addr, 16).toUpper()));
|
||||||
// Re-read and update cache
|
// Re-read and update cache
|
||||||
m_resultTable->blockSignals(true);
|
m_resultTable->blockSignals(true);
|
||||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||||
|
|||||||
@@ -34,15 +34,13 @@ QVector<TypeSuggestion> inferTypes(
|
|||||||
const InferHints& hints = {},
|
const InferHints& hints = {},
|
||||||
int maxResults = 3);
|
int maxResults = 3);
|
||||||
|
|
||||||
// Format top suggestion as short display string (e.g. "ptr64 strong", "float×2 moderate")
|
// Format top suggestion as short type label (e.g. "ptr64", "int32_t×2")
|
||||||
inline QString formatHint(const TypeSuggestion& s) {
|
inline QString formatHint(const TypeSuggestion& s) {
|
||||||
if (s.kinds.isEmpty()) return {};
|
if (s.kinds.isEmpty()) return {};
|
||||||
const char* name = kindMeta(s.kinds[0])->typeName;
|
const char* name = kindMeta(s.kinds[0])->typeName;
|
||||||
QString base = (s.kinds.size() == 1)
|
return (s.kinds.size() == 1)
|
||||||
? QString::fromLatin1(name)
|
? QString::fromLatin1(name)
|
||||||
: QStringLiteral("%1\u00D7%2").arg(QString::fromLatin1(name)).arg(s.kinds.size());
|
: QStringLiteral("%1\u00D7%2").arg(QString::fromLatin1(name)).arg(s.kinds.size());
|
||||||
const char* conf = s.strength >= 3 ? " strong" : " moderate";
|
|
||||||
return base + QLatin1String(conf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Implementation (header-only) ──
|
// ── Implementation (header-only) ──
|
||||||
@@ -258,7 +256,7 @@ inline FeatureResult countInt16Features(uint16_t val,
|
|||||||
int passed = 0, checked = 2;
|
int passed = 0, checked = 2;
|
||||||
int16_t sv = (int16_t)val;
|
int16_t sv = (int16_t)val;
|
||||||
passed += (val != 0) ? 1 : 0;
|
passed += (val != 0) ? 1 : 0;
|
||||||
passed += (sv >= -4096 && sv <= 4096) ? 1 : 0;
|
passed += (sv >= -16384 && sv <= 16384) ? 1 : 0;
|
||||||
|
|
||||||
if (h.sampleCount > 0 && minP && maxP) {
|
if (h.sampleCount > 0 && minP && maxP) {
|
||||||
checked += 2;
|
checked += 2;
|
||||||
@@ -373,7 +371,6 @@ inline void tryWhole1(const uint8_t* data, QVector<Candidate>& out) {
|
|||||||
uint8_t v = data[0];
|
uint8_t v = data[0];
|
||||||
int score = (v == 0 || v == 1) ? 50 : 25;
|
int score = (v == 0 || v == 1) ? 50 : 25;
|
||||||
addCandidate(out, NodeKind::UInt8, score);
|
addCandidate(out, NodeKind::UInt8, score);
|
||||||
if (v <= 1) addCandidate(out, NodeKind::Bool, 60);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Try uniform splits ──
|
// ── Try uniform splits ──
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ inline QStandardItem* makeTypeItem(const Node* node, const NodeTree* tree,
|
|||||||
|
|
||||||
// Full rebuild — used by benchmarks and first build.
|
// Full rebuild — used by benchmarks and first build.
|
||||||
inline void buildProjectExplorer(QStandardItemModel* model,
|
inline void buildProjectExplorer(QStandardItemModel* model,
|
||||||
const QVector<TabInfo>& tabs) {
|
const QVector<TabInfo>& tabs,
|
||||||
|
const QSet<uint64_t>& pinnedIds = {}) {
|
||||||
model->clear();
|
model->clear();
|
||||||
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
model->setHorizontalHeaderLabels({QStringLiteral("Name")});
|
||||||
|
|
||||||
@@ -113,18 +114,32 @@ inline void buildProjectExplorer(QStandardItemModel* model,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& e : types)
|
// Pinned items at the very top, then structs, then enums
|
||||||
|
QVector<Entry> pinned;
|
||||||
|
QVector<Entry> unpinnedTypes, unpinnedEnums;
|
||||||
|
for (const auto& e : types) {
|
||||||
|
if (pinnedIds.contains(e.node->id)) pinned.append(e);
|
||||||
|
else unpinnedTypes.append(e);
|
||||||
|
}
|
||||||
|
for (const auto& e : enums) {
|
||||||
|
if (pinnedIds.contains(e.node->id)) pinned.append(e);
|
||||||
|
else unpinnedEnums.append(e);
|
||||||
|
}
|
||||||
|
for (const auto& e : pinned)
|
||||||
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||||
for (const auto& e : enums)
|
for (const auto& e : unpinnedTypes)
|
||||||
|
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||||
|
for (const auto& e : unpinnedEnums)
|
||||||
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
model->appendRow(makeTypeItem(e.node, e.tree, e.subPtr));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Incremental sync — preserves tree expansion/scroll state.
|
// Incremental sync — preserves tree expansion/scroll state.
|
||||||
inline void syncProjectExplorer(QStandardItemModel* model,
|
inline void syncProjectExplorer(QStandardItemModel* model,
|
||||||
const QVector<TabInfo>& tabs) {
|
const QVector<TabInfo>& tabs,
|
||||||
|
const QSet<uint64_t>& pinnedIds = {}) {
|
||||||
// First call — full build
|
// First call — full build
|
||||||
if (model->rowCount() == 0 && !tabs.isEmpty()) {
|
if (model->rowCount() == 0 && !tabs.isEmpty()) {
|
||||||
buildProjectExplorer(model, tabs);
|
buildProjectExplorer(model, tabs, pinnedIds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,27 +291,39 @@ public:
|
|||||||
QString name = (dashPos > 1) ? fullText.left(dashPos - 1) : fullText;
|
QString name = (dashPos > 1) ? fullText.left(dashPos - 1) : fullText;
|
||||||
QString count = (dashPos > 1) ? fullText.mid(dashPos + 2).trimmed() : QString();
|
QString count = (dashPos > 1) ? fullText.mid(dashPos + 2).trimmed() : QString();
|
||||||
|
|
||||||
|
bool pinned = index.data(Qt::UserRole + 4).toBool();
|
||||||
|
|
||||||
|
// Reserve right side for pin icon + count pill
|
||||||
|
int rightEdge = textRect.right();
|
||||||
if (!count.isEmpty()) {
|
if (!count.isEmpty()) {
|
||||||
int cw = opt.fontMetrics.horizontalAdvance(count) + 10;
|
int cw = opt.fontMetrics.horizontalAdvance(count) + 10;
|
||||||
int ch = opt.fontMetrics.height();
|
int ch = opt.fontMetrics.height();
|
||||||
int cy = textRect.y() + (textRect.height() - ch) / 2;
|
int cy = textRect.y() + (textRect.height() - ch) / 2;
|
||||||
QRect pill(textRect.right() - cw, cy, cw, ch);
|
QRect pill(rightEdge - cw, cy, cw, ch);
|
||||||
// Draw name clipped before pill
|
rightEdge = pill.left() - 2;
|
||||||
if (pill.left() > textRect.left() + 4) {
|
|
||||||
QRect nameRect = textRect;
|
|
||||||
nameRect.setRight(pill.left() - 4);
|
|
||||||
QString elided = opt.fontMetrics.elidedText(name, Qt::ElideRight, nameRect.width());
|
|
||||||
painter->setPen(m_text);
|
|
||||||
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, elided);
|
|
||||||
}
|
|
||||||
painter->setPen(Qt::NoPen);
|
painter->setPen(Qt::NoPen);
|
||||||
painter->setBrush(m_badgeBg);
|
painter->setBrush(m_badgeBg);
|
||||||
painter->drawRect(pill);
|
painter->drawRect(pill);
|
||||||
painter->setPen(m_textMuted);
|
painter->setPen(m_textMuted);
|
||||||
painter->drawText(pill, Qt::AlignCenter, count);
|
painter->drawText(pill, Qt::AlignCenter, count);
|
||||||
} else {
|
}
|
||||||
|
if (pinned) {
|
||||||
|
static const QIcon pinIcon(":/vsicons/pin.svg");
|
||||||
|
int isz = opt.fontMetrics.height() - 2;
|
||||||
|
int iy = textRect.y() + (textRect.height() - isz) / 2;
|
||||||
|
QRect pinRect(rightEdge - isz, iy, isz, isz);
|
||||||
|
pinIcon.paint(painter, pinRect);
|
||||||
|
rightEdge = pinRect.left() - 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw name clipped before right-side elements
|
||||||
|
if (rightEdge > textRect.left() + 4) {
|
||||||
|
QRect nameRect = textRect;
|
||||||
|
nameRect.setRight(rightEdge);
|
||||||
|
QString elided = opt.fontMetrics.elidedText(name, Qt::ElideRight, nameRect.width());
|
||||||
painter->setPen(m_text);
|
painter->setPen(m_text);
|
||||||
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, name);
|
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, elided);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Child: "TypeName fieldName"
|
// Child: "TypeName fieldName"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ static void buildSmallTree(NodeTree& tree) {
|
|||||||
root.name = "root";
|
root.name = "root";
|
||||||
root.parentId = 0;
|
root.parentId = 0;
|
||||||
root.offset = 0;
|
root.offset = 0;
|
||||||
|
root.collapsed = false;
|
||||||
int ri = tree.addNode(root);
|
int ri = tree.addNode(root);
|
||||||
uint64_t rootId = tree.nodes[ri].id;
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
|
|||||||
@@ -1964,6 +1964,7 @@ private slots:
|
|||||||
root.structTypeName = "Chain";
|
root.structTypeName = "Chain";
|
||||||
root.name = "chain";
|
root.name = "chain";
|
||||||
root.parentId = 0;
|
root.parentId = 0;
|
||||||
|
root.collapsed = false;
|
||||||
int ri = tree.addNode(root);
|
int ri = tree.addNode(root);
|
||||||
uint64_t rootId = tree.nodes[ri].id;
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
@@ -1974,6 +1975,7 @@ private slots:
|
|||||||
inner.name = "Inner";
|
inner.name = "Inner";
|
||||||
inner.parentId = 0;
|
inner.parentId = 0;
|
||||||
inner.offset = 300;
|
inner.offset = 300;
|
||||||
|
inner.collapsed = false;
|
||||||
int ii = tree.addNode(inner);
|
int ii = tree.addNode(inner);
|
||||||
uint64_t innerId = tree.nodes[ii].id;
|
uint64_t innerId = tree.nodes[ii].id;
|
||||||
{
|
{
|
||||||
@@ -1990,6 +1992,7 @@ private slots:
|
|||||||
outer.name = "Outer";
|
outer.name = "Outer";
|
||||||
outer.parentId = 0;
|
outer.parentId = 0;
|
||||||
outer.offset = 200;
|
outer.offset = 200;
|
||||||
|
outer.collapsed = false;
|
||||||
int oi = tree.addNode(outer);
|
int oi = tree.addNode(outer);
|
||||||
uint64_t outerId = tree.nodes[oi].id;
|
uint64_t outerId = tree.nodes[oi].id;
|
||||||
{
|
{
|
||||||
@@ -2002,6 +2005,7 @@ private slots:
|
|||||||
p.kind = NodeKind::Pointer64; p.name = "pInner";
|
p.kind = NodeKind::Pointer64; p.name = "pInner";
|
||||||
p.parentId = outerId; p.offset = 8;
|
p.parentId = outerId; p.offset = 8;
|
||||||
p.refId = innerId;
|
p.refId = innerId;
|
||||||
|
p.collapsed = false;
|
||||||
tree.addNode(p);
|
tree.addNode(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2011,6 +2015,7 @@ private slots:
|
|||||||
p.kind = NodeKind::Pointer64; p.name = "pOuter";
|
p.kind = NodeKind::Pointer64; p.name = "pOuter";
|
||||||
p.parentId = rootId; p.offset = 0;
|
p.parentId = rootId; p.offset = 0;
|
||||||
p.refId = outerId;
|
p.refId = outerId;
|
||||||
|
p.collapsed = false;
|
||||||
tree.addNode(p);
|
tree.addNode(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2706,6 +2711,7 @@ private slots:
|
|||||||
sf.offset = 0;
|
sf.offset = 0;
|
||||||
sf.isStatic = true;
|
sf.isStatic = true;
|
||||||
sf.offsetExpr = QStringLiteral("base + 0x10");
|
sf.offsetExpr = QStringLiteral("base + 0x10");
|
||||||
|
sf.collapsed = false;
|
||||||
tree.addNode(sf);
|
tree.addNode(sf);
|
||||||
|
|
||||||
NullProvider prov;
|
NullProvider prov;
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -125,39 +125,27 @@ private slots:
|
|||||||
QVERIFY(r[0].kinds[0] == NodeKind::Int16 || r[0].kinds[0] == NodeKind::UInt16);
|
QVERIFY(r[0].kinds[0] == NodeKind::Int16 || r[0].kinds[0] == NodeKind::UInt16);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hex8: bool-like ──
|
// ── Hex8: uint8 ──
|
||||||
void hex8_bool() {
|
void hex8_uint() {
|
||||||
uint8_t d[1] = {1};
|
uint8_t d[1] = {1};
|
||||||
auto r = inferTypes(d, 1);
|
auto r = inferTypes(d, 1);
|
||||||
QVERIFY(!r.isEmpty());
|
QVERIFY(!r.isEmpty());
|
||||||
bool foundBool = false;
|
QCOMPARE(r[0].kinds[0], NodeKind::UInt8);
|
||||||
for (const auto& s : r)
|
|
||||||
if (s.kinds.size() == 1 && s.kinds[0] == NodeKind::Bool)
|
|
||||||
foundBool = true;
|
|
||||||
QVERIFY(foundBool);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── formatHint ──
|
// ── formatHint ──
|
||||||
void formatHint_strong() {
|
void formatHint_single() {
|
||||||
TypeSuggestion s;
|
TypeSuggestion s;
|
||||||
s.kinds = {NodeKind::Float};
|
s.kinds = {NodeKind::Float};
|
||||||
s.strength = 3;
|
s.strength = 3;
|
||||||
QCOMPARE(formatHint(s), QStringLiteral("float strong"));
|
QCOMPARE(formatHint(s), QStringLiteral("float"));
|
||||||
}
|
|
||||||
void formatHint_moderate() {
|
|
||||||
TypeSuggestion s;
|
|
||||||
s.kinds = {NodeKind::Float};
|
|
||||||
s.strength = 2;
|
|
||||||
QCOMPARE(formatHint(s), QStringLiteral("float moderate"));
|
|
||||||
}
|
}
|
||||||
void formatHint_split() {
|
void formatHint_split() {
|
||||||
TypeSuggestion s;
|
TypeSuggestion s;
|
||||||
s.kinds = {NodeKind::Float, NodeKind::Float};
|
s.kinds = {NodeKind::Float, NodeKind::Float};
|
||||||
s.strength = 3;
|
s.strength = 3;
|
||||||
QString h = formatHint(s);
|
QString h = formatHint(s);
|
||||||
QVERIFY(h.contains("float"));
|
QCOMPARE(h, QStringLiteral("float\u00D72"));
|
||||||
QVERIFY(h.contains("2"));
|
|
||||||
QVERIFY(h.endsWith("strong"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Denormal rejection ──
|
// ── Denormal rejection ──
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user