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.
| Location | Role today |
|---|---|
packages/ts/obsidian-editor/src/commands.ts#save | Emits state.doc.toString() |
packages/ts/obsidian-editor/src/vue/index.ts#useObsidianEditor | Update listener and doc watcher compare and emit toString(); returns getOutline() (unused by consumers) |
packages/ts/obsidian-editor/src/state.ts | Mode facet, compartment, setMode, scrollLineToTop; no line-separator or redraw plumbing |
packages/ts/obsidian-editor/src/extensions.ts#obsidianEditorExtensions | No EditorState.lineSeparator |
packages/ts/obsidian-editor/src/decorations.ts | Layers rebuild on docChanged, selectionSet, viewport change; nothing external can request a rebuild |
solutions/determined/experiences/notes/app/components/NotesEditor.vue | redraw() re-dispatches the current selection to force a decoration rebuild after an asset or resolution lands |
packages/ts/obsidian-editor/test/helpers.ts#makeState | Headless state builder the corpus test reuses |
packages/ts/obsidian-md/test/fixtures/every-feature.md | Seed fixture for the corpus |
packages/ts/obsidian-editor/moon.yml | test and typecheck tasks only |
Proposed
Section titled “Proposed”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.
Approach
Section titled “Approach”- Create the seven corpus fixtures;
crlf.mdisfeatures.mdre-encoded with\r\n. - Write the swap-writer test (identity and command matrix). The CRLF identity case fails before step 3 and passes after.
- Add
lineSeparatorFor,redrawEffect,requestRedrawto the state module; install the line separator inobsidianEditorExtensions. - Switch
saveand both Vue sync paths tosliceDoc(); replacegetOutline()with theoutlineshallow ref; extend the Vue test (CRLF round trip,outlineupdates). - Make
livePreviewPluginandblockWidgetFieldrebuild onredrawEffect; add a test that a provider answer change followed byrequestRedrawchanges the decoration set. - Replace the selection re-dispatch in
NotesEditor.vuewithrequestRedraw(view). - Write the bench runner and comparer, generate the baseline, add the
benchmoon task andbench/bench:baselinepackage scripts.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
packages/ts/obsidian-editor/src/state.ts | modify | lineSeparatorFor, redrawEffect, requestRedraw |
packages/ts/obsidian-editor/src/extensions.ts | modify | Install EditorState.lineSeparator for CRLF documents |
packages/ts/obsidian-editor/src/commands.ts | modify | save emits sliceDoc(); export ALL_COMMANDS: StateCommand[] |
packages/ts/obsidian-editor/src/decorations.ts | modify | Rebuild on redrawEffect |
packages/ts/obsidian-editor/src/vue/index.ts | modify | sliceDoc() in both sync paths; outline shallow ref replaces getOutline |
packages/ts/obsidian-editor/src/index.ts | modify | Export requestRedraw, redrawEffect, lineSeparatorFor |
packages/ts/obsidian-editor/test/fixtures/corpus/features.md | new | Seed fixture copied from obsidian-md |
packages/ts/obsidian-editor/test/fixtures/corpus/tables.md | new | GFM tables, alignment row, escaped pipes |
packages/ts/obsidian-editor/test/fixtures/corpus/math.md | new | Inline and block math |
packages/ts/obsidian-editor/test/fixtures/corpus/details.md | new | <details> blocks, nested and with <summary> |
packages/ts/obsidian-editor/test/fixtures/corpus/crlf.md | new | CRLF line endings |
packages/ts/obsidian-editor/test/fixtures/corpus/whitespace.md | new | Trailing spaces, tabs, no final newline |
packages/ts/obsidian-editor/test/fixtures/corpus/nested-callouts.md | new | Callouts inside callouts and lists |
packages/ts/obsidian-editor/test/swap-writer.test.ts | new | Identity and command matrix over the corpus |
packages/ts/obsidian-editor/test/vue.test.ts | modify | CRLF round trip; outline ref |
packages/ts/obsidian-editor/bench/run.ts | new | Timings over corpus and synthetic 10k-line note; writes the baseline |
packages/ts/obsidian-editor/bench/compare.ts | new | Fails at more than 1.5× baseline |
packages/ts/obsidian-editor/bench/baseline.json | new | Recorded baseline |
packages/ts/obsidian-editor/moon.yml | modify | bench task |
packages/ts/obsidian-editor/package.json | modify | bench and bench:baseline scripts |
solutions/determined/experiences/notes/app/components/NotesEditor.vue | modify | redraw() calls requestRedraw(view) |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
bun test packages/ts/obsidian-editor/test/swap-writer.test.tspasses, including thecrlf.mdidentity case that fails before this task. - AC-2:
grep -rn "doc.toString()" packages/ts/obsidian-editor/srcreturns 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’sresolveLinkanswer and the wikilink decoration class changes fromcm-wikilink-missingtocm-wikilink-resolved. - AC-5:
grep -n "selection: current.state.selection"oversolutions/determined/experiences/notes/app/components/NotesEditor.vuereturns nothing. - AC-6:
useObsidianEditorreturnsoutlineand nogetOutline; the Vue test assertsoutline.valueupdates after a heading is inserted. - AC-7:
bun run bench:baselinein the package writes the baseline with entriesrebuild-viewport-10k,keystroke-10k, and oneopen-<fixture>per corpus fixture;moon run obsidian-editor:benchexits 1 when a measure is forced to 2× its baseline. - AC-8: the baseline records
rebuild-viewport-10kbelow 8 ms andkeystroke-10kp95 below 16 ms on the machine that generates it (the budgets in the program plan).
Out of scope
Section titled “Out of scope”- Running the command matrix over the catalogue (T-EXQ7-md-engine-command-catalogue
redefines
ALL_COMMANDSover the catalogue; the test needs no change). - Browser-side perf measurement (T-J92O-md-test-apps, T-I630-md-ui-playground-e2e-perf).
- Deleting
NotesEditor.vue(T-3DFJ-notes-adopts-markdown-ui).
Dependencies
Section titled “Dependencies”- 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.
Discovery context
Section titled “Discovery context”Member of milestone M-931L; engine step E2 of decision D-K3EM-markdown-editor-chrome-and-plugin-environment.
Depends on
Section titled “Depends on”T-4TE2-md-engine-plugin-contract