Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ All notable user-visible changes to Hunk are documented in this file.

### Fixed

- Preserved split diff alignment when horizontal scrolling starts inside a wide CJK or emoji character.
- Made diff syntax highlighting follow the active theme: keyword, function, number, and variable token colors now resolve to the configured theme's palette (including custom themes) in both light and dark, instead of passing through Pierre's built-in syntax colors.
- Expanded the diff window during rapid scrolling bursts so large reviews keep real rows mounted instead of falling back to blank placeholder regions.

Expand Down
5 changes: 5 additions & 0 deletions src/ui/lib/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export function sliceTextByWidth(text: string, offset: number, width: number) {
continue;
}
if (clusterStart < startOffset) {
const hiddenCellWidth = Math.min(clusterEnd, startOffset + maxWidth) - startOffset;
if (hiddenCellWidth > 0) {
visibleText += " ".repeat(hiddenCellWidth);
usedWidth += hiddenCellWidth;
}
continue;
}
if (usedWidth + clusterWidth > maxWidth) {
Expand Down
4 changes: 3 additions & 1 deletion src/ui/lib/ui-lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,9 @@ describe("ui helpers", () => {
test("text helpers measure and slice wide characters by terminal cells", () => {
expect(measureTextWidth("日本語")).toBe(6);
expect(sliceTextByWidth("a日本b", 1, 4)).toEqual({ text: "日本", width: 4 });
expect(sliceTextByWidth("a日本b", 2, 4)).toEqual({ text: "本b", width: 3 });
expect(sliceTextByWidth("a日本b", 2, 4)).toEqual({ text: " 本b", width: 4 });
expect(sliceTextByWidth("日本b", 3, 3)).toEqual({ text: " b", width: 2 });
expect(sliceTextByWidth("日", 1, 1)).toEqual({ text: " ", width: 1 });
expect(fitText("日本語", 5)).toBe("日本.");
expect(measureTextWidth(padText("日本", 6))).toBe(6);
});
Comment on lines 314 to 322

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test coverage thin for adjacent and viewport-spanning wide-char splits

The new regression only covers the case where offset lands in the second cell of the first wide character. Two additional edge cases are worth asserting:

  1. Consecutive wide chars, offset in the second character — e.g. sliceTextByWidth("日本b", 3, 3) where the offset lands inside (cells 2–3). Expected: { text: " b", width: 2 }.
  2. Wide character that spans the entire viewport — e.g. a 4-cell emoji at position 0, offset=1, width=2. The entire result should be two spaces: { text: " ", width: 2 }. This exercises the Math.min(clusterEnd, startOffset + maxWidth) cap path that is currently unreachable by any test.

Without these, a future change that accidentally removes the Math.min cap or breaks multi-cluster accumulation would still pass the test suite.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/lib/ui-lib.test.ts
Line: 314-320

Comment:
**Test coverage thin for adjacent and viewport-spanning wide-char splits**

The new regression only covers the case where `offset` lands in the second cell of the first wide character. Two additional edge cases are worth asserting:

1. **Consecutive wide chars, offset in the second character** — e.g. `sliceTextByWidth("日本b", 3, 3)` where the offset lands inside `` (cells 2–3). Expected: `{ text: " b", width: 2 }`.
2. **Wide character that spans the entire viewport** — e.g. a 4-cell emoji at position 0, offset=1, width=2. The entire result should be two spaces: `{ text: "  ", width: 2 }`. This exercises the `Math.min(clusterEnd, startOffset + maxWidth)` cap path that is currently unreachable by any test.

Without these, a future change that accidentally removes the `Math.min` cap or breaks multi-cluster accumulation would still pass the test suite.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the adjacent-wide-character case (sliceTextByWidth("日本b", 3, 3)) and a viewport-cap case (sliceTextByWidth("日", 1, 1)). I used a 2-cell wide glyph for the cap path because our terminal width model treats emoji grapheme clusters as width 2, not 4.

Responded by Pi using OpenAI GPT-5.

This comment was generated by Pi using OpenAI GPT-5

Expand Down
Loading