diff --git a/src/editors/automerge/livePreview/codeDecoration.ts b/src/editors/automerge/livePreview/codeDecoration.ts index 6279b22..ad01186 100644 --- a/src/editors/automerge/livePreview/codeDecoration.ts +++ b/src/editors/automerge/livePreview/codeDecoration.ts @@ -86,11 +86,13 @@ export function buildCodeDecorations( if (node.name === 'FencedCode') { const endPos = Math.min(node.to, state.doc.length); const startLine = state.doc.lineAt(node.from).number; - let endLine = state.doc.lineAt(endPos).number; - // Trailing newline: don't paint the empty line after the block. - if (endPos === state.doc.lineAt(endPos).from && endLine > startLine) { - endLine--; - } + // 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( diff --git a/src/editors/automerge/livePreview/livePreview.test.ts b/src/editors/automerge/livePreview/livePreview.test.ts index 9ad066c..0cf45e2 100644 --- a/src/editors/automerge/livePreview/livePreview.test.ts +++ b/src/editors/automerge/livePreview/livePreview.test.ts @@ -186,6 +186,27 @@ describe('codeDecoration', () => { expect(withClass(list, 'cm-codeblock').length).toBeGreaterThanOrEqual(3); expect(withClass(list, 'cm-inline-formatting-hidden')).toHaveLength(0); }); + + it('paints the active empty line of an unterminated block (after Enter)', () => { + // "```js\n" — opening fence then Enter; the new line 2 is where the + // cursor sits and must be inside the block. + const state = makeState('```js\n', 6); + const lines = withClass( + decos(state, buildCodeDecorations(state)), + 'cm-codeblock', + ); + expect(lines).toHaveLength(2); // fence line + the new empty line + }); + + it('does not paint the blank line after a closed block', () => { + const state = makeState('```js\ncode\n```\n\nafter', 0); + const lines = withClass( + decos(state, buildCodeDecorations(state)), + 'cm-codeblock', + ); + // Exactly the three block lines (open fence, code, close fence). + expect(lines).toHaveLength(3); + }); }); describe('linkDecoration', () => {