Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131/** * 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);