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:
Test
2026-02-27 22:46:41 +01:00
parent cd1246d43c
commit bb0a5c5c98
2 changed files with 72 additions and 2 deletions

View 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('')
})
})

View File

@@ -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