/** * Code: inline code (`` `x` ``) and fenced code blocks (``` ``` ```). * * - Inline code: apply `cm-inline-code` to the content, hide the backtick * `CodeMark`s unless the cursor is near. Uses a pure distance check (no * same-line gate) because CommonMark code spans may cross newlines. * - Fenced code: apply a `cm-codeblock` line decoration to every line of the * block, and hide the opening/closing fence rows unless the cursor is on any * line within the block. The language label (`CodeInfo`) is left visible. * * Hidden marker ranges are registered as atomic. Line decorations are not. */ import { type EditorState } from '@codemirror/state'; import { Decoration } from '@codemirror/view'; import { syntaxTree } from '@codemirror/language'; import { type BuildResult, type Range, PROXIMITY_CHARS, DecoCollector, fullRanges, isCursorInRange, isCursorOnBlockLine, decorationExtension, } from './context'; import { inlineFormattingHiddenMark } from './hiddenClasses'; const inlineCodeMark = Decoration.mark({ class: 'cm-inline-code' }); // Per-line background. The first/last lines additionally get rounded corners // and vertical padding so the block reads as one continuous box (no vertical // margin between lines, which would show the editor background through gaps). const codeBlockLine = Decoration.line({ class: 'cm-codeblock' }); const codeBlockLineFirst = Decoration.line({ class: 'cm-codeblock cm-codeblock-first', }); const codeBlockLineLast = Decoration.line({ class: 'cm-codeblock cm-codeblock-last', }); const codeBlockLineOnly = Decoration.line({ class: 'cm-codeblock cm-codeblock-first cm-codeblock-last', }); function codeBlockLineDeco(line: number, first: number, last: number) { if (line === first && line === last) return codeBlockLineOnly; if (line === first) return codeBlockLineFirst; if (line === last) return codeBlockLineLast; return codeBlockLine; } export function buildCodeDecorations( state: EditorState, ranges: readonly Range[] = fullRanges(state), ): BuildResult { const out = new DecoCollector(); for (const { from, to } of ranges) { syntaxTree(state).iterate({ from, to, enter(node) { if (node.name === 'InlineCode') { const near = isCursorInRange( state, node.from, node.to, PROXIMITY_CHARS, ); // Highlight the whole span, backticks included, so the code // styling wraps the markers when they are visible (and when they // are hidden they collapse to zero width inside the same box). out.push(node.from, node.to, inlineCodeMark); if (!near) { const first = node.node.firstChild; const last = node.node.lastChild; if (first?.name === 'CodeMark') { out.push(first.from, first.to, inlineFormattingHiddenMark, true); } if (last?.name === 'CodeMark') { out.push(last.from, last.to, inlineFormattingHiddenMark, true); } } return false; // don't descend into code text } if (node.name === 'FencedCode') { const endPos = Math.min(node.to, state.doc.length); const startLine = state.doc.lineAt(node.from).number; // Paint every line the node spans. @lezer/markdown excludes the // trailing newline from a *closed* block, so node.to always lands on // the closing-fence line and we never over-paint the blank line // after it. For an *unterminated* block (still being typed) node.to // includes the newline just added by Enter, so this correctly keeps // the active empty line inside the block instead of dropping it. const endLine = state.doc.lineAt(endPos).number; for (let l = startLine; l <= endLine; l++) { const line = state.doc.line(l); out.push( line.from, line.from, codeBlockLineDeco(l, startLine, endLine), ); } if (!isCursorOnBlockLine(state, node.from, node.to)) { const first = node.node.firstChild; const last = node.node.lastChild; if (first?.name === 'CodeMark') { out.push(first.from, first.to, inlineFormattingHiddenMark, true); } if (last?.name === 'CodeMark') { out.push(last.from, last.to, inlineFormattingHiddenMark, true); } // Hide the language label too, so the opening fence line does not // leave a stray `js` floating above the block. const info = node.node.getChild('CodeInfo'); if (info) { out.push(info.from, info.to, inlineFormattingHiddenMark, true); } } return false; } }, }); } return out.result(); } export const codeDecoration = decorationExtension(buildCodeDecorations);