fix: swallow a following decorator when a removal window reaches into it - #83
Merged
jjrodcast merged 1 commit intoJul 30, 2026
Conversation
Two holes let delete/replace leave decorator text stranded mid-line, where the export silently drops it and later inserts crash on the leaked offsets: - isLastDecoratorPartiallySelected (delete and replace multi-paragraph paths) passed the decorator piece's buffer offset into document-offset math, so once an item's decorator lived deep in the ADDED buffer the predicate went false and a window ending inside the decorator deleted it only partially. Rewritten in document coordinates. - The replace path had no equivalent of the delete path's extendPastOrphanedDecorator: replacing the line break between two items merged the lines but kept the second item's decorator. Mirrored the rule, with one difference — a non-empty replacement is itself a head for the merged line, so the extension also applies when the whole first line is replaced. Both re-use the existing partially-selected-decorator handling, so merges renumber remaining numbered items as before.
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes a corruption/crash class in multi-paragraph delete/replace operations where a removal window can partially consume (or stop exactly at) a following list item’s decorator, leaving decorator fragments mid-line and causing later offset-based edits to crash.
Changes:
- Compute
isLastDecoratorPartiallySelectedusing document offsets (not piece/buffer offsets) so partial-decorator swallowing works even when decorators live deep in the ADDED buffer. - Add
TextUpdateTransaction.extendPastOrphanedDecoratorto mirror the delete-path behavior: when a replace consumes a terminating line break and stops at the next list item start, extend one char into the decorator and re-dispatch so existing logic swallows/renumbers correctly. - Add
RemovalWindowIntoDecoratorTestcovering delete/replace windows reaching into decorators, break-replacement merges for bullet/task items, renumbering, and the downstream insert-crash regression.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| shared/src/commonTest/kotlin/com/jjrodcast/textkit/RemovalWindowIntoDecoratorTest.kt | Adds regression tests ensuring decorator fragments are not left mid-line after multi-paragraph delete/replace and that export/merge behavior remains stable. |
| shared/src/commonMain/kotlin/com/jjrodcast/textkit/editor/core/transactions/text/TextUpdateTransaction.kt | Extends replace windows into following decorators in the specific orphaning case, and fixes partial-decorator detection to use document coordinates. |
| shared/src/commonMain/kotlin/com/jjrodcast/textkit/editor/core/transactions/text/TextDeletedTransaction.kt | Fixes partial-decorator detection to use document coordinates, preventing missed “swallow decorator” behavior when buffer/document offsets diverge. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jjrodcast
approved these changes
Jul 30, 2026
Contributor
Author
|
@jjrodcast I think it can be merged |
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 #82
Two changes, both in the multi-paragraph removal paths:
isLastDecoratorPartiallySelectedis now computed in document coordinates (window end strictly inside the last item's decorator span, and only for list items). The previous form compared the decorator piece's buffer offset against document offsets, so the partial-decorator swallow stopped firing once decorators sat deep in the ADDED buffer.TextUpdateTransactiongets the replace-side mirror ofextendPastOrphanedDecorator(Deleting across a paragraph boundary up to a list item's decorator orphans the decorator in the live text #67/TextDeletedTransaction): a removal window that consumes the terminating line break and stops at the next item's start extends one char into the decorator and re-dispatches, so the existing handling swallows it whole and renumbers. One difference from the delete rule: a non-empty replacement is itself a head for the merged line, so the extension also applies when the whole first line is replaced (and not when the replacement itself ends with a line break).Tests:
RemovalWindowIntoDecoratorTest— 6 cases covering delete/replace windows ending inside a decorator, replacing the break between bulleted/task items, renumbering after the merge, and the downstream insert-crash. All 6 fail onmain. Full suite passes, JS/Wasm compile clean. In the seeded sweep (100 seeds × 250 ops) these two holes accounted for every remaining crash; none remain with the fix.