From 1bf633e98f70ca53a3b92a35e65b04dc7cf4e60f Mon Sep 17 00:00:00 2001 From: Vadim Melnicuk Date: Mon, 23 Feb 2026 08:38:37 +0000 Subject: [PATCH] feat: enhance frontmatter lists handling and styling for YAML fields --- webview/src/editor.js | 4 +- webview/src/helpers/frontmatter.js | 118 +++++++++++- webview/src/helpers/listMarkers.js | 9 + webview/src/index.js | 296 +++++++++++++++++++++++++++-- webview/src/liveMode.js | 53 +++++- webview/src/styles.css | 22 +++ 6 files changed, 463 insertions(+), 39 deletions(-) diff --git a/webview/src/editor.js b/webview/src/editor.js index c0b08a7..bdd1108 100644 --- a/webview/src/editor.js +++ b/webview/src/editor.js @@ -1,6 +1,6 @@ import { EditorState, Compartment, Transaction, StateEffect, StateField, RangeSetBuilder } from '@codemirror/state'; import { EditorView, keymap, highlightActiveLine, lineNumbers, highlightActiveLineGutter, scrollPastEnd, Decoration } from '@codemirror/view'; -import { defaultKeymap, history, historyKeymap, indentWithTab, indentLess, undo, redo } from '@codemirror/commands'; +import { defaultKeymap, history, historyKeymap, indentMore, indentLess, undo, redo } from '@codemirror/commands'; import { markdown, markdownKeymap, markdownLanguage } from '@codemirror/lang-markdown'; import { indentUnit, syntaxHighlighting, syntaxTree, forceParsing } from '@codemirror/language'; import { highlightStyle } from './theme'; @@ -366,7 +366,7 @@ export function createEditor({ EditorState.tabSize.of(4), indentUnit.of(' '), keymap.of([ - { key: 'Tab', run: (view) => indentListByTwoSpaces(view) || indentWithTab(view) }, + { key: 'Tab', run: (view) => indentListByTwoSpaces(view) || indentMore(view) }, { key: 'Shift-Tab', run: (view) => outdentListByTwoSpaces(view) || indentLess(view) }, { key: 'Backspace', run: deleteBackwardSmart }, { diff --git a/webview/src/helpers/frontmatter.js b/webview/src/helpers/frontmatter.js index 6b52b0b..3342d46 100644 --- a/webview/src/helpers/frontmatter.js +++ b/webview/src/helpers/frontmatter.js @@ -16,6 +16,14 @@ export function isThematicBreakLine(lineText) { return thematicBreakRe.test(lineText); } +export function isInsideFrontmatter(frontmatter, pos) { + return Boolean(frontmatter && pos >= frontmatter.from && pos < frontmatter.to); +} + +export function isInsideFrontmatterContent(frontmatter, pos) { + return Boolean(frontmatter && pos >= frontmatter.contentFrom && pos < frontmatter.contentTo); +} + export function parseFrontmatter(state) { const { doc } = state; const cached = frontmatterCache.get(doc); @@ -76,6 +84,92 @@ export const sourceFrontmatterField = StateField.define({ const sourceFrontmatterContentLineDeco = Decoration.line({ class: 'meo-md-frontmatter-line meo-md-frontmatter-content' }); const sourceFrontmatterDelimiterLineDeco = Decoration.line({ class: 'meo-md-frontmatter-delimiter-line' }); +const sourceFrontmatterKeyDeco = Decoration.mark({ class: 'meo-md-frontmatter-key' }); +const sourceFrontmatterValueDeco = Decoration.mark({ class: 'meo-md-frontmatter-value' }); + +export function yamlFrontmatterFieldOffsets(lineText) { + let offset = 0; + while (offset < lineText.length && (lineText[offset] === ' ' || lineText[offset] === '\t')) { + offset += 1; + } + + if (lineText[offset] === '-' && /\s/.test(lineText[offset + 1] ?? '')) { + offset += 1; + while (offset < lineText.length && (lineText[offset] === ' ' || lineText[offset] === '\t')) { + offset += 1; + } + } + + if (offset >= lineText.length || lineText[offset] === '#') { + return null; + } + + const colonOffset = lineText.indexOf(':', offset); + if (colonOffset < 0) { + return null; + } + + let keyEndOffset = colonOffset; + while (keyEndOffset > offset && (lineText[keyEndOffset - 1] === ' ' || lineText[keyEndOffset - 1] === '\t')) { + keyEndOffset -= 1; + } + if (keyEndOffset <= offset) { + return null; + } + + let valueStartOffset = colonOffset + 1; + while ( + valueStartOffset < lineText.length && + (lineText[valueStartOffset] === ' ' || lineText[valueStartOffset] === '\t') + ) { + valueStartOffset += 1; + } + + return { + keyFromOffset: offset, + keyToOffset: colonOffset + 1, + valueFromOffset: valueStartOffset < lineText.length ? valueStartOffset : null + }; +} + +function frontmatterContentLineRange(state, frontmatter) { + if (!frontmatter || frontmatter.contentTo <= frontmatter.contentFrom) { + return null; + } + + return { + startLineNo: state.doc.lineAt(frontmatter.contentFrom).number, + endLineNo: state.doc.lineAt(frontmatter.contentTo - 1).number + }; +} + +export function forEachFrontmatterContentLine(state, frontmatter, callback) { + const range = frontmatterContentLineRange(state, frontmatter); + if (!range) { + return; + } + + for (let lineNo = range.startLineNo; lineNo <= range.endLineNo; lineNo += 1) { + callback(state.doc.line(lineNo)); + } +} + +export function forEachYamlFrontmatterField(state, frontmatter, callback) { + forEachFrontmatterContentLine(state, frontmatter, (line) => { + const offsets = yamlFrontmatterFieldOffsets(line.text); + if (!offsets) { + return; + } + + callback({ + line, + keyFrom: line.from + offsets.keyFromOffset, + keyTo: line.from + offsets.keyToOffset, + valueFrom: offsets.valueFromOffset === null ? null : line.from + offsets.valueFromOffset, + valueTo: line.to + }); + }); +} function buildSourceFrontmatterDecorations(state) { const builder = new RangeSetBuilder(); @@ -87,15 +181,23 @@ function buildSourceFrontmatterDecorations(state) { const openingLine = state.doc.lineAt(frontmatter.openingFrom); builder.add(openingLine.from, openingLine.from, sourceFrontmatterDelimiterLineDeco); - const contentStartLineNo = state.doc.lineAt(frontmatter.contentFrom).number; - const contentEndLineNo = frontmatter.contentTo > frontmatter.contentFrom - ? state.doc.lineAt(frontmatter.contentTo - 1).number - : contentStartLineNo - 1; - - for (let lineNo = contentStartLineNo; lineNo <= contentEndLineNo; lineNo++) { - const line = state.doc.line(lineNo); + forEachFrontmatterContentLine(state, frontmatter, (line) => { builder.add(line.from, line.from, sourceFrontmatterContentLineDeco); - } + + const offsets = yamlFrontmatterFieldOffsets(line.text); + if (!offsets) { + return; + } + + builder.add(line.from + offsets.keyFromOffset, line.from + offsets.keyToOffset, sourceFrontmatterKeyDeco); + + if (offsets.valueFromOffset !== null) { + const valueFrom = line.from + offsets.valueFromOffset; + if (valueFrom < line.to) { + builder.add(valueFrom, line.to, sourceFrontmatterValueDeco); + } + } + }); const closingLine = state.doc.lineAt(frontmatter.closingFrom); builder.add(closingLine.from, closingLine.from, sourceFrontmatterDelimiterLineDeco); diff --git a/webview/src/helpers/listMarkers.js b/webview/src/helpers/listMarkers.js index 09a25db..7e2434b 100644 --- a/webview/src/helpers/listMarkers.js +++ b/webview/src/helpers/listMarkers.js @@ -1,6 +1,7 @@ import { StateField, RangeSetBuilder } from '@codemirror/state'; import { Decoration, WidgetType, EditorView } from '@codemirror/view'; import { base02 } from '../theme'; +import { parseFrontmatter, isInsideFrontmatterContent } from './frontmatter'; const sourceListMarkerDeco = Decoration.mark({ class: 'meo-md-list-prefix' }); const taskCompleteDeco = Decoration.mark({ class: 'meo-task-complete' }); @@ -61,6 +62,10 @@ function forEachSelectionLine(state, callback) { } } +function lineIsInFrontmatterContent(frontmatter, line) { + return isInsideFrontmatterContent(frontmatter, line.from); +} + function isListLine(lineText) { return listItemRegex.test(lineText); } @@ -561,9 +566,13 @@ export function collectOrderedListRenumberChanges(state) { function computeSourceListBorders(state) { const stylesByLine = detectListIndentStylesByLine(state); + const frontmatter = parseFrontmatter(state); const ranges = new RangeSetBuilder(); for (let lineNo = 1; lineNo <= state.doc.lines; lineNo += 1) { const line = state.doc.line(lineNo); + if (lineIsInFrontmatterContent(frontmatter, line)) { + continue; + } const lineText = state.doc.sliceString(line.from, line.to); const style = lineIndentStyle(lineNo, stylesByLine); const marker = listMarkerData(lineText, null, style); diff --git a/webview/src/index.js b/webview/src/index.js index 517b74a..ff92b39 100644 --- a/webview/src/index.js +++ b/webview/src/index.js @@ -15,6 +15,15 @@ let latestWikiLinkRequestId = ''; let pendingWikiStatusRefresh = null; const wikiStatusDebounceMs = 1000; const vscodeEditorFontFamily = 'var(--vscode-editor-font-family)'; +const largeDocThresholds = Object.freeze({ + charCount: 300_000, + lineCount: 6_000, + maxLineLength: 12_000 +}); +const largeDocGuardNoticeMessage = 'Large document opened in Source mode for stability. You can switch to Live mode manually.'; +const largeDocLiveRetryNoticeMessage = 'Large document is in Live mode (manual retry). Rendering may be slow or fail.'; +const liveModeFailureNoticeMessage = 'Live mode failed to render this document. Switched to Source mode.'; +const editorUpdateFailureNoticeMessage = 'Editor failed to update this document. Try reopening the file.'; const normalizeThemeLineHeight = (value, fallback) => { if (typeof value !== 'number' || !Number.isFinite(value)) { @@ -23,6 +32,55 @@ const normalizeThemeLineHeight = (value, fallback) => { return Math.min(maxThemeLineHeight, Math.max(minThemeLineHeight, value)); }; +const getDocumentComplexity = (text) => { + const value = typeof text === 'string' ? text : String(text ?? ''); + const charCount = value.length; + if (charCount === 0) { + return { + charCount: 0, + lineCount: 0, + maxLineLength: 0, + isLarge: false + }; + } + + let lineCount = 1; + let currentLineLength = 0; + let maxLineLength = 0; + + for (let i = 0; i < value.length; i += 1) { + const code = value.charCodeAt(i); + if (code === 13) { + maxLineLength = Math.max(maxLineLength, currentLineLength); + currentLineLength = 0; + lineCount += 1; + if (value.charCodeAt(i + 1) === 10) { + i += 1; + } + continue; + } + if (code === 10) { + maxLineLength = Math.max(maxLineLength, currentLineLength); + currentLineLength = 0; + lineCount += 1; + continue; + } + currentLineLength += 1; + } + + maxLineLength = Math.max(maxLineLength, currentLineLength); + + return { + charCount, + lineCount, + maxLineLength, + isLarge: + charCount >= largeDocThresholds.charCount || + lineCount >= largeDocThresholds.lineCount || + maxLineLength >= largeDocThresholds.maxLineLength + }; +}; + const applyThemeSettings = (theme) => { const rootStyle = document.documentElement.style; const colors = theme?.colors ?? {}; @@ -615,11 +673,80 @@ let hasLocalModePreference = false; let findPanelVisible = false; let pendingInitialText = null; let initialEditorMountQueued = false; +let initialMountRecoveryAttempted = false; +let largeDocGuardActive = false; +let largeDocManualLiveOverride = false; +let lastDocumentComplexity = null; +let editorFailureNoticeMessage = ''; +let editorFailureNoticeKind = 'error'; const hideSelectionMenu = () => { selectionMenu.classList.remove('is-visible'); }; +const setEditorNotice = (_message, _kind = 'info') => {}; + +const clearEditorNotice = () => {}; + +const updateEditorNotice = () => { + if (editorFailureNoticeMessage) { + setEditorNotice(editorFailureNoticeMessage, editorFailureNoticeKind); + return; + } + + if (largeDocGuardActive && !largeDocManualLiveOverride && currentMode === 'source') { + setEditorNotice(largeDocGuardNoticeMessage, 'warning'); + return; + } + + if (largeDocGuardActive && largeDocManualLiveOverride && currentMode === 'live') { + setEditorNotice(largeDocLiveRetryNoticeMessage, 'warning'); + return; + } + + clearEditorNotice(); +}; + +const setFailureNotice = (message, kind = 'error') => { + editorFailureNoticeMessage = message; + editorFailureNoticeKind = kind; + updateEditorNotice(); +}; + +const clearFailureNotice = () => { + if (!editorFailureNoticeMessage) { + return; + } + editorFailureNoticeMessage = ''; + editorFailureNoticeKind = 'error'; + updateEditorNotice(); +}; + +const updateDocumentComplexityState = (text) => { + lastDocumentComplexity = getDocumentComplexity(text); + largeDocGuardActive = lastDocumentComplexity.isLarge; + if (!largeDocGuardActive) { + largeDocManualLiveOverride = false; + } + updateEditorNotice(); + return lastDocumentComplexity; +}; + +const logWebviewRenderError = (context, error, extra = {}) => { + const metrics = lastDocumentComplexity ?? getDocumentComplexity(getCurrentExportText()); + console.error('[MEO webview] render error', { + context, + mode: currentMode, + largeDocGuardActive, + largeDocManualLiveOverride, + charCount: metrics.charCount, + lineCount: metrics.lineCount, + maxLineLength: metrics.maxLineLength, + ...extra, + error + }); +}; + const setFindStatus = (text, isError = false) => { findStatus.textContent = text; findStatus.classList.toggle('is-error', isError); @@ -776,11 +903,18 @@ const updateModeUI = () => { } }; -const applyMode = (mode, { post = true, persist = true, userTriggered = false } = {}) => { +const applyMode = (mode, { post = true, persist = true, userTriggered = false, reason = 'user' } = {}) => { if (mode !== 'live' && mode !== 'source') { - return; + return false; } + if (reason === 'large-doc-guard') { + largeDocManualLiveOverride = false; + } else if (userTriggered && largeDocGuardActive) { + largeDocManualLiveOverride = mode === 'live'; + } + + const previousMode = currentMode; currentMode = mode; if (userTriggered) { hasLocalModePreference = true; @@ -788,7 +922,40 @@ const applyMode = (mode, { post = true, persist = true, userTriggered = false } updateModeUI(); if (editor) { - editor.setMode(mode); + try { + editor.setMode(mode); + if (mode === 'live') { + clearFailureNotice(); + } + } catch (error) { + logWebviewRenderError('applyMode', error, { requestedMode: mode, reason }); + + if (mode === 'live') { + setFailureNotice(liveModeFailureNoticeMessage, 'warning'); + if (largeDocGuardActive) { + largeDocManualLiveOverride = false; + } + + try { + editor.setMode('source'); + currentMode = 'source'; + updateModeUI(); + updateEditorNotice(); + if (post) { + vscode.postMessage({ type: 'setMode', mode: 'source' }); + } + return false; + } catch (fallbackError) { + logWebviewRenderError('applyMode.fallbackSource', fallbackError, { requestedMode: mode, reason }); + setFailureNotice(editorUpdateFailureNoticeMessage, 'error'); + } + } + + currentMode = previousMode; + updateModeUI(); + updateEditorNotice(); + return false; + } } if (persist) { @@ -798,6 +965,54 @@ const applyMode = (mode, { post = true, persist = true, userTriggered = false } if (post) { vscode.postMessage({ type: 'setMode', mode }); } + + updateEditorNotice(); + return true; +}; + +const enforceLargeDocGuardMode = ({ post = true } = {}) => { + if (!largeDocGuardActive || largeDocManualLiveOverride || currentMode === 'source') { + updateEditorNotice(); + return false; + } + applyMode('source', { post, persist: false, reason: 'large-doc-guard' }); + updateEditorNotice(); + return true; +}; + +const setEditorTextSafely = (text, context) => { + if (!editor) { + return false; + } + + try { + editor.setText(text); + return true; + } catch (error) { + logWebviewRenderError('setText', error, { context }); + + if (currentMode === 'live') { + setFailureNotice(liveModeFailureNoticeMessage, 'warning'); + if (largeDocGuardActive) { + largeDocManualLiveOverride = false; + } + applyMode('source', { post: true, persist: false, reason: 'render-failure' }); + if (!editor) { + return false; + } + try { + editor.setText(text); + return true; + } catch (retryError) { + logWebviewRenderError('setText.retryInSource', retryError, { context }); + setFailureNotice(editorUpdateFailureNoticeMessage, 'error'); + return false; + } + } + + setFailureNotice(editorUpdateFailureNoticeMessage, 'error'); + return false; + } }; const flushChanges = () => { @@ -1054,19 +1269,41 @@ const mountInitialEditor = () => { return; } const initialText = pendingInitialText; - pendingInitialText = null; - editor = createEditor({ - parent: editorHost, - text: initialText, - initialMode: currentMode, - initialLineNumbers: lineNumbersVisible, - onApplyChanges: queueChanges, - onOpenLink: (href) => { - vscode.postMessage({ type: 'openLink', href }); - }, - onSelectionChange: updateSelectionMenu - }); - requestWikiLinkStatuses(initialText); + try { + editor = createEditor({ + parent: editorHost, + text: initialText, + initialMode: currentMode, + initialLineNumbers: lineNumbersVisible, + onApplyChanges: queueChanges, + onOpenLink: (href) => { + vscode.postMessage({ type: 'openLink', href }); + }, + onSelectionChange: updateSelectionMenu + }); + pendingInitialText = null; + initialMountRecoveryAttempted = false; + if (currentMode === 'live') { + clearFailureNotice(); + } + requestWikiLinkStatuses(initialText); + updateEditorNotice(); + } catch (error) { + logWebviewRenderError('mountInitialEditor', error); + + if (currentMode === 'live' && !initialMountRecoveryAttempted) { + initialMountRecoveryAttempted = true; + setFailureNotice(liveModeFailureNoticeMessage, 'warning'); + if (largeDocGuardActive) { + largeDocManualLiveOverride = false; + } + applyMode('source', { post: true, persist: false, reason: 'render-failure' }); + scheduleInitialEditorMount(); + return; + } + + setFailureNotice(editorUpdateFailureNoticeMessage, 'error'); + } }; const scheduleInitialEditorMount = () => { @@ -1091,7 +1328,7 @@ const handleInit = (message) => { pendingInitialText = message.text; scheduleInitialEditorMount(); } else { - editor.setText(message.text); + setEditorTextSafely(message.text, 'init'); } if (typeof message.autoSave === 'boolean') { autoSaveEnabled = message.autoSave; @@ -1117,7 +1354,11 @@ window.addEventListener('message', (event) => { if (message.type === 'init') { applyThemeSettings(message.theme); + initialMountRecoveryAttempted = false; + clearFailureNotice(); + updateDocumentComplexityState(message.text); const nextMode = hasLocalModePreference ? currentMode : message.mode; + const guardedMode = largeDocGuardActive && !largeDocManualLiveOverride ? 'source' : nextMode; documentVersion = message.version; syncedText = message.text; pendingText = null; @@ -1127,10 +1368,19 @@ window.addEventListener('message', (event) => { handleInit(message); if (hasLocalModePreference) { - applyMode(nextMode, { post: true, persist: true }); + applyMode(guardedMode, { + post: true, + persist: guardedMode === nextMode, + reason: guardedMode === 'source' && guardedMode !== nextMode ? 'large-doc-guard' : 'init' + }); } else { - applyMode(nextMode, { post: false, persist: false }); + applyMode(guardedMode, { + post: guardedMode !== nextMode, + persist: false, + reason: guardedMode === 'source' && guardedMode !== nextMode ? 'large-doc-guard' : 'init' + }); } + updateEditorNotice(); return; } @@ -1140,13 +1390,17 @@ window.addEventListener('message', (event) => { } if (message.type === 'docChanged' && !editor && pendingInitialText !== null) { + updateDocumentComplexityState(message.text); documentVersion = message.version; syncedText = message.text; pendingInitialText = message.text; + enforceLargeDocGuardMode({ post: true }); return; } if (message.type === 'docChanged' && editor) { + updateDocumentComplexityState(message.text); + enforceLargeDocGuardMode({ post: true }); const incomingText = normalizeEol(message.text); const currentText = normalizeEol(editor.getText()); const pendingNormalized = pendingText === null ? null : normalizeEol(pendingText); @@ -1204,7 +1458,9 @@ window.addEventListener('message', (event) => { pendingDebounce = null; } - editor.setText(message.text); + if (!setEditorTextSafely(message.text, 'docChanged')) { + return; + } scheduleWikiLinkStatusRefresh(message.text); updateFindStatusSummary(); return; diff --git a/webview/src/liveMode.js b/webview/src/liveMode.js index 4e5d563..1765320 100644 --- a/webview/src/liveMode.js +++ b/webview/src/liveMode.js @@ -22,7 +22,13 @@ import { } from './helpers/headingCollapse'; import { addListMarkerDecoration, listMarkerData, detectListIndentStylesByLine } from './helpers/listMarkers'; import { addTableDecorations, addTableDecorationsForLineRange, isTableDelimiterLine, parseTableInfo } from './helpers/tables'; -import { parseFrontmatter, isThematicBreakLine } from './helpers/frontmatter'; +import { + forEachYamlFrontmatterField, + parseFrontmatter, + isInsideFrontmatter, + isInsideFrontmatterContent, + isThematicBreakLine +} from './helpers/frontmatter'; import { isWikiLinkNode, parseWikiLinkData, getWikiLinkStatus } from './helpers/wikiLinks'; const markerDeco = Decoration.mark({ class: 'meo-md-marker' }); @@ -63,6 +69,8 @@ const lineStyleDecos = { hrActive: Decoration.line({ class: 'meo-md-hr-active' }), hr: Decoration.line({ class: 'meo-md-hr' }) }; +const frontmatterKeyDeco = Decoration.mark({ class: 'meo-md-frontmatter-key' }); +const frontmatterValueDeco = Decoration.mark({ class: 'meo-md-frontmatter-value' }); const listLineDecoCache = new Map(); const listIndentWidgetCache = new Map(); @@ -142,6 +150,12 @@ const inlineStyleDecos = { function addFrontmatterBoundaryDecorations(builder, state, frontmatter, activeLines) { if (frontmatter.contentTo > frontmatter.contentFrom) { addLineClass(builder, state, frontmatter.contentFrom, frontmatter.contentTo, lineStyleDecos.frontmatterContent); + forEachYamlFrontmatterField(state, frontmatter, ({ keyFrom, keyTo, valueFrom, valueTo }) => { + addRange(builder, keyFrom, keyTo, frontmatterKeyDeco); + if (valueFrom !== null && valueFrom < valueTo) { + addRange(builder, valueFrom, valueTo, frontmatterValueDeco); + } + }); } const boundaries = [ @@ -161,10 +175,6 @@ function addFrontmatterBoundaryDecorations(builder, state, frontmatter, activeLi } } -function isInsideFrontmatter(frontmatter, pos) { - return Boolean(frontmatter && pos >= frontmatter.from && pos < frontmatter.to); -} - function addThematicBreakDecorations(builder, state, from, to, activeLines) { addLineClass(builder, state, from, to, lineStyleDecos.hr); const lineNo = state.doc.lineAt(from).number; @@ -458,7 +468,7 @@ function addAtxHeadingPrefixMarkers(builder, state, from, activeLines) { addRange(builder, line.from, prefixTo, markerDeco); } -function addListLineDecorations(builder, state, indentSelectedLines) { +function addListLineDecorations(builder, state, indentSelectedLines, frontmatter = null) { const stylesByLine = detectListIndentStylesByLine(state); const orderedCountsByLevel = []; @@ -483,6 +493,14 @@ function addListLineDecorations(builder, state, indentSelectedLines) { orderedCountsByLevel[level] = 0; } + const inFrontmatterContent = isInsideFrontmatterContent(frontmatter, line.from); + if (inFrontmatterContent) { + // Preserve visible list markers inside YAML front matter, but avoid list-line + // layout widgets/styles that reinterpret YAML indentation as Markdown layout. + addListMarkerDecoration(builder, state, line.from, orderedDisplayIndex, style); + continue; + } + if (marker.fromOffset > 0 && (marker.indentColumns ?? 0) > 0) { builder.push( Decoration.replace({ @@ -692,7 +710,7 @@ function buildDecorations(state) { addFallbackTableDecorations(ranges, state, tree, parsedTableRanges); addSingleTildeStrikeDecorations(ranges, state, activeLines, strikeRanges); - addListLineDecorations(ranges, state, indentSelectedLines); + addListLineDecorations(ranges, state, indentSelectedLines, frontmatter); for (const section of collapsedHeadingSections) { addLineClass(ranges, state, section.lineFrom, section.lineTo, collapsedHeadingLineDeco); addRange(ranges, section.collapseFrom, section.collapseTo, collapsedHeadingBodyDeco); @@ -702,14 +720,31 @@ function buildDecorations(state) { return result; } +function safeBuildDecorations(state, fallback, context, extra = {}) { + try { + return buildDecorations(state); + } catch (error) { + console.error('[MEO liveMode] decoration build failed', { + context, + docLength: state.doc.length, + ...extra, + error + }); + return fallback; + } +} + const liveDecorationField = StateField.define({ create(state) { - return buildDecorations(state); + return safeBuildDecorations(state, Decoration.none, 'create'); }, update(decorations, transaction) { // Recompute on every transaction so live mode stays in sync with parser updates // that may arrive without direct doc/selection changes. - const next = buildDecorations(transaction.state); + const next = safeBuildDecorations(transaction.state, decorations, 'update', { + docChanged: transaction.docChanged, + selection: transaction.selection + }); // Guard against transient empty parse results on selection-only transactions. if (!transaction.docChanged && isEmptyDecorationSet(next) && !isEmptyDecorationSet(decorations)) { diff --git a/webview/src/styles.css b/webview/src/styles.css index dfc52df..54e0bbe 100644 --- a/webview/src/styles.css +++ b/webview/src/styles.css @@ -1555,3 +1555,25 @@ body { .cm-editor.meo-mode-source .cm-line.meo-md-frontmatter-content { color: var(--meo-color-base07) !important; } + +.cm-editor.meo-mode-live .cm-line.meo-md-frontmatter-content *, +.cm-editor.meo-mode-source .cm-line.meo-md-frontmatter-content * { + color: inherit !important; +} + +.cm-editor.meo-mode-live .cm-line.meo-md-frontmatter-content .meo-md-list-marker, +.cm-editor.meo-mode-source .cm-line.meo-md-frontmatter-content .meo-md-list-marker, +.cm-editor.meo-mode-live .cm-line.meo-md-frontmatter-content .meo-md-list-prefix, +.cm-editor.meo-mode-source .cm-line.meo-md-frontmatter-content .meo-md-list-prefix { + color: var(--meo-color-base02) !important; +} + +.cm-editor.meo-mode-live .cm-line.meo-md-frontmatter-content .meo-md-frontmatter-key, +.cm-editor.meo-mode-source .cm-line.meo-md-frontmatter-content .meo-md-frontmatter-key { + color: var(--meo-color-base07) !important; +} + +.cm-editor.meo-mode-live .cm-line.meo-md-frontmatter-content .meo-md-frontmatter-value, +.cm-editor.meo-mode-source .cm-line.meo-md-frontmatter-content .meo-md-frontmatter-value { + color: var(--vscode-editor-foreground) !important; +}