mirror of
https://github.com/vadimmelnicuk/meo.git
synced 2026-05-03 20:50:45 +00:00
feat: enhance frontmatter lists handling and styling for YAML fields
This commit is contained in:
@@ -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 },
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+276
-20
@@ -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;
|
||||
|
||||
+44
-9
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user