diff --git a/src/editors/automerge/automergeEditor.css b/src/editors/automerge/automergeEditor.css index f6cb13a..c170aa9 100644 --- a/src/editors/automerge/automergeEditor.css +++ b/src/editors/automerge/automergeEditor.css @@ -88,16 +88,31 @@ font-size: 0.9em; } +/* + * Fenced code block. Applied as one line decoration per line, so the + * background must be seamless: no vertical margin (which would show the + * editor background through the gaps) and only horizontal padding here. + * The first/last lines add the vertical padding and rounded corners so the + * whole run reads as a single box. + */ .automerge-editor-container .cm-codeblock { background: rgba(175, 184, 193, 0.1); - border-radius: 6px; - padding: 0.8em 1em; - margin: 0.5em 0; + padding: 0 1em; font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; font-size: 0.9em; line-height: 1.5; } +.automerge-editor-container .cm-codeblock-first { + padding-top: 0.6em; + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} +.automerge-editor-container .cm-codeblock-last { + padding-bottom: 0.6em; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; +} /* Lists */ .automerge-editor-container .cm-list-1 { diff --git a/src/editors/automerge/livePreview/codeDecoration.ts b/src/editors/automerge/livePreview/codeDecoration.ts index d415bda..6279b22 100644 --- a/src/editors/automerge/livePreview/codeDecoration.ts +++ b/src/editors/automerge/livePreview/codeDecoration.ts @@ -27,7 +27,26 @@ import { 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, @@ -74,7 +93,11 @@ export function buildCodeDecorations( } for (let l = startLine; l <= endLine; l++) { const line = state.doc.line(l); - out.push(line.from, line.from, codeBlockLine); + out.push( + line.from, + line.from, + codeBlockLineDeco(l, startLine, endLine), + ); } if (!isCursorOnBlockLine(state, node.from, node.to)) { diff --git a/src/editors/automerge/livePreview/livePreview.test.ts b/src/editors/automerge/livePreview/livePreview.test.ts index 83639e7..9ad066c 100644 --- a/src/editors/automerge/livePreview/livePreview.test.ts +++ b/src/editors/automerge/livePreview/livePreview.test.ts @@ -170,6 +170,10 @@ describe('codeDecoration', () => { const state = makeState('```js\ncode\n```\n\nafter', 18); // in "after" const list = decos(state, buildCodeDecorations(state)); expect(withClass(list, 'cm-codeblock').length).toBeGreaterThanOrEqual(3); + // First and last block lines are tagged so the box is seamless (one + // continuous background, rounded only at the ends). + expect(withClass(list, 'cm-codeblock-first')).toHaveLength(1); + expect(withClass(list, 'cm-codeblock-last')).toHaveLength(1); // Opening and closing fence rows hidden. expect( withClass(list, 'cm-inline-formatting-hidden').length,