Skip to content

T-0GV4-md-engine-line-endings-corpus

State: closed/done · Impact: high · Complexity: small

state.doc.toString() joins lines with \n, so a CRLF note saves as LF: the editor violates the swap-writer rule of D-7VMX today. This task fixes emission with EditorState.lineSeparator and sliceDoc(), replaces the notes selection re-dispatch hack with requestRedraw, and lands the swap-writer corpus and the headless bench that every later engine task is measured against.

LocationRole today
packages/ts/obsidian-editor/src/commands.ts#saveEmits state.doc.toString()
packages/ts/obsidian-editor/src/vue/index.ts#useObsidianEditorUpdate listener and doc watcher compare and emit toString(); returns getOutline() (unused by consumers)
packages/ts/obsidian-editor/src/state.tsMode facet, compartment, setMode, scrollLineToTop; no line-separator or redraw plumbing
packages/ts/obsidian-editor/src/extensions.ts#obsidianEditorExtensionsNo EditorState.lineSeparator
packages/ts/obsidian-editor/src/decorations.tsLayers rebuild on docChanged, selectionSet, viewport change; nothing external can request a rebuild
solutions/determined/experiences/notes/app/components/NotesEditor.vueredraw() re-dispatches the current selection to force a decoration rebuild after an asset or resolution lands
packages/ts/obsidian-editor/test/helpers.ts#makeStateHeadless state builder the corpus test reuses
packages/ts/obsidian-md/test/fixtures/every-feature.mdSeed fixture for the corpus
packages/ts/obsidian-editor/moon.ymltest and typecheck tasks only

The state module gains lineSeparatorFor(doc: string): '\n' | '\r\n' (CRLF when the first line break is CRLF, per D-4QGN item 4), redrawEffect: StateEffectType<null> and requestRedraw(view: EditorView): void. obsidianEditorExtensions installs EditorState.lineSeparator.of('\r\n') when the initial document is CRLF. Every emission — save, the Vue doc write-back, the Vue doc watcher — uses state.sliceDoc(). The inline plugin and the block field rebuild when a transaction carries redrawEffect.

useObsidianEditor returns outline: ShallowRef<OutlineEntry[]> (recomputed on doc change) instead of getOutline().

Swap-writer corpus under packages/ts/obsidian-editor/test/fixtures/corpus/: features.md (copied from the obsidian-md seed fixture), tables.md, math.md, details.md, crlf.md, whitespace.md (trailing spaces, tabs, no final newline), nested-callouts.md. packages/ts/obsidian-editor/test/swap-writer.test.ts asserts open → sliceDoc() identity for every fixture and, for every command in ALL_COMMANDS (a new export of the commands module: toggleBold, toggleItalic, insertLink today; T-EXQ7-md-engine-command-catalogue redefines it over the catalogue) at every line start and every inline node boundary of every fixture, that bytes outside the changed ranges are identical.

Bench under packages/ts/obsidian-editor/bench/: run.ts measures with performance.now() over the corpus plus a synthetic 10 000-line note — viewport decoration rebuild, keystroke transaction (insert one character, rebuild visible decorations), open-to-first-state — and writes baseline.json; compare.ts exits 1 when any measure exceeds 1.5× its baseline. A bench moon task runs the comparison.

  1. Create the seven corpus fixtures; crlf.md is features.md re-encoded with \r\n.
  2. Write the swap-writer test (identity and command matrix). The CRLF identity case fails before step 3 and passes after.
  3. Add lineSeparatorFor, redrawEffect, requestRedraw to the state module; install the line separator in obsidianEditorExtensions.
  4. Switch save and both Vue sync paths to sliceDoc(); replace getOutline() with the outline shallow ref; extend the Vue test (CRLF round trip, outline updates).
  5. Make livePreviewPlugin and blockWidgetField rebuild on redrawEffect; add a test that a provider answer change followed by requestRedraw changes the decoration set.
  6. Replace the selection re-dispatch in NotesEditor.vue with requestRedraw(view).
  7. Write the bench runner and comparer, generate the baseline, add the bench moon task and bench / bench:baseline package scripts.
LocationKindChange
packages/ts/obsidian-editor/src/state.tsmodifylineSeparatorFor, redrawEffect, requestRedraw
packages/ts/obsidian-editor/src/extensions.tsmodifyInstall EditorState.lineSeparator for CRLF documents
packages/ts/obsidian-editor/src/commands.tsmodifysave emits sliceDoc(); export ALL_COMMANDS: StateCommand[]
packages/ts/obsidian-editor/src/decorations.tsmodifyRebuild on redrawEffect
packages/ts/obsidian-editor/src/vue/index.tsmodifysliceDoc() in both sync paths; outline shallow ref replaces getOutline
packages/ts/obsidian-editor/src/index.tsmodifyExport requestRedraw, redrawEffect, lineSeparatorFor
packages/ts/obsidian-editor/test/fixtures/corpus/features.mdnewSeed fixture copied from obsidian-md
packages/ts/obsidian-editor/test/fixtures/corpus/tables.mdnewGFM tables, alignment row, escaped pipes
packages/ts/obsidian-editor/test/fixtures/corpus/math.mdnewInline and block math
packages/ts/obsidian-editor/test/fixtures/corpus/details.mdnew<details> blocks, nested and with <summary>
packages/ts/obsidian-editor/test/fixtures/corpus/crlf.mdnewCRLF line endings
packages/ts/obsidian-editor/test/fixtures/corpus/whitespace.mdnewTrailing spaces, tabs, no final newline
packages/ts/obsidian-editor/test/fixtures/corpus/nested-callouts.mdnewCallouts inside callouts and lists
packages/ts/obsidian-editor/test/swap-writer.test.tsnewIdentity and command matrix over the corpus
packages/ts/obsidian-editor/test/vue.test.tsmodifyCRLF round trip; outline ref
packages/ts/obsidian-editor/bench/run.tsnewTimings over corpus and synthetic 10k-line note; writes the baseline
packages/ts/obsidian-editor/bench/compare.tsnewFails at more than 1.5× baseline
packages/ts/obsidian-editor/bench/baseline.jsonnewRecorded baseline
packages/ts/obsidian-editor/moon.ymlmodifybench task
packages/ts/obsidian-editor/package.jsonmodifybench and bench:baseline scripts
solutions/determined/experiences/notes/app/components/NotesEditor.vuemodifyredraw() calls requestRedraw(view)
  • AC-1: bun test packages/ts/obsidian-editor/test/swap-writer.test.ts passes, including the crlf.md identity case that fails before this task.
  • AC-2: grep -rn "doc.toString()" packages/ts/obsidian-editor/src returns nothing.
  • AC-3: for each of the seven corpus fixtures, each command in ALL_COMMANDS, and each position in the test’s position set, bytes outside the changed ranges are identical (the matrix in AC-1).
  • AC-4: a test dispatches requestRedraw(view) after changing a stub provider’s resolveLink answer and the wikilink decoration class changes from cm-wikilink-missing to cm-wikilink-resolved.
  • AC-5: grep -n "selection: current.state.selection" over solutions/determined/experiences/notes/app/components/NotesEditor.vue returns nothing.
  • AC-6: useObsidianEditor returns outline and no getOutline; the Vue test asserts outline.value updates after a heading is inserted.
  • AC-7: bun run bench:baseline in the package writes the baseline with entries rebuild-viewport-10k, keystroke-10k, and one open-<fixture> per corpus fixture; moon run obsidian-editor:bench exits 1 when a measure is forced to 2× its baseline.
  • AC-8: the baseline records rebuild-viewport-10k below 8 ms and keystroke-10k p95 below 16 ms on the machine that generates it (the budgets in the program plan).
  • T-4TE2-md-engine-plugin-contract so the redraw trigger lands in the dispatching builders.
  • D-4QGN item 4 (line-ending preservation) and D-7VMX (swap writer) define the pass condition.

Member of milestone M-931L; engine step E2 of decision D-K3EM-markdown-editor-chrome-and-plugin-environment.

T-4TE2-md-engine-plugin-contract


← Back to Tasks