refactor: consolidate editor mode handoff

This commit is contained in:
lucaronin
2026-05-13 02:18:19 +02:00
parent 16a836b59c
commit 502077170d
8 changed files with 193 additions and 121 deletions

View File

@@ -658,7 +658,7 @@ flowchart LR
style E fill:#d4edda,stroke:#28a745,color:#000
```
Rich-editor change events are coalesced before this serialization runs. `useEditorTabSwap` keeps the latest BlockNote state in the editor, schedules one Markdown serialization for a short idle window, and exposes an explicit flush hook for save, note switch, raw-mode entry, and destructive note actions. This keeps long notes from paying full-document Markdown serialization on every keystroke while preserving the disk-first save path.
Rich-editor change events are coalesced before this serialization runs. `useEditorTabSwap` keeps the latest BlockNote state in the editor, schedules one Markdown serialization for a short idle window, and exposes an explicit flush hook for save, note switch, raw-mode entry, and destructive note actions. `src/utils/richEditorMarkdown.ts` is the shared BlockNote-to-Markdown owner for autosave/tab-swap and raw-mode entry, so wikilink restoration, durable schema-node serialization, frontmatter preservation, and portable attachment paths cannot drift between editor modes. This keeps long notes from paying full-document Markdown serialization on every keystroke while preserving the disk-first save path.
Autosave then waits for a 1.5s idle window before invoking `save_note_content`. If an older save resolves after the user has already typed newer content, the older save is treated as stale and cannot clear the newer pending buffer or repaint tab state over it; the latest pending content remains scheduled for its own save.
@@ -674,6 +674,7 @@ Wikilink resolution (`resolveEntry` in `src/utils/wikilink.ts`) uses multi-pass
### Raw Editor Mode
Toggle via Cmd+K → "Raw Editor" or breadcrumb bar button. Uses CodeMirror 6 (`useCodeMirror` hook) to edit the raw markdown + frontmatter directly. Changes saved via the same `save_note_content` command.
`useRawModeWithFlush` owns the rich/raw transition model: pending raw-exit content and raw-mode overrides move together as one content transition, while cursor/scroll restoration moves through one restore-transition ref consumed by `useEditorModePositionSync`. The raw editor should not carry independent pending-content or pending-position refs outside that handoff.
While the user types, `useEditorSaveWithLinks` derives a transient `VaultEntry` patch from parseable frontmatter so the Inspector, relationship chips, and note-list-visible metadata stay in sync with the raw editor before the next vault reload. Temporarily invalid or half-typed frontmatter is ignored until it becomes parseable again, which avoids clobbering the last known good derived state.
Current-note find/replace is intentionally backed by raw CodeMirror mode. `Cmd+F`, "Find in Note", and "Replace in Note" switch the active Markdown/text note to raw mode, show the compact find bar above CodeMirror, and operate on the current note only. Plain text matching is case-insensitive by default, `Aa` toggles case sensitivity, `.*` toggles JavaScript-regex matching, and regex replacement supports capture groups through JavaScript replacement syntax.

View File

@@ -1,9 +1,10 @@
import type { useCreateBlockNote } from '@blocknote/react'
import type { MutableRefObject, SetStateAction } from 'react'
import type { VaultEntry } from '../types'
import { splitFrontmatter, restoreWikilinksInBlocks } from '../utils/wikilinks'
import { compactMarkdown } from '../utils/compact-markdown'
import { hasDurableEditorBlocks, serializeDurableEditorBlocks } from '../utils/editorDurableMarkdown'
import { portableImageUrls } from '../utils/vaultImages'
import { hasDurableEditorBlocks } from '../utils/editorDurableMarkdown'
import {
serializeRichEditorDocumentToMarkdown,
} from '../utils/richEditorMarkdown'
interface Tab {
entry: VaultEntry
@@ -15,6 +16,45 @@ export interface PendingRawExitContent {
content: string
}
export interface RawModeContentTransition {
pendingExitContent: PendingRawExitContent | null
rawModeContentOverride: PendingRawExitContent | null
}
export function createRawModeContentTransition(): RawModeContentTransition {
return {
pendingExitContent: null,
rawModeContentOverride: null,
}
}
function resolvePendingRawExitContentAction(
action: SetStateAction<PendingRawExitContent | null>,
current: PendingRawExitContent | null,
): PendingRawExitContent | null {
return typeof action === 'function' ? action(current) : action
}
export function withPendingRawExitContent(
transition: RawModeContentTransition,
action: SetStateAction<PendingRawExitContent | null>,
): RawModeContentTransition {
return {
...transition,
pendingExitContent: resolvePendingRawExitContentAction(action, transition.pendingExitContent),
}
}
export function withRawModeContentOverride(
transition: RawModeContentTransition,
action: SetStateAction<PendingRawExitContent | null>,
): RawModeContentTransition {
return {
...transition,
rawModeContentOverride: resolvePendingRawExitContentAction(action, transition.rawModeContentOverride),
}
}
export function buildPendingRawExitContent(
path: string | null,
content: string | null,
@@ -29,12 +69,7 @@ export function serializeEditorDocumentToMarkdown(
vaultPath?: string,
notePath?: string,
): string {
const blocks = editor.document
const restored = restoreWikilinksInBlocks(blocks)
const rawBodyMarkdown = compactMarkdown(serializeDurableEditorBlocks(editor, restored))
const bodyMarkdown = vaultPath ? portableImageUrls(rawBodyMarkdown, vaultPath, notePath) : rawBodyMarkdown
const [frontmatter] = splitFrontmatter(tabContent)
return `${frontmatter}${bodyMarkdown}`
return serializeRichEditorDocumentToMarkdown(editor, tabContent, vaultPath, notePath)
}
export function applyPendingRawExitContent(
@@ -78,7 +113,7 @@ export function syncActiveTabIntoRawBuffer(options: {
editor: ReturnType<typeof useCreateBlockNote>
activeTabPath: string | null
activeTabContent: string | null
rawLatestContentRef: React.MutableRefObject<string | null>
rawLatestContentRef: MutableRefObject<string | null>
serializeRichEditorContent?: boolean
vaultPath?: string
}) {
@@ -104,7 +139,7 @@ export function rememberPendingRawExitContent(options: {
activeTabPath: string | null
activeTabContent: string | null
rawInitialContent: string | null
rawLatestContentRef: React.MutableRefObject<string | null>
rawLatestContentRef: MutableRefObject<string | null>
onContentChange?: (path: string, content: string) => void
}) {
const {

View File

@@ -4,12 +4,13 @@ import {
buildCodeMirrorRestoreState,
captureRawEditorPositionSnapshot,
captureRichEditorPositionSnapshot,
type CodeMirrorRestoreState,
type BlockNotePositionEditor,
type CodeMirrorViewLike,
type RawEditorPositionSnapshot,
} from './editorModePosition'
import { useEditorModePositionSync } from './useEditorModePositionSync'
import {
createEditorModeRestoreTransition,
useEditorModePositionSync,
} from './useEditorModePositionSync'
interface MockBlock {
id: string
@@ -54,6 +55,10 @@ function installRawView(view: CodeMirrorViewLike) {
document.body.appendChild(host)
}
function createRestoreTransitionRef() {
return { current: createEditorModeRestoreTransition() }
}
describe('useEditorModePositionSync', () => {
beforeEach(() => {
installBlockNoteScrollHost()
@@ -73,9 +78,7 @@ describe('useEditorModePositionSync', () => {
const editor = makeEditor()
const dispatch = vi.fn()
const focus = vi.fn()
const pendingRawRestoreRef = { current: null as CodeMirrorRestoreState | null }
const pendingRoundTripRawRestoreRef = { current: null as { path: string; state: CodeMirrorRestoreState } | null }
const pendingRichRestoreRef = { current: null as RawEditorPositionSnapshot | null }
const restoreTransitionRef = createRestoreTransitionRef()
installRawView({
state: {
doc: { toString: () => content },
@@ -91,19 +94,17 @@ describe('useEditorModePositionSync', () => {
useEditorModePositionSync({
activeTabPath: 'note.md',
editor: editor as never,
pendingRawRestoreRef,
pendingRoundTripRawRestoreRef,
pendingRichRestoreRef,
restoreTransitionRef,
rawMode,
})
return { pendingRawRestoreRef, pendingRoundTripRawRestoreRef, pendingRichRestoreRef }
return { restoreTransitionRef }
},
{ initialProps: { rawMode: false } },
)
act(() => {
const snapshot = captureRichEditorPositionSnapshot(editor, document)
result.current.pendingRawRestoreRef.current = snapshot
result.current.restoreTransitionRef.current.rawRestore = snapshot
? buildCodeMirrorRestoreState(editor, content, snapshot)
: null
})
@@ -118,9 +119,7 @@ describe('useEditorModePositionSync', () => {
it('restores the BlockNote cursor after toggling back from raw mode', async () => {
const editor = makeEditor()
const paragraphOffset = content.indexOf('Paragraph one') + 5
const pendingRawRestoreRef = { current: null as CodeMirrorRestoreState | null }
const pendingRoundTripRawRestoreRef = { current: null as { path: string; state: CodeMirrorRestoreState } | null }
const pendingRichRestoreRef = { current: null as RawEditorPositionSnapshot | null }
const restoreTransitionRef = createRestoreTransitionRef()
installRawView({
state: {
doc: { toString: () => content },
@@ -136,18 +135,16 @@ describe('useEditorModePositionSync', () => {
useEditorModePositionSync({
activeTabPath: 'note.md',
editor: editor as never,
pendingRawRestoreRef,
pendingRoundTripRawRestoreRef,
pendingRichRestoreRef,
restoreTransitionRef,
rawMode,
})
return { pendingRawRestoreRef, pendingRoundTripRawRestoreRef, pendingRichRestoreRef }
return { restoreTransitionRef }
},
{ initialProps: { rawMode: true } },
)
act(() => {
result.current.pendingRichRestoreRef.current = captureRawEditorPositionSnapshot(document)
result.current.restoreTransitionRef.current.richRestore = captureRawEditorPositionSnapshot(document)
})
rerender({ rawMode: false })
await act(async () => {
@@ -166,9 +163,7 @@ describe('useEditorModePositionSync', () => {
it('cancels a pending BlockNote restore when raw mode starts again before the frame runs', () => {
const editor = makeEditor()
const paragraphOffset = content.indexOf('Paragraph one') + 5
const pendingRawRestoreRef = { current: null as CodeMirrorRestoreState | null }
const pendingRoundTripRawRestoreRef = { current: null as { path: string; state: CodeMirrorRestoreState } | null }
const pendingRichRestoreRef = { current: null as RawEditorPositionSnapshot | null }
const restoreTransitionRef = createRestoreTransitionRef()
const frames = new Map<number, FrameRequestCallback>()
let nextFrame = 1
vi.mocked(window.requestAnimationFrame).mockImplementation((callback: FrameRequestCallback) => {
@@ -192,18 +187,16 @@ describe('useEditorModePositionSync', () => {
useEditorModePositionSync({
activeTabPath: 'note.md',
editor: editor as never,
pendingRawRestoreRef,
pendingRoundTripRawRestoreRef,
pendingRichRestoreRef,
restoreTransitionRef,
rawMode,
})
return { pendingRawRestoreRef, pendingRoundTripRawRestoreRef, pendingRichRestoreRef }
return { restoreTransitionRef }
},
{ initialProps: { rawMode: true } },
)
act(() => {
result.current.pendingRichRestoreRef.current = captureRawEditorPositionSnapshot(document)
result.current.restoreTransitionRef.current.richRestore = captureRawEditorPositionSnapshot(document)
})
rerender({ rawMode: false })
act(() => {

View File

@@ -9,26 +9,40 @@ import {
const MAX_RAW_RESTORE_ATTEMPTS = 5
export interface EditorModeRestoreTransition {
rawRestore: CodeMirrorRestoreState | null
roundTripRawRestore: { path: string; state: CodeMirrorRestoreState } | null
richRestore: RawEditorPositionSnapshot | null
}
export function createEditorModeRestoreTransition(): EditorModeRestoreTransition {
return {
rawRestore: null,
roundTripRawRestore: null,
richRestore: null,
}
}
function useRawEditorRestoreEffect({
activeTabPath,
pendingRawRestoreRef,
restoreTransitionRef,
rawMode,
}: {
activeTabPath: string | null
pendingRawRestoreRef: React.MutableRefObject<CodeMirrorRestoreState | null>
restoreTransitionRef: React.MutableRefObject<EditorModeRestoreTransition>
rawMode: boolean
}) {
useEffect(() => {
if (!rawMode || !pendingRawRestoreRef.current) return
if (!rawMode || !restoreTransitionRef.current.rawRestore) return
let frame = 0
let attempts = 0
const tryRestore = () => {
const pendingState = pendingRawRestoreRef.current
const pendingState = restoreTransitionRef.current.rawRestore
if (!pendingState) return
if (restoreCodeMirrorView(document, pendingState)) {
pendingRawRestoreRef.current = null
restoreTransitionRef.current.rawRestore = null
return
}
attempts += 1
@@ -43,20 +57,18 @@ function useRawEditorRestoreEffect({
window.cancelAnimationFrame(frame)
}
}
}, [activeTabPath, pendingRawRestoreRef, rawMode])
}, [activeTabPath, restoreTransitionRef, rawMode])
}
function useBlockNoteRestoreEffect({
activeTabPath,
editor,
pendingRoundTripRawRestoreRef,
pendingRichRestoreRef,
restoreTransitionRef,
rawMode,
}: {
activeTabPath: string | null
editor: ReturnType<typeof useCreateBlockNote>
pendingRoundTripRawRestoreRef: React.MutableRefObject<{ path: string; state: CodeMirrorRestoreState } | null>
pendingRichRestoreRef: React.MutableRefObject<RawEditorPositionSnapshot | null>
restoreTransitionRef: React.MutableRefObject<EditorModeRestoreTransition>
rawMode: boolean
}) {
useEffect(() => {
@@ -74,7 +86,7 @@ function useBlockNoteRestoreEffect({
}
const handleEditorTabSwapped = (event: Event) => {
const pendingSnapshot = pendingRichRestoreRef.current
const pendingSnapshot = restoreTransitionRef.current.richRestore
if (!activeTabPath || !pendingSnapshot) return
const customEvent = event as CustomEvent<{ path: string }>
@@ -89,8 +101,8 @@ function useBlockNoteRestoreEffect({
if (canceled) return
restoreBlockNoteView(editor, pendingSnapshot, document)
pendingRoundTripRawRestoreRef.current = null
pendingRichRestoreRef.current = null
restoreTransitionRef.current.roundTripRawRestore = null
restoreTransitionRef.current.richRestore = null
})
}
@@ -99,24 +111,20 @@ function useBlockNoteRestoreEffect({
cancelPendingRestore()
window.removeEventListener('laputa:editor-tab-swapped', handleEditorTabSwapped)
}
}, [activeTabPath, editor, pendingRichRestoreRef, pendingRoundTripRawRestoreRef, rawMode])
}, [activeTabPath, editor, restoreTransitionRef, rawMode])
}
export function useEditorModePositionSync({
activeTabPath,
editor,
pendingRawRestoreRef,
pendingRoundTripRawRestoreRef,
pendingRichRestoreRef,
restoreTransitionRef,
rawMode,
}: {
activeTabPath: string | null
editor: ReturnType<typeof useCreateBlockNote>
pendingRawRestoreRef: React.MutableRefObject<CodeMirrorRestoreState | null>
pendingRoundTripRawRestoreRef: React.MutableRefObject<{ path: string; state: CodeMirrorRestoreState } | null>
pendingRichRestoreRef: React.MutableRefObject<RawEditorPositionSnapshot | null>
restoreTransitionRef: React.MutableRefObject<EditorModeRestoreTransition>
rawMode: boolean
}) {
useRawEditorRestoreEffect({ activeTabPath, pendingRawRestoreRef, rawMode })
useBlockNoteRestoreEffect({ activeTabPath, editor, pendingRoundTripRawRestoreRef, pendingRichRestoreRef, rawMode })
useRawEditorRestoreEffect({ activeTabPath, restoreTransitionRef, rawMode })
useBlockNoteRestoreEffect({ activeTabPath, editor, restoreTransitionRef, rawMode })
}

View File

@@ -8,15 +8,21 @@ import {
captureRawEditorPositionSnapshot,
captureRichEditorPositionSnapshot,
type CodeMirrorRestoreState,
type RawEditorPositionSnapshot,
} from './editorModePosition'
import {
type PendingRawExitContent,
buildPendingRawExitContent,
createRawModeContentTransition,
rememberPendingRawExitContent,
syncActiveTabIntoRawBuffer,
withPendingRawExitContent,
withRawModeContentOverride,
} from './editorRawModeSync'
import { useEditorModePositionSync } from './useEditorModePositionSync'
import {
createEditorModeRestoreTransition,
type EditorModeRestoreTransition,
useEditorModePositionSync,
} from './useEditorModePositionSync'
interface PendingRoundTripRawRestore {
path: string
@@ -25,14 +31,14 @@ interface PendingRoundTripRawRestore {
function getRoundTripRawRestore({
activeTabPath,
pendingRoundTripRawRestore,
restoreTransition,
}: {
activeTabPath: string | null
pendingRoundTripRawRestore: PendingRoundTripRawRestore | null
restoreTransition: EditorModeRestoreTransition
}) {
if (!activeTabPath) return null
return pendingRoundTripRawRestore?.path === activeTabPath
? pendingRoundTripRawRestore.state
return restoreTransition.roundTripRawRestore?.path === activeTabPath
? restoreTransition.roundTripRawRestore.state
: null
}
@@ -40,18 +46,18 @@ function buildPendingRawRestore({
activeTabContent,
activeTabPath,
editor,
pendingRoundTripRawRestore,
restoreTransition,
syncedContent,
}: {
activeTabContent: string | null
activeTabPath: string | null
editor: ReturnType<typeof useCreateBlockNote>
pendingRoundTripRawRestore: PendingRoundTripRawRestore | null
restoreTransition: EditorModeRestoreTransition
syncedContent: string | null
}) {
const roundTripRestore = getRoundTripRawRestore({
activeTabPath,
pendingRoundTripRawRestore,
restoreTransition,
})
if (roundTripRestore) return roundTripRestore
@@ -148,8 +154,7 @@ function useHandleFlushPending({
rawLatestContentRef,
rawSourceContentRef,
flushPendingEditorChangeRef,
pendingRawRestoreRef,
pendingRoundTripRawRestoreRef,
restoreTransitionRef,
setRawModeContentOverride,
vaultPath,
}: {
@@ -160,8 +165,7 @@ function useHandleFlushPending({
rawLatestContentRef: React.MutableRefObject<string | null>
rawSourceContentRef: React.MutableRefObject<string | null>
flushPendingEditorChangeRef?: React.MutableRefObject<(() => boolean) | null>
pendingRawRestoreRef: React.MutableRefObject<CodeMirrorRestoreState | null>
pendingRoundTripRawRestoreRef: React.MutableRefObject<PendingRoundTripRawRestore | null>
restoreTransitionRef: React.MutableRefObject<EditorModeRestoreTransition>
setRawModeContentOverride: React.Dispatch<React.SetStateAction<PendingRawExitContent | null>>
vaultPath?: string
}) {
@@ -177,14 +181,15 @@ function useHandleFlushPending({
vaultPath,
})
rawInitialContentRef.current = syncedContent ?? activeTabContent
pendingRawRestoreRef.current = buildPendingRawRestore({
const restoreTransition = restoreTransitionRef.current
restoreTransition.rawRestore = buildPendingRawRestore({
activeTabContent,
activeTabPath,
editor,
pendingRoundTripRawRestore: pendingRoundTripRawRestoreRef.current,
restoreTransition,
syncedContent,
})
pendingRoundTripRawRestoreRef.current = null
restoreTransition.roundTripRawRestore = null
setRawModeContentOverride(buildPendingRawExitContent(activeTabPath, syncedContent))
clearTableResizeState(editor)
return true
@@ -193,8 +198,7 @@ function useHandleFlushPending({
activeTabPath,
editor,
flushPendingEditorChangeRef,
pendingRawRestoreRef,
pendingRoundTripRawRestoreRef,
restoreTransitionRef,
rawInitialContentRef,
rawLatestContentRef,
rawSourceContentRef,
@@ -211,9 +215,7 @@ function useHandleBeforeRawEnd({
rawBufferPathRef,
rawLatestContentRef,
rawSourceContentRef,
pendingRawRestoreRef,
pendingRichRestoreRef,
pendingRoundTripRawRestoreRef,
restoreTransitionRef,
setPendingRawExitContent,
setRawModeContentOverride,
}: {
@@ -224,16 +226,15 @@ function useHandleBeforeRawEnd({
rawBufferPathRef: React.MutableRefObject<string | null>
rawLatestContentRef: React.MutableRefObject<string | null>
rawSourceContentRef: React.MutableRefObject<string | null>
pendingRawRestoreRef: React.MutableRefObject<CodeMirrorRestoreState | null>
pendingRichRestoreRef: React.MutableRefObject<RawEditorPositionSnapshot | null>
pendingRoundTripRawRestoreRef: React.MutableRefObject<PendingRoundTripRawRestore | null>
restoreTransitionRef: React.MutableRefObject<EditorModeRestoreTransition>
setPendingRawExitContent: React.Dispatch<React.SetStateAction<PendingRawExitContent | null>>
setRawModeContentOverride: React.Dispatch<React.SetStateAction<PendingRawExitContent | null>>
}) {
return useCallback(() => {
pendingRoundTripRawRestoreRef.current = capturePendingRoundTripRawRestore(activeTabPath)
pendingRichRestoreRef.current = captureRawEditorPositionSnapshot(document)
pendingRawRestoreRef.current = null
const restoreTransition = restoreTransitionRef.current
restoreTransition.roundTripRawRestore = capturePendingRoundTripRawRestore(activeTabPath)
restoreTransition.richRestore = captureRawEditorPositionSnapshot(document)
restoreTransition.rawRestore = null
setPendingRawExitContent(rememberPendingRawExitContent({
activeTabPath,
activeTabContent,
@@ -247,9 +248,7 @@ function useHandleBeforeRawEnd({
activeTabContent,
activeTabPath,
onContentChange,
pendingRawRestoreRef,
pendingRichRestoreRef,
pendingRoundTripRawRestoreRef,
restoreTransitionRef,
rawInitialContentRef,
rawBufferPathRef,
rawLatestContentRef,
@@ -295,15 +294,18 @@ export function useRawModeWithFlush(
const rawInitialContentRef = useRef<string | null>(null)
const rawBufferPathRef = useRef<string | null>(null)
const rawSourceContentRef = useRef<string | null>(null)
const pendingRawRestoreRef = useRef<CodeMirrorRestoreState | null>(null)
const pendingRichRestoreRef = useRef<RawEditorPositionSnapshot | null>(null)
const pendingRoundTripRawRestoreRef = useRef<PendingRoundTripRawRestore | null>(null)
const [pendingRawExitContent, setPendingRawExitContent] = useState<PendingRawExitContent | null>(null)
const [rawModeContentOverride, setRawModeContentOverride] = useState<PendingRawExitContent | null>(null)
const restoreTransitionRef = useRef(createEditorModeRestoreTransition())
const [contentTransition, setContentTransition] = useState(createRawModeContentTransition)
const setPendingRawExitContent = useCallback<React.Dispatch<React.SetStateAction<PendingRawExitContent | null>>>((action) => {
setContentTransition(transition => withPendingRawExitContent(transition, action))
}, [])
const setRawModeContentOverride = useCallback<React.Dispatch<React.SetStateAction<PendingRawExitContent | null>>>((action) => {
setContentTransition(transition => withRawModeContentOverride(transition, action))
}, [])
const effectiveActiveTabContent = resolveActiveTabContent({
activeTabContent,
activeTabPath,
pendingRawExitContent,
pendingRawExitContent: contentTransition.pendingExitContent,
})
useTrackRawBuffer({
activeTabContent: effectiveActiveTabContent,
@@ -328,8 +330,7 @@ export function useRawModeWithFlush(
rawLatestContentRef,
rawSourceContentRef,
flushPendingEditorChangeRef,
pendingRawRestoreRef,
pendingRoundTripRawRestoreRef,
restoreTransitionRef,
setRawModeContentOverride,
vaultPath,
})
@@ -341,9 +342,7 @@ export function useRawModeWithFlush(
rawBufferPathRef,
rawLatestContentRef,
rawSourceContentRef,
pendingRawRestoreRef,
pendingRichRestoreRef,
pendingRoundTripRawRestoreRef,
restoreTransitionRef,
setPendingRawExitContent,
setRawModeContentOverride,
})
@@ -356,11 +355,16 @@ export function useRawModeWithFlush(
useEditorModePositionSync({
activeTabPath,
editor,
pendingRawRestoreRef,
pendingRoundTripRawRestoreRef,
pendingRichRestoreRef,
restoreTransitionRef,
rawMode,
})
return { rawMode, handleToggleRaw, rawLatestContentRef, pendingRawExitContent, setPendingRawExitContent, rawModeContentOverride }
return {
rawMode,
handleToggleRaw,
rawLatestContentRef,
pendingRawExitContent: contentTransition.pendingExitContent,
setPendingRawExitContent,
rawModeContentOverride: contentTransition.rawModeContentOverride,
}
}

View File

@@ -1,11 +1,12 @@
import { useCallback, useEffect, useRef, type MutableRefObject } from 'react'
import type { useCreateBlockNote } from '@blocknote/react'
import type { VaultEntry } from '../types'
import { splitFrontmatter, restoreWikilinksInBlocks } from '../utils/wikilinks'
import { compactMarkdown } from '../utils/compact-markdown'
import { serializeDurableEditorBlocks } from '../utils/editorDurableMarkdown'
import { failNoteOpenTrace, finishNoteOpenTrace } from '../utils/noteOpenPerformance'
import { portableImageUrls } from '../utils/vaultImages'
import {
serializeRichEditorBodyToMarkdown,
serializeRichEditorDocumentToMarkdown,
} from '../utils/richEditorMarkdown'
import { useEditorMountState, useLatestRef } from './editorTabSwapLifecycle'
import {
applyBlankStateToEditor,
@@ -122,8 +123,7 @@ function findActiveTab(options: {
}
function serializeEditorBody(editor: ReturnType<typeof useCreateBlockNote>): string {
const restored = restoreWikilinksInBlocks(editor.document)
return compactMarkdown(serializeDurableEditorBlocks(editor, restored))
return serializeRichEditorBodyToMarkdown(editor)
}
function trySerializeEditorBody(
@@ -201,13 +201,15 @@ function serializedEditorChange(options: {
}): { blocks: CachedTabState['blocks'], content: string } | null {
const { editor, path, previousContent, vaultPath } = options
const blocks = editor.document
const rawBodyMarkdown = trySerializeEditorBody(editor, 'editor change')
if (rawBodyMarkdown === null) return null
const bodyMarkdown = vaultPath
? portableImageUrls(rawBodyMarkdown, vaultPath, path)
: rawBodyMarkdown
const [frontmatter] = splitFrontmatter(previousContent)
return { blocks, content: `${frontmatter}${bodyMarkdown}` }
try {
return {
blocks,
content: serializeRichEditorDocumentToMarkdown(editor, previousContent, vaultPath, path),
}
} catch (error) {
console.warn('[editor] Skipped editor change because BlockNote document could not be serialized:', error)
return null
}
}
function useEditorChangeHandler(options: {

View File

@@ -0,0 +1,26 @@
import type { useCreateBlockNote } from '@blocknote/react'
import { compactMarkdown } from './compact-markdown'
import { serializeDurableEditorBlocks } from './editorDurableMarkdown'
import { portableImageUrls } from './vaultImages'
import { restoreWikilinksInBlocks, splitFrontmatter } from './wikilinks'
export function serializeRichEditorBodyToMarkdown(
editor: ReturnType<typeof useCreateBlockNote>,
): string {
const restored = restoreWikilinksInBlocks(editor.document)
return compactMarkdown(serializeDurableEditorBlocks(editor, restored))
}
export function serializeRichEditorDocumentToMarkdown(
editor: ReturnType<typeof useCreateBlockNote>,
tabContent: string,
vaultPath?: string,
notePath?: string,
): string {
const rawBodyMarkdown = serializeRichEditorBodyToMarkdown(editor)
const bodyMarkdown = vaultPath
? portableImageUrls(rawBodyMarkdown, vaultPath, notePath)
: rawBodyMarkdown
const [frontmatter] = splitFrontmatter(tabContent)
return `${frontmatter}${bodyMarkdown}`
}

View File

@@ -1,4 +1,5 @@
import { test, expect, type Page } from '@playwright/test'
import { sendShortcut } from './helpers'
interface RawEditorState {
lineCount: number
@@ -28,6 +29,8 @@ function isPreferredBodyLine(line: string) {
&& !trimmed.startsWith('#')
&& !trimmed.startsWith('- ')
&& !trimmed.startsWith('* ')
&& !trimmed.startsWith('|')
&& trimmed !== '---'
&& !/^\d+\.\s/.test(trimmed)
}
@@ -58,12 +61,12 @@ function rawOffsetForLine(lines: string[], lineIndex: number) {
}
async function openRawEditor(page: Page) {
await page.keyboard.press('Control+Backslash')
await sendShortcut(page, 'Backslash', ['Control'])
await expect(page.locator('[data-testid="raw-editor-codemirror"]')).toBeVisible({ timeout: 5_000 })
}
async function openRichEditor(page: Page) {
await page.keyboard.press('Control+Backslash')
await sendShortcut(page, 'Backslash', ['Control'])
await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 })
}