mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
fix: WinDbg provider stops auto-selecting module, new tabs inherit source
- WinDbg provider no longer picks arbitrary module[0] as name/base (was showing "WS2_32" for kernel dumps). Name is now generic "WinDbg (Live)" / "WinDbg (Dump)", base stays 0 so controller doesn't override user's address. - Added throttled read failure logging to WinDbg provider. - New tabs (File→New Class, workspace right-click) inherit the current tab's source/provider so users don't have to re-attach. - Updated WinDbg provider tests for new behavior.
This commit is contained in:
@@ -372,6 +372,21 @@ if(BUILD_TESTING)
|
|||||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||||
|
|
||||||
|
add_executable(test_source_provider tests/test_source_provider.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}
|
||||||
|
src/resources.qrc)
|
||||||
|
target_include_directories(test_source_provider PRIVATE src third_party/fadec)
|
||||||
|
target_link_libraries(test_source_provider PRIVATE
|
||||||
|
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test ${QT}::Svg
|
||||||
|
QScintilla::QScintilla)
|
||||||
|
if(WIN32)
|
||||||
|
target_link_libraries(test_source_provider PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||||
|
endif()
|
||||||
|
add_test(NAME test_source_provider COMMAND test_source_provider)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||||
@@ -381,6 +396,19 @@ if(BUILD_TESTING)
|
|||||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
add_executable(bench_large_class tests/bench_large_class.cpp
|
||||||
|
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
|
||||||
|
src/providerregistry.cpp
|
||||||
|
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
||||||
|
target_include_directories(bench_large_class PRIVATE src third_party/fadec)
|
||||||
|
target_link_libraries(bench_large_class PRIVATE
|
||||||
|
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
|
||||||
|
QScintilla::QScintilla)
|
||||||
|
if(WIN32)
|
||||||
|
target_link_libraries(bench_large_class PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
|
||||||
|
endif()
|
||||||
|
add_test(NAME bench_large_class COMMAND bench_large_class)
|
||||||
|
|
||||||
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
# Deploy Qt runtime DLLs for tests (run windeployqt on a representative test exe
|
||||||
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
# that links the broadest set of Qt modules; all test exes share the same output dir)
|
||||||
if(TARGET ${QT}::windeployqt)
|
if(TARGET ${QT}::windeployqt)
|
||||||
|
|||||||
@@ -197,53 +197,15 @@ void WinDbgMemoryProvider::querySessionInfo()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_symbols) {
|
// WinDbg provides access to the entire virtual address space.
|
||||||
ULONG numModules = 0, numUnloaded = 0;
|
// Do NOT auto-select a module as base — let the user set their
|
||||||
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
|
// own base address. m_base stays 0 so the controller won't
|
||||||
qDebug() << "[WinDbg] GetNumberModules hr=" << Qt::hex << (unsigned long)hr
|
// override tree.baseAddress.
|
||||||
<< "loaded=" << numModules << "unloaded=" << numUnloaded;
|
m_name = m_isLive ? QStringLiteral("WinDbg (Live)")
|
||||||
if (SUCCEEDED(hr) && numModules > 0) {
|
: QStringLiteral("WinDbg (Dump)");
|
||||||
char modName[256] = {};
|
|
||||||
ULONG modSize = 0;
|
|
||||||
hr = m_symbols->GetModuleNames(0, 0, nullptr, 0, nullptr,
|
|
||||||
modName, sizeof(modName), &modSize,
|
|
||||||
nullptr, 0, nullptr);
|
|
||||||
if (SUCCEEDED(hr) && modSize > 0)
|
|
||||||
m_name = QString::fromUtf8(modName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_name.isEmpty())
|
|
||||||
m_name = m_isLive ? QStringLiteral("DbgEng (Live)") : QStringLiteral("DbgEng (Dump)");
|
|
||||||
|
|
||||||
if (m_symbols) {
|
|
||||||
ULONG numModules = 0, numUnloaded = 0;
|
|
||||||
hr = m_symbols->GetNumberModules(&numModules, &numUnloaded);
|
|
||||||
if (SUCCEEDED(hr) && numModules > 0) {
|
|
||||||
ULONG64 moduleBase = 0;
|
|
||||||
hr = m_symbols->GetModuleByIndex(0, &moduleBase);
|
|
||||||
qDebug() << "[WinDbg] Module 0 base=" << Qt::hex << moduleBase;
|
|
||||||
if (SUCCEEDED(hr))
|
|
||||||
m_base = moduleBase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_base && m_dataSpaces) {
|
|
||||||
uint8_t probe[2] = {};
|
|
||||||
ULONG got = 0;
|
|
||||||
hr = m_dataSpaces->ReadVirtual(m_base, probe, 2, &got);
|
|
||||||
qDebug() << "[WinDbg] Probe read at" << Qt::hex << m_base
|
|
||||||
<< "hr=" << (unsigned long)hr << "got=" << got
|
|
||||||
<< "bytes:" << (int)probe[0] << (int)probe[1];
|
|
||||||
if (FAILED(hr) || got == 0) {
|
|
||||||
qWarning() << "[WinDbg] Probe read FAILED — cleaning up";
|
|
||||||
cleanup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
qDebug() << "[WinDbg] Ready. name=" << m_name
|
qDebug() << "[WinDbg] Ready. name=" << m_name
|
||||||
<< "base=" << Qt::hex << m_base << "isLive=" << m_isLive;
|
<< "isLive=" << m_isLive;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,8 +267,18 @@ bool WinDbgMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
|||||||
dispatchToOwner([&]() {
|
dispatchToOwner([&]() {
|
||||||
ULONG bytesRead = 0;
|
ULONG bytesRead = 0;
|
||||||
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
|
HRESULT hr = m_dataSpaces->ReadVirtual(addr, buf, (ULONG)len, &bytesRead);
|
||||||
if (FAILED(hr) || (int)bytesRead < len)
|
if (SUCCEEDED(hr) && (int)bytesRead >= len) {
|
||||||
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
result = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Partial or failed read — zero-fill remainder and log
|
||||||
|
memset((char*)buf + bytesRead, 0, len - bytesRead);
|
||||||
|
++m_readFailCount;
|
||||||
|
if (m_readFailCount <= 5 || (m_readFailCount % 100) == 0)
|
||||||
|
qDebug() << "[WinDbg] ReadVirtual FAILED addr=0x" << Qt::hex << addr
|
||||||
|
<< "len=" << Qt::dec << len
|
||||||
|
<< "hr=0x" << Qt::hex << (unsigned long)hr
|
||||||
|
<< "got=" << Qt::dec << bytesRead;
|
||||||
result = bytesRead > 0;
|
result = bytesRead > 0;
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ private:
|
|||||||
bool m_isLive = false;
|
bool m_isLive = false;
|
||||||
bool m_writable = false;
|
bool m_writable = false;
|
||||||
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
|
bool m_isRemote = false; // true when connected via DebugConnect (tcp/npipe)
|
||||||
|
mutable int m_readFailCount = 0;
|
||||||
|
|
||||||
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe
|
// Dedicated thread for DbgEng COM operations. The remote TCP/pipe
|
||||||
// transport is thread-affine — all calls must happen on the thread
|
// transport is thread-affine — all calls must happen on the thread
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
|||||||
lm.lineKind = LineKind::Field;
|
lm.lineKind = LineKind::Field;
|
||||||
lm.nodeKind = node.elementKind;
|
lm.nodeKind = node.elementKind;
|
||||||
lm.isArrayElement = true;
|
lm.isArrayElement = true;
|
||||||
|
lm.arrayElementIdx = i;
|
||||||
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
|
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
|
||||||
lm.offsetAddr = elemAddr;
|
lm.offsetAddr = elemAddr;
|
||||||
lm.ptrBase = state.currentPtrBase;
|
lm.ptrBase = state.currentPtrBase;
|
||||||
|
|||||||
@@ -569,9 +569,9 @@ void RcxController::refresh() {
|
|||||||
// Prune stale selections (nodes removed by undo/redo/delete)
|
// Prune stale selections (nodes removed by undo/redo/delete)
|
||||||
QSet<uint64_t> valid;
|
QSet<uint64_t> valid;
|
||||||
for (uint64_t id : m_selIds) {
|
for (uint64_t id : m_selIds) {
|
||||||
uint64_t nodeId = id & ~kFooterIdBit; // Strip footer bit for lookup
|
uint64_t nodeId = id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
|
||||||
if (m_doc->tree.indexOfId(nodeId) >= 0)
|
if (m_doc->tree.indexOfId(nodeId) >= 0)
|
||||||
valid.insert(id); // Keep original ID (with footer bit if present)
|
valid.insert(id); // Keep original ID (with footer/array bits if present)
|
||||||
}
|
}
|
||||||
m_selIds = valid;
|
m_selIds = valid;
|
||||||
|
|
||||||
@@ -1583,13 +1583,35 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
|||||||
|
|
||||||
// ── Always-available actions ──
|
// ── Always-available actions ──
|
||||||
|
|
||||||
menu.addAction(icon("diff-added.svg"), "Append 128 bytes", [this]() {
|
menu.addAction(icon("diff-added.svg"), "Append bytes...", [this, &menu]() {
|
||||||
|
bool ok;
|
||||||
|
QString input = QInputDialog::getText(menu.parentWidget(),
|
||||||
|
QStringLiteral("Append bytes"),
|
||||||
|
QStringLiteral("Byte count (decimal or 0x hex):"),
|
||||||
|
QLineEdit::Normal, QStringLiteral("128"), &ok);
|
||||||
|
if (!ok || input.trimmed().isEmpty()) return;
|
||||||
|
|
||||||
|
QString trimmed = input.trimmed();
|
||||||
|
int byteCount = 0;
|
||||||
|
if (trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
|
||||||
|
byteCount = trimmed.mid(2).toInt(&ok, 16);
|
||||||
|
else
|
||||||
|
byteCount = trimmed.toInt(&ok, 10);
|
||||||
|
if (!ok || byteCount <= 0) return;
|
||||||
|
|
||||||
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
uint64_t target = m_viewRootId ? m_viewRootId : 0;
|
||||||
|
int hex64Count = byteCount / 8;
|
||||||
|
int remainBytes = byteCount % 8;
|
||||||
|
|
||||||
m_suppressRefresh = true;
|
m_suppressRefresh = true;
|
||||||
m_doc->undoStack.beginMacro(QStringLiteral("Append 128 bytes"));
|
m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
|
||||||
for (int i = 0; i < 16; i++)
|
int idx = 0;
|
||||||
|
for (int i = 0; i < hex64Count; i++, idx++)
|
||||||
insertNode(target, -1, NodeKind::Hex64,
|
insertNode(target, -1, NodeKind::Hex64,
|
||||||
QStringLiteral("field_%1").arg(i));
|
QStringLiteral("field_%1").arg(idx));
|
||||||
|
for (int i = 0; i < remainBytes; i++, idx++)
|
||||||
|
insertNode(target, -1, NodeKind::Hex8,
|
||||||
|
QStringLiteral("field_%1").arg(idx));
|
||||||
m_doc->undoStack.endMacro();
|
m_doc->undoStack.endMacro();
|
||||||
m_suppressRefresh = false;
|
m_suppressRefresh = false;
|
||||||
refresh();
|
refresh();
|
||||||
@@ -1674,11 +1696,17 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
|||||||
bool ctrl = mods & Qt::ControlModifier;
|
bool ctrl = mods & Qt::ControlModifier;
|
||||||
bool shift = mods & Qt::ShiftModifier;
|
bool shift = mods & Qt::ShiftModifier;
|
||||||
|
|
||||||
// Compute effective selection ID: footers use nodeId | kFooterIdBit
|
// Compute effective selection ID:
|
||||||
|
// footers → nodeId | kFooterIdBit
|
||||||
|
// array elements → nodeId | kArrayElemBit | (elemIdx << 48)
|
||||||
|
// everything else → nodeId
|
||||||
auto effectiveId = [this](int ln, uint64_t nid) -> uint64_t {
|
auto effectiveId = [this](int ln, uint64_t nid) -> uint64_t {
|
||||||
if (ln >= 0 && ln < m_lastResult.meta.size() &&
|
if (ln < 0 || ln >= m_lastResult.meta.size()) return nid;
|
||||||
m_lastResult.meta[ln].lineKind == LineKind::Footer)
|
const auto& lm = m_lastResult.meta[ln];
|
||||||
|
if (lm.lineKind == LineKind::Footer)
|
||||||
return nid | kFooterIdBit;
|
return nid | kFooterIdBit;
|
||||||
|
if (lm.isArrayElement && lm.arrayElementIdx >= 0)
|
||||||
|
return makeArrayElemSelId(nid, lm.arrayElementIdx);
|
||||||
return nid;
|
return nid;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1727,8 +1755,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
|
|||||||
|
|
||||||
if (m_selIds.size() == 1) {
|
if (m_selIds.size() == 1) {
|
||||||
uint64_t sid = *m_selIds.begin();
|
uint64_t sid = *m_selIds.begin();
|
||||||
// Strip footer bit for node lookup
|
// Strip footer/array bits for node lookup
|
||||||
int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit);
|
int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask));
|
||||||
if (idx >= 0) emit nodeSelected(idx);
|
if (idx >= 0) emit nodeSelected(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2298,11 +2326,11 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t newBase = provider->base();
|
|
||||||
m_doc->undoStack.clear();
|
m_doc->undoStack.clear();
|
||||||
m_doc->provider = std::move(provider);
|
m_doc->provider = std::move(provider);
|
||||||
m_doc->dataPath.clear();
|
m_doc->dataPath.clear();
|
||||||
m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress;
|
// Don't overwrite baseAddress — caller (e.g. selfTest) already set it.
|
||||||
|
// User-initiated source switches go through selectSource() which does update it.
|
||||||
|
|
||||||
// Re-evaluate stored formula against the new provider
|
// Re-evaluate stored formula against the new provider
|
||||||
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
||||||
@@ -2467,6 +2495,12 @@ void RcxController::clearSources() {
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxController::copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx) {
|
||||||
|
m_savedSources = sources;
|
||||||
|
m_activeSourceIdx = activeIdx;
|
||||||
|
pushSavedSourcesToEditors();
|
||||||
|
}
|
||||||
|
|
||||||
void RcxController::pushSavedSourcesToEditors() {
|
void RcxController::pushSavedSourcesToEditors() {
|
||||||
QVector<SavedSourceDisplay> display;
|
QVector<SavedSourceDisplay> display;
|
||||||
display.reserve(m_savedSources.size());
|
display.reserve(m_savedSources.size());
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ public:
|
|||||||
void switchSource(int idx) { switchToSavedSource(idx); }
|
void switchSource(int idx) { switchToSavedSource(idx); }
|
||||||
void clearSources();
|
void clearSources();
|
||||||
void selectSource(const QString& text);
|
void selectSource(const QString& text);
|
||||||
|
void copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx);
|
||||||
|
|
||||||
// Value tracking toggle (per-tab, off by default)
|
// Value tracking toggle (per-tab, off by default)
|
||||||
bool trackValues() const { return m_trackValues; }
|
bool trackValues() const { return m_trackValues; }
|
||||||
|
|||||||
11
src/core.h
11
src/core.h
@@ -481,6 +481,17 @@ static constexpr uint64_t kCommandRowId = UINT64_MAX;
|
|||||||
static constexpr int kCommandRowLine = 0;
|
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 kArrayElemShift = 48; // bits 48-61 hold element index
|
||||||
|
static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements
|
||||||
|
|
||||||
|
// Encode an array element selection ID: nodeId | kArrayElemBit | (elemIdx << 48)
|
||||||
|
inline uint64_t makeArrayElemSelId(uint64_t nodeId, int elemIdx) {
|
||||||
|
return nodeId | kArrayElemBit | ((uint64_t)(elemIdx & 0x3FFF) << kArrayElemShift);
|
||||||
|
}
|
||||||
|
inline int arrayElemIdxFromSelId(uint64_t selId) {
|
||||||
|
return (int)((selId & kArrayElemMask) >> kArrayElemShift);
|
||||||
|
}
|
||||||
|
|
||||||
struct LineMeta {
|
struct LineMeta {
|
||||||
int nodeIdx = -1;
|
int nodeIdx = -1;
|
||||||
|
|||||||
114
src/editor.cpp
114
src/editor.cpp
@@ -787,6 +787,14 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
m_meta = result.meta;
|
m_meta = result.meta;
|
||||||
m_layout = result.layout;
|
m_layout = result.layout;
|
||||||
|
|
||||||
|
// Build nodeId → display-line index for O(1) hover/selection lookup
|
||||||
|
m_nodeLineIndex.clear();
|
||||||
|
m_nodeLineIndex.reserve(m_meta.size());
|
||||||
|
for (int i = 0; i < m_meta.size(); i++) {
|
||||||
|
if (m_meta[i].nodeId != 0)
|
||||||
|
m_nodeLineIndex[m_meta[i].nodeId].append(i);
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamically resize margin to fit the current hex digit tier
|
// Dynamically resize margin to fit the current hex digit tier
|
||||||
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
|
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
|
||||||
m_sci->setMarginWidth(0, marginSizer);
|
m_sci->setMarginWidth(0, marginSizer);
|
||||||
@@ -835,9 +843,12 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
m_applyingDocument = false;
|
m_applyingDocument = false;
|
||||||
|
|
||||||
// Re-apply hover markers (setText() clears all Scintilla markers).
|
// Re-apply hover markers (setText() clears all Scintilla markers).
|
||||||
|
// Reset m_prevHoveredNodeId so the incremental logic re-adds markers.
|
||||||
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
|
// applyHoverCursor() is NOT called here — it evaluates hitTest() against
|
||||||
// composed text that updateCommandRow() will overwrite. The correct call
|
// composed text that updateCommandRow() will overwrite. The correct call
|
||||||
// happens via applySelectionOverlays() after all text is finalized.
|
// happens via applySelectionOverlays() after all text is finalized.
|
||||||
|
m_prevHoveredNodeId = 0;
|
||||||
|
m_prevHoveredLine = -1;
|
||||||
applyHoverHighlight();
|
applyHoverHighlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1064,18 +1075,33 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_EDITABLE);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORCLEARRANGE, (unsigned long)0, docLen);
|
||||||
|
|
||||||
for (int i = 0; i < m_meta.size(); i++) {
|
// Use index: iterate selected IDs, look up their lines
|
||||||
if (isSyntheticLine(m_meta[i])) continue;
|
for (uint64_t selId : selIds) {
|
||||||
uint64_t nodeId = m_meta[i].nodeId;
|
bool isFooterSel = (selId & kFooterIdBit) != 0;
|
||||||
bool isFooter = (m_meta[i].lineKind == LineKind::Footer);
|
bool isArrayElemSel = (selId & kArrayElemBit) != 0;
|
||||||
|
int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1;
|
||||||
// Footers check for footerId, non-footers check for plain nodeId
|
uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
|
||||||
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId;
|
auto it = m_nodeLineIndex.constFind(nodeId);
|
||||||
if (selIds.contains(checkId)) {
|
if (it == m_nodeLineIndex.constEnd()) continue;
|
||||||
m_sci->markerAdd(i, M_SELECTED);
|
for (int ln : *it) {
|
||||||
m_sci->markerAdd(i, M_ACCENT);
|
if (isSyntheticLine(m_meta[ln])) continue;
|
||||||
|
bool isFooter = (m_meta[ln].lineKind == LineKind::Footer);
|
||||||
|
// Match selection type to line type
|
||||||
|
if (isFooterSel && !isFooter) continue;
|
||||||
|
if (!isFooterSel && isFooter) continue;
|
||||||
|
// Array element: match by element index
|
||||||
|
if (isArrayElemSel) {
|
||||||
|
if (!m_meta[ln].isArrayElement || m_meta[ln].arrayElementIdx != arrayElemIdx)
|
||||||
|
continue;
|
||||||
|
} else if (m_meta[ln].isArrayElement) {
|
||||||
|
// Plain nodeId selection shouldn't highlight individual array elements
|
||||||
|
// (the header line is enough)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_sci->markerAdd(ln, M_SELECTED);
|
||||||
|
m_sci->markerAdd(ln, M_ACCENT);
|
||||||
if (!isFooter)
|
if (!isFooter)
|
||||||
paintEditableSpans(i);
|
paintEditableSpans(ln);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1088,28 +1114,63 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::applyHoverHighlight() {
|
void RcxEditor::applyHoverHighlight() {
|
||||||
m_sci->markerDeleteAll(M_HOVER);
|
uint64_t prevId = m_prevHoveredNodeId;
|
||||||
|
int prevLine = m_prevHoveredLine;
|
||||||
|
m_prevHoveredNodeId = m_hoveredNodeId;
|
||||||
|
m_prevHoveredLine = m_hoveredLine;
|
||||||
|
|
||||||
|
// Fast path: nothing changed (same node AND same line)
|
||||||
|
if (prevId == m_hoveredNodeId && prevLine == m_hoveredLine
|
||||||
|
&& m_hoveredNodeId != 0) return;
|
||||||
|
|
||||||
|
// Remove old hover markers
|
||||||
|
if (prevId != 0) {
|
||||||
|
// Check if old hovered line was a single-line highlight (footer or array element)
|
||||||
|
bool prevSingleLine = (prevLine >= 0 && prevLine < m_meta.size() &&
|
||||||
|
(m_meta[prevLine].lineKind == LineKind::Footer || m_meta[prevLine].isArrayElement));
|
||||||
|
if (prevSingleLine) {
|
||||||
|
m_sci->markerDelete(prevLine, M_HOVER);
|
||||||
|
} else {
|
||||||
|
auto it = m_nodeLineIndex.constFind(prevId);
|
||||||
|
if (it != m_nodeLineIndex.constEnd()) {
|
||||||
|
for (int ln : *it)
|
||||||
|
m_sci->markerDelete(ln, M_HOVER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (m_editState.active) return;
|
if (m_editState.active) return;
|
||||||
if (!m_hoverInside) return;
|
if (!m_hoverInside) return;
|
||||||
if (m_hoveredNodeId == 0) return;
|
if (m_hoveredNodeId == 0) return;
|
||||||
|
|
||||||
// Check if hovered line is a footer - footers highlight independently
|
// Footer and array elements highlight only the specific line
|
||||||
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
bool hoveringFooter = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||||
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
|
m_meta[m_hoveredLine].lineKind == LineKind::Footer);
|
||||||
|
bool hoveringArrayElem = (m_hoveredLine >= 0 && m_hoveredLine < m_meta.size() &&
|
||||||
|
m_meta[m_hoveredLine].isArrayElement);
|
||||||
|
|
||||||
// Check if the hovered item is already selected (using appropriate ID)
|
// Check if the hovered item is already selected (using appropriate ID)
|
||||||
uint64_t checkId = hoveringFooter ? (m_hoveredNodeId | kFooterIdBit) : m_hoveredNodeId;
|
uint64_t checkId;
|
||||||
|
if (hoveringFooter)
|
||||||
|
checkId = m_hoveredNodeId | kFooterIdBit;
|
||||||
|
else if (hoveringArrayElem)
|
||||||
|
checkId = makeArrayElemSelId(m_hoveredNodeId, m_meta[m_hoveredLine].arrayElementIdx);
|
||||||
|
else
|
||||||
|
checkId = m_hoveredNodeId;
|
||||||
if (m_currentSelIds.contains(checkId)) return;
|
if (m_currentSelIds.contains(checkId)) return;
|
||||||
|
|
||||||
if (hoveringFooter) {
|
if (hoveringFooter || hoveringArrayElem) {
|
||||||
// Footer: only highlight this specific line
|
// Single-line highlight for footers and array elements
|
||||||
m_sci->markerAdd(m_hoveredLine, M_HOVER);
|
m_sci->markerAdd(m_hoveredLine, M_HOVER);
|
||||||
} else {
|
} else {
|
||||||
// Non-footer: highlight all matching lines except footers
|
// Non-footer, non-array-element: highlight all lines for this node
|
||||||
for (int i = 0; i < m_meta.size(); i++) {
|
auto it = m_nodeLineIndex.constFind(m_hoveredNodeId);
|
||||||
if (m_meta[i].nodeId == m_hoveredNodeId &&
|
if (it != m_nodeLineIndex.constEnd()) {
|
||||||
m_meta[i].lineKind != LineKind::Footer)
|
for (int ln : *it) {
|
||||||
m_sci->markerAdd(i, M_HOVER);
|
if (m_meta[ln].lineKind != LineKind::Footer &&
|
||||||
|
!m_meta[ln].isArrayElement)
|
||||||
|
m_sci->markerAdd(ln, M_HOVER);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2617,11 +2678,16 @@ void RcxEditor::updateEditableIndicators(int line) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to check if a line's node is selected (handles footer IDs)
|
// Helper to check if a line's node is selected (handles footer/array element IDs)
|
||||||
auto isLineSelected = [this](const LineMeta* lm) -> bool {
|
auto isLineSelected = [this](const LineMeta* lm) -> bool {
|
||||||
if (!lm) return false;
|
if (!lm) return false;
|
||||||
bool isFooter = (lm->lineKind == LineKind::Footer);
|
uint64_t checkId;
|
||||||
uint64_t checkId = isFooter ? (lm->nodeId | kFooterIdBit) : lm->nodeId;
|
if (lm->lineKind == LineKind::Footer)
|
||||||
|
checkId = lm->nodeId | kFooterIdBit;
|
||||||
|
else if (lm->isArrayElement && lm->arrayElementIdx >= 0)
|
||||||
|
checkId = makeArrayElemSelId(lm->nodeId, lm->arrayElementIdx);
|
||||||
|
else
|
||||||
|
checkId = lm->nodeId;
|
||||||
return m_currentSelIds.contains(checkId);
|
return m_currentSelIds.contains(checkId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
|
#include <QHash>
|
||||||
|
|
||||||
class QsciScintilla;
|
class QsciScintilla;
|
||||||
class QsciLexerCPP;
|
class QsciLexerCPP;
|
||||||
@@ -95,8 +96,12 @@ private:
|
|||||||
bool m_hoverInside = false;
|
bool m_hoverInside = false;
|
||||||
uint64_t m_hoveredNodeId = 0;
|
uint64_t m_hoveredNodeId = 0;
|
||||||
int m_hoveredLine = -1;
|
int m_hoveredLine = -1;
|
||||||
|
uint64_t m_prevHoveredNodeId = 0; // for incremental marker update
|
||||||
|
int m_prevHoveredLine = -1; // for incremental marker update
|
||||||
QSet<uint64_t> m_currentSelIds;
|
QSet<uint64_t> m_currentSelIds;
|
||||||
QVector<int> m_hoverSpanLines; // Lines with hover span indicators
|
QVector<int> m_hoverSpanLines; // Lines with hover span indicators
|
||||||
|
// ── nodeId → display-line index (built in applyDocument) ──
|
||||||
|
QHash<uint64_t, QVector<int>> m_nodeLineIndex;
|
||||||
// ── Drag selection ──
|
// ── Drag selection ──
|
||||||
bool m_dragging = false;
|
bool m_dragging = false;
|
||||||
bool m_dragStarted = false; // true once drag threshold exceeded
|
bool m_dragStarted = false; // true once drag threshold exceeded
|
||||||
|
|||||||
15
src/main.cpp
15
src/main.cpp
@@ -1994,7 +1994,22 @@ QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
|
|||||||
|
|
||||||
buildEmptyStruct(doc->tree, classKeyword);
|
buildEmptyStruct(doc->tree, classKeyword);
|
||||||
|
|
||||||
|
// Inherit source from current tab (if any)
|
||||||
|
auto* currentCtrl = activeController();
|
||||||
|
if (currentCtrl && currentCtrl->document()->provider
|
||||||
|
&& currentCtrl->document()->provider->isValid()) {
|
||||||
|
doc->provider = currentCtrl->document()->provider;
|
||||||
|
}
|
||||||
|
|
||||||
auto* sub = createTab(doc);
|
auto* sub = createTab(doc);
|
||||||
|
|
||||||
|
// Copy saved sources to new tab's controller
|
||||||
|
if (currentCtrl && !currentCtrl->savedSources().isEmpty()) {
|
||||||
|
auto& newTab = m_tabs[sub];
|
||||||
|
newTab.ctrl->copySavedSources(currentCtrl->savedSources(),
|
||||||
|
currentCtrl->activeSourceIndex());
|
||||||
|
}
|
||||||
|
|
||||||
rebuildWorkspaceModel();
|
rebuildWorkspaceModel();
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,8 +256,9 @@ private slots:
|
|||||||
{
|
{
|
||||||
WinDbgMemoryProvider prov(m_connString);
|
WinDbgMemoryProvider prov(m_connString);
|
||||||
QVERIFY(prov.isValid());
|
QVERIFY(prov.isValid());
|
||||||
QVERIFY2(prov.base() != 0, "Should have a non-zero base from first module");
|
// WinDbg provider no longer auto-selects a module base — it returns 0
|
||||||
qDebug() << "Base address:" << QString("0x%1").arg(prov.base(), 0, 16);
|
// so the controller doesn't override the user's chosen base address.
|
||||||
|
QCOMPARE(prov.base(), (uint64_t)0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Read: MZ header on main thread ──
|
// ── Read: MZ header on main thread ──
|
||||||
@@ -446,6 +447,139 @@ private slots:
|
|||||||
QCOMPARE(raw->Name(), std::string("WinDbg Memory"));
|
QCOMPARE(raw->Name(), std::string("WinDbg Memory"));
|
||||||
delete raw;
|
delete raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Kernel session tests ──
|
||||||
|
// Requires a WinDbg instance with a kernel dump loaded and
|
||||||
|
// .server tcp:port=5055 running. Skipped automatically if
|
||||||
|
// no server is available. Override with WINDBG_KERNEL_CONN env var.
|
||||||
|
|
||||||
|
void provider_kernel_connect()
|
||||||
|
{
|
||||||
|
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||||
|
"tcp:Port=5055,Server=localhost");
|
||||||
|
if (!canConnect(kernelConn))
|
||||||
|
QSKIP("No kernel debug server available (set WINDBG_KERNEL_CONN)");
|
||||||
|
|
||||||
|
WinDbgMemoryProvider prov(kernelConn);
|
||||||
|
QVERIFY2(prov.isValid(), "Should connect to kernel debug server");
|
||||||
|
QCOMPARE(prov.kind(), QStringLiteral("WinDbg"));
|
||||||
|
|
||||||
|
qDebug() << "Kernel provider name:" << prov.name();
|
||||||
|
qDebug() << "Kernel provider base:" << QString("0x%1").arg(prov.base(), 0, 16);
|
||||||
|
qDebug() << "Kernel provider isLive:" << prov.isLive();
|
||||||
|
|
||||||
|
// Name should not be an arbitrary user-mode DLL
|
||||||
|
QVERIFY2(!prov.name().contains("WS2_32", Qt::CaseInsensitive),
|
||||||
|
qPrintable("Name should not be 'WS2_32', got: " + prov.name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_kernel_read_base()
|
||||||
|
{
|
||||||
|
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||||
|
"tcp:Port=5055,Server=localhost");
|
||||||
|
if (!canConnect(kernelConn))
|
||||||
|
QSKIP("No kernel debug server available");
|
||||||
|
|
||||||
|
WinDbgMemoryProvider prov(kernelConn);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
|
||||||
|
// Provider no longer auto-selects a base. Use a known kernel address
|
||||||
|
// from env, or skip.
|
||||||
|
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||||
|
if (addrStr.isEmpty())
|
||||||
|
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
||||||
|
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
|
||||||
|
|
||||||
|
uint8_t buf[16] = {};
|
||||||
|
ok = prov.read(addr, buf, 16);
|
||||||
|
QVERIFY2(ok, "Should read from kernel address");
|
||||||
|
|
||||||
|
bool allZero = true;
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
if (buf[i] != 0) { allZero = false; break; }
|
||||||
|
}
|
||||||
|
QVERIFY2(!allZero, "Kernel read returned all zeros");
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_kernel_read_high_address()
|
||||||
|
{
|
||||||
|
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||||
|
"tcp:Port=5055,Server=localhost");
|
||||||
|
if (!canConnect(kernelConn))
|
||||||
|
QSKIP("No kernel debug server available");
|
||||||
|
|
||||||
|
WinDbgMemoryProvider prov(kernelConn);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
|
||||||
|
// Use env var for a specific kernel address (e.g. _EPROCESS),
|
||||||
|
// otherwise fall back to the provider's base.
|
||||||
|
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||||
|
uint64_t addr = 0;
|
||||||
|
if (!addrStr.isEmpty()) {
|
||||||
|
bool ok = false;
|
||||||
|
addr = addrStr.toULongLong(&ok, 16);
|
||||||
|
if (!ok) addr = 0;
|
||||||
|
}
|
||||||
|
if (addr == 0) addr = prov.base();
|
||||||
|
|
||||||
|
uint8_t buf[64] = {};
|
||||||
|
bool ok = prov.read(addr, buf, 64);
|
||||||
|
QVERIFY2(ok, qPrintable(QString("Should read kernel addr 0x%1")
|
||||||
|
.arg(addr, 0, 16)));
|
||||||
|
|
||||||
|
bool allZero = true;
|
||||||
|
for (int i = 0; i < 64; ++i) {
|
||||||
|
if (buf[i] != 0) { allZero = false; break; }
|
||||||
|
}
|
||||||
|
QVERIFY2(!allZero, "Kernel high-address read returned all zeros");
|
||||||
|
|
||||||
|
qDebug() << "Read 64 bytes at" << QString("0x%1").arg(addr, 0, 16)
|
||||||
|
<< "first 8:" << QString("%1 %2 %3 %4 %5 %6 %7 %8")
|
||||||
|
.arg(buf[0], 2, 16, QChar('0'))
|
||||||
|
.arg(buf[1], 2, 16, QChar('0'))
|
||||||
|
.arg(buf[2], 2, 16, QChar('0'))
|
||||||
|
.arg(buf[3], 2, 16, QChar('0'))
|
||||||
|
.arg(buf[4], 2, 16, QChar('0'))
|
||||||
|
.arg(buf[5], 2, 16, QChar('0'))
|
||||||
|
.arg(buf[6], 2, 16, QChar('0'))
|
||||||
|
.arg(buf[7], 2, 16, QChar('0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
void provider_kernel_read_backgroundThread()
|
||||||
|
{
|
||||||
|
QString kernelConn = qEnvironmentVariable("WINDBG_KERNEL_CONN",
|
||||||
|
"tcp:Port=5055,Server=localhost");
|
||||||
|
if (!canConnect(kernelConn))
|
||||||
|
QSKIP("No kernel debug server available");
|
||||||
|
|
||||||
|
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
||||||
|
if (addrStr.isEmpty())
|
||||||
|
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
||||||
|
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
|
||||||
|
|
||||||
|
WinDbgMemoryProvider prov(kernelConn);
|
||||||
|
QVERIFY(prov.isValid());
|
||||||
|
|
||||||
|
// Simulate the controller's async refresh pattern
|
||||||
|
QFuture<QByteArray> future = QtConcurrent::run([&prov, addr]() -> QByteArray {
|
||||||
|
return prov.readBytes(addr, 4096);
|
||||||
|
});
|
||||||
|
future.waitForFinished();
|
||||||
|
QByteArray data = future.result();
|
||||||
|
|
||||||
|
QCOMPARE(data.size(), 4096);
|
||||||
|
bool allZero = true;
|
||||||
|
for (int i = 0; i < data.size(); ++i) {
|
||||||
|
if (data[i] != '\0') { allZero = false; break; }
|
||||||
|
}
|
||||||
|
QVERIFY2(!allZero, "Kernel background read returned all zeros");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_MAIN(TestWinDbgProvider)
|
QTEST_MAIN(TestWinDbgProvider)
|
||||||
|
|||||||
Reference in New Issue
Block a user