From 96f93ec79d299aec6fceb71e878ad6d3a5df896d Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 9 Oct 2026 08:08:31 +0200 Subject: [PATCH] Show the diff view's narrow selection bar on the right edge too This is an improvement for users who use a side-by-side diff renderer on a very large screen; in this scenario it was hard to see which hunk on the right side was selected. --- pkg/gocui/view.go | 22 +++++++++++++--------- pkg/gocui/view_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index fb22ca13c..784f04aab 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -174,9 +174,11 @@ type View struct { HighlightInactive bool // If SelectedLineColorWidth is greater than zero, a highlighted line is painted - // in the selection colors on that many columns at its left edge only, rather - // than across its whole width, leaving the line's own colors to show through. - // For content that conveys meaning by color of its own. + // in the selection colors on that many columns at its left and right edges only, + // rather than across its whole width, leaving the line's own colors to show + // through. For content that conveys meaning by color of its own. A double-width + // character that reaches into these columns is painted whole, so the colored part + // can be a column wider. SelectedLineColorWidth int // InclusionGutterMarker is the glyph the inclusion gutter draws on a marked line @@ -936,8 +938,8 @@ func (v *View) InclusionGutterWidthWhenShown() int { // setCharacter sets a character (grapheme cluster) at the given point relative to the view. It applies // the specified colors, taking into account if the cell must be highlighted. Also, it checks if the -// position is valid. -func (v *View) setCharacter(x, y int, ch string, fgColor, bgColor Attribute, isWindowFocused bool) { +// position is valid. width is the number of columns the character takes up. +func (v *View) setCharacter(x, y int, ch string, width int, fgColor, bgColor Attribute, isWindowFocused bool) { maxX, maxY := v.Size() if x < 0 || x >= maxX || y < 0 || y >= maxY { return @@ -957,7 +959,8 @@ func (v *View) setCharacter(x, y int, ch string, fgColor, bgColor Attribute, isW } colorWidth := v.SelectedLineColorWidth - if y >= rangeSelectStart && y <= rangeSelectEnd && (colorWidth == 0 || x < colorWidth) { + if y >= rangeSelectStart && y <= rangeSelectEnd && + (colorWidth == 0 || x < colorWidth || x+width > v.InnerWidth()-colorWidth) { fgColor = applySelTextColor(fgColor, v.SelTextColor) if v.HighlightInactive || !isWindowFocused { bgColor = (bgColor & AttrStyleBits) | v.InactiveViewSelBgColor @@ -1869,10 +1872,11 @@ func (v *View) draw(isWindowFocused bool) { // content begins after it. The blanks go through setCharacter like everything // else, so that a selection reaching the left edge covers the gutter too. for gx := range gutterWidth { - v.setCharacter(gx, y, " ", v.FgColor, v.BgColor, isWindowFocused) + v.setCharacter(gx, y, " ", 1, v.FgColor, v.BgColor, isWindowFocused) } if gutterWidth > 0 && vline.linesY < len(v.inclusionGutterMarks) && v.inclusionGutterMarks[vline.linesY] { - v.setCharacter(0, y, v.InclusionGutterMarker, v.InclusionGutterMarkerColor, v.BgColor, isWindowFocused) + v.setCharacter(0, y, v.InclusionGutterMarker, uniseg.StringWidth(v.InclusionGutterMarker), + v.InclusionGutterMarkerColor, v.BgColor, isWindowFocused) } // x tracks the current x position in the view, and cellIdx tracks the @@ -1925,7 +1929,7 @@ func (v *View) draw(isWindowFocused bool) { fgColor |= AttrUnderline } - v.setCharacter(x, y, c.chr, fgColor, bgColor, isWindowFocused) + v.setCharacter(x, y, c.chr, c.width, fgColor, bgColor, isWindowFocused) x += c.width cellIdx++ diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index bd0aabd40..7daf10ced 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -1139,6 +1139,30 @@ func TestLineFlashReversesTheSelectionBarWithoutChangingSelection(t *testing.T) assert.False(t, style.HasReverse(), "clearing should remove the flash") } +// The selection bar on the right edge covers a double-width character that reaches +// into it, like the bar on the left edge does. +func TestSelectionBarOnTheRightEdgeCoversADoubleWidthCharacter(t *testing.T) { + WithSimulationScreen(t, 14, 6) + + v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10 + v.Highlight = true + v.SelBgColor = ColorBlue + v.SelectedLineColorWidth = 2 + + // に takes view columns 7 and 8, and the bar the last two, 8 and 9. + v.writeString("abcdefgにj\n") + v.draw(true) + + // The frame puts view column x at screen column x+1. + highlighted := func(x int) bool { + _, style, _ := Screen.Get(x+1, 1) + return style.GetBackground() != tcell.ColorDefault + } + assert.False(t, highlighted(6)) + assert.True(t, highlighted(7), "に reaches into the bar") + assert.True(t, highlighted(9)) +} + // Resizing a view throws away the wrapping of its content and wraps it again for // the new width, which moves every line of it to a different view line. The // positions into the view count view lines, so they all have to come along.