What happens
dot_config/nvim/lua/custom/plugins/markdown.lua formats markdown on save with
Prettier at --prose-wrap always --print-width 80. When a document contains an
inline code span that crosses a line break, Prettier rewraps it and loses the
spaces around the span, so saving the file silently changes what the document
says.
Real example, from a planning document in another repo. Before:
`pivotedIdentityLossy: true` marker is emitted on each
`_sheets[]` entry whose `conditions` is empty and whose
After a save:
true`marker is emitted on each`\_sheets[]`entry whose`conditions`is empty
The code span boundaries move, so the rendered output changes. Older Prettier
versions also escape underscores (\_sheets[]), which is cosmetic in prose but
wrong inside a span.
The nasty part: the corruption is idempotent. Formatting again produces the
same broken output, so prettier --check passes afterwards and nothing
downstream flags it. Upstream says the same thing in
prettier/prettier#19116.
Why it matters here
Format-on-save is enabled for markdown, so this happens while writing, not at
some reviewable gate:
opts.format_on_save = function(bufnr)
local ft = vim.bo[bufnr].filetype
if ft == 'markdown' then return { timeout_ms = 3000, lsp_format = 'never' } end
In the repo where I hit this, 108 inline code spans across 18 files cross a
line break, so the exposure is not one unlucky paragraph.
Upstream state
Open, not fixed: prettier/prettier#19116 (filed May 2026 against 3.8.3), with
prettier/prettier#19248 and prettier/prettier#18990 pending. Related and much
older: prettier/prettier#11372.
I tested three formatters on the same file:
| Tool |
escapes introduced |
code spans jammed |
| prettier 3.1.0 |
2 |
1 |
| prettier 3.6.2 |
2 |
1 |
| prettier 3.9.6 (current) |
0 |
1 |
mdformat --wrap 80 |
1 |
1 |
Upgrading fixes the escaping and not the jamming. --prose-wrap preserve does
not fix it either: Prettier still normalizes spans that already cross lines, and
that run produced 2 jammed spans.
Second, separate problem: version drift
The formatter resolves its binary with conform.util.from_node_modules 'prettier',
which searches upward for node_modules/.bin/prettier and otherwise falls back
to PATH. In a repo with no node_modules, what actually runs depends on how
Neovim was started:
- Homebrew
prettier on PATH: 3.1.0
- mise shim inside a repo pinning prettier: 3.6.2
- current release: 3.9.6
So the same file formats differently depending on the shell that launched the
editor. That is worth fixing regardless of the wrapping bug.
Options
- Pin the version explicitly so at least the behaviour is consistent, and take
3.9.6 to lose the escaping class.
- Turn off format-on-save for markdown and format deliberately, so a rewrite is
something you choose and can inspect.
- Rely on per-repo
.prettierignore for documents that must not be rewritten.
I confirmed Prettier honours the ignore file through --stdin-filepath, so
this does protect the editor path, but it is opt-in per repo and does nothing
for documents elsewhere.
- Evaluate a formatter with a preservation guarantee. jcreinhold/mdwright
states that fmt refuses any rewrite that would change the rendered DOM, and
preserves emphasis delimiters and list markers by default. It is a cargo
install, which also suits the aqua/vfox/cargo backend preference in the mise
security notes. It is young, so it wants review before trusting it.
Reproducing
The trigger is an inline code span that crosses a line break inside a list
item. A span that sits on one line is handled correctly, so the wrapping alone
is not enough to provoke it.
printf '%s\n' \
'- item:' \
' - value already contains `" - "` (e.g., `COSO Internal Control' \
' - Integrated Framework`), so splitting on the separator is' \
' unsafe. A machine-readable `pivotedIdentityLossy: true` marker is' \
' emitted on each `_field[]` entry whose `conditions` is empty.' \
> /tmp/repro.md
prettier --parser markdown --prose-wrap always --print-width 80 /tmp/repro.md
Output, with prettier 3.1.0:
- item:
- value already contains `" - "` (e.g., `COSO Internal Control
- Integrated
Framework`), so splitting on the separator is unsafe. A machine-readable `pivotedIdentityLossy:
true`marker is emitted on each`\_field[]`entry whose`conditions` is empty.
Three things went wrong in one pass: the spans lost their surrounding spaces,
an underscore got escaped inside a span, and the last two lines run past the
80 column target that prompted the reformat.
What happens
dot_config/nvim/lua/custom/plugins/markdown.luaformats markdown on save withPrettier at
--prose-wrap always --print-width 80. When a document contains aninline code span that crosses a line break, Prettier rewraps it and loses the
spaces around the span, so saving the file silently changes what the document
says.
Real example, from a planning document in another repo. Before:
After a save:
true`marker is emitted on each`\_sheets[]`entry whose`conditions`is emptyThe code span boundaries move, so the rendered output changes. Older Prettier
versions also escape underscores (
\_sheets[]), which is cosmetic in prose butwrong inside a span.
The nasty part: the corruption is idempotent. Formatting again produces the
same broken output, so
prettier --checkpasses afterwards and nothingdownstream flags it. Upstream says the same thing in
prettier/prettier#19116.
Why it matters here
Format-on-save is enabled for markdown, so this happens while writing, not at
some reviewable gate:
In the repo where I hit this, 108 inline code spans across 18 files cross a
line break, so the exposure is not one unlucky paragraph.
Upstream state
Open, not fixed: prettier/prettier#19116 (filed May 2026 against 3.8.3), with
prettier/prettier#19248 and prettier/prettier#18990 pending. Related and much
older: prettier/prettier#11372.
I tested three formatters on the same file:
--wrap 80Upgrading fixes the escaping and not the jamming.
--prose-wrap preservedoesnot fix it either: Prettier still normalizes spans that already cross lines, and
that run produced 2 jammed spans.
Second, separate problem: version drift
The formatter resolves its binary with
conform.util.from_node_modules 'prettier',which searches upward for
node_modules/.bin/prettierand otherwise falls backto
PATH. In a repo with nonode_modules, what actually runs depends on howNeovim was started:
prettieronPATH: 3.1.0So the same file formats differently depending on the shell that launched the
editor. That is worth fixing regardless of the wrapping bug.
Options
3.9.6 to lose the escaping class.
something you choose and can inspect.
.prettierignorefor documents that must not be rewritten.I confirmed Prettier honours the ignore file through
--stdin-filepath, sothis does protect the editor path, but it is opt-in per repo and does nothing
for documents elsewhere.
states that
fmtrefuses any rewrite that would change the rendered DOM, andpreserves emphasis delimiters and list markers by default. It is a cargo
install, which also suits the aqua/vfox/cargo backend preference in the mise
security notes. It is young, so it wants review before trusting it.
Reproducing
The trigger is an inline code span that crosses a line break inside a list
item. A span that sits on one line is handled correctly, so the wrapping alone
is not enough to provoke it.
Output, with prettier 3.1.0:
Three things went wrong in one pass: the spans lost their surrounding spaces,
an underscore got escaped inside a span, and the last two lines run past the
80 column target that prompted the reformat.