fix: clip CJK split rows by terminal cell width#287
Conversation
Greptile SummaryThis PR fixes split-view rendering for long CJK lines by replacing UTF-16
Confidence Score: 3/5Safe to merge for the reported vertical-overflow bug, but horizontal scrolling of CJK lines can produce misaligned output due to an unaddressed edge case in sliceSpansWindow. When a user horizontally scrolls a CJK line to an offset that falls inside a 2-cell wide character, sliceTextByTerminalCells correctly skips the glyph but returns clipped=false and width=0, giving sliceSpansWindow no signal to deduct the blank right-half cell from its remaining budget. The next span's content then fills that blank cell, shifting everything one cell to the left. This happens any time the horizontal scroll offset is odd and the character at that boundary is a fullwidth CJK glyph. src/ui/lib/text.ts (sliceTextByTerminalCells, lines 108-113) and src/ui/diff/renderRows.tsx (sliceSpansWindow, the text.length===0 / continue branch) together own the edge case. All test and harness files look correct. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[diff line spans] --> B[sliceSpansWindow\noffset, width]
B --> C{remainingOffset\n>= spanWidth?}
C -- yes --> D[skip span entirely\nsubtract spanWidth]
D --> B
C -- no --> E[sliceTextByTerminalCells\nspan.text, offset, remaining]
E --> F{clipped?}
F -- yes --> G[append slice\nbreak outer loop]
F -- no --> H{text empty?}
H -- yes, clipped+offset=0 --> I[break: wide char\ndoes not fit]
H -- yes, otherwise --> J[continue to next span\n⚠️ blank right-half\nnot deducted from remaining]
H -- no --> K[append slice\nsubtract textWidth from remaining]
K --> B
G --> L[renderInlineSpans\npad to full width]
I --> L
J --> B
L --> M[React spans output]
Prompt To Fix All With AIFix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
src/ui/lib/text.ts:108-113
**Skipped wide-char right-half not deducted from remaining window**
When a wide character straddles `windowStart` (e.g., offset=1, first char is 2-cell "中"), the function skips the glyph and returns `{ clipped: false, text: "", width: 0 }`. The caller (`sliceSpansWindow`) sees `clipped=false` and moves on to the next span with `remaining` unchanged, which causes the next span's first character to be rendered at the blank position that the skipped glyph's right half should occupy. Concretely: with spans `["中", "文"]` and `offset=1, width=3`, the output will be `"文"` rather than a 1-cell blank followed by nothing — every character in a CJK-heavy line appears one cell too far to the left whenever the horizontal scroll offset lands inside a wide glyph.
The blank right-half cells consumed by the skipped glyph need to be deducted from `remaining` (or reported back to the caller) so that `sliceSpansWindow` does not pull in content from subsequent spans to fill them.
Reviews (1): Last reviewed commit: "fix(ui): clip CJK split rows by cell wid..." | Re-trigger Greptile |
| // If the requested window starts in the middle of a fullwidth glyph, omit that glyph entirely. | ||
| if (cellCursor < windowStart) { | ||
| cellCursor = nextCellCursor; | ||
| includedPreviousVisibleCodePoint = false; | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Skipped wide-char right-half not deducted from remaining window
When a wide character straddles windowStart (e.g., offset=1, first char is 2-cell "中"), the function skips the glyph and returns { clipped: false, text: "", width: 0 }. The caller (sliceSpansWindow) sees clipped=false and moves on to the next span with remaining unchanged, which causes the next span's first character to be rendered at the blank position that the skipped glyph's right half should occupy. Concretely: with spans ["中", "文"] and offset=1, width=3, the output will be "文" rather than a 1-cell blank followed by nothing — every character in a CJK-heavy line appears one cell too far to the left whenever the horizontal scroll offset lands inside a wide glyph.
The blank right-half cells consumed by the skipped glyph need to be deducted from remaining (or reported back to the caller) so that sliceSpansWindow does not pull in content from subsequent spans to fill them.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/lib/text.ts
Line: 108-113
Comment:
**Skipped wide-char right-half not deducted from remaining window**
When a wide character straddles `windowStart` (e.g., offset=1, first char is 2-cell "中"), the function skips the glyph and returns `{ clipped: false, text: "", width: 0 }`. The caller (`sliceSpansWindow`) sees `clipped=false` and moves on to the next span with `remaining` unchanged, which causes the next span's first character to be rendered at the blank position that the skipped glyph's right half should occupy. Concretely: with spans `["中", "文"]` and `offset=1, width=3`, the output will be `"文"` rather than a 1-cell blank followed by nothing — every character in a CJK-heavy line appears one cell too far to the left whenever the horizontal scroll offset lands inside a wide glyph.
The blank right-half cells consumed by the skipped glyph need to be deducted from `remaining` (or reported back to the caller) so that `sliceSpansWindow` does not pull in content from subsequent spans to fill them.
How can I resolve this? If you propose a fix, please make it concise.eb2cde9 to
f683fd2
Compare
f683fd2 to
1ac4bc8
Compare
Split-pane diff rendering measured text by code-point count rather than terminal cell width, so fullwidth CJK content broke in two ways: - Long CJK lines overflowed the split pane after mouse-wheel repaints. Measure and slice diff text by terminal cell width so rows stay clipped to the pane. - Horizontal scrolling that began inside a fullwidth glyph shifted later spans left by half a cell. Reserve the hidden half-cell so alignment is preserved. Add unit coverage for Chinese, Japanese, and Korean width handling and the mid-glyph scroll-offset boundary, a scroll-regression test, and a PTY regression for untracked Mandarin-only content in split nowrap mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1ac4bc8 to
ae362a7
Compare
Fixes split-view rendering for long CJK lines, especially untracked files with Mandarin content, where mouse-wheel repainting could let wide characters spill past the right split pane into the left side.
Changes:
Before

After
