From bab90de2fd0099aa37897f693101fe228bac692a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 9 Oct 2026 11:54:26 +0200 Subject: [PATCH 1/2] Add tests for double-width characters at the edges of a view If a double-width character starts in the last column of a view that doesn't wrap, its second half is drawn over the view's frame. If one is scrolled halfway out to the left, the column it still reaches into isn't drawn at all. This column then has neither the selection highlight nor the line's background. Both happen in every view that doesn't wrap: the side panels, and the main view with gui.wrapLinesInDiffView set to false. Co-Authored-By: Claude Opus 5.5 (1M context) --- pkg/gocui/view_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index 5731d853e..8fbda4911 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -1166,6 +1166,54 @@ func TestResizingAWrappingViewKeepsItsPlaceInTheContent(t *testing.T) { assert.Equal(t, []string{"another wrapping line"}, v.SelectedLines()) } +// A double-width character that starts in the last column doesn't fit, and shows as a +// blank there rather than reaching into the frame. +func TestDoubleWidthCharacterAtTheRightEdge(t *testing.T) { + WithSimulationScreen(t, 14, 6) + + v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10 + + v.writeString("abcdefghiプ\n") + v.draw(true) + + // The frame puts view column 9 at screen column 10. + chr, _, width := Screen.Get(10, 1) + /* EXPECTED: + assert.Equal(t, " ", chr) + assert.Equal(t, 1, width) + ACTUAL: */ + assert.Equal(t, "プ", chr) + assert.Equal(t, 2, width) +} + +// A double-width character that is scrolled halfway out to the left shows as a blank +// in the column it still reaches into, so that the column is drawn like every other. +func TestDoubleWidthCharacterCutOffAtTheLeftEdge(t *testing.T) { + WithSimulationScreen(t, 14, 6) + + v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10 + v.Highlight = true + v.SelBgColor = ColorBlue + + v.writeString("にプ\n") + v.SetOriginX(1) + v.draw(true) + + // The frame puts view column 0 at screen column 1. + chr, style, _ := Screen.Get(1, 1) + assert.Equal(t, " ", chr) + assert.Equal(t, + /* EXPECTED: + true, + ACTUAL: */ + false, + style.GetBackground() != tcell.ColorDefault, + "the selection covers the column") + + chr, _, _ = Screen.Get(2, 1) + assert.Equal(t, "プ", chr) +} + // The inclusion gutter reserves columns at the left of every line, draws its marker // on the marked lines only, and moves the content out of the way. func TestInclusionGutter(t *testing.T) { From e314ad884a1cca75ad3de3f9986ccb14d0003593 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 9 Oct 2026 11:57:33 +0200 Subject: [PATCH 2/2] Draw a blank for a double-width character cut off at the edge of a view A double-width character that starts in the last column of a view is drawn over the view's frame. One that is scrolled halfway out to the left is skipped, and the column it reaches into isn't drawn at all. The draw loop clips cells against the edges of the view as if every cell were one column wide. A terminal can't show half of a double-width character. So draw a blank in the column that is on screen, in the character's colors, and let the selection highlight cover that column like any other. Co-Authored-By: Claude Opus 5.5 (1M context) --- pkg/gocui/view.go | 24 +++++++++++++++++------- pkg/gocui/view_test.go | 7 ------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index b195768dc..ec20ab429 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -1878,13 +1878,7 @@ func (v *View) draw(isWindowFocused bool) { var c cell for x < maxX { - if x < 0 { - if cellIdx < len(vline.line) { - x += uniseg.StringWidth(vline.line[cellIdx].chr) - cellIdx++ - continue - } - + if x < 0 && cellIdx >= len(vline.line) { // no more characters to write so we're only going to be printing empty cells // past this point x = gutterWidth @@ -1897,6 +1891,22 @@ func (v *View) draw(isWindowFocused bool) { c = vline.line[cellIdx] } + // Skip the cells scrolled out to the left. + if x < 0 && x+c.width <= 0 { + x += c.width + cellIdx++ + continue + } + + // A terminal can't show half of a double-width character, so one that is cut + // off by the left or right edge shows as a blank in the column of it that is + // on screen. + if x < 0 || x+c.width > maxX { + c.chr = " " + c.width = 1 + x = max(x, 0) + } + fgColor := c.fgColor if fgColor == ColorDefault { fgColor = v.FgColor diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index 8fbda4911..d5eed55ac 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -1178,12 +1178,8 @@ func TestDoubleWidthCharacterAtTheRightEdge(t *testing.T) { // The frame puts view column 9 at screen column 10. chr, _, width := Screen.Get(10, 1) - /* EXPECTED: assert.Equal(t, " ", chr) assert.Equal(t, 1, width) - ACTUAL: */ - assert.Equal(t, "プ", chr) - assert.Equal(t, 2, width) } // A double-width character that is scrolled halfway out to the left shows as a blank @@ -1203,10 +1199,7 @@ func TestDoubleWidthCharacterCutOffAtTheLeftEdge(t *testing.T) { chr, style, _ := Screen.Get(1, 1) assert.Equal(t, " ", chr) assert.Equal(t, - /* EXPECTED: true, - ACTUAL: */ - false, style.GetBackground() != tcell.ColorDefault, "the selection covers the column")