fix: clip CJK split rows by terminal cell width#287
fix: clip CJK split rows by terminal cell width#287iamken1204 wants to merge 2 commits intomodem-dev:mainfrom
Conversation
Measure and slice diff text by terminal cell width so long CJK lines stay within split panes after mouse-wheel repaints. Add unit coverage for Chinese, Japanese, and Korean width handling plus a PTY regression for untracked Mandarin-only content in split nowrap mode.
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.Reserve the hidden half-cell when horizontal scrolling starts inside a fullwidth glyph so later spans do not shift left. Add unit coverage for the mid-glyph CJK offset boundary.
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
