fix(core/text-buffer-view): fill partially wrapped lines across chunk boundaries - #1330
Open
3aKHP wants to merge 1 commit into
Open
fix(core/text-buffer-view): fill partially wrapped lines across chunk boundaries#13303aKHP wants to merge 1 commit into
3aKHP wants to merge 1 commit into
Conversation
… boundaries Rope edits split chunks at the edit offset. In the word-mode virtual line walker, when an unbreakable chunk arrived at a partially filled line and no wrap break had been recorded on that line, the final else branch committed the virtual line unconditionally — treating the chunk boundary as a wrap point. After a mid-line edit of an already wrapped line this anchored the first wrap boundary at the edit offset and left the row permanently truncated (e.g. 100 x 'a' at wrap width 56, insert one char at col 20: 3 virtual lines with row 1 ending at col 21 instead of 2 full-width rows). A chunk boundary is not a wrap point. The branch now hard-fills the remaining space on the current line from the chunk via findWrapPosByWidth (mirroring the line_position == 0 path), committing only when the line is already full or when the next grapheme does not fit the remaining space. Segmentation after edits is now identical to a fresh setText of the same content; word-wrap backtracking via last_wrap_* (anomalyco#1078 machinery) is untouched since this branch only runs when no break was recorded on the current line. Fixes anomalyco#1288
3aKHP
requested review from
Hona,
kommander,
msmps and
simonklee
as code owners
August 4, 2026 11:13
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.
Fixes #1288
Summary
With
wrapMode="word", inserting into the middle of an already soft-wrapped logical line anchors the first wrap boundary at the edit offset. The affected row stays truncated — e.g.initialValue = "a" * 100at wrap width 56, insert"X"at col 20: the virtual line count becomes 3 with row 1 ending at col 21, where a freshsetTextof the same 101 characters yields 2 full-width rows. Reproduction is in #1288.Root cause
This is not stale caching — all wrapping is recomputed from scratch per update by
calculateVirtualLinesGeneric. It is deterministic chunk-segmentation divergence:["a"*20]["X"]["a"*80]instead of one chunk.text-buffer-view.zig), when a chunk arrives at a partially filled line and no wrap break has been recorded on that line (remaining_in_chunk > remaining_on_line, nolast_wrap_that_fits,last_wrap_chunk_count == 0), the final else branch calledcommitVirtualLine()unconditionally — treating the chunk boundary as a wrap point — then filled the next line at full width.line_position == 0hard-fill branch instead and wraps correctly. Hence "only broken after editing".Fix
One hunk in the else branch: a chunk boundary is not a wrap point. Hard-fill the remaining space on the current line from the chunk via
utf8.findWrapPosByWidth(remaining_bytes, remaining_on_line, ...), mirroring theline_position == 0path. Commit only when:remaining_on_line == 0), orcolumns_used == 0, e.g. 1 column left and a width-2 CJK grapheme next) — then the short row commits and the next iteration hard-fills a fresh line. The force-one-grapheme guard is kept only for the full-line case, exactly as before; applying it mid-line would have split wide graphemes across rows and changed single-chunk behavior too (single-chunk walks also reach this branch after the first hard-filled row).All other branches — fit,
last_wrap_that_fits,line_position == 0, and thelast_wrap_chunk_count > 0backtrack (#1078 machinery) — are byte-identical. This branch only runs when no wrap break was recorded on the current line, so word-wrap backtracking is unaffected.Intended behavior change: an unbreakable run arriving at a partially filled line no longer pushes to the next row. Example: chunks
["hello"]["x"*100]at wrap 56 (e.g. insert"hello"at offset 0 of an x-run) — old: rows[5, 56, 44]; new:[56, 49], identical to a freshsetTextof the same content. Edge behavior preserved: a wide grapheme that doesn't fit the remaining space still wraps whole (["a"*54]["世"*20]at wrap 55 →[54, 40], matching single-chunk behavior before and after; pinned by test).Tests
11 new tests in
packages/core/src/zig/tests/word-wrap-editing_test.zig(file already registered intest.zig). The central oracle isexpectSegmentationMatchesFreshSet: read the edited buffer's full text,setTextit into a fresh buffer + view with the same wrap mode/width, and assert identical virtual-line segmentation (count,source_line,source_col_offset,width_cols) — segmentation must depend only on text content, never on edit history.expected 2, found 3pre-fix)Each was run individually and fails pre-fix except the two behavior guards.
zig build test: 1877 passed, 6 skipped, 0 failed.zig fmtclean.wrapMode="char"/"none"untouched — the change is entirely inside the word-mode arm and their existing tests pass unchanged.Performance
No asymptotic change: per loop iteration the work is identical — one
findWrapPosByWidthscan over the same bytes, only the width argument differs, and the trackedbyte_offsetinvariant (the O(n²) guard) holds. A commit is removed from the common path; the new commit-and-continue is bounded by the number of virtual lines.zig build bench("TextBuffer Wrapping", word wrap avg, before → after): multi-line w40 27.69→18.60 ms, w80 21.52→15.00 ms, w120 17.44→13.19 ms; single-line w40 28.29→25.18 ms, w80 27.74→26.00 ms, w120 26.34→27.22 ms — no regression (differences are machine noise in our favor).