mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: value history heatmap, write-fail guard, crash handler hardening
- Value history ring buffer (10 slots) tracks per-node change frequency - Three-level heatmap: cold (blue), warm (amber), hot (red) via theme - Heat persists indefinitely (no fade) — shows analysis history - Calltip on hover shows previous values list - Old themes auto-derive heat colors from existing palette - Write failures no longer apply optimistic visual updates - Crash handler: re-entrancy guard, context dump before risky APIs
This commit is contained in:
@@ -633,6 +633,52 @@ void RcxController::refresh() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update value history and compute heat levels
|
||||||
|
// Use the snapshot provider if active; skip entirely if no valid provider
|
||||||
|
{
|
||||||
|
const Provider* prov = nullptr;
|
||||||
|
if (m_snapshotProv)
|
||||||
|
prov = m_snapshotProv.get();
|
||||||
|
else if (m_doc->provider && m_doc->provider->isValid())
|
||||||
|
prov = m_doc->provider.get();
|
||||||
|
|
||||||
|
if (prov) {
|
||||||
|
for (auto& lm : m_lastResult.meta) {
|
||||||
|
if (lm.nodeIdx < 0 || lm.nodeIdx >= m_doc->tree.nodes.size()) continue;
|
||||||
|
if (isSyntheticLine(lm) || lm.isContinuation) continue;
|
||||||
|
if (lm.lineKind != LineKind::Field) continue;
|
||||||
|
|
||||||
|
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
|
||||||
|
// Skip containers — they don't have scalar values
|
||||||
|
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue;
|
||||||
|
// Skip hex preview nodes — they show raw bytes, not a single value
|
||||||
|
if (isHexPreview(node.kind)) continue;
|
||||||
|
|
||||||
|
int64_t nodeOff = m_doc->tree.computeOffset(lm.nodeIdx);
|
||||||
|
uint64_t addr = m_doc->tree.baseAddress + static_cast<uint64_t>(nodeOff);
|
||||||
|
int sz = node.byteSize();
|
||||||
|
if (sz <= 0 || !prov->isReadable(addr, sz)) continue;
|
||||||
|
|
||||||
|
QString val = fmt::readValue(node, *prov, addr, lm.subLine);
|
||||||
|
if (!val.isEmpty()) {
|
||||||
|
m_valueHistory[lm.nodeId].record(val);
|
||||||
|
lm.heatLevel = m_valueHistory[lm.nodeId].heatLevel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply persisted heat levels even when provider is unavailable
|
||||||
|
if (!prov) {
|
||||||
|
for (auto& lm : m_lastResult.meta) {
|
||||||
|
if (lm.nodeId != 0) {
|
||||||
|
auto it = m_valueHistory.find(lm.nodeId);
|
||||||
|
if (it != m_valueHistory.end())
|
||||||
|
lm.heatLevel = it->heatLevel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -656,6 +702,7 @@ void RcxController::refresh() {
|
|||||||
|
|
||||||
for (auto* editor : m_editors) {
|
for (auto* editor : m_editors) {
|
||||||
editor->setCustomTypeNames(customTypes);
|
editor->setCustomTypeNames(customTypes);
|
||||||
|
editor->setValueHistoryRef(&m_valueHistory);
|
||||||
ViewState vs = editor->saveViewState();
|
ViewState vs = editor->saveViewState();
|
||||||
editor->applyDocument(m_lastResult);
|
editor->applyDocument(m_lastResult);
|
||||||
editor->restoreViewState(vs);
|
editor->restoreViewState(vs);
|
||||||
@@ -914,11 +961,13 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
int ai = tree.indexOfId(adj.nodeId);
|
int ai = tree.indexOfId(adj.nodeId);
|
||||||
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
if (ai >= 0) tree.nodes[ai].offset = adj.newOffset;
|
||||||
}
|
}
|
||||||
// Remove nodes
|
// Remove nodes and their value history
|
||||||
QVector<int> indices = tree.subtreeIndices(c.nodeId);
|
QVector<int> indices = tree.subtreeIndices(c.nodeId);
|
||||||
std::sort(indices.begin(), indices.end(), std::greater<int>());
|
std::sort(indices.begin(), indices.end(), std::greater<int>());
|
||||||
for (int idx : indices)
|
for (int idx : indices) {
|
||||||
|
m_valueHistory.remove(tree.nodes[idx].id);
|
||||||
tree.nodes.remove(idx);
|
tree.nodes.remove(idx);
|
||||||
|
}
|
||||||
tree.invalidateIdCache();
|
tree.invalidateIdCache();
|
||||||
}
|
}
|
||||||
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
|
} else if constexpr (std::is_same_v<T, cmd::ChangeBase>) {
|
||||||
@@ -932,11 +981,14 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
|
|||||||
resetSnapshot();
|
resetSnapshot();
|
||||||
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
|
} else if constexpr (std::is_same_v<T, cmd::WriteBytes>) {
|
||||||
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
const QByteArray& bytes = isUndo ? c.oldBytes : c.newBytes;
|
||||||
if (!m_doc->provider->writeBytes(c.addr, bytes))
|
// Write through snapshot (patches pages only on success) or provider directly.
|
||||||
|
// If write fails, the snapshot is NOT patched, so the next compose shows the
|
||||||
|
// real unchanged value — no optimistic visual leak.
|
||||||
|
bool ok = m_snapshotProv
|
||||||
|
? m_snapshotProv->write(c.addr, bytes.constData(), bytes.size())
|
||||||
|
: m_doc->provider->writeBytes(c.addr, bytes);
|
||||||
|
if (!ok)
|
||||||
qWarning() << "WriteBytes failed at address" << QString::number(c.addr, 16);
|
qWarning() << "WriteBytes failed at address" << QString::number(c.addr, 16);
|
||||||
// Patch snapshot so compose sees the new value immediately
|
|
||||||
if (m_snapshotProv)
|
|
||||||
m_snapshotProv->patchPages(c.addr, bytes.constData(), bytes.size());
|
|
||||||
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
|
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
|
||||||
int idx = tree.indexOfId(c.nodeId);
|
int idx = tree.indexOfId(c.nodeId);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
@@ -1019,8 +1071,21 @@ void RcxController::setNodeValue(int nodeIdx, int subLine, const QString& text,
|
|||||||
// Validate write range before pushing command
|
// Validate write range before pushing command
|
||||||
if (!m_doc->provider->isReadable(addr, writeSize)) return;
|
if (!m_doc->provider->isReadable(addr, writeSize)) return;
|
||||||
|
|
||||||
|
// Read old bytes before writing (for undo)
|
||||||
QByteArray oldBytes = m_doc->provider->readBytes(addr, writeSize);
|
QByteArray oldBytes = m_doc->provider->readBytes(addr, writeSize);
|
||||||
|
|
||||||
|
// Test the write first — don't push a command that will silently fail.
|
||||||
|
// This prevents optimistic visual updates for read-only providers.
|
||||||
|
bool writeOk = m_snapshotProv
|
||||||
|
? m_snapshotProv->write(addr, newBytes.constData(), newBytes.size())
|
||||||
|
: m_doc->provider->writeBytes(addr, newBytes);
|
||||||
|
if (!writeOk) {
|
||||||
|
qWarning() << "Write failed at address" << QString::number(addr, 16);
|
||||||
|
refresh(); // refresh to show the real unchanged value
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write succeeded — push undo command (redo will write again, which is harmless)
|
||||||
m_doc->undoStack.push(new RcxCommand(this,
|
m_doc->undoStack.push(new RcxCommand(this,
|
||||||
cmd::WriteBytes{addr, oldBytes, newBytes}));
|
cmd::WriteBytes{addr, oldBytes, newBytes}));
|
||||||
}
|
}
|
||||||
@@ -2194,6 +2259,7 @@ void RcxController::resetSnapshot() {
|
|||||||
m_snapshotProv.reset();
|
m_snapshotProv.reset();
|
||||||
m_prevPages.clear();
|
m_prevPages.clear();
|
||||||
m_changedOffsets.clear();
|
m_changedOffsets.clear();
|
||||||
|
m_valueHistory.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxController::handleMarginClick(RcxEditor* editor, int margin,
|
void RcxController::handleMarginClick(RcxEditor* editor, int margin,
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ private:
|
|||||||
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
||||||
PageMap m_prevPages;
|
PageMap m_prevPages;
|
||||||
QSet<int64_t> m_changedOffsets;
|
QSet<int64_t> m_changedOffsets;
|
||||||
|
QHash<uint64_t, ValueHistory> m_valueHistory;
|
||||||
uint64_t m_refreshGen = 0;
|
uint64_t m_refreshGen = 0;
|
||||||
uint64_t m_readGen = 0;
|
uint64_t m_readGen = 0;
|
||||||
bool m_readInFlight = false;
|
bool m_readInFlight = false;
|
||||||
|
|||||||
45
src/core.h
45
src/core.h
@@ -8,6 +8,7 @@
|
|||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
|
||||||
@@ -405,6 +406,49 @@ struct NodeTree {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Value History (ring buffer for heatmap) ──
|
||||||
|
|
||||||
|
struct ValueHistory {
|
||||||
|
static constexpr int kCapacity = 10;
|
||||||
|
std::array<QString, kCapacity> values;
|
||||||
|
int count = 0; // total unique values recorded
|
||||||
|
int head = 0; // next write position in ring
|
||||||
|
|
||||||
|
void record(const QString& v) {
|
||||||
|
if (count > 0) {
|
||||||
|
int last = (head + kCapacity - 1) % kCapacity;
|
||||||
|
if (values[last] == v) return; // no change
|
||||||
|
}
|
||||||
|
values[head] = v;
|
||||||
|
head = (head + 1) % kCapacity;
|
||||||
|
if (count < INT_MAX) count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int uniqueCount() const { return qMin(count, kCapacity); }
|
||||||
|
|
||||||
|
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
|
||||||
|
int heatLevel() const {
|
||||||
|
if (count <= 1) return 0;
|
||||||
|
if (count == 2) return 1;
|
||||||
|
if (count <= 4) return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString last() const {
|
||||||
|
if (count == 0) return {};
|
||||||
|
return values[(head + kCapacity - 1) % kCapacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate from oldest to newest (up to uniqueCount entries)
|
||||||
|
template<typename Fn>
|
||||||
|
void forEach(Fn&& fn) const {
|
||||||
|
int n = uniqueCount();
|
||||||
|
int start = (head + kCapacity - n) % kCapacity;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
fn(values[(start + i) % kCapacity]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ── LineMeta ──
|
// ── LineMeta ──
|
||||||
|
|
||||||
enum class LineKind : uint8_t {
|
enum class LineKind : uint8_t {
|
||||||
@@ -439,6 +483,7 @@ struct LineMeta {
|
|||||||
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
|
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
|
||||||
uint32_t markerMask = 0;
|
uint32_t markerMask = 0;
|
||||||
bool dataChanged = false; // true if any byte in this node changed since last refresh
|
bool dataChanged = false; // true if any byte in this node changed since last refresh
|
||||||
|
int heatLevel = 0; // 0=static, 1=cold, 2=warm, 3=hot (from ValueHistory)
|
||||||
QVector<int> changedByteIndices; // Hex preview: which byte indices (0-based) changed on this line
|
QVector<int> changedByteIndices; // Hex preview: which byte indices (0-based) changed on this line
|
||||||
int lineByteCount = 0; // Hex preview: actual data byte count on this line
|
int lineByteCount = 0; // Hex preview: actual data byte count on this line
|
||||||
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
int effectiveTypeW = 14; // Per-line type column width used for rendering
|
||||||
|
|||||||
112
src/editor.cpp
112
src/editor.cpp
@@ -24,10 +24,12 @@ static constexpr int IND_HEX_DIM = 9;
|
|||||||
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
||||||
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like)
|
||||||
static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans
|
static constexpr int IND_CMD_PILL = 12; // Rounded chip behind command row spans
|
||||||
static constexpr int IND_DATA_CHANGED = 13; // Amber text for changed data values
|
static constexpr int IND_HEAT_COLD = 13; // Heatmap level 1 (changed once)
|
||||||
static constexpr int IND_CLASS_NAME = 14; // Teal text for root class name
|
static constexpr int IND_CLASS_NAME = 14; // Teal text for root class name
|
||||||
static constexpr int IND_HINT_GREEN = 15; // Green text for hint/comment text
|
static constexpr int IND_HINT_GREEN = 15; // Green text for hint/comment text
|
||||||
static constexpr int IND_LOCAL_OFF = 16; // Dim text for inline local offset in relative mode
|
static constexpr int IND_LOCAL_OFF = 16; // Dim text for inline local offset in relative mode
|
||||||
|
static constexpr int IND_HEAT_WARM = 17; // Heatmap level 2 (moderate changes)
|
||||||
|
static constexpr int IND_HEAT_HOT = 18; // Heatmap level 3 (frequent changes)
|
||||||
|
|
||||||
static QString g_fontName = "JetBrains Mono";
|
static QString g_fontName = "JetBrains Mono";
|
||||||
|
|
||||||
@@ -161,9 +163,13 @@ void RcxEditor::setupScintilla() {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETUNDER,
|
||||||
IND_CMD_PILL, (long)1);
|
IND_CMD_PILL, (long)1);
|
||||||
|
|
||||||
// Data-changed indicator
|
// Heatmap indicators (cold / warm / hot)
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
IND_DATA_CHANGED, 17 /*INDIC_TEXTFORE*/);
|
IND_HEAT_COLD, 17 /*INDIC_TEXTFORE*/);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
|
IND_HEAT_WARM, 17 /*INDIC_TEXTFORE*/);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
|
IND_HEAT_HOT, 17 /*INDIC_TEXTFORE*/);
|
||||||
|
|
||||||
// Root class name — type color
|
// Root class name — type color
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
@@ -300,8 +306,13 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
|||||||
IND_HOVER_SPAN, theme.indHoverSpan);
|
IND_HOVER_SPAN, theme.indHoverSpan);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_CMD_PILL, theme.indCmdPill);
|
IND_CMD_PILL, theme.indCmdPill);
|
||||||
|
// Heatmap colors
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_DATA_CHANGED, theme.indDataChanged);
|
IND_HEAT_COLD, theme.indHeatCold);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
|
IND_HEAT_WARM, theme.indHeatWarm);
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
|
IND_HEAT_HOT, theme.indHeatHot);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
IND_CLASS_NAME, theme.syntaxType);
|
IND_CLASS_NAME, theme.syntaxType);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||||
@@ -401,7 +412,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
|||||||
applyMarkers(result.meta);
|
applyMarkers(result.meta);
|
||||||
applyFoldLevels(result.meta);
|
applyFoldLevels(result.meta);
|
||||||
applyHexDimming(result.meta);
|
applyHexDimming(result.meta);
|
||||||
applyDataChangedHighlight(result.meta);
|
applyHeatmapHighlight(result.meta);
|
||||||
applyCommandRowPills();
|
applyCommandRowPills();
|
||||||
|
|
||||||
// Reset hint line - applySelectionOverlay will repaint indicators
|
// Reset hint line - applySelectionOverlay will repaint indicators
|
||||||
@@ -765,35 +776,48 @@ static QString getLineText(QsciScintilla* sci, int line) {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::applyDataChangedHighlight(const QVector<LineMeta>& meta) {
|
void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
|
||||||
for (int i = 0; i < meta.size(); i++) {
|
static constexpr int heatIndicators[] = { IND_HEAT_COLD, IND_HEAT_WARM, IND_HEAT_HOT };
|
||||||
if (!meta[i].dataChanged) continue;
|
|
||||||
if (isSyntheticLine(meta[i])) continue;
|
|
||||||
|
|
||||||
|
for (int i = 0; i < meta.size(); i++) {
|
||||||
const LineMeta& lm = meta[i];
|
const LineMeta& lm = meta[i];
|
||||||
|
if (isSyntheticLine(lm)) continue;
|
||||||
|
|
||||||
|
int heat = lm.heatLevel;
|
||||||
int typeW = lm.effectiveTypeW;
|
int typeW = lm.effectiveTypeW;
|
||||||
int nameW = lm.effectiveNameW;
|
int nameW = lm.effectiveNameW;
|
||||||
|
|
||||||
if (isHexPreview(lm.nodeKind) && !lm.changedByteIndices.isEmpty()) {
|
// For hex preview nodes: use dataChanged + changedByteIndices (per-byte heat)
|
||||||
// Per-byte highlighting in ASCII + hex areas
|
if (isHexPreview(lm.nodeKind) && lm.dataChanged && !lm.changedByteIndices.isEmpty()) {
|
||||||
|
// Hex nodes don't track heatLevel (they're skipped in controller).
|
||||||
|
// Use IND_HEAT_COLD for any changed byte (simple visual feedback).
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
int asciiStart = ind + typeW + kSepWidth;
|
int asciiStart = ind + typeW + kSepWidth;
|
||||||
// ASCII column is padded to nameW (aligned with value column)
|
|
||||||
int hexStart = asciiStart + nameW + kSepWidth;
|
int hexStart = asciiStart + nameW + kSepWidth;
|
||||||
|
|
||||||
for (int byteIdx : lm.changedByteIndices) {
|
for (int byteIdx : lm.changedByteIndices) {
|
||||||
// Highlight in ASCII area (1 char per byte)
|
fillIndicatorCols(IND_HEAT_COLD, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
|
||||||
fillIndicatorCols(IND_DATA_CHANGED, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
|
|
||||||
// Highlight in hex area (2 hex chars per byte at position byteIdx*3)
|
|
||||||
int hexCol = hexStart + byteIdx * 3;
|
int hexCol = hexStart + byteIdx * 3;
|
||||||
fillIndicatorCols(IND_DATA_CHANGED, i, hexCol, hexCol + 2);
|
fillIndicatorCols(IND_HEAT_COLD, i, hexCol, hexCol + 2);
|
||||||
}
|
}
|
||||||
} else {
|
continue;
|
||||||
// Non-hex nodes: highlight entire value span
|
}
|
||||||
QString lineText = getLineText(m_sci, i);
|
|
||||||
ColumnSpan vs = valueSpan(lm, lineText.size(), typeW, nameW);
|
// Non-hex nodes: apply heat-level indicator to value span
|
||||||
if (vs.valid)
|
if (heat <= 0) continue;
|
||||||
fillIndicatorCols(IND_DATA_CHANGED, i, vs.start, vs.end);
|
|
||||||
|
QString lineText = getLineText(m_sci, i);
|
||||||
|
ColumnSpan vs = valueSpan(lm, lineText.size(), typeW, nameW);
|
||||||
|
if (!vs.valid) continue;
|
||||||
|
|
||||||
|
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
|
||||||
|
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
|
||||||
|
fillIndicatorCols(activeInd, i, vs.start, vs.end);
|
||||||
|
|
||||||
|
// Clear the other two heat indicators on this span to avoid overlap
|
||||||
|
for (int hi : heatIndicators) {
|
||||||
|
if (hi != activeInd)
|
||||||
|
clearIndicatorLine(hi, i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2204,8 +2228,13 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse left viewport - set Arrow
|
// Mouse left viewport - set Arrow, cancel calltip
|
||||||
if (!m_hoverInside) {
|
if (!m_hoverInside) {
|
||||||
|
if (m_calltipVisible) {
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_CALLTIPCANCEL);
|
||||||
|
m_calltipVisible = false;
|
||||||
|
m_calltipLine = -1;
|
||||||
|
}
|
||||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2294,6 +2323,43 @@ void RcxEditor::applyHoverCursor() {
|
|||||||
m_hoverSpanLines.append(h.line);
|
m_hoverSpanLines.append(h.line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Value history calltip on hover
|
||||||
|
{
|
||||||
|
bool showCalltip = false;
|
||||||
|
if (m_valueHistory && h.line >= 0 && h.line < m_meta.size() && !m_editState.active) {
|
||||||
|
const LineMeta& lm = m_meta[h.line];
|
||||||
|
if (lm.heatLevel > 0 && lm.nodeId != 0) {
|
||||||
|
auto it = m_valueHistory->find(lm.nodeId);
|
||||||
|
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
|
||||||
|
// Check cursor is over the value span
|
||||||
|
QString lineText = getLineText(m_sci, h.line);
|
||||||
|
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
|
||||||
|
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
|
||||||
|
QString tip = QStringLiteral("Previous Values:");
|
||||||
|
it->forEach([&](const QString& v) {
|
||||||
|
tip += QStringLiteral("\n ") + v;
|
||||||
|
});
|
||||||
|
if (m_calltipLine != h.line) {
|
||||||
|
long pos = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||||
|
(unsigned long)h.line);
|
||||||
|
QByteArray tipUtf8 = tip.toUtf8();
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_CALLTIPSHOW,
|
||||||
|
pos, tipUtf8.constData());
|
||||||
|
m_calltipLine = h.line;
|
||||||
|
m_calltipVisible = true;
|
||||||
|
}
|
||||||
|
showCalltip = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!showCalltip && m_calltipVisible) {
|
||||||
|
m_sci->SendScintilla(QsciScintillaBase::SCI_CALLTIPCANCEL);
|
||||||
|
m_calltipVisible = false;
|
||||||
|
m_calltipLine = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine cursor shape based on interaction type
|
// Determine cursor shape based on interaction type
|
||||||
Qt::CursorShape desired = Qt::ArrowCursor;
|
Qt::CursorShape desired = Qt::ArrowCursor;
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ public:
|
|||||||
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
|
// Custom type names (struct types from the tree) shown in type picker + lexer GlobalClass coloring
|
||||||
QString textWithMargins() const;
|
QString textWithMargins() const;
|
||||||
void setCustomTypeNames(const QStringList& names);
|
void setCustomTypeNames(const QStringList& names);
|
||||||
|
void setValueHistoryRef(const QHash<uint64_t, ValueHistory>* ref) { m_valueHistory = ref; }
|
||||||
|
|
||||||
// Saved sources for quick-switch in source picker
|
// Saved sources for quick-switch in source picker
|
||||||
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
|
||||||
@@ -129,6 +130,11 @@ private:
|
|||||||
// ── Saved sources for quick-switch ──
|
// ── Saved sources for quick-switch ──
|
||||||
QVector<SavedSourceDisplay> m_savedSourceDisplay;
|
QVector<SavedSourceDisplay> m_savedSourceDisplay;
|
||||||
|
|
||||||
|
// ── Value history ref (owned by controller) ──
|
||||||
|
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
|
||||||
|
bool m_calltipVisible = false;
|
||||||
|
int m_calltipLine = -1;
|
||||||
|
|
||||||
// ── Reentrancy guards ──
|
// ── Reentrancy guards ──
|
||||||
bool m_clampingSelection = false;
|
bool m_clampingSelection = false;
|
||||||
bool m_updatingComment = false;
|
bool m_updatingComment = false;
|
||||||
@@ -145,7 +151,7 @@ private:
|
|||||||
void applyMarkers(const QVector<LineMeta>& meta);
|
void applyMarkers(const QVector<LineMeta>& meta);
|
||||||
void applyFoldLevels(const QVector<LineMeta>& meta);
|
void applyFoldLevels(const QVector<LineMeta>& meta);
|
||||||
void applyHexDimming(const QVector<LineMeta>& meta);
|
void applyHexDimming(const QVector<LineMeta>& meta);
|
||||||
void applyDataChangedHighlight(const QVector<LineMeta>& meta);
|
void applyHeatmapHighlight(const QVector<LineMeta>& meta);
|
||||||
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
|
||||||
void applyCommandRowPills();
|
void applyCommandRowPills();
|
||||||
|
|
||||||
|
|||||||
47
src/main.cpp
47
src/main.cpp
@@ -67,29 +67,56 @@ static void setDarkTitleBar(QWidget* widget) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guard flag to prevent re-entrant crash inside the handler
|
||||||
|
static volatile LONG s_inCrashHandler = 0;
|
||||||
|
|
||||||
static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
||||||
|
// Prevent re-entrant crash: if we fault inside the handler, skip the
|
||||||
|
// risky dbghelp work and just terminate with what we already printed.
|
||||||
|
if (InterlockedCompareExchange(&s_inCrashHandler, 1, 0) != 0) {
|
||||||
|
fprintf(stderr, "\n(re-entrant fault inside crash handler — aborting)\n");
|
||||||
|
fflush(stderr);
|
||||||
|
return EXCEPTION_EXECUTE_HANDLER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: always-safe output (no allocations, no complex APIs)
|
||||||
fprintf(stderr, "\n=== UNHANDLED EXCEPTION ===\n");
|
fprintf(stderr, "\n=== UNHANDLED EXCEPTION ===\n");
|
||||||
fprintf(stderr, "Code : 0x%08lX\n", ep->ExceptionRecord->ExceptionCode);
|
fprintf(stderr, "Code : 0x%08lX\n", ep->ExceptionRecord->ExceptionCode);
|
||||||
fprintf(stderr, "Addr : %p\n", ep->ExceptionRecord->ExceptionAddress);
|
fprintf(stderr, "Addr : %p\n", ep->ExceptionRecord->ExceptionAddress);
|
||||||
|
#ifdef _M_X64
|
||||||
|
fprintf(stderr, "RIP : 0x%016llx\n", (unsigned long long)ep->ContextRecord->Rip);
|
||||||
|
fprintf(stderr, "RSP : 0x%016llx\n", (unsigned long long)ep->ContextRecord->Rsp);
|
||||||
|
#else
|
||||||
|
fprintf(stderr, "EIP : 0x%08lx\n", (unsigned long)ep->ContextRecord->Eip);
|
||||||
|
#endif
|
||||||
|
fflush(stderr);
|
||||||
|
|
||||||
|
// Phase 2: attempt symbol resolution + stack walk
|
||||||
|
// Copy context so StackWalk64 can mutate it safely
|
||||||
|
CONTEXT ctxCopy = *ep->ContextRecord;
|
||||||
|
|
||||||
HANDLE process = GetCurrentProcess();
|
HANDLE process = GetCurrentProcess();
|
||||||
HANDLE thread = GetCurrentThread();
|
HANDLE thread = GetCurrentThread();
|
||||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
|
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_FAIL_CRITICAL_ERRORS);
|
||||||
SymInitialize(process, NULL, TRUE);
|
if (!SymInitialize(process, NULL, TRUE)) {
|
||||||
|
fprintf(stderr, "\n(SymInitialize failed — no stack trace available)\n");
|
||||||
|
fprintf(stderr, "=== END CRASH ===\n");
|
||||||
|
fflush(stderr);
|
||||||
|
return EXCEPTION_EXECUTE_HANDLER;
|
||||||
|
}
|
||||||
|
|
||||||
CONTEXT* ctx = ep->ContextRecord;
|
|
||||||
STACKFRAME64 frame = {};
|
STACKFRAME64 frame = {};
|
||||||
DWORD machineType;
|
DWORD machineType;
|
||||||
#ifdef _M_X64
|
#ifdef _M_X64
|
||||||
machineType = IMAGE_FILE_MACHINE_AMD64;
|
machineType = IMAGE_FILE_MACHINE_AMD64;
|
||||||
frame.AddrPC.Offset = ctx->Rip;
|
frame.AddrPC.Offset = ctxCopy.Rip;
|
||||||
frame.AddrFrame.Offset = ctx->Rbp;
|
frame.AddrFrame.Offset = ctxCopy.Rbp;
|
||||||
frame.AddrStack.Offset = ctx->Rsp;
|
frame.AddrStack.Offset = ctxCopy.Rsp;
|
||||||
#else
|
#else
|
||||||
machineType = IMAGE_FILE_MACHINE_I386;
|
machineType = IMAGE_FILE_MACHINE_I386;
|
||||||
frame.AddrPC.Offset = ctx->Eip;
|
frame.AddrPC.Offset = ctxCopy.Eip;
|
||||||
frame.AddrFrame.Offset = ctx->Ebp;
|
frame.AddrFrame.Offset = ctxCopy.Ebp;
|
||||||
frame.AddrStack.Offset = ctx->Esp;
|
frame.AddrStack.Offset = ctxCopy.Esp;
|
||||||
#endif
|
#endif
|
||||||
frame.AddrPC.Mode = AddrModeFlat;
|
frame.AddrPC.Mode = AddrModeFlat;
|
||||||
frame.AddrFrame.Mode = AddrModeFlat;
|
frame.AddrFrame.Mode = AddrModeFlat;
|
||||||
@@ -97,7 +124,7 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) {
|
|||||||
|
|
||||||
fprintf(stderr, "\nStack trace:\n");
|
fprintf(stderr, "\nStack trace:\n");
|
||||||
for (int i = 0; i < 64; i++) {
|
for (int i = 0; i < 64; i++) {
|
||||||
if (!StackWalk64(machineType, process, thread, &frame, ctx,
|
if (!StackWalk64(machineType, process, thread, &frame, &ctxCopy,
|
||||||
NULL, SymFunctionTableAccess64,
|
NULL, SymFunctionTableAccess64,
|
||||||
SymGetModuleBase64, NULL))
|
SymGetModuleBase64, NULL))
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
"indHoverSpan": "#E6B450",
|
"indHoverSpan": "#E6B450",
|
||||||
"indCmdPill": "#2a2a2a",
|
"indCmdPill": "#2a2a2a",
|
||||||
"indDataChanged": "#8fbc7a",
|
"indDataChanged": "#8fbc7a",
|
||||||
|
"indHeatCold": "#569cd6",
|
||||||
|
"indHeatWarm": "#E6B450",
|
||||||
|
"indHeatHot": "#f44747",
|
||||||
"indHintGreen": "#5a8248",
|
"indHintGreen": "#5a8248",
|
||||||
"markerPtr": "#f44747",
|
"markerPtr": "#f44747",
|
||||||
"markerCycle": "#e5a00d",
|
"markerCycle": "#e5a00d",
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
"indHoverSpan": "#b180d7",
|
"indHoverSpan": "#b180d7",
|
||||||
"indCmdPill": "#2d2d30",
|
"indCmdPill": "#2d2d30",
|
||||||
"indDataChanged": "#8fbc7a",
|
"indDataChanged": "#8fbc7a",
|
||||||
|
"indHeatCold": "#569cd6",
|
||||||
|
"indHeatWarm": "#d69d85",
|
||||||
|
"indHeatHot": "#f44747",
|
||||||
"indHintGreen": "#5a8248",
|
"indHintGreen": "#5a8248",
|
||||||
"markerPtr": "#f44747",
|
"markerPtr": "#f44747",
|
||||||
"markerCycle": "#e5a00d",
|
"markerCycle": "#e5a00d",
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
"indHoverSpan": "#AA9565",
|
"indHoverSpan": "#AA9565",
|
||||||
"indCmdPill": "#2a2a2a",
|
"indCmdPill": "#2a2a2a",
|
||||||
"indDataChanged": "#6B959F",
|
"indDataChanged": "#6B959F",
|
||||||
|
"indHeatCold": "#6B959F",
|
||||||
|
"indHeatWarm": "#AA9565",
|
||||||
|
"indHeatHot": "#A05040",
|
||||||
"indHintGreen": "#464646",
|
"indHintGreen": "#464646",
|
||||||
"markerPtr": "#6B3B21",
|
"markerPtr": "#6B3B21",
|
||||||
"markerCycle": "#AA9565",
|
"markerCycle": "#AA9565",
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ const ThemeFieldMeta kThemeFields[] = {
|
|||||||
{"indHoverSpan", "Hover Span", "Indicators", &Theme::indHoverSpan},
|
{"indHoverSpan", "Hover Span", "Indicators", &Theme::indHoverSpan},
|
||||||
{"indCmdPill", "Cmd Pill", "Indicators", &Theme::indCmdPill},
|
{"indCmdPill", "Cmd Pill", "Indicators", &Theme::indCmdPill},
|
||||||
{"indDataChanged","Data Changed", "Indicators", &Theme::indDataChanged},
|
{"indDataChanged","Data Changed", "Indicators", &Theme::indDataChanged},
|
||||||
|
{"indHeatCold", "Heat Cold", "Indicators", &Theme::indHeatCold},
|
||||||
|
{"indHeatWarm", "Heat Warm", "Indicators", &Theme::indHeatWarm},
|
||||||
|
{"indHeatHot", "Heat Hot", "Indicators", &Theme::indHeatHot},
|
||||||
{"indHintGreen", "Hint Green", "Indicators", &Theme::indHintGreen},
|
{"indHintGreen", "Hint Green", "Indicators", &Theme::indHintGreen},
|
||||||
{"markerPtr", "Pointer", "Markers", &Theme::markerPtr},
|
{"markerPtr", "Pointer", "Markers", &Theme::markerPtr},
|
||||||
{"markerCycle", "Cycle", "Markers", &Theme::markerCycle},
|
{"markerCycle", "Cycle", "Markers", &Theme::markerCycle},
|
||||||
@@ -50,6 +53,14 @@ Theme Theme::fromJson(const QJsonObject& o) {
|
|||||||
if (o.contains(kThemeFields[i].key))
|
if (o.contains(kThemeFields[i].key))
|
||||||
t.*kThemeFields[i].ptr = QColor(o[kThemeFields[i].key].toString());
|
t.*kThemeFields[i].ptr = QColor(o[kThemeFields[i].key].toString());
|
||||||
}
|
}
|
||||||
|
// Derive heat colors from the theme's own palette when keys are absent
|
||||||
|
// cold = keyword blue, warm = hover/string amber, hot = marker red
|
||||||
|
if (!t.indHeatCold.isValid())
|
||||||
|
t.indHeatCold = t.syntaxKeyword;
|
||||||
|
if (!t.indHeatWarm.isValid())
|
||||||
|
t.indHeatWarm = t.indHoverSpan.isValid() ? t.indHoverSpan : t.syntaxString;
|
||||||
|
if (!t.indHeatHot.isValid())
|
||||||
|
t.indHeatHot = t.markerPtr;
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ struct Theme {
|
|||||||
// ── Indicators ──
|
// ── Indicators ──
|
||||||
QColor indHoverSpan; // hover link text
|
QColor indHoverSpan; // hover link text
|
||||||
QColor indCmdPill; // command row pill bg
|
QColor indCmdPill; // command row pill bg
|
||||||
QColor indDataChanged; // changed data values
|
QColor indDataChanged; // changed data values (legacy, fallback for old themes)
|
||||||
|
QColor indHeatCold; // heatmap level 1 (changed once)
|
||||||
|
QColor indHeatWarm; // heatmap level 2 (moderate changes)
|
||||||
|
QColor indHeatHot; // heatmap level 3 (frequent changes)
|
||||||
QColor indHintGreen; // comment/hint text
|
QColor indHintGreen; // comment/hint text
|
||||||
|
|
||||||
// ── Markers ──
|
// ── Markers ──
|
||||||
|
|||||||
@@ -583,6 +583,94 @@ private slots:
|
|||||||
QCOMPARE(norm.size(), 1);
|
QCOMPARE(norm.size(), 1);
|
||||||
QVERIFY(norm.contains(rootId));
|
QVERIFY(norm.contains(rootId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── ValueHistory tests ──
|
||||||
|
|
||||||
|
void testValueHistory_empty() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
QCOMPARE(h.heatLevel(), 0);
|
||||||
|
QCOMPARE(h.uniqueCount(), 0);
|
||||||
|
QCOMPARE(h.last(), QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_singleValue() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
h.record("42");
|
||||||
|
QCOMPARE(h.heatLevel(), 0); // only 1 unique → static
|
||||||
|
QCOMPARE(h.uniqueCount(), 1);
|
||||||
|
QCOMPARE(h.last(), QString("42"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_duplicateIgnored() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
h.record("42");
|
||||||
|
h.record("42");
|
||||||
|
h.record("42");
|
||||||
|
QCOMPARE(h.count, 1);
|
||||||
|
QCOMPARE(h.heatLevel(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_heatLevels() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
h.record("a");
|
||||||
|
QCOMPARE(h.heatLevel(), 0); // 1 unique
|
||||||
|
|
||||||
|
h.record("b");
|
||||||
|
QCOMPARE(h.heatLevel(), 1); // 2 unique → cold
|
||||||
|
|
||||||
|
h.record("c");
|
||||||
|
QCOMPARE(h.heatLevel(), 2); // 3 unique → warm
|
||||||
|
|
||||||
|
h.record("d");
|
||||||
|
QCOMPARE(h.heatLevel(), 2); // 4 unique → warm
|
||||||
|
|
||||||
|
h.record("e");
|
||||||
|
QCOMPARE(h.heatLevel(), 3); // 5 unique → hot
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_ringWrap() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
// Fill beyond capacity
|
||||||
|
for (int i = 0; i < 15; i++)
|
||||||
|
h.record(QString::number(i));
|
||||||
|
|
||||||
|
QCOMPARE(h.count, 15);
|
||||||
|
QCOMPARE(h.uniqueCount(), 10); // capped at kCapacity
|
||||||
|
QCOMPARE(h.heatLevel(), 3); // hot
|
||||||
|
QCOMPARE(h.last(), QString("14"));
|
||||||
|
|
||||||
|
// Verify oldest values were pushed out, newest 10 remain
|
||||||
|
QStringList collected;
|
||||||
|
h.forEach([&](const QString& v) { collected.append(v); });
|
||||||
|
QCOMPARE(collected.size(), 10);
|
||||||
|
QCOMPARE(collected.first(), QString("5")); // oldest surviving
|
||||||
|
QCOMPARE(collected.last(), QString("14")); // newest
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_forEach() {
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
h.record("x");
|
||||||
|
h.record("y");
|
||||||
|
h.record("z");
|
||||||
|
|
||||||
|
QStringList items;
|
||||||
|
h.forEach([&](const QString& v) { items.append(v); });
|
||||||
|
QCOMPARE(items.size(), 3);
|
||||||
|
QCOMPARE(items[0], QString("x"));
|
||||||
|
QCOMPARE(items[1], QString("y"));
|
||||||
|
QCOMPARE(items[2], QString("z"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testValueHistory_oscillation() {
|
||||||
|
// Values that oscillate (A → B → A → B) should still count each unique transition
|
||||||
|
rcx::ValueHistory h;
|
||||||
|
h.record("A");
|
||||||
|
h.record("B");
|
||||||
|
h.record("A");
|
||||||
|
h.record("B");
|
||||||
|
QCOMPARE(h.count, 4); // 4 transitions
|
||||||
|
QCOMPARE(h.heatLevel(), 2); // warm (count=4 → 3-4 range)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_MAIN(TestCore)
|
QTEST_MAIN(TestCore)
|
||||||
|
|||||||
Reference in New Issue
Block a user