Repository navigation
fix(diff): keep each copy of a whole-replaced wrapper's comment intact (#1157) - #1169
Merged
Merged
Conversation
#1157) When a comparison replaces a w:sdt or w:customXml wrapper whole and a comment range sits between the wrapper's blocks, NormalizeComments treated the bare range markers between blocks as surviving both views and collapsed the comment to one marker of each kind across the deleted and inserted copies. Accepting then displaced the comment reference into a body-level paragraph before the wrapper. A marker's revision view is now read from its nearest tracked context: a w:ins/w:del around it, or a block wrapper emitted as a whole deleted or inserted copy (detected from the customXml range envelope that StructuredRevisionOps places). Phases A, A2 and B share that classifier, so such a comment skips the collapse and is deduplicated: the deleted copy gets a fresh id and a cloned definition. Range markers directly inside a w:customXml wrapper are now reconciled as well.
…clones (#1157) Review follow-ups to the whole-replaced-wrapper comment fix: - Only a block-level w:customXml joins the comment normalizer's run-level allow-list. The fine path emits an inline w:customXml whole inside one w:ins/w:del, so its markers already toggle with that copy and cannot be lifted out; collapsing them broke the range in both views. - Phase B's threading clones now re-point a cloned reply's paraIdParent at its parent's clone when the parent was cloned too, and give each cloned commentsIds entry a fresh durableId instead of repeating the original's. - The CHANGELOG and design doc name the shapes that remain inexact (#1167) accurately, including how they differ from before for customXml.
This was referenced Oct 16, 2026
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 #1157
First of a three-PR stack for the comparison engine: this one (#1157), then #1156, then #1149. All three change how the redline renderer reconciles markers, so they are stacked to keep them from conflicting.
Why
When a comparison replaces a block wrapper (a
w:sdtcontent control or a block-levelw:customXml) whole, the redline holds a deleted copy and an inserted copy of it. When a comment range sat between the wrapper's blocks, accepting the redline moved the comment's reference out of the wrapper into a paragraph of its own before it, and left an empty paragraph inside. Reject was exact.How it works
NormalizeComments(inIrMarkupRenderer) reconciles comment markers after rendering. Its first phase collapses a comment that both documents share down to one surviving start, end and reference, but only if those markers survive in both the accept and the reject view. It judged that by looking for aw:ins/w:delancestor. Markers between a replaced wrapper's blocks have no such ancestor. They are one-sided only because the wrapper copy itself is tracked (with Word'scustomXmlDelRange/customXmlInsRangeenvelope markers). So the comment was treated as unchanged, and the survivors were taken from different copies.CommentMarkerView. It says which view a marker belongs to: deleted, inserted, or both. The nearest tracked ancestor decides, whether that is aw:ins/w:delor a wrapper emitted as a whole deleted or inserted copy. Recognising that wrapper shape lives next to the code that emits it (StructuredRevisionOps.TrackedWrapperIsDeleted).main.w15:paraIdParentnow points at the cloned parent, and each clone gets a freshw16cid:durableId.w:customXmlwere never reconciled at all, which left duplicate range ids. They are now reconciled. Inline (run-level)w:customXmlis deliberately left alone, because the fine diff emits it whole inside onew:ins/w:del, so its markers already follow that copy.Validation
BlockWrapperNestedDiffTests:w:sdtandw:customXml; plain wrapper, nested wrapper, and a table cell inside the wrapper; each with and without the wrapper inside a body table cell. Each row checks the block-for-block round trip, including where the reference lands. It also checks that each copy keeps a complete range and reference under its own id resolving to the comment's text, and that no new validator errors appear. All 12 fail onmain.w:customXml: each view keeps a whole range.w:customXmlallow-list, the inline exclusion, the parent remapping, and the fresh durable ids.Compare_MarkerBetweenTheWrappersBlocks_StaysInTheRedlinechecked only the reject side for its comment rows and cited this issue. It now runs the full round trip, as its bookmark rows already did.What is still wrong (tracked in #1167)
main: forw:sdt, both shapes behave as onmain. Forw:customXml,mainkept both copies' markers under one duplicated id. The redline is now well formed, but these two shapes lose the range or the reference's position.Adversarial review
One round of hostile review ran before opening. It confirmed the fix for the block wrapper shapes and for Consolidate, and found three problems, all fixed in the second commit:
w:customXmlcomment ranges. Widening the allow-list had broken both views; it is now limited to block-level wrappers.main.It also checked that property-level revisions, moves, nested wrappers, and wholly added or deleted wrappers classify correctly.