Repository navigation
Live Preview editor: inline callouts/code/tables/math/mermaid + Edit/Read modes - #114
Conversation
- calloutWidgetTheme: display:flow-root on .cm-lp-callout establishes a block formatting context so <Callout>'s my-3 margins are contained in the measured box. Without it those child margins collapsed through the container, so CM measured a height ~24px shorter than the callout actually paints and mapped clicks/cursor a line or two too low. - ResizeObserver on the widget container re-measures when height settles after the synchronous mount (async markdown/image resolution, fonts, collapsible toggle); guarded for environments without ResizeObserver. - Prec.high ArrowDown/ArrowUp keymap: a block replace decoration is atomic, so default vertical motion skipped the whole callout and it never unfolded. The handler redirects a move from the adjacent line into the callout's first line, which unfolds it via the existing activeLines mechanism. - foldedCalloutFromCursor extracted + tested; ResizeObserver stub added to the component test setup.
Tier 2 block widgets, continued (callouts already shipped). - BlockPreviewWidget: shared base encoding the callout POC's hard-won lessons — synchronous flushSync mount, ResizeObserver re-measure, estimatedHeight=-1, ignoreEvent=false, and a flow-root container (contains child margins so the measured height matches the painted height). Each widget only supplies its React element + an eq() check. - CalloutWidget refactored onto the base (behavior unchanged). - CodeBlockWidget: renders fenced code (```lang) inline via the existing <CodeBlock> component (syntax highlighting + copy); parseFencedCode handles ```/~~~, info strings, and unclosed-while-typing fences. - livePreview.ts generalized: findCalloutBlocks -> findBlockWidgets (callouts + FencedCode), calloutField -> blockWidgetField, and the arrow-key nav (foldedCalloutFromCursor -> foldedBlockFromCursor) now enters any folded block widget. CALLOUTS_ENABLED -> BLOCK_WIDGETS_ENABLED kill-switch. - Tests: parseFencedCode unit cases; code-block render/cursor-inside/arrow-nav.
Adds a third mode between Write and Read: Raw turns Live Preview off
(livePreview={false}) so you edit the plain markdown source — no inline
block widgets or marker hiding — without leaving the editor. Useful when a
callout/code-block widget gets in the way of editing the underlying syntax.
- EditorMode: 'write' | 'raw' | 'read'; Raw button (Code icon) in the mode
toggle, persisted like the other modes.
- Reuses the existing livePreviewCompartment (livePreview prop already drives
it) — Raw is just Write with live preview off.
- AI/format toolbar and the wikilink picker now show in any editing mode
(mode !== 'read') rather than write-only, so they work in Raw too.
Toggling the Live Preview compartment dispatched synchronously inside a
useEffect; that rebuilds decorations and mounts block widgets, whose toDOM
uses flushSync for a synchronous first measure. flushSync is illegal while
React is still flushing the effect, so it threw
('flushSync was called from inside a lifecycle method').
- Defer the compartment reconfigure to a microtask so the decoration rebuild
(and toDOM) runs in CodeMirror's own context, where flushSync is legal.
- Guard toDOM's flushSync in try/catch as a safety net for any other
render-phase path (e.g. initial mount, which also builds the view in an
effect): on failure, fall back to a plain async render — the ResizeObserver
still corrects the height once React paints.
The bottom preview panel now follows the cursor into a structured block (table, callout, fenced code, math) and renders it live, so you can see a table/callout render — and immediately spot broken markdown — while typing, without needing a selection. - extractStructuredBlockAtOffset (pure, tested): given raw markdown + cursor offset, returns the enclosing table/callout/code/math block, or null for plain prose (paragraphs/headings/lists don't trigger the panel). - note-editor: on cursor move (write or raw mode), preview the enclosing block. Selection preview still takes priority; nothing shows in Read mode. - MDPreviewPanel: optional label prop so the header reads e.g. 'table preview'.
The selection/block preview only refreshed on cursor/selection activity, so after switching notes the previous note's preview lingered until you clicked into the new one. Clear previewText + blockPreview in the note-id change effect (alongside the existing title reset).
…trolled warning) GFM task-list checkboxes came through NoteMarkdownPreview as bare <input type=checkbox> where `checked` is present only when checked. When the previewed text changed (e.g. the editor's live preview updating as you drag-select across checkboxes), the same input flipped checked undefined→true, tripping React's 'changing an uncontrolled input to be controlled' warning. Add an `input` component override that renders a stable read-only, disabled checkbox with an always-boolean `checked`. Non-checkbox inputs pass through. Regression test asserts no controlled-ness flip across content changes.
The preview panel's height was hardcoded to 30vh. Add an S/M/L toggle in the panel header that sets its max-height (15/30/50vh), persisted like the editor mode. Small also densifies the text: the panel sets --preview-scale, and globals.css scales the rem-based prose-cairn sizes under [data-md-preview-portal] by it (S=0.82, M/L=1) so a smaller panel shows more at once.
Raw and Write were both editing modes differing only in whether Live Preview was on, so collapse the top bar to Edit | Read and move Live Preview to its own toggle (shown while editing, beside the mode switch). - EditorMode: 'edit' | 'read' (was write/raw/read). Legacy persisted 'write'/'raw' migrate to 'edit'; 'read' unchanged. - livePreviewOn state, default on, persisted separately (NOTE_LIVE_PREVIEW_KEY). livePreview prop = mode === 'edit' && livePreviewOn. - Toolbar/wikilink/preview gating already keyed on mode !== 'read', so still correct for the merged edit mode.
v2.6.2 already shipped (package.json is 2.6.2, released), so the inline code-block / Edit-Read-LivePreview / current-block-preview entries I'd appended to changelogs/v2.6.2.md were editing a released changelog. Per the changelog rule, move them to a new changelogs/v2.6.3.md (one patch ahead) and restore v2.6.2.md byte-identical to the release. Also add a 'Live Preview Editor' What's New feature entry (v2.6.3) for the headline editor work.
…im fix Completes the Tier 2 block widgets so the editing story needs no follow-up: - Tables: GFM Table nodes render inline via NoteMarkdownPreview (matches read mode); isTableSource guards against lone-pipe prose. - Math: $$…$$ display math renders inline. It isn't a distinct lezer node (lands in a Paragraph), so findMathBlocks scans delimiter pairs separately. - Mermaid: ```mermaid fences render as a diagram via MermaidDiagram (mirrors read-mode's code-fence routing) instead of a code listing. - Parser range-trim: a callout/blockquote with no trailing blank line no longer swallows the following paragraph — countLeadingQuoteLines clamps the widget to the actual `>` lines. buildBlockWidgetDecorations now sorts + drops overlapping candidates defensively so a stray overlap can't throw. All three reuse BlockPreviewWidget (sync mount, ResizeObserver, flow-root, click-to-edit) and the existing arrow-key nav. Tests cover render/cursor-inside for each, the range-trim, and isTableSource.
|
Warning Review limit reached
Next review available in: 40 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe editor adds inline previews for code, tables, math, and Mermaid blocks. It separates Edit, Read, and Live Preview controls, adds cursor-following previews, persists preview size, and fixes callout, note-switching, and task-list behavior. ChangesLive Preview Editor
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant NoteEditor
participant MarkdownEditor
participant LivePreview
participant BlockPreviewWidget
participant NoteMarkdownPreview
NoteEditor->>MarkdownEditor: Enable Live Preview
MarkdownEditor->>LivePreview: Install block decorations
LivePreview->>BlockPreviewWidget: Create preview widget
BlockPreviewWidget->>NoteMarkdownPreview: Render Markdown block
BlockPreviewWidget-->>LivePreview: Report size changes
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/components/notes/markdown-editor.tsx (1)
404-416: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSkip the redundant reconfigure on first mount.
The mount effect at Line 369 already initializes the compartment with
livePreviewEnabled ? livePreview() : []. This effect then runs once on mount and dispatches a second reconfigure with a new, non-identical extension array. CodeMirror compares extensions by identity, so it tears down and rebuilds the whole live-preview extension set right after the first render. Every block widget unmounts and remounts, and Mermaid and math widgets restart their async rendering.Track the applied value and dispatch only on a real change.
♻️ Proposed refactor
+ const appliedLivePreview = useRef(livePreviewEnabled); useEffect(() => { + if (appliedLivePreview.current === livePreviewEnabled && viewRef.current) return; let cancelled = false; queueMicrotask(() => { const view = viewRef.current; if (cancelled || !view) return; + appliedLivePreview.current = livePreviewEnabled; view.dispatch({ effects: livePreviewCompartment.current.reconfigure( livePreviewEnabled ? livePreview() : [], ), }); }); return () => { cancelled = true; }; }, [livePreviewEnabled]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/notes/markdown-editor.tsx` around lines 404 - 416, Update the live-preview effect around livePreviewCompartment to track the value already applied during mount initialization and skip the queued reconfigure when livePreviewEnabled has not changed. Dispatch a compartment reconfigure only after a real enabled-state transition, while preserving cancellation and the existing livePreviewEnabled/on-off extension selection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/notes/note-editor.tsx`:
- Around line 61-77: Update the state initialization around NOTE_EDITOR_MODE_KEY
and NOTE_LIVE_PREVIEW_KEY so a legacy saved "raw" mode migrates to edit with
livePreviewOn initialized to false when the new preference is absent; otherwise
preserve the stored live-preview value and the default-on behavior. Add a
regression test covering this raw-mode migration and preference initialization.
In `@src/lib/livePreview.ts`:
- Around line 126-181: Optimize findBlockWidgets to avoid repeated full-document
scans: cache its BlockWidget[] result per EditorState with a WeakMap so
buildBlockWidgetDecorations, blockWidgetLineSet, and foldedBlockFromCursor reuse
the same computation within an update cycle. Preserve existing widget behavior
and ensure the cache is keyed by the exact EditorState instance; measure
performance with a 5000+ line document.
- Around line 530-541: Update moveIntoBlock so the target line uses
block.lineEnd when dir === -1 and block.lineStart otherwise. Keep the existing
dispatch, unfolding behavior, and return handling unchanged.
---
Nitpick comments:
In `@src/components/notes/markdown-editor.tsx`:
- Around line 404-416: Update the live-preview effect around
livePreviewCompartment to track the value already applied during mount
initialization and skip the queued reconfigure when livePreviewEnabled has not
changed. Dispatch a compartment reconfigure only after a real enabled-state
transition, while preserving cancellation and the existing
livePreviewEnabled/on-off extension selection.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b59fe7b3-905f-41d1-b8ea-468b93067bf3
📒 Files selected for processing (20)
changelogs/v2.6.3.mdscripts/features.config.jssrc/app/globals.csssrc/components/notes/MDPreviewPanel.component.test.tsxsrc/components/notes/MDPreviewPanel.tsxsrc/components/notes/NoteMarkdownPreview.component.test.tsxsrc/components/notes/NoteMarkdownPreview.tsxsrc/components/notes/markdown-editor.tsxsrc/components/notes/note-editor-utils.test.tssrc/components/notes/note-editor-utils.tssrc/components/notes/note-editor.tsxsrc/lib/block-preview-widget.tsxsrc/lib/callout-widget.tsxsrc/lib/code-block-widget.tsxsrc/lib/constants.tssrc/lib/livePreview.component.test.tsxsrc/lib/livePreview.tssrc/lib/math-block-widget.tsxsrc/lib/table-block-widget.tsxvitest.setup.components.ts
…arrow near-edge - note-editor: a legacy saved 'raw' mode now migrates to Edit with Live Preview OFF (was defaulting on, losing the user's raw preference) when no explicit Live Preview preference exists. Extracted migrateEditorMode / initialLivePreviewOn as pure helpers + regression tests. - livePreview: cache findBlockWidgets per EditorState (WeakMap) so the StateField, ViewPlugin, and arrow keymap don't each re-scan the full syntax tree within an update cycle (2–3 scans → 1 per state; states are immutable so instance-keyed caching is safe/self-evicting). - moveIntoBlock: enter a folded block at its NEAR edge — lineEnd when moving up, lineStart when moving down — instead of always jumping to the first line. - markdown-editor: skip the Live Preview compartment reconfigure when the enabled state hasn't changed since what's applied (notably on mount), avoiding a redundant decoration rebuild + widget remount.
…, inline chrome Fixes reported for tables/callouts in the Live Preview editor: - Huge empty gap above tables/callouts: the widget lives inside .cm-content.cm-lineWrapping (CM sets white-space: pre-wrap), which was inherited by the widget's prose-cairn. rehypeRaw leaves structural newlines between blocks as text nodes; under pre-wrap those rendered as real blank lines. Reset white-space: normal on the .cm-lp-block container so inter-block whitespace collapses like read mode. (Read mode was unaffected — its preview isn't inside .cm-content.) - flushSync crash opening a note with a table / toggling Live Preview: dropped flushSync in the widget toDOM entirely (illegal when CM calls toDOM from inside a React commit). Height now settles via the ResizeObserver + estimatedHeight=-1. - Callout/table/math rendered differently from read mode: NoteMarkdownPreview gains an mode that drops the full-pane chrome (px-6 py-5, h-full, overflow) and zeroes first/last child margins, so embedded blocks sit compact. - Trim widget ranges of trailing blank lines + trim raw before rendering. - markdown-editor: skip the Live Preview reconfigure when unchanged (mount).
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/notes/note-editor-utils.ts (1)
159-193: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake fenced-code closing fences match CommonMark rules.
extractStructuredBlockAtOffsetcurrently accepts closing fences atFENCE_REboundaries, so it will include shorter fences and mixed marker fences. Require the same marker character and a closing fence length ≥ the opener length before selecting the fenced-code block.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/notes/note-editor-utils.ts` around lines 159 - 193, Update the fenced-code handling in extractStructuredBlockAtOffset so a closing fence matches CommonMark rules: require the same marker character as the opener and a fence length at least as long as the opener before closing the block. Track the opener’s full fence length alongside openFence, and only select the block when the closing candidate satisfies both conditions; preserve unclosed-opener behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/notes/NoteMarkdownPreview.tsx`:
- Around line 195-201: Update the empty-content early return in
NoteMarkdownPreview so its container uses the same inline-dependent layout as
the main preview div: inline mode must avoid the standalone h-full and p-8
styling, while non-inline mode preserves the existing full-height padded layout.
---
Outside diff comments:
In `@src/components/notes/note-editor-utils.ts`:
- Around line 159-193: Update the fenced-code handling in
extractStructuredBlockAtOffset so a closing fence matches CommonMark rules:
require the same marker character as the opener and a fence length at least as
long as the opener before closing the block. Track the opener’s full fence
length alongside openFence, and only select the block when the closing candidate
satisfies both conditions; preserve unclosed-opener behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d82d55a3-d33c-468d-b012-6a07538713b5
📒 Files selected for processing (12)
src/components/notes/NoteMarkdownPreview.component.test.tsxsrc/components/notes/NoteMarkdownPreview.tsxsrc/components/notes/markdown-editor.tsxsrc/components/notes/note-editor-utils.test.tssrc/components/notes/note-editor-utils.tssrc/components/notes/note-editor.tsxsrc/lib/block-preview-widget.tsxsrc/lib/callout-widget.tsxsrc/lib/livePreview.component.test.tsxsrc/lib/livePreview.tssrc/lib/math-block-widget.tsxsrc/lib/table-block-widget.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
- src/components/notes/markdown-editor.tsx
- src/lib/table-block-widget.tsx
- src/lib/callout-widget.tsx
- src/lib/math-block-widget.tsx
- src/lib/livePreview.ts
- src/components/notes/note-editor.tsx
- src/components/notes/NoteMarkdownPreview.component.test.tsx
- src/lib/livePreview.component.test.tsx
- NoteMarkdownPreview: the empty-content early return now honours inline mode (drops h-full/p-8) instead of always using the standalone padded layout. - extractStructuredBlockAtOffset: a fenced-code block only closes on a fence that uses the SAME marker char AND is at least as long as the opener (CommonMark). A shorter/embedded same-char fence is content, not a close — fixes early-closing for nested code fences. Tracks the opener's fence length; unclosed-opener behaviour preserved. + regression test.
What does this PR do?
Makes the note editor's Live Preview a complete story. It fixes the callout widget that shipped broken in 2.6.2, extracts a reusable base for block widgets, and builds the rest of the Tier 2 set (code, tables, math, Mermaid) on it — plus editor-mode simplification and a follow-the-cursor preview panel. Targets v2.6.3.
Every block uses the same click-to-edit model: it renders inline (matching reading mode) and, when your cursor enters its range, folds back to the raw Markdown for editing. No edit-round-trip inside the widget.
1. Callout fixes — the 2.6.2 widget was mis-behaving (
7221285)Two real bugs found in use:
<Callout>hasmy-3; the widget container wasdisplay:block, so those child margins collapsed through it — CodeMirror measured a border-box ~24px shorter than the callout actually painted and mapped clicks/cursor a line or two too low. Fixed withdisplay: flow-root(establishes a BFC → margins contained → measured height == painted height). AResizeObserveralso re-measures when height settles after the async mount.block:truereplace decoration is atomic, so ArrowUp/Down jumped over it and it never unfolded. APrec.highkeymap now steps the cursor into a folded block from the adjacent line.2. Shared base —
BlockPreviewWidget(33efdc3)Extracted the hard-won lessons (sync
flushSyncmount,ResizeObserverre-measure,estimatedHeight=-1,ignoreEvent=false,flow-rootcontainer) into a base so each new widget only suppliesrender()+eq(). Callout refactored onto it.3. Code / Tables / Math / Mermaid inline (
33efdc3,21df71a1)<CodeBlock>(highlighting + copy).Tablenode →NoteMarkdownPreview;isTableSourceguards against lone-pipe prose.$$…$$isn't a distinct lezer node (it lands in aParagraph), so a separate line-scan (findMathBlocks) detects delimiter pairs.```mermaidrenders as a diagram (was a code listing), mirroring read-mode routing.4. Editor modes: Edit / Read + a Live Preview toggle (
3ca8f098)Collapsed the old three-mode bar (Write/Raw/Read) — Raw was just "Edit with Live Preview off". Now Edit | Read, with a Live Preview toggle shown while editing. Legacy persisted
write/rawmigrate toedit. The toggle persists.5. Follow-the-cursor preview panel (
cd599f8,a2472298)The bottom preview now renders the structured block your cursor is in (table/callout/code/math) live as you type, so you can watch it form and instantly spot broken Markdown. Selection preview still wins; plain prose doesn't trigger it. Added an S / M / L size toggle (S also densifies the text via a scoped
--preview-scale).6. Fixes surfaced along the way
91bb7c2): the compartment reconfigure ran inside a React effect, whereflushSyncis illegal. Deferred to a microtask (runs in CM's own context) + atry/catchfallback for the mount path.60588ed): GFM task-list checkboxes now render as stable read-only inputs.9bbba15): cleared on note change.21df71a1): a callout/table with no trailing blank line no longer swallows the following paragraph (range clamped to the actual block; overlapping candidates dropped defensively).Type of change
Screenshots / recording
Checklist
npm run type-check:allpassesnpm run lintpassesnpm testpasses — 1,861 passed / 12 pre-existing skipsnpm run compile(esbuild) +npm run test:e2e(17/17) passv2.6.3-live-preview-editor) + regeneratedchangelogs/v2.6.3.md(v2.6.2 already shipped; restored byte-identical)text-[Npx]classes — preview font-scaling uses a--preview-scaleCSS var +calc()Notes for reviewer
Focus:
src/lib/livePreview.ts(findBlockWidgets— the one place block types are detected;buildBlockWidgetDecorations— sorts + drops overlaps so a stray overlap can't throw a RangeSet error) andsrc/lib/block-preview-widget.tsx(the base every widget inherits).Deferred on purpose: the task-checkbox inline widget (card
Zs8LYKyGjYN9) is the one remaining Tier 2 item. It's a different interaction — an inline toggle that writes[ ]↔[x]back to the source — not the block click-to-edit base, so it's kept out of this branch to stay focused and low-risk.Honest testing caveat: the component tests run in jsdom, which has no layout engine — element heights are 0 and
coordsAtPosthrows there. So the widget tests verify wiring (widget mounts, cursor-inside folds to raw, arrow-nav decision, parse/guard functions) and the fixes are argued by mechanism (BFC margin containment, microtask ordering), not by pixel/coordinate assertions. The layout-dependent behaviour (click mapping, cursor position around widgets) needs a real-app pass in Electron/Chromium — done informally during development (mouse mapping + arrow-nav confirmed by the author), but a reviewer should click through table/math/mermaid rendering, click-to-edit, and arrow navigation on the built app.Board: tables (
ZZcXhMqwhHHO), mermaid+math (GPnfcv9l0YBI), and the parser quirk (Qw0hhwXnqixP) are in Review; the audit note "Live Preview Editor — Tier 2 Block Widgets" is updated with the full implementation record.Summary by CodeRabbit