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:
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user