refactor: consolidate editor mode handoff
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
26
src/utils/richEditorMarkdown.ts
Normal file
26
src/utils/richEditorMarkdown.ts
Normal 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}`
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user