diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index e8947a665..77492b9b1 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -444,12 +444,28 @@ type SearchPosition struct { type viewLine struct { linesX, linesY int // coordinates relative to v.lines line []cell + + // Colors used to extend the bg past this wrapped segment's content. + // Derived at wrap time from the source line — see refreshViewLinesIfNeeded + // for the per-segment rule. + trailingFillAttributes *trailingFillAttributes } -// lineType is one of v.lines: the cells of a source lineType, plus any per-lineType -// metadata about how it was terminated (added in later commits). +// lineType is one of v.lines: the cells of a source line, plus optional +// trailingFillAttributes recording the colors used to extend the bg +// past the line's content when the writer emitted '\x1b[K'. type lineType struct { - cells cells + cells cells + trailingFillAttributes *trailingFillAttributes +} + +// trailingFillAttributes describes the fg/bg colors that draw() should +// use for cells past the end of a wrapped segment's content. On a source +// line this records what the writer asked for via '\x1b[K' (and so opts +// the line in to trailing fill at all); the per-segment values on each +// viewLine are derived from it at wrap time. +type trailingFillAttributes struct { + fg, bg Attribute } type cell struct { @@ -953,16 +969,19 @@ func (v *View) parseInput(ch []byte, width int, x int, _ int) (bool, []cell) { } else { repeatCount := 1 if _, ok := v.ei.instruction.(eraseInLineFromCursor); ok { - // fill rest of line + // Discard any old content past the cursor and record the + // fill colors so draw() paints the trailing area with them. + // This extends the bg to the right edge in both the + // content-fits and content-wraps cases — for the latter, + // the metadata is what reaches every wrapped segment past + // the last word. v.ei.instructionRead() - cx := 0 - for _, cell := range v.lines[v.wy].cells[0:v.wx] { - cx += cell.width - } - repeatCount = v.InnerWidth() - cx - ch = []byte{' '} - width = 1 truncateLine = true + v.lines[v.wy].trailingFillAttributes = &trailingFillAttributes{ + fg: v.ei.curFgColor, + bg: v.ei.curBgColor, + } + return truncateLine, []cell{} } else if isEscape { // do not output anything return truncateLine, nil @@ -1252,6 +1271,15 @@ func (v *View) draw() { break } + // Decide the colors used for cells past the end of vline.line: + // the source line's trailingFillAttributes (set by '\x1b[K') if + // any, otherwise plain defaults. + trailingCell := emptyCell + if attrs := vline.trailingFillAttributes; attrs != nil { + trailingCell.fgColor = attrs.fg + trailingCell.bgColor = attrs.bg + } + // x tracks the current x position in the view, and cellIdx tracks the // index of the cell. If we print a double-sized rune, we increment cellIdx // by one but x by two. @@ -1274,7 +1302,7 @@ func (v *View) draw() { // if we're out of cells to write, we'll just print empty cells. if cellIdx > len(vline.line)-1 { - c = emptyCell + c = trailingCell } else { c = vline.line[cellIdx] } @@ -1312,7 +1340,25 @@ func (v *View) refreshViewLinesIfNeeded() { ls := lineWrap(line.cells, wrap) for j := range ls { - vline := viewLine{linesX: j, linesY: i, line: ls[j]} + // Per-segment trailing fill. When the source line opted in + // via '\x1b[K', the LAST wrapped segment uses those colors + // directly; earlier segments use the colors of their own + // last cell, so the trailing area matches the bg active + // where that segment ended rather than bleeding the + // '\x1b[K' bg back across color changes in the line. + var attrs *trailingFillAttributes + if line.trailingFillAttributes != nil { + if j == len(ls)-1 { + attrs = line.trailingFillAttributes + } else if len(ls[j]) > 0 { + last := ls[j][len(ls[j])-1] + attrs = &trailingFillAttributes{fg: last.fgColor, bg: last.bgColor} + } + } + vline := viewLine{ + linesX: j, linesY: i, line: ls[j], + trailingFillAttributes: attrs, + } if lineIdx > len(v.viewLines)-1 { v.viewLines = append(v.viewLines, vline) diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index 9d0e9a4ef..f65418821 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -508,10 +508,10 @@ func TestShortFilledLineExtendsBgWithoutWrap(t *testing.T) { } } -// TestWrappedFilledLineExtendsBgToEdge demonstrates that when a line is +// TestWrappedFilledLineExtendsBgToEdge verifies that when a line is // filled to the edge with \x1b[K (the pattern used by `delta` for diff -// lines) but exceeds the view's inner width, every wrapped segment loses -// the fill background past its content. +// lines) but exceeds the view's inner width, every wrapped segment +// extends the fill background past its content to the right edge. func TestWrappedFilledLineExtendsBgToEdge(t *testing.T) { WithSimulationScreen(t, 14, 6) @@ -524,24 +524,18 @@ func TestWrappedFilledLineExtendsBgToEdge(t *testing.T) { // Content with spaces so word wrap ends each segment before the // right edge: "aaa bbb ccc ddd eee" wraps at InnerWidth=10 to three // segments — "aaa bbb" / "ccc ddd" / "eee". Each row's trailing area - // should pick up the red fill from \x1b[K but currently falls back to - // the view default bg. + // must pick up the red fill from \x1b[K. v.writeString("\x1b[41m" + "aaa bbb ccc ddd eee" + "\x1b[0m\x1b[41m\x1b[K\x1b[0m\n") v.draw() - // trailingFrom is 1-indexed: each row's content ends at column - // trailingFrom[y]-1, so columns trailingFrom[y]..10 are the trailing - // fill area where the bug shows. - trailingFrom := []int{8, 8, 4} + // All three wrapped rows should have the red fill background across + // the full InnerWidth, including the trailing cells past each row's + // last word. for y := 1; y <= 3; y++ { - for x := trailingFrom[y-1]; x <= 10; x++ { + for x := 1; x <= 10; x++ { _, style, _ := Screen.Get(x, y) - /* EXPECTED: assert.Equal(t, color.Maroon, style.GetBackground(), - "trailing cell at (%d, %d) should have red bg", x, y) - ACTUAL: */ - assert.Equal(t, tcell.ColorDefault, style.GetBackground(), - "trailing cell at (%d, %d) falls back to default bg", x, y) + "cell at (%d, %d) should have red bg", x, y) } } } @@ -572,12 +566,8 @@ func TestMulticolorWrappedFillUsesLastCellOfEachSegment(t *testing.T) { // 8..10 should pick up red rather than the \x1b[K's green. for x := 8; x <= 10; x++ { _, style, _ := Screen.Get(x, 1) - /* EXPECTED: assert.Equal(t, color.Maroon, style.GetBackground(), "trailing cell at (%d, 1) should have red bg (matching segment's last cell)", x) - ACTUAL: */ - assert.Equal(t, tcell.ColorDefault, style.GetBackground(), - "trailing cell at (%d, 1) falls back to default bg", x) } // Row 2's content ends with a green cell at x=3, so trailing @@ -585,11 +575,7 @@ func TestMulticolorWrappedFillUsesLastCellOfEachSegment(t *testing.T) { // last cell and the \x1b[K bg — these happen to agree here). for x := 4; x <= 10; x++ { _, style, _ := Screen.Get(x, 2) - /* EXPECTED: assert.Equal(t, color.Green, style.GetBackground(), "trailing cell at (%d, 2) should have green bg", x) - ACTUAL: */ - assert.Equal(t, tcell.ColorDefault, style.GetBackground(), - "trailing cell at (%d, 2) falls back to default bg", x) } }