Stop a delete claiming whitespace an insert never gave it - #32
Merged
Merged
Conversation
In a container holding any text or CDATA child, inserting a node and then
deleting it destroyed a sibling's indentation: the document came back one node
short, silently, in a region the user never edited. Since byte-preserving
saves are the point of the whitespace bookkeeping, that quietly reformatted
somebody's document.
Insert and delete disagreed about who owned the whitespace in front of a node.
XmlLayout.HasTextContent returned true for ANY text-ish child, so a container
that is block-formatted and happens to hold one CDATA among its indented
elements was classified inline: InsertNode.PlanLayout planned no indentation
and added one node, while DeleteNode claimed whatever whitespace preceded the
node and removed two. Net minus one per cycle.
Both halves of the disagreement are fixed, because each covers a case the
other does not.
HasTextContent now asks whether the text shares a line with anything else,
rather than whether it exists. Text that starts a line is preceded by
whitespace carrying a line break; the "Call me " of <p>Call me <i>Ishmael</i>
</p> is not, because with PreserveWhitespace on there is no separate
whitespace node in front of it to find. So a CDATA section on a line of its
own leaves its container block-formatted, and an insert there is indented like
its siblings.
DeleteNode claims the leading whitespace only where ShouldIndent says an
insert would have created it — the same question PlanLayout asks, so the two
cannot drift. In a container that really is inline this leaves a pre-existing
node's indent behind rather than destroying it, which is the deliberate
direction to err: a delete should not invent layout changes.
Pinned by a new drill section that builds both container shapes rather than
relying on a fixture, so it runs on every document CI drills. The oracle is
OuterXml, not a node count: it catches whitespace that changed value as well as
whitespace that went missing.
Mutation-testing is why both fixes are here rather than one. Against the
issue's own reproducing document, reverting EITHER fix alone still passed —
each is independently sufficient there, so shipping both with only that
document as evidence would have left one of them asserted by nothing. The new
section separates them: reverting the classification turns the block
container's checks red, reverting the delete gate turns the inline container's
round-trip red ("4 of 5 children" — the net minus one, by name), and reverting
both reproduces the original failure.
The drill's own diagnosis is improved as the issue asked: the two sections that
shared the message "the document is back to its original children" now say
which one failed and print the counts, which is what made this diagnosable at
all.
One correction to the issue: it cites src/Application/Samples/basket.xml as
failing the same way. That file no longer exists — it went with the WinForms
tree in 1ad7cf6 — so that half of the evidence cannot be reproduced from the
repo. The minimal documents in the issue still reproduce it exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X4pgrsMBcsXjqWDo2gBBt
| // The oracle is OuterXml, not a node count: it catches whitespace that changed value | ||
| // as well as whitespace that went missing. | ||
| { | ||
| var layoutPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "fux-drill-layout.xml"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1.
In a container holding any text or CDATA child, inserting a node and then deleting it destroyed
a sibling's indentation — the document came back one node short, silently, in a region the user
never edited. Since byte-preserving saves are the point of the whitespace bookkeeping, that
quietly reformatted somebody's document.
Cause
Insert and delete disagreed about who owned the whitespace in front of a node.
XmlLayout.HasTextContentreturned true for any text-ish child, so a container that isblock-formatted and happens to hold one CDATA among its indented elements was classified inline:
InsertNode.PlanLayoutplanned no indentation and added one node, whileDeleteNodeclaimedwhatever whitespace preceded the node and removed two.
The fix — both halves, because each covers a case the other does not
HasTextContentnow asks whether the text shares a line, rather than whether it exists. Textthat starts a line is preceded by whitespace carrying a line break; the
"Call me "of<p>Call me <i>Ishmael</i></p>is not, because withPreserveWhitespaceon there is no separatewhitespace node in front of it to find. So a CDATA section on a line of its own leaves its
container block-formatted, and an insert there is indented like its siblings.
DeleteNodeclaims the leading whitespace only whereShouldIndentsays an insert would havecreated it — the same question
PlanLayoutasks, so the two cannot drift. In a container thatreally is inline this leaves a pre-existing node's indent behind rather than destroying it, which
is the deliberate direction to err: a delete should not invent layout changes.
Why both, and how they are pinned
Mutation testing is why both fixes are here rather than one. Against the issue's own
reproducing document, reverting either fix alone still passed — each is independently sufficient
there, so shipping both with only that document as evidence would have left one asserted by
nothing.
A new drill section builds both container shapes rather than relying on a fixture, so it runs on
every document CI drills. The oracle is
OuterXml, not a node count — it catches whitespace thatchanged value as well as whitespace that went missing.
HasTextContentback to "any text child"a container holding CDATA on a line of its own is still laid out as a block,the insert brings its own indentation (7 -> 8, want +2)insert then delete leaves the inline container byte for byte (4 of 5 children)— the net −1, by nameAll five CI fixtures pass (405 / 409 / 396 / 383 / 388), as do both minimal documents from the
issue (381 each) —
withcdata.xmlfailed before this change and passes now.Also done, as the issue asked
The two sections that shared the message
the document is back to its original childrennow saywhich one failed and print the counts. "was 19, now 18" is what made this diagnosable in the
first place, and the drill's output is the only diagnostic a PTY run leaves behind.
One correction to the issue
It cites
src/Application/Samples/basket.xmlas failing the same way. That file no longerexists — it went with the WinForms tree in
1ad7cf6— so that half of the evidence cannot bereproduced from the repo. The minimal documents in the issue still reproduce it exactly.
XmlFormatWriterhas its ownHasTextContentand is deliberately untouched: it governspretty-print reflow, not the whitespace-preserving edit path, and changing it would move saved
bytes.
🤖 Generated with Claude Code
https://claude.ai/code/session_016X4pgrsMBcsXjqWDo2gBBt