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:
IChooseYou
2026-02-23 08:08:46 -07:00
parent 67218d3e48
commit 078a6028f0
11 changed files with 354 additions and 86 deletions

View File

@@ -303,6 +303,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.lineKind = LineKind::Field;
lm.nodeKind = node.elementKind;
lm.isArrayElement = true;
lm.arrayElementIdx = i;
lm.offsetText = fmt::fmtOffsetMargin(elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = elemAddr;
lm.ptrBase = state.currentPtrBase;

View File

@@ -569,9 +569,9 @@ void RcxController::refresh() {
// Prune stale selections (nodes removed by undo/redo/delete)
QSet<uint64_t> valid;
for (uint64_t id : m_selIds) {
uint64_t nodeId = id & ~kFooterIdBit; // Strip footer bit for lookup
uint64_t nodeId = id & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
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;
@@ -1583,13 +1583,35 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
// ── 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;
int hex64Count = byteCount / 8;
int remainBytes = byteCount % 8;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Append 128 bytes"));
for (int i = 0; i < 16; i++)
m_doc->undoStack.beginMacro(QStringLiteral("Append %1 bytes").arg(byteCount));
int idx = 0;
for (int i = 0; i < hex64Count; i++, idx++)
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_suppressRefresh = false;
refresh();
@@ -1674,11 +1696,17 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
bool ctrl = mods & Qt::ControlModifier;
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 {
if (ln >= 0 && ln < m_lastResult.meta.size() &&
m_lastResult.meta[ln].lineKind == LineKind::Footer)
if (ln < 0 || ln >= m_lastResult.meta.size()) return nid;
const auto& lm = m_lastResult.meta[ln];
if (lm.lineKind == LineKind::Footer)
return nid | kFooterIdBit;
if (lm.isArrayElement && lm.arrayElementIdx >= 0)
return makeArrayElemSelId(nid, lm.arrayElementIdx);
return nid;
};
@@ -1727,8 +1755,8 @@ void RcxController::handleNodeClick(RcxEditor* source, int line,
if (m_selIds.size() == 1) {
uint64_t sid = *m_selIds.begin();
// Strip footer bit for node lookup
int idx = m_doc->tree.indexOfId(sid & ~kFooterIdBit);
// Strip footer/array bits for node lookup
int idx = m_doc->tree.indexOfId(sid & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask));
if (idx >= 0) emit nodeSelected(idx);
}
}
@@ -2298,11 +2326,11 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt
return;
}
uint64_t newBase = provider->base();
m_doc->undoStack.clear();
m_doc->provider = std::move(provider);
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
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
@@ -2467,6 +2495,12 @@ void RcxController::clearSources() {
refresh();
}
void RcxController::copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx) {
m_savedSources = sources;
m_activeSourceIdx = activeIdx;
pushSavedSourcesToEditors();
}
void RcxController::pushSavedSourcesToEditors() {
QVector<SavedSourceDisplay> display;
display.reserve(m_savedSources.size());

View File

@@ -131,6 +131,7 @@ public:
void switchSource(int idx) { switchToSavedSource(idx); }
void clearSources();
void selectSource(const QString& text);
void copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx);
// Value tracking toggle (per-tab, off by default)
bool trackValues() const { return m_trackValues; }

View File

@@ -481,6 +481,17 @@ static constexpr uint64_t kCommandRowId = UINT64_MAX;
static constexpr int kCommandRowLine = 0;
static constexpr int kFirstDataLine = 1;
static constexpr uint64_t kFooterIdBit = 0x8000000000000000ULL;
static constexpr uint64_t kArrayElemBit = 0x4000000000000000ULL; // marks array element selection
static constexpr uint64_t kArrayElemShift = 48; // bits 48-61 hold element index
static constexpr uint64_t kArrayElemMask = 0x3FFF000000000000ULL; // 14 bits → max 16383 elements
// 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 {
int nodeIdx = -1;

View File

@@ -787,6 +787,14 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
m_meta = result.meta;
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
QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0'));
m_sci->setMarginWidth(0, marginSizer);
@@ -835,9 +843,12 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
m_applyingDocument = false;
// 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
// composed text that updateCommandRow() will overwrite. The correct call
// happens via applySelectionOverlays() after all text is finalized.
m_prevHoveredNodeId = 0;
m_prevHoveredLine = -1;
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_INDICATORCLEARRANGE, (unsigned long)0, docLen);
for (int i = 0; i < m_meta.size(); i++) {
if (isSyntheticLine(m_meta[i])) continue;
uint64_t nodeId = m_meta[i].nodeId;
bool isFooter = (m_meta[i].lineKind == LineKind::Footer);
// Footers check for footerId, non-footers check for plain nodeId
uint64_t checkId = isFooter ? (nodeId | kFooterIdBit) : nodeId;
if (selIds.contains(checkId)) {
m_sci->markerAdd(i, M_SELECTED);
m_sci->markerAdd(i, M_ACCENT);
// Use index: iterate selected IDs, look up their lines
for (uint64_t selId : selIds) {
bool isFooterSel = (selId & kFooterIdBit) != 0;
bool isArrayElemSel = (selId & kArrayElemBit) != 0;
int arrayElemIdx = isArrayElemSel ? arrayElemIdxFromSelId(selId) : -1;
uint64_t nodeId = selId & ~(kFooterIdBit | kArrayElemBit | kArrayElemMask);
auto it = m_nodeLineIndex.constFind(nodeId);
if (it == m_nodeLineIndex.constEnd()) continue;
for (int ln : *it) {
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)
paintEditableSpans(i);
paintEditableSpans(ln);
}
}
@@ -1088,28 +1114,63 @@ void RcxEditor::applySelectionOverlay(const QSet<uint64_t>& selIds) {
}
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_hoverInside) 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() &&
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)
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 (hoveringFooter) {
// Footer: only highlight this specific line
if (hoveringFooter || hoveringArrayElem) {
// Single-line highlight for footers and array elements
m_sci->markerAdd(m_hoveredLine, M_HOVER);
} else {
// Non-footer: highlight all matching lines except footers
for (int i = 0; i < m_meta.size(); i++) {
if (m_meta[i].nodeId == m_hoveredNodeId &&
m_meta[i].lineKind != LineKind::Footer)
m_sci->markerAdd(i, M_HOVER);
// Non-footer, non-array-element: highlight all lines for this node
auto it = m_nodeLineIndex.constFind(m_hoveredNodeId);
if (it != m_nodeLineIndex.constEnd()) {
for (int ln : *it) {
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;
}
// 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 {
if (!lm) return false;
bool isFooter = (lm->lineKind == LineKind::Footer);
uint64_t checkId = isFooter ? (lm->nodeId | kFooterIdBit) : lm->nodeId;
uint64_t checkId;
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);
};

View File

@@ -4,6 +4,7 @@
#include <QWidget>
#include <QSet>
#include <QPoint>
#include <QHash>
class QsciScintilla;
class QsciLexerCPP;
@@ -95,8 +96,12 @@ private:
bool m_hoverInside = false;
uint64_t m_hoveredNodeId = 0;
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;
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 ──
bool m_dragging = false;
bool m_dragStarted = false; // true once drag threshold exceeded

View File

@@ -1994,7 +1994,22 @@ QMdiSubWindow* MainWindow::project_new(const QString& 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);
// 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();
return sub;
}