diff --git a/src/compose.cpp b/src/compose.cpp index 4590a22..766cd2e 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -115,7 +115,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, lm.isContinuation = isCont; lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field; lm.nodeKind = node.kind; - lm.offsetText = fmt::fmtOffsetMargin(absAddr, isCont); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont); lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth); lm.foldLevel = computeFoldLevel(depth, false); @@ -145,7 +145,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.nodeId = node.id; lm.depth = depth; lm.lineKind = LineKind::Field; - lm.offsetText = fmt::fmtOffsetMargin(absAddr, false); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false); lm.nodeKind = node.kind; lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR); lm.foldLevel = computeFoldLevel(depth, false); @@ -162,13 +162,19 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.nodeId = node.id; lm.depth = depth; lm.lineKind = LineKind::Header; - lm.offsetText = fmt::fmtOffsetMargin(absAddr, false); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false); lm.nodeKind = node.kind; lm.foldHead = true; lm.foldCollapsed = node.collapsed; lm.foldLevel = computeFoldLevel(depth, true); lm.markerMask = (1u << M_STRUCT_BG); - state.emitLine(fmt::fmtStructHeader(node, depth), lm); + lm.isRootHeader = (node.parentId == 0); // Root-level struct + + // Root structs show base address, nested structs show normal header + QString headerText = lm.isRootHeader + ? fmt::fmtStructHeaderWithBase(node, depth, tree.baseAddress) + : fmt::fmtStructHeader(node, depth); + state.emitLine(headerText, lm); } if (!node.collapsed) { @@ -215,7 +221,7 @@ void composeNode(ComposeState& state, const NodeTree& tree, lm.nodeId = node.id; lm.depth = depth; lm.lineKind = LineKind::Field; - lm.offsetText = fmt::fmtOffsetMargin(absAddr, false); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false); lm.nodeKind = node.kind; lm.foldHead = true; lm.foldCollapsed = node.collapsed; diff --git a/src/controller.cpp b/src/controller.cpp index 44be30e..9e6e251 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -145,6 +145,58 @@ void RcxController::connectEditor(RcxEditor* editor) { case EditTarget::Value: setNodeValue(nodeIdx, subLine, text); break; + case EditTarget::BaseAddress: { + QString s = text.trimmed(); + // Support simple equations: 0x10+0x4, 0x100-0x10, etc. + uint64_t newBase = 0; + bool ok = true; + int pos = 0; + bool firstTerm = true; + bool adding = true; + + while (pos < s.size() && ok) { + // Skip whitespace + while (pos < s.size() && s[pos].isSpace()) pos++; + if (pos >= s.size()) break; + + // Check for +/- operator (except first term) + if (!firstTerm) { + if (s[pos] == '+') { adding = true; pos++; } + else if (s[pos] == '-') { adding = false; pos++; } + else { ok = false; break; } + while (pos < s.size() && s[pos].isSpace()) pos++; + } + + // Parse hex number (with or without 0x prefix) + int start = pos; + bool hasPrefix = (pos + 1 < s.size() && + s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X')); + if (hasPrefix) pos += 2; + + int numStart = pos; + while (pos < s.size() && (s[pos].isDigit() || + (s[pos] >= 'a' && s[pos] <= 'f') || + (s[pos] >= 'A' && s[pos] <= 'F'))) pos++; + + if (pos == numStart) { ok = false; break; } + + QString numStr = s.mid(numStart, pos - numStart); + uint64_t val = numStr.toULongLong(&ok, 16); + if (!ok) break; + + if (adding) newBase += val; + else newBase -= val; + + firstTerm = false; + } + + if (ok && newBase != m_doc->tree.baseAddress) { + uint64_t oldBase = m_doc->tree.baseAddress; + m_doc->undoStack.push(new RcxCommand(this, + cmd::ChangeBase{oldBase, newBase})); + } + break; + } } // Always refresh to restore canonical text (handles parse failures, no-ops, etc.) refresh(); diff --git a/src/core.h b/src/core.h index 9187c92..1d8619c 100644 --- a/src/core.h +++ b/src/core.h @@ -427,6 +427,7 @@ struct LineMeta { bool foldHead = false; bool foldCollapsed = false; bool isContinuation = false; + bool isRootHeader = false; // true for top-level struct headers (base address editable) LineKind lineKind = LineKind::Field; NodeKind nodeKind = NodeKind::Int32; QString offsetText; @@ -468,15 +469,16 @@ struct ColumnSpan { bool valid = false; }; -enum class EditTarget { Name, Type, Value }; +enum class EditTarget { Name, Type, Value, BaseAddress }; // Column layout constants (shared with format.cpp span computation) -inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line -inline constexpr int kColType = 10; -inline constexpr int kColName = 22; -inline constexpr int kColValue = 32; -inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits -inline constexpr int kSepWidth = 2; +inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line +inline constexpr int kColType = 10; +inline constexpr int kColName = 22; +inline constexpr int kColValue = 32; +inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits +inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address) +inline constexpr int kSepWidth = 2; inline ColumnSpan typeSpanFor(const LineMeta& lm) { if (lm.lineKind != LineKind::Field || lm.isContinuation) return {}; @@ -541,6 +543,33 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength) { return {start, lineLength, start < lineLength}; } +// Base address span (only valid for root struct headers) +// Line format: " - struct Name { base: 0x00400000" +inline ColumnSpan baseAddressSpanFor(const LineMeta& lm, const QString& lineText) { + if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {}; + // Find "base: " after the opening brace + int baseIdx = lineText.indexOf(QStringLiteral("base: ")); + if (baseIdx < 0) return {}; + int startPos = baseIdx + 6; // after "base: " + // Value goes to end of line + int endPos = lineText.size(); + while (endPos > startPos && lineText[endPos-1].isSpace()) + endPos--; + if (endPos <= startPos) return {}; + return {startPos, endPos, true}; +} + +// Full "base: 0x..." span for coloring (includes "base: " prefix) +inline ColumnSpan baseAddressFullSpanFor(const LineMeta& lm, const QString& lineText) { + if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {}; + int baseIdx = lineText.indexOf(QStringLiteral("base: ")); + if (baseIdx < 0) return {}; + int endPos = lineText.size(); + while (endPos > baseIdx && lineText[endPos-1].isSpace()) + endPos--; + return {baseIdx, endPos, true}; +} + // ── ViewState ── struct ViewState { @@ -573,7 +602,9 @@ namespace fmt { const QString& comment = {}); QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation); QString fmtStructHeader(const Node& node, int depth); + QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress); QString fmtStructFooter(const Node& node, int depth, int totalSize = -1); + QString validateBaseAddress(const QString& text); QString indent(int depth); QString readValue(const Node& node, const Provider& prov, uint64_t addr, int subLine); diff --git a/src/editor.cpp b/src/editor.cpp index 6e803ad..0f1ecae 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -21,8 +21,10 @@ static const QColor kBgMargin("#252526"); static const QColor kFgMargin("#858585"); static const QColor kFgMarginDim("#505050"); -static constexpr int IND_EDITABLE = 8; -static constexpr int IND_HEX_DIM = 9; +static constexpr int IND_EDITABLE = 8; +static constexpr int IND_HEX_DIM = 9; +static constexpr int IND_BASE_ADDR = 10; // Green color for base address +static constexpr int IND_HOVER_SPAN = 11; // Blue text on hover (link-like) static QString g_fontName = "Consolas"; @@ -85,6 +87,8 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { if (!m_editState.active) return; if (m_editState.target == EditTarget::Value) QTimer::singleShot(0, this, &RcxEditor::validateEditLive); + if (m_editState.target == EditTarget::Type) + QTimer::singleShot(0, this, &RcxEditor::updateTypeListFilter); }); connect(m_sci, &QsciScintilla::selectionChanged, @@ -129,6 +133,17 @@ void RcxEditor::setupScintilla() { m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, IND_HEX_DIM, QColor("#505050")); + // Base address indicator — green like comments + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, + IND_BASE_ADDR, 17 /*INDIC_TEXTFORE*/); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, + IND_BASE_ADDR, QColor("#6a9955")); + + // Hover span indicator — blue text like a link + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, + IND_HOVER_SPAN, 17 /*INDIC_TEXTFORE*/); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE, + IND_HOVER_SPAN, QColor("#569cd6")); } void RcxEditor::setupLexer() { @@ -277,6 +292,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) { applyMarkers(result.meta); applyFoldLevels(result.meta); applyHexDimming(result.meta); + applyBaseAddressColoring(result.meta); // Reset hint line - applySelectionOverlay will repaint indicators m_hintLine = -1; @@ -468,6 +484,23 @@ static QString getLineText(QsciScintilla* sci, int line) { return text; } +void RcxEditor::applyBaseAddressColoring(const QVector& meta) { + m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_BASE_ADDR); + for (int i = 0; i < meta.size(); i++) { + const LineMeta& lm = meta[i]; + if (!lm.isRootHeader) continue; + QString lineText = getLineText(m_sci, i); + ColumnSpan span = baseAddressFullSpanFor(lm, lineText); + if (!span.valid) continue; + long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, + (unsigned long)i); + long posA = lineStart + span.start; + long posB = lineStart + span.end; + if (posB > posA) + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA); + } +} + // ── Shared inline-edit shutdown ── RcxEditor::EndEditInfo RcxEditor::endInlineEdit() { @@ -557,9 +590,10 @@ bool RcxEditor::resolvedSpanFor(int line, EditTarget t, ColumnSpan s; switch (t) { - case EditTarget::Type: s = typeSpan(*lm); break; - case EditTarget::Name: s = nameSpan(*lm); break; - case EditTarget::Value: s = valueSpan(*lm, textLen); break; + case EditTarget::Type: s = typeSpan(*lm); break; + case EditTarget::Name: s = nameSpan(*lm); break; + case EditTarget::Value: s = valueSpan(*lm, textLen); break; + case EditTarget::BaseAddress: s = baseAddressSpanFor(*lm, lineText); break; } if (!s.valid && t == EditTarget::Name) @@ -623,6 +657,7 @@ static bool hitTestTarget(QsciScintilla* sci, ColumnSpan ts = RcxEditor::typeSpan(lm); ColumnSpan ns = RcxEditor::nameSpan(lm); ColumnSpan vs = RcxEditor::valueSpan(lm, textLen); + ColumnSpan bs = baseAddressSpanFor(lm, lineText); // Base address for root headers if (!ns.valid) ns = headerNameSpan(lm, lineText); @@ -631,7 +666,8 @@ static bool hitTestTarget(QsciScintilla* sci, return s.valid && col >= s.start && col < s.end; }; - if (inSpan(ts)) outTarget = EditTarget::Type; + if (inSpan(bs)) outTarget = EditTarget::BaseAddress; + else if (inSpan(ts)) outTarget = EditTarget::Type; else if (inSpan(ns)) outTarget = EditTarget::Name; else if (inSpan(vs)) outTarget = EditTarget::Value; else return false; @@ -841,38 +877,22 @@ bool RcxEditor::handleNormalKey(QKeyEvent* ke) { // ── Edit mode key handling ── bool RcxEditor::handleEditKey(QKeyEvent* ke) { - bool autocActive = m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCACTIVE); + // User list is handled via userListActivated signal, not here + // SCI_AUTOCACTIVE is for autocomplete, not user lists switch (ke->key()) { case Qt::Key_Return: case Qt::Key_Enter: case Qt::Key_Tab: - if (autocActive && m_editState.target == EditTarget::Type) { - // Extract selected typeName directly from autocomplete - QByteArray buf(256, '\0'); - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCGETCURRENTTEXT, - (unsigned long)256, (void*)buf.data()); - QString selectedType = QString::fromUtf8(buf.constData()); - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL); - - auto info = endInlineEdit(); - emit inlineEditCommitted(info.nodeIdx, info.subLine, EditTarget::Type, selectedType); - return true; - } commitInlineEdit(); return true; case Qt::Key_Escape: - if (autocActive) { - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL); - return true; // close popup, stay in edit mode - } cancelInlineEdit(); return true; case Qt::Key_Up: case Qt::Key_Down: case Qt::Key_PageUp: case Qt::Key_PageDown: - if (autocActive) return false; // let Scintilla navigate list return true; // block line navigation case Qt::Key_Delete: return true; // block to prevent eating trailing content @@ -963,11 +983,17 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1, QColor("#264f78")); - long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, - (unsigned long)line); - long posStart = lineStart + m_editState.spanStart; - long posEnd = posStart + trimmed.toUtf8().size(); - m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posStart, posEnd); + // Use correct UTF-8 position conversion (not lineStart + col!) + m_editState.posStart = posFromCol(m_sci, line, norm.start); + m_editState.posEnd = posFromCol(m_sci, line, norm.end); + + // For Value/BaseAddress: skip 0x prefix in selection (select only the number) + long selStart = m_editState.posStart; + if ((target == EditTarget::Value || target == EditTarget::BaseAddress) && + trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) { + selStart = m_editState.posStart + 2; // Skip "0x" + } + m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, selStart, m_editState.posEnd); // Show initial edit hint in comment column if (target == EditTarget::Value) @@ -998,34 +1024,31 @@ void RcxEditor::clampEditSelection() { int editEnd = editEndCol(); bool isCursor = (selStartLine == selEndLine && selStartCol == selEndCol); + // Don't fight cursor positioning - only clamp actual selections if (isCursor) { - // Cursor positioning (no selection) - only clamp if outside bounds - if (selStartLine != m_editState.line || - selStartCol < m_editState.spanStart || selStartCol > editEnd) { - int clampedCol = qBound(m_editState.spanStart, selStartCol, editEnd); - m_sci->setCursorPosition(m_editState.line, clampedCol); - } - } else { - // Actual selection - clamp both ends to edit span - bool clamped = false; - - // Force to edit line - if (selStartLine != m_editState.line || selEndLine != m_editState.line) { - m_sci->setSelection(m_editState.line, m_editState.spanStart, - m_editState.line, editEnd); - s_clamping = false; - return; - } - - if (selStartCol < m_editState.spanStart) { selStartCol = m_editState.spanStart; clamped = true; } - if (selEndCol < m_editState.spanStart) { selEndCol = m_editState.spanStart; clamped = true; } - if (selStartCol > editEnd) { selStartCol = editEnd; clamped = true; } - if (selEndCol > editEnd) { selEndCol = editEnd; clamped = true; } - - if (clamped) - m_sci->setSelection(selStartLine, selStartCol, selEndLine, selEndCol); + s_clamping = false; + return; } + // Actual selection - clamp both ends to edit span + bool clamped = false; + + // Force to edit line + if (selStartLine != m_editState.line || selEndLine != m_editState.line) { + m_sci->setSelection(m_editState.line, m_editState.spanStart, + m_editState.line, editEnd); + s_clamping = false; + return; + } + + if (selStartCol < m_editState.spanStart) { selStartCol = m_editState.spanStart; clamped = true; } + if (selEndCol < m_editState.spanStart) { selEndCol = m_editState.spanStart; clamped = true; } + if (selStartCol > editEnd) { selStartCol = editEnd; clamped = true; } + if (selEndCol > editEnd) { selEndCol = editEnd; clamped = true; } + + if (clamped) + m_sci->setSelection(selStartLine, selStartCol, selEndLine, selEndCol); + s_clamping = false; } @@ -1043,6 +1066,10 @@ void RcxEditor::commitInlineEdit() { if (editedLen > 0) editedText = lineText.mid(m_editState.spanStart, editedLen).trimmed(); + // For Type edits: if nothing changed, commit original + if (m_editState.target == EditTarget::Type && editedText.isEmpty()) + editedText = m_editState.original; + auto info = endInlineEdit(); emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, editedText); } @@ -1056,27 +1083,63 @@ void RcxEditor::cancelInlineEdit() { emit inlineEditCancelled(); } -// ── Type autocomplete ── +// ── Type picker (user list) ── void RcxEditor::showTypeAutocomplete() { + // Replace original type with spaces (keeps layout, clears for typing) + int len = m_editState.original.size(); + QString spaces(len, ' '); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, + m_editState.posStart, m_editState.posEnd); + m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL, + (uintptr_t)0, spaces.toUtf8().constData()); + + // Position cursor at start + m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart); + + showTypeListFiltered(QString()); // Show full list initially +} + +void RcxEditor::showTypeListFiltered(const QString& filter) { if (!m_editState.active || m_editState.target != EditTarget::Type) return; - // Selection stays intact - typing/autocomplete will replace selected text + // Filter type names by prefix + QStringList all = allTypeNamesForUI(); + QStringList filtered; + for (const QString& t : all) { + if (filter.isEmpty() || t.startsWith(filter, Qt::CaseInsensitive)) + filtered << t; + } + if (filtered.isEmpty()) return; // No matches - keep list hidden - // Build list from typeName (matches what the editor displays) - QByteArray list = allTypeNamesForUI().join(' ').toUtf8(); + // Show user list (id=1 for types) - selection handled by userListActivated signal + QByteArray list = filtered.join(' ').toUtf8(); m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)' '); - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETIGNORECASE, (long)1); - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETDROPRESTOFWORD, (long)1); - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSHOW, - (uintptr_t)0, list.constData()); + m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW, + (uintptr_t)1, list.constData()); +} - // Highlight the current type in the list - QByteArray cur = m_editState.original.toUtf8(); - m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSELECT, - (uintptr_t)0, cur.constData()); +void RcxEditor::updateTypeListFilter() { + if (!m_editState.active || m_editState.target != EditTarget::Type) + return; + // Get currently typed text from line + QString lineText = getLineText(m_sci, m_editState.line); + long curPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETCURRENTPOS); + long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, + (unsigned long)m_editState.line); + int col = (int)(curPos - lineStart); + + // Extract text from spanStart to cursor + int len = col - m_editState.spanStart; + if (len <= 0) { + showTypeListFiltered(QString()); // Show full list + return; + } + + QString typed = lineText.mid(m_editState.spanStart, len); + showTypeListFiltered(typed); } // ── Editable-field text-color indicator ── @@ -1129,6 +1192,12 @@ void RcxEditor::updateEditableIndicators(int line) { // ── Hover cursor ── void RcxEditor::applyHoverCursor() { + // Clear previous hover span indicator + if (m_hoverSpanLine >= 0) { + clearIndicatorLine(IND_HOVER_SPAN, m_hoverSpanLine); + m_hoverSpanLine = -1; + } + // Edit mode handles its own cursor (I-beam) if (m_editState.active) return; @@ -1147,6 +1216,15 @@ void RcxEditor::applyHoverCursor() { int line; EditTarget t; bool tokenHit = hitTestTarget(m_sci, m_meta, m_lastHoverPos, line, t); + // Apply hover span indicator (blue text like a link) + if (tokenHit) { + NormalizedSpan span; + if (resolvedSpanFor(line, t, span)) { + fillIndicatorCols(IND_HOVER_SPAN, line, span.start, span.end); + m_hoverSpanLine = line; + } + } + // Also show pointer cursor for fold column on fold-head lines bool interactive = tokenHit; if (!interactive) { @@ -1175,18 +1253,24 @@ void RcxEditor::setEditComment(const QString& comment) { if (s_updating) return; s_updating = true; - // Comment is always at end of line - calculate dynamically as value length changes QString lineText = getLineText(m_sci, m_editState.line); - int startCol = lineText.size() - kColComment; - if (startCol < 0) { s_updating = false; return; } - QString padded = comment.leftJustified(kColComment, ' ').left(kColComment); + // Place comment 2 spaces after current value, prefixed with // + int valueEnd = editEndCol(); + int startCol = valueEnd + 2; // 2 spaces after value + int endCol = lineText.size(); + int availWidth = endCol - startCol; + if (availWidth <= 0) { s_updating = false; return; } + + // Format as "//" (no space after //) + QString formatted = QStringLiteral("//") + comment; + QString padded = formatted.leftJustified(availWidth, ' ').left(availWidth); // Use direct position calculation from line start long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE, (unsigned long)m_editState.line); long posA = lineStart + startCol; - long posB = lineStart + startCol + kColComment; + long posB = lineStart + endCol; QByteArray utf8 = padded.toUtf8(); m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, posA); @@ -1194,6 +1278,10 @@ void RcxEditor::setEditComment(const QString& comment) { m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, (uintptr_t)utf8.size(), utf8.constData()); + // Apply green color to hint text (reuse IND_BASE_ADDR which is green) + m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_BASE_ADDR); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, posA, posB - posA); + s_updating = false; } diff --git a/src/editor.h b/src/editor.h index 5d9d339..7665582 100644 --- a/src/editor.h +++ b/src/editor.h @@ -65,6 +65,7 @@ private: bool m_cursorOverridden = false; uint64_t m_hoveredNodeId = 0; QSet m_currentSelIds; + int m_hoverSpanLine = -1; // Line with hover span indicator // ── Drag selection ── bool m_dragging = false; bool m_dragStarted = false; // true once drag threshold exceeded @@ -87,6 +88,8 @@ private: int spanStart = 0; int linelenAfterReplace = 0; QString original; + long posStart = 0; // Scintilla position of edit start + long posEnd = 0; // Scintilla position of edit end NodeKind editKind = NodeKind::Int32; int commentCol = -1; // fixed comment column (stored at edit start) bool lastValidationOk = true; // track state to avoid redundant updates @@ -104,12 +107,15 @@ private: void applyMarkers(const QVector& meta); void applyFoldLevels(const QVector& meta); void applyHexDimming(const QVector& meta); + void applyBaseAddressColoring(const QVector& meta); void commitInlineEdit(); int editEndCol() const; bool handleNormalKey(QKeyEvent* ke); bool handleEditKey(QKeyEvent* ke); void showTypeAutocomplete(); + void showTypeListFiltered(const QString& filter); + void updateTypeListFilter(); void paintEditableSpans(int line); void updateEditableIndicators(int line); void applyHoverCursor(); diff --git a/src/format.cpp b/src/format.cpp index 031ffe9..3d41182 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -87,6 +87,14 @@ QString fmtStructHeader(const Node& node, int depth) { QStringLiteral(" ") + node.name + QStringLiteral(" {"); } +QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress) { + // Format: "struct Name { base: 0x00400000" - single space after { + QString header = indent(depth) + typeName(node.kind).trimmed() + + QStringLiteral(" ") + node.name + QStringLiteral(" { "); + QString baseHex = QStringLiteral("0x") + QString::number(baseAddress, 16).toUpper(); + return header + QStringLiteral("base: ") + baseHex; +} + QString fmtStructFooter(const Node& node, int depth, int totalSize) { QString s = indent(depth) + QStringLiteral("};"); if (totalSize > 0) @@ -444,9 +452,48 @@ QString validateValue(NodeKind kind, const QString& text) { const auto* m = kindMeta(kind); if (m && m->size > 0 && m->size <= 8) { uint64_t maxVal = (m->size == 8) ? ~0ULL : ((1ULL << (m->size * 8)) - 1); - return QStringLiteral("max 0x%1").arg(maxVal, m->size * 2, 16, QChar('0')); + return QStringLiteral("too large! max=0x%1").arg(maxVal, m->size * 2, 16, QChar('0')); } return QStringLiteral("invalid"); } +// ── Base address validation (supports simple +/- equations) ── + +QString validateBaseAddress(const QString& text) { + QString s = text.trimmed(); + if (s.isEmpty()) return QStringLiteral("empty"); + + int pos = 0; + bool firstTerm = true; + + while (pos < s.size()) { + // Skip whitespace + while (pos < s.size() && s[pos].isSpace()) pos++; + if (pos >= s.size()) break; + + // Check for +/- operator (except first term) + if (!firstTerm) { + if (s[pos] == '+' || s[pos] == '-') pos++; + else return QStringLiteral("invalid '%1'").arg(s[pos]); + while (pos < s.size() && s[pos].isSpace()) pos++; + } + + // Skip 0x prefix if present + if (pos + 1 < s.size() && s[pos] == '0' && (s[pos+1] == 'x' || s[pos+1] == 'X')) + pos += 2; + + // Must have at least one hex digit + int numStart = pos; + while (pos < s.size() && (s[pos].isDigit() || + (s[pos] >= 'a' && s[pos] <= 'f') || + (s[pos] >= 'A' && s[pos] <= 'F'))) pos++; + + if (pos == numStart) return QStringLiteral("invalid"); + + firstTerm = false; + } + + return {}; +} + } // namespace rcx::fmt diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index 723473f..cb22612 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -455,87 +455,99 @@ private slots: QVERIFY(sel.contains(lm->nodeIdx)); } - // ── Test: value edit echoes to comment column ── - void testValueEditCommentEcho() { - m_editor->applyDocument(m_result); + // ── Test: base address changes affect header display ── + void testBaseAddressDisplay() { + // Create tree with base address 0x10 + NodeTree tree = makeTestTree(); + tree.baseAddress = 0x10; + FileProvider prov = makeTestProvider(); + ComposeResult result = compose(tree, prov); - // Begin value edit on line 1 (UInt16 field) - bool ok = m_editor->beginInlineEdit(EditTarget::Value, 1); - QVERIFY(ok); - QVERIFY(m_editor->isEditing()); + m_editor->applyDocument(result); - // Get the line text before any typing - QString lineBefore; - int len = (int)m_editor->scintilla()->SendScintilla( - QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1); - if (len > 0) { - QByteArray buf(len + 1, '\0'); - m_editor->scintilla()->SendScintilla( - QsciScintillaBase::SCI_GETLINE, (unsigned long)1, (void*)buf.data()); - lineBefore = QString::fromUtf8(buf.constData(), len).trimmed(); - } + // Line 0 should be the struct header with isRootHeader=true + const LineMeta* lm = m_editor->metaForLine(0); + QVERIFY(lm); + QCOMPARE(lm->lineKind, LineKind::Header); + QVERIFY(lm->isRootHeader); - // Initial comment should contain "Enter=Save Esc=Cancel" - QVERIFY2(lineBefore.contains("Enter=Save"), - qPrintable("Initial comment missing, got: " + lineBefore)); - - // Type a digit to trigger validateEditLive - QKeyEvent key5(QEvent::KeyPress, Qt::Key_5, Qt::NoModifier, "5"); - QApplication::sendEvent(m_editor->scintilla(), &key5); - QApplication::processEvents(); - - // Get line text after typing - QString lineAfter; - len = (int)m_editor->scintilla()->SendScintilla( - QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1); - if (len > 0) { - QByteArray buf(len + 1, '\0'); - m_editor->scintilla()->SendScintilla( - QsciScintillaBase::SCI_GETLINE, (unsigned long)1, (void*)buf.data()); - lineAfter = QString::fromUtf8(buf.constData(), len).trimmed(); - } - - // Comment should show "!" prefix for invalid value - // Since "0x5a4d" + "5" = "0x5a4d5" = 370509 > 65535, it's invalid for UInt16 - QVERIFY2(lineAfter.contains("! "), - qPrintable("Comment should show '!' for invalid value, got: " + lineAfter)); - - // Cancel and reset - m_editor->cancelInlineEdit(); - m_editor->applyDocument(m_result); - } - - // ── Test: value validation shows error indicator ── - void testValueValidationError() { - m_editor->applyDocument(m_result); - - // Begin value edit on line 1 (UInt16 field, value = 23117) - bool ok = m_editor->beginInlineEdit(EditTarget::Value, 1); - QVERIFY(ok); - - // Type "999" to make value invalid for UInt16 (appends to existing, making it too large) - // Original value 23117 -> typing "999" at end makes it invalid (23117999 > 65535) - const char* digits = "999"; - for (int i = 0; digits[i]; i++) { - QKeyEvent key(QEvent::KeyPress, Qt::Key_9, Qt::NoModifier, QString(digits[i])); - QApplication::sendEvent(m_editor->scintilla(), &key); - QApplication::processEvents(); - } - - // Get line text - comment should show "! " prefix (error) + // Get header line text - should contain "0x10" QString lineText; int len = (int)m_editor->scintilla()->SendScintilla( - QsciScintillaBase::SCI_LINELENGTH, (unsigned long)1); + QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0); if (len > 0) { QByteArray buf(len + 1, '\0'); m_editor->scintilla()->SendScintilla( - QsciScintillaBase::SCI_GETLINE, (unsigned long)1, (void*)buf.data()); + QsciScintillaBase::SCI_GETLINE, (unsigned long)0, (void*)buf.data()); lineText = QString::fromUtf8(buf.constData(), len).trimmed(); } - // Comment should show "! " prefix for invalid value - QVERIFY2(lineText.contains("! "), - qPrintable("Comment should show '! ' for invalid value, got: " + lineText)); + // Verify base address appears in header + QVERIFY2(lineText.contains("0x10") || lineText.contains("0X10"), + qPrintable("Header should contain base address 0x10, got: " + lineText)); + + // Verify struct keyword is present + QVERIFY2(lineText.contains("struct"), + qPrintable("Header should contain 'struct', got: " + lineText)); + + // Reset to original result + m_editor->applyDocument(m_result); + } + + // ── Test: base address span is valid for root headers ── + void testBaseAddressSpan() { + NodeTree tree = makeTestTree(); + tree.baseAddress = 0x140000000; // Large address to test span width + FileProvider prov = makeTestProvider(); + ComposeResult result = compose(tree, prov); + + m_editor->applyDocument(result); + + // Line 0 should be root header + const LineMeta* lm = m_editor->metaForLine(0); + QVERIFY(lm); + QVERIFY(lm->isRootHeader); + + // Get line text for span calculation + QString lineText; + int len = (int)m_editor->scintilla()->SendScintilla( + QsciScintillaBase::SCI_LINELENGTH, (unsigned long)0); + if (len > 0) { + QByteArray buf(len + 1, '\0'); + m_editor->scintilla()->SendScintilla( + QsciScintillaBase::SCI_GETLINE, (unsigned long)0, (void*)buf.data()); + lineText = QString::fromUtf8(buf.constData(), len); + while (lineText.endsWith('\n') || lineText.endsWith('\r')) + lineText.chop(1); + } + + // Base address span should be valid + ColumnSpan bs = baseAddressSpanFor(*lm, lineText); + QVERIFY2(bs.valid, "Base address span should be valid for root header"); + QVERIFY(bs.start < bs.end); + + // The span should cover the hex address + QString spanText = lineText.mid(bs.start, bs.end - bs.start); + QVERIFY2(spanText.contains("0x") || spanText.startsWith("0X"), + qPrintable("Span should contain hex address, got: " + spanText)); + + // Reset + m_editor->applyDocument(m_result); + } + + // ── Test: base address edit begins on root header ── + void testBaseAddressEditBegins() { + NodeTree tree = makeTestTree(); + tree.baseAddress = 0x10; + FileProvider prov = makeTestProvider(); + ComposeResult result = compose(tree, prov); + + m_editor->applyDocument(result); + + // Begin base address edit on line 0 (root header) + bool ok = m_editor->beginInlineEdit(EditTarget::BaseAddress, 0); + QVERIFY2(ok, "Should be able to begin base address edit on root header"); + QVERIFY(m_editor->isEditing()); // Cancel and reset m_editor->cancelInlineEdit();