fix: make new note editor immediately interactive
Extract heading-stripping logic into `extractEditorBody` — fixes a bug where the regex failed to strip the title heading from newly created notes (because `splitFrontmatter` left a leading newline that the regex didn't account for). Add a fast path in `doSwap` for empty body content: when the body is empty (common case for new notes), skip the potentially-async `tryParseMarkdownToBlocks` pipeline entirely and apply a single empty paragraph block synchronously. This eliminates the multi-second delay caused by BlockNote's async markdown parser initialization. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
54
src/hooks/useEditorTabSwap.test.ts
Normal file
54
src/hooks/useEditorTabSwap.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { extractEditorBody } from './useEditorTabSwap'
|
||||
|
||||
describe('extractEditorBody', () => {
|
||||
it('strips frontmatter and title heading from new note content (double newline)', () => {
|
||||
const content = '---\ntitle: Untitled note\ntype: Note\nstatus: Active\n---\n\n# Untitled note\n\n'
|
||||
expect(extractEditorBody(content)).toBe('')
|
||||
})
|
||||
|
||||
it('strips frontmatter and title heading from content with single newline', () => {
|
||||
const content = '---\ntitle: Test\n---\n# Test\n\nBody text here.'
|
||||
expect(extractEditorBody(content)).toBe('Body text here.')
|
||||
})
|
||||
|
||||
it('preserves body content after the heading', () => {
|
||||
const content = '---\ntitle: My Note\ntype: Note\n---\n\n# My Note\n\nFirst paragraph.\n\nSecond paragraph.'
|
||||
expect(extractEditorBody(content)).toBe('First paragraph.\n\nSecond paragraph.')
|
||||
})
|
||||
|
||||
it('handles content without frontmatter', () => {
|
||||
const content = '# Just a Heading\n\nSome body text.'
|
||||
expect(extractEditorBody(content)).toBe('Some body text.')
|
||||
})
|
||||
|
||||
it('handles content without frontmatter or heading', () => {
|
||||
const content = 'Just plain text.'
|
||||
expect(extractEditorBody(content)).toBe('Just plain text.')
|
||||
})
|
||||
|
||||
it('handles completely empty content', () => {
|
||||
expect(extractEditorBody('')).toBe('')
|
||||
})
|
||||
|
||||
it('handles frontmatter-only content', () => {
|
||||
const content = '---\ntitle: Empty\n---\n'
|
||||
expect(extractEditorBody(content)).toBe('')
|
||||
})
|
||||
|
||||
it('preserves wikilinks in body', () => {
|
||||
const content = '---\ntitle: Test\n---\n\n# Test\n\nSee [[Other Note]] for details.'
|
||||
expect(extractEditorBody(content)).toBe('See [[Other Note]] for details.')
|
||||
})
|
||||
|
||||
it('does not strip heading that is not at the start of body', () => {
|
||||
const content = '---\ntitle: Test\n---\n\nSome intro text.\n\n# A Heading\n\nMore text.'
|
||||
expect(extractEditorBody(content)).toBe('Some intro text.\n\n# A Heading\n\nMore text.')
|
||||
})
|
||||
|
||||
it('returns empty for buildNoteContent output', () => {
|
||||
// Exactly what buildNoteContent('My Project', 'Project', 'Active') produces
|
||||
const content = '---\ntitle: My Project\ntype: Project\nstatus: Active\n---\n\n# My Project\n\n'
|
||||
expect(extractEditorBody(content)).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -15,6 +15,13 @@ interface UseEditorTabSwapOptions {
|
||||
onContentChange?: (path: string, content: string) => void
|
||||
}
|
||||
|
||||
/** Strip the YAML frontmatter and the title heading (# ...) from raw file
|
||||
* content, returning only the body that should appear in the editor. */
|
||||
export function extractEditorBody(rawFileContent: string): string {
|
||||
const [, rawBody] = splitFrontmatter(rawFileContent)
|
||||
return rawBody.trimStart().replace(/^# [^\n]*\n?/, '').trimStart()
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the tab content-swap machinery for the BlockNote editor.
|
||||
*
|
||||
@@ -151,10 +158,19 @@ export function useEditorTabSwap({ tabs, activeTabPath, editor, onContentChange
|
||||
return
|
||||
}
|
||||
|
||||
const [, rawBody] = splitFrontmatter(tab.content)
|
||||
const body = rawBody.replace(/^# [^\n]*\n?/, '').trimStart()
|
||||
const body = extractEditorBody(tab.content)
|
||||
const preprocessed = preProcessWikilinks(body)
|
||||
|
||||
// Fast path: empty body (e.g. newly created notes). Skip the
|
||||
// potentially-async markdown parser and set a single empty paragraph
|
||||
// so the editor is immediately interactive.
|
||||
if (!preprocessed.trim()) {
|
||||
const emptyDoc = [{ type: 'paragraph', content: [] }]
|
||||
cache.set(targetPath, emptyDoc)
|
||||
applyBlocks(emptyDoc)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = editor.tryParseMarkdownToBlocks(preprocessed)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- BlockNote block arrays
|
||||
|
||||
Reference in New Issue
Block a user