fix: sync raw editor (CodeMirror) content to BlockNote on mode switch

When toggling from raw mode back to BlockNote, the editor now correctly
re-parses content from tab.content instead of using stale cached blocks.

Key changes:
- useEditorTabSwap: detect rawMode true→false transition, invalidate
  block cache, and re-parse from tab.content. Added rawSwapPendingRef
  guard to prevent a second effect run from re-caching stale blocks
  before the deferred doSwap microtask runs.
- useRawMode: added onBeforeRawEnd callback to flush debounced raw
  editor content synchronously before toggling off.
- Editor.tsx: wired rawLatestContentRef and handleBeforeRawEnd to
  ensure the latest raw content reaches tab.content before the swap.
- RawEditorView: exposed latestContentRef so parent can read the
  latest keystroke content without waiting for the 500ms debounce.
- EditorContent: threaded rawLatestContentRef through to RawEditorView.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-11 22:14:20 +01:00
parent ab02aa5e96
commit fafa4e394b
8 changed files with 408 additions and 50 deletions

View File

@@ -150,9 +150,24 @@ export const Editor = memo(function Editor({
onTitleSync: onTitleSync ?? (() => {}),
})
// Ref updated by RawEditorView on every keystroke — used to flush
// debounced content synchronously before leaving raw mode.
const rawLatestContentRef = useRef<string | null>(null)
const handleBeforeRawEnd = useCallback(() => {
if (rawLatestContentRef.current != null && activeTabPath) {
onContentChange?.(activeTabPath, rawLatestContentRef.current)
}
rawLatestContentRef.current = null
}, [activeTabPath, onContentChange])
const { rawMode, handleToggleRaw } = useRawMode({
activeTabPath, onBeforeRawEnd: handleBeforeRawEnd,
})
const { handleEditorChange, editorMountedRef } = useEditorTabSwap({
tabs, activeTabPath, editor, onContentChange,
onH1Change: onH1Changed, syncActiveRef,
onH1Change: onH1Changed, syncActiveRef, rawMode,
})
useEditorFocus(editor, editorMountedRef)
@@ -166,8 +181,6 @@ export const Editor = memo(function Editor({
activeTabPath, onLoadDiff, onLoadDiffAtCommit,
})
const { rawMode, handleToggleRaw } = useRawMode({ activeTabPath })
const { handleToggleDiffExclusive, handleToggleRawExclusive } = useEditorModeExclusion({
diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, diffToggleRef,
})
@@ -224,6 +237,7 @@ export const Editor = memo(function Editor({
onUnarchiveNote={onUnarchiveNote}
vaultPath={vaultPath}
isDarkTheme={isDarkTheme}
rawLatestContentRef={rawLatestContentRef}
/>
}
{(showAIChat || !inspectorCollapsed) && <ResizeHandle onResize={onInspectorResize} />}

View File

@@ -1,3 +1,4 @@
import type React from 'react'
import type { VaultEntry, NoteStatus } from '../types'
import type { useCreateBlockNote } from '@blocknote/react'
import { DiffView } from './DiffView'
@@ -41,6 +42,8 @@ interface EditorContentProps {
onUnarchiveNote?: (path: string) => void
vaultPath?: string
isDarkTheme?: boolean
/** Ref updated by RawEditorView on every keystroke with the latest doc. */
rawLatestContentRef?: React.MutableRefObject<string | null>
}
function EditorLoadingSkeleton() {
@@ -72,7 +75,7 @@ function DiffModeView({ diffContent, onToggleDiff }: { diffContent: string | nul
}
function RawModeEditorSection({
rawMode, activeTab, entries, onContentChange, onSave, isDark,
rawMode, activeTab, entries, onContentChange, onSave, isDark, latestContentRef,
}: {
rawMode: boolean
activeTab: Tab | null
@@ -80,6 +83,7 @@ function RawModeEditorSection({
onContentChange?: (path: string, content: string) => void
onSave?: () => void
isDark?: boolean
latestContentRef?: React.MutableRefObject<string | null>
}) {
if (!rawMode || !activeTab) return null
return (
@@ -91,6 +95,7 @@ function RawModeEditorSection({
onContentChange={onContentChange ?? (() => {})}
onSave={onSave ?? (() => {})}
isDark={isDark}
latestContentRef={latestContentRef}
/>
)
}
@@ -129,19 +134,20 @@ function ActiveTabBreadcrumb({ activeTab, props }: {
)
}
function EditorBody({ activeTab, isLoadingNewTab, entries, editor, diffMode, diffContent, onToggleDiff, rawMode, onRawContentChange, onSave, onNavigateWikilink, onEditorChange, vaultPath, isDarkTheme, isTrashed }: {
function EditorBody({ activeTab, isLoadingNewTab, entries, editor, diffMode, diffContent, onToggleDiff, rawMode, onRawContentChange, onSave, onNavigateWikilink, onEditorChange, vaultPath, isDarkTheme, isTrashed, rawLatestContentRef }: {
activeTab: Tab | null; isLoadingNewTab: boolean; entries: VaultEntry[]
editor: ReturnType<typeof useCreateBlockNote>
diffMode: boolean; diffContent: string | null; onToggleDiff: () => void
rawMode: boolean; onRawContentChange?: (path: string, content: string) => void; onSave?: () => void
onNavigateWikilink: (target: string) => void; onEditorChange?: () => void
vaultPath?: string; isDarkTheme?: boolean; isTrashed: boolean
rawLatestContentRef?: React.MutableRefObject<string | null>
}) {
const showEditor = !diffMode && !rawMode
return (
<>
{diffMode && <DiffModeView diffContent={diffContent} onToggleDiff={onToggleDiff} />}
<RawModeEditorSection rawMode={rawMode} activeTab={activeTab} entries={entries} onContentChange={onRawContentChange} onSave={onSave} isDark={isDarkTheme} />
<RawModeEditorSection rawMode={rawMode} activeTab={activeTab} entries={entries} onContentChange={onRawContentChange} onSave={onSave} isDark={isDarkTheme} latestContentRef={rawLatestContentRef} />
{showEditor && activeTab && (
<div style={{ display: 'flex', flex: 1, flexDirection: 'column', minHeight: 0 }}>
<SingleEditorView editor={editor} entries={entries} onNavigateWikilink={onNavigateWikilink} onChange={onEditorChange} vaultPath={vaultPath} isDarkTheme={isDarkTheme} editable={!isTrashed} />
@@ -157,7 +163,7 @@ export function EditorContent({
diffMode, diffContent, onToggleDiff,
rawMode, onToggleRaw, onRawContentChange, onSave,
onNavigateWikilink, onEditorChange, vaultPath, isDarkTheme,
onDeleteNote,
onDeleteNote, rawLatestContentRef,
...breadcrumbProps
}: EditorContentProps) {
const isTrashed = activeTab?.entry.trashed ?? false
@@ -179,7 +185,7 @@ export function EditorContent({
{activeTab?.entry.archived && breadcrumbProps.onUnarchiveNote && (
<ArchivedNoteBanner onUnarchive={() => breadcrumbProps.onUnarchiveNote!(activeTab.entry.path)} />
)}
<EditorBody activeTab={activeTab} isLoadingNewTab={isLoadingNewTab} entries={entries} editor={editor} diffMode={diffMode} diffContent={diffContent} onToggleDiff={onToggleDiff} rawMode={rawMode} onRawContentChange={onRawContentChange} onSave={onSave} onNavigateWikilink={onNavigateWikilink} onEditorChange={onEditorChange} vaultPath={vaultPath} isDarkTheme={isDarkTheme} isTrashed={isTrashed} />
<EditorBody activeTab={activeTab} isLoadingNewTab={isLoadingNewTab} entries={entries} editor={editor} diffMode={diffMode} diffContent={diffContent} onToggleDiff={onToggleDiff} rawMode={rawMode} onRawContentChange={onRawContentChange} onSave={onSave} onNavigateWikilink={onNavigateWikilink} onEditorChange={onEditorChange} vaultPath={vaultPath} isDarkTheme={isDarkTheme} isTrashed={isTrashed} rawLatestContentRef={rawLatestContentRef} />
</div>
)
}

View File

@@ -23,6 +23,9 @@ export interface RawEditorViewProps {
onContentChange: (path: string, content: string) => void
onSave: () => void
isDark?: boolean
/** Mutable ref updated on every keystroke with the latest doc string.
* Allows the parent to flush debounced content before unmount. */
latestContentRef?: React.MutableRefObject<string | null>
}
const DEBOUNCE_MS = 500
@@ -35,7 +38,7 @@ function getCursorCoords(view: EditorView): { top: number; left: number } | null
return { top: coords.bottom, left: coords.left }
}
export function RawEditorView({ content, path, entries, onContentChange, onSave, isDark = false }: RawEditorViewProps) {
export function RawEditorView({ content, path, entries, onContentChange, onSave, isDark = false, latestContentRef }: RawEditorViewProps) {
const containerRef = useRef<HTMLDivElement>(null)
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const pathRef = useRef(path)
@@ -43,6 +46,8 @@ export function RawEditorView({ content, path, entries, onContentChange, onSave,
const onSaveRef = useRef(onSave)
const latestDocRef = useRef(content)
useEffect(() => { pathRef.current = path }, [path])
// Expose latest doc content to parent via ref
useEffect(() => { if (latestContentRef) latestContentRef.current = content }, [latestContentRef, content])
useEffect(() => { onContentChangeRef.current = onContentChange }, [onContentChange])
useEffect(() => { onSaveRef.current = onSave }, [onSave])
@@ -64,8 +69,12 @@ export function RawEditorView({ content, path, entries, onContentChange, onSave,
const insertWikilinkRef = useRef<(entryTitle: string) => void>(() => {})
const latestContentRefStable = useRef(latestContentRef)
useEffect(() => { latestContentRefStable.current = latestContentRef }, [latestContentRef])
const handleDocChange = useCallback((doc: string) => {
latestDocRef.current = doc
if (latestContentRefStable.current) latestContentRefStable.current.current = doc
setYamlError(detectYamlError(doc))
if (debounceRef.current) clearTimeout(debounceRef.current)
debounceRef.current = setTimeout(() => {