Skip to content

fix: leave whitespace that ends a line out of the saved file - #340

Merged
Azganoth merged 1 commit into
mainfrom
bug/line-final-whitespace
Aug 31, 2026
Merged

fix: leave whitespace that ends a line out of the saved file#340
Azganoth merged 1 commit into
mainfrom
bug/line-final-whitespace

Conversation

@Azganoth

Copy link
Copy Markdown
Owner

Summary

A space typed at the end of a line was written to the file and then discarded by the next parse, so the document stopped converging on its own serialization: the first save kept the space, the reload dropped it, and the second save differed from the first.

serializeMarkdownText split trailing whitespace off the value, escaped the rest through state.safe, and appended the whitespace verbatim. mdast-util-to-markdown encodes a space only where a line ending is adjacent to it, so that append changed the written bytes in exactly one situation — whitespace that ends a line — and returned what state.safe would have written anyway everywhere else. The special case had no effect other than producing the byte the parser discards.

The whitespace is now written only where something on the same line follows it. closesTrimmedContent asks whether the text node is the last child of a paragraph, a heading, or a table cell, which are the parents a parse trims, and after carries what will actually be written so the escape passes read the real line ending rather than a space that never arrives. Only the last child can hold such whitespace: state.safe encodes whitespace a line ending follows, and Milkdown hoists whitespace out of a mark before the mark is written.

The report named a paragraph. The same line reaches every parent a parse trims — heading, list item, blockquote, table cell — and any edit that leaves whitespace there, a deletion as much as a keystroke, a tab as much as a space. Two spaces ending a paragraph go with them: they spell a hard break only while another line follows them inside the paragraph, and at the end of the last line there is nothing to break.

Not writing the character is the direction, rather than encoding it as  . Both reverse the assertion that guarded #242's intent, since plain tail \n holds either way; writing nothing keeps that intent, while encoding contradicts it and returns entity noise to every file whose line happens to end in whitespace. CommonMark strips the character on read and a block renders it as nothing, so neither direction preserves anything an author can observe.

Related Issue

Closes #278

Verification

markdownCompatibility.test.ts gains Line-final whitespace, which drives the editor mount through edit, save, reload, save, and asserts each stage rather than the bytes alone. Eight shapes that diverged now converge: a paragraph, a paragraph already holding a space, emphasis closing a paragraph, two spaces, a tab, a heading, a list item, and a blockquote. Alongside them, a table cell, whitespace reached by deleting a character rather than typing one, and the reloaded paragraph matching the text the editor showed before the space was typed.

Three rows assert what must not change, since each is whitespace a parse keeps: a space that later text on the same line follows, a space in front of a hard break, and whitespace inside fenced code.

Existing expectations that encode the behavior this replaces were updated rather than worked around:

  • markdownCompatibility.test.ts asserted plain tail \n under the name "writes an ordinary trailing space as itself". That byte is the defect, so the row now asserts plain tail\n under a name that says what it proves. The three typed-link fixtures beside it drop the same space; what they exist to prove, that typed source commits as the link it describes once a space follows it, is unchanged and still asserted.
  • sourceProjectionTypedLink.test.ts expected \[test taillink](./test.html) \n after a word is dragged out of the end of the paragraph. The drag leaves a line-final space, so it is no longer written. The escaped literal the row exists for is unchanged, and \[test link](./test.html) without a trailing space already has a row asserting that it reloads as literal text.

The corpus round-trip guard cannot see any of this and is unchanged. It runs file, save, file, save, and a file it has opened once no longer holds line-final whitespace, so its baseline is already stable. Only an edit puts the whitespace there, which is why the new coverage edits a mounted document instead of adding a fixture.

pnpm check:frontend passes. The backend is untouched, so pnpm check:backend was not run.

Not verified: the manual pass over corpus/ in the running application, which covers rendering, interaction, and navigator behavior beyond the automated round trip.

Notes

An authored   at a trimmed position is now dropped on save. It was lost before this change as well, one save later, and it is not reachable from the stored source: parsing it produces a text node carrying a leafdownCharacterReference mark, and Milkdown's SerializerState hoists the ASCII space out of that mark into a plain sibling before the mark is written, so the handler never sees the reference.   and * survive under the identical mark because the hoist tests for the literal space, which is why the character-reference preservation shows no sign of this anywhere else.

The opening edge of the same line is filed as #339 and deliberately left alone here. A typed leading space is written as   correctly on the first save, reloads with the space intact, is written raw on the second, and is gone on the third open. #278 never named that edge, and its answer is not quite the same question: the closing edge never round-tripped at all, while this one survives a single save.

The issue body was corrected while diagnosing this. Two of its statements did not survive measurement: two trailing spaces ending a paragraph fail exactly like one rather than round-tripping as a hard break, and authored whitespace is not always unaffected, since an authored   is lost. Both are recorded there with the measurements.

Encoding it as ` ` would preserve it, at the cost of returning the
entity noise #242 removed to every file whose line happens to end in
whitespace. A parse strips the character on read and a block renders it
as nothing, so neither direction preserves anything an author can
observe.

An authored ` ` at a trimmed position is dropped on save as a
result. It is lost today as well, one save later: Milkdown hoists the
space out of the mark that records the reference before serialization
sees it, so the authored form is not reachable from the stored source.
@Azganoth Azganoth added the Bug Something isn't working label Aug 31, 2026
@Azganoth Azganoth self-assigned this Aug 31, 2026
@Azganoth
Azganoth merged commit 7d9f1d8 into main Aug 31, 2026
3 checks passed
@Azganoth
Azganoth deleted the bug/line-final-whitespace branch August 31, 2026 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Whitespace that ends a line is saved and then lost on reload

1 participant