mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Fix coloring of wrapped delta lines (#5711)
When using delta as a custom pager, and the view is so narrow that delta needs to wrap long lines, only the last segment of such a wrapped line would have its background color extended to the right edge of the view, resulting in a block of lines with a frayed right edge, like this: <img width="623" height="218" alt="Screenshot 2026-06-18 at 17 25 36" src="https://github.com/user-attachments/assets/346156af-212f-44c3-b8aa-2a66c23ed836" /> Fix this by extending the wrapped line's background colors to the right edge as well, making it look like this: <img width="623" height="218" alt="Screenshot 2026-06-18 at 17 25 23" src="https://github.com/user-attachments/assets/9af46ba3-701b-47ce-b38d-bdffaf5182fa" />
This commit is contained in:
commit
c81c653c55
|
|
@ -1345,7 +1345,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
|||
}
|
||||
|
||||
visibleLineWidth := 0
|
||||
for _, c := range v.lines[newY] {
|
||||
for _, c := range v.lines[newY].cells {
|
||||
visibleLineWidth += c.width
|
||||
}
|
||||
if visibleLineWidth < newX {
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ const (
|
|||
// position.
|
||||
type View struct {
|
||||
name string
|
||||
x0, y0, x1, y1 int // left top right bottom
|
||||
ox, oy int // view offsets
|
||||
cx, cy int // cursor position
|
||||
rx, ry int // Read() offsets
|
||||
wx, wy int // Write() offsets
|
||||
lines [][]cell // All the data
|
||||
x0, y0, x1, y1 int // left top right bottom
|
||||
ox, oy int // view offsets
|
||||
cx, cy int // cursor position
|
||||
rx, ry int // Read() offsets
|
||||
wx, wy int // Write() offsets
|
||||
lines []lineType // All the data
|
||||
outMode OutputMode
|
||||
// The y position of the first line of a range selection.
|
||||
// This is not relative to the view's origin: it is relative to the first line
|
||||
|
|
@ -444,6 +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 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
|
||||
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 {
|
||||
|
|
@ -453,7 +475,7 @@ type cell struct {
|
|||
hyperlink string
|
||||
}
|
||||
|
||||
type lineType []cell
|
||||
type cells []cell
|
||||
|
||||
func characterEquals(chr []byte, b byte) bool {
|
||||
return len(chr) == 1 && chr[0] == b
|
||||
|
|
@ -464,7 +486,7 @@ func isCRLF(chr []byte) bool {
|
|||
}
|
||||
|
||||
// String returns a string from a given cell slice.
|
||||
func (l lineType) String() string {
|
||||
func (l cells) String() string {
|
||||
var str strings.Builder
|
||||
for _, c := range l {
|
||||
str.WriteString(c.chr)
|
||||
|
|
@ -738,20 +760,20 @@ func (v *View) makeWriteable(x, y int) {
|
|||
}
|
||||
v.lines = v.lines[:newLen]
|
||||
} else {
|
||||
v.lines = append(v.lines, nil)
|
||||
v.lines = append(v.lines, lineType{})
|
||||
}
|
||||
}
|
||||
// cell `x` need not be index-able (that's why `<`)
|
||||
// append should be used by `lines[y]` user if he wants to write beyond `x`
|
||||
for len(v.lines[y]) < x {
|
||||
if cap(v.lines[y]) > len(v.lines[y]) {
|
||||
newLen := cap(v.lines[y])
|
||||
for len(v.lines[y].cells) < x {
|
||||
if cap(v.lines[y].cells) > len(v.lines[y].cells) {
|
||||
newLen := cap(v.lines[y].cells)
|
||||
if newLen > x {
|
||||
newLen = x
|
||||
}
|
||||
v.lines[y] = v.lines[y][:newLen]
|
||||
v.lines[y].cells = v.lines[y].cells[:newLen]
|
||||
} else {
|
||||
v.lines[y] = append(v.lines[y], cell{})
|
||||
v.lines[y].cells = append(v.lines[y].cells, cell{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -761,7 +783,7 @@ func (v *View) makeWriteable(x, y int) {
|
|||
func (v *View) writeCells(cells []cell) {
|
||||
var newLen int
|
||||
// use maximum len available
|
||||
line := v.lines[v.wy][:cap(v.lines[v.wy])]
|
||||
line := v.lines[v.wy].cells[:cap(v.lines[v.wy].cells)]
|
||||
maxCopy := len(line) - v.wx
|
||||
if maxCopy < len(cells) {
|
||||
copy(line[v.wx:], cells[:maxCopy])
|
||||
|
|
@ -770,11 +792,11 @@ func (v *View) writeCells(cells []cell) {
|
|||
} else { // maxCopy >= len(cells)
|
||||
copy(line[v.wx:], cells)
|
||||
newLen = v.wx + len(cells)
|
||||
if newLen < len(v.lines[v.wy]) {
|
||||
newLen = len(v.lines[v.wy])
|
||||
if newLen < len(v.lines[v.wy].cells) {
|
||||
newLen = len(v.lines[v.wy].cells)
|
||||
}
|
||||
}
|
||||
v.lines[v.wy] = line[:newLen]
|
||||
v.lines[v.wy].cells = line[:newLen]
|
||||
v.wx += len(cells)
|
||||
}
|
||||
|
||||
|
|
@ -800,21 +822,13 @@ func (v *View) write(p []byte) {
|
|||
|
||||
finishLine := func() {
|
||||
v.autoRenderHyperlinksInCurrentLine()
|
||||
if v.wx >= len(v.lines[v.wy]) {
|
||||
v.writeCells([]cell{{
|
||||
chr: "",
|
||||
width: 0,
|
||||
fgColor: 0,
|
||||
bgColor: 0,
|
||||
}})
|
||||
}
|
||||
}
|
||||
|
||||
advanceToNextLine := func() {
|
||||
v.wx = 0
|
||||
v.wy++
|
||||
if v.wy >= len(v.lines) {
|
||||
v.lines = append(v.lines, nil)
|
||||
v.lines = append(v.lines, lineType{})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -851,7 +865,7 @@ func (v *View) write(p []byte) {
|
|||
}
|
||||
v.writeCells(cells)
|
||||
if truncateLine {
|
||||
v.lines[v.wy] = v.lines[v.wy][:v.wx]
|
||||
v.lines[v.wy].cells = v.lines[v.wy].cells[:v.wx]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -910,7 +924,7 @@ func (v *View) autoRenderHyperlinksInCurrentLine() {
|
|||
return
|
||||
}
|
||||
|
||||
line := v.lines[v.wy]
|
||||
line := v.lines[v.wy].cells
|
||||
start := 0
|
||||
for {
|
||||
linkStart := findLinkStart(line[start:])
|
||||
|
|
@ -927,7 +941,7 @@ func (v *View) autoRenderHyperlinksInCurrentLine() {
|
|||
link.WriteString(line[linkEnd].chr)
|
||||
}
|
||||
for i := linkStart; i < linkEnd; i++ {
|
||||
v.lines[v.wy][i].hyperlink = link.String()
|
||||
v.lines[v.wy].cells[i].hyperlink = link.String()
|
||||
}
|
||||
start = linkEnd
|
||||
}
|
||||
|
|
@ -955,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][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
|
||||
|
|
@ -1010,8 +1027,8 @@ func (v *View) Read(p []byte) (n int, err error) {
|
|||
v.readBuffer = nil
|
||||
}
|
||||
for v.ry < len(v.lines) {
|
||||
for v.rx < len(v.lines[v.ry]) {
|
||||
s := v.lines[v.ry][v.rx].chr
|
||||
for v.rx < len(v.lines[v.ry].cells) {
|
||||
s := v.lines[v.ry].cells[v.rx].chr
|
||||
count := len(s)
|
||||
copy(p[offset:], s)
|
||||
v.rx++
|
||||
|
|
@ -1175,9 +1192,9 @@ func (v *View) updateSearchPositions() {
|
|||
}
|
||||
|
||||
// If a view line exists for this line index:
|
||||
if v.lines[result.Y] != nil {
|
||||
if v.lines[result.Y].cells != nil {
|
||||
// search this view line for the search string
|
||||
positions := searchPositionsForLine(v.lines[result.Y], result.Y)
|
||||
positions := searchPositionsForLine(v.lines[result.Y].cells, result.Y)
|
||||
if len(positions) > 0 {
|
||||
// If we found any occurrences, add them
|
||||
v.searcher.searchPositions = append(v.searcher.searchPositions, positions...)
|
||||
|
|
@ -1248,13 +1265,21 @@ func (v *View) draw() {
|
|||
}
|
||||
|
||||
emptyCell := cell{chr: " ", width: 1, fgColor: ColorDefault, bgColor: ColorDefault}
|
||||
var prevFgColor Attribute
|
||||
|
||||
for y, vline := range v.viewLines[start:] {
|
||||
if y >= maxY {
|
||||
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.
|
||||
|
|
@ -1277,14 +1302,9 @@ 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.fgColor = prevFgColor
|
||||
c = trailingCell
|
||||
} else {
|
||||
c = vline.line[cellIdx]
|
||||
// capturing previous foreground colour so that if we're using the reverse
|
||||
// attribute we honour the final character's colour and don't awkwardly switch
|
||||
// to a new background colour for the remainder of the line
|
||||
prevFgColor = c.fgColor
|
||||
}
|
||||
|
||||
fgColor := c.fgColor
|
||||
|
|
@ -1318,9 +1338,27 @@ func (v *View) refreshViewLinesIfNeeded() {
|
|||
wrap = maxX
|
||||
}
|
||||
|
||||
ls := lineWrap(line, wrap)
|
||||
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)
|
||||
|
|
@ -1411,9 +1449,7 @@ func (v *View) BufferLines() []string {
|
|||
|
||||
lines := make([]string, len(v.lines))
|
||||
for i, l := range v.lines {
|
||||
str := lineType(l).String()
|
||||
str = strings.ReplaceAll(str, "\x00", "")
|
||||
lines[i] = str
|
||||
lines[i] = l.cells.String()
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
|
@ -1434,9 +1470,7 @@ func (v *View) ViewBufferLines() []string {
|
|||
|
||||
lines := make([]string, len(v.viewLines))
|
||||
for i, l := range v.viewLines {
|
||||
str := lineType(l.line).String()
|
||||
str = strings.ReplaceAll(str, "\x00", "")
|
||||
lines[i] = str
|
||||
lines[i] = cells(l.line).String()
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
|
@ -1458,12 +1492,12 @@ func (v *View) ViewLinesHeight() int {
|
|||
// ViewBuffer returns a string with the contents of the view's buffer that is
|
||||
// shown to the user.
|
||||
func (v *View) ViewBuffer() string {
|
||||
lines := make([][]cell, len(v.viewLines))
|
||||
strs := make([]string, len(v.viewLines))
|
||||
for i := range v.viewLines {
|
||||
lines[i] = v.viewLines[i].line
|
||||
strs[i] = cells(v.viewLines[i].line).String()
|
||||
}
|
||||
|
||||
return linesToString(lines)
|
||||
return strings.Join(strs, "\n")
|
||||
}
|
||||
|
||||
// Line returns a string with the line of the view's internal buffer
|
||||
|
|
@ -1478,7 +1512,7 @@ func (v *View) Line(y int) (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
return lineType(v.lines[y]).String(), true
|
||||
return v.lines[y].cells.String(), true
|
||||
}
|
||||
|
||||
// Word returns a string with the word of the view's internal buffer
|
||||
|
|
@ -1489,11 +1523,11 @@ func (v *View) Word(x, y int) (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y]) {
|
||||
if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y].cells) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
str := lineType(v.lines[y]).String()
|
||||
str := v.lines[y].cells.String()
|
||||
|
||||
nl := strings.LastIndexFunc(str[:x], indexFunc)
|
||||
if nl == -1 {
|
||||
|
|
@ -1523,9 +1557,8 @@ func (v *View) SetHighlight(y int, on bool) {
|
|||
return
|
||||
}
|
||||
|
||||
line := v.lines[y]
|
||||
cells := make([]cell, 0)
|
||||
for _, c := range line {
|
||||
cells := make([]cell, 0, len(v.lines[y].cells))
|
||||
for _, c := range v.lines[y].cells {
|
||||
if on {
|
||||
c.bgColor = v.SelBgColor
|
||||
c.fgColor = v.SelFgColor
|
||||
|
|
@ -1536,7 +1569,7 @@ func (v *View) SetHighlight(y int, on bool) {
|
|||
cells = append(cells, c)
|
||||
}
|
||||
v.tainted = true
|
||||
v.lines[y] = cells
|
||||
v.lines[y].cells = cells
|
||||
v.clearHover()
|
||||
}
|
||||
|
||||
|
|
@ -1602,17 +1635,10 @@ func lineWrap(line []cell, columns int) [][]cell {
|
|||
return lines
|
||||
}
|
||||
|
||||
func linesToString(lines [][]cell) string {
|
||||
func linesToString(lines []lineType) string {
|
||||
str := make([]string, len(lines))
|
||||
for i := range lines {
|
||||
rns := make([]rune, 0, len(lines[i]))
|
||||
line := lineType(lines[i]).String()
|
||||
for _, c := range line {
|
||||
if c != '\x00' {
|
||||
rns = append(rns, c)
|
||||
}
|
||||
}
|
||||
str[i] = string(rns)
|
||||
str[i] = lines[i].cells.String()
|
||||
}
|
||||
|
||||
return strings.Join(str, "\n")
|
||||
|
|
@ -1682,9 +1708,7 @@ func (v *View) SelectedLines() []string {
|
|||
}
|
||||
|
||||
func (v *View) lineContentAtIdx(idx int) string {
|
||||
line := v.lines[idx]
|
||||
str := lineType(line).String()
|
||||
return strings.ReplaceAll(str, "\x00", "")
|
||||
return v.lines[idx].cells.String()
|
||||
}
|
||||
|
||||
func (v *View) SelectedPoint() (int, int) {
|
||||
|
|
@ -1788,11 +1812,11 @@ func (v *View) OverwriteLinesAndClearEverythingElse(lineCount int, y int, conten
|
|||
v.overwriteLines(y, content)
|
||||
|
||||
for i := range y {
|
||||
v.lines[i] = nil
|
||||
v.lines[i] = lineType{}
|
||||
}
|
||||
|
||||
for i := v.wy + 1; i < len(v.lines); i += 1 {
|
||||
v.lines[i] = nil
|
||||
v.lines[i] = lineType{}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1935,7 +1959,7 @@ func (v *View) scrollMargin() int {
|
|||
// foreground color
|
||||
func (v *View) ContainsColoredText(fgColor string, text string) bool {
|
||||
for _, line := range v.lines {
|
||||
if containsColoredTextInLine(fgColor, text, line) {
|
||||
if containsColoredTextInLine(fgColor, text, line.cells) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,28 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/gdamore/tcell/v3/color"
|
||||
"github.com/rivo/uniseg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// WithSimulationScreen swaps the package-level Screen for a tcell
|
||||
// terminfo-backed mock terminal so tests can call view.draw() and
|
||||
// inspect rendered cells via Screen.Get(). The previous Screen is
|
||||
// restored on test cleanup.
|
||||
func WithSimulationScreen(t *testing.T, width, height int) {
|
||||
t.Helper()
|
||||
saved := Screen
|
||||
if err := (&Gui{}).tcellInitSimulation(width, height); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
Screen.Fini()
|
||||
Screen = saved
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteString(t *testing.T) {
|
||||
tests := []struct {
|
||||
existingLines []string
|
||||
|
|
@ -26,17 +44,17 @@ func TestWriteString(t *testing.T) {
|
|||
{
|
||||
[]string{},
|
||||
[]string{"1\n"},
|
||||
[][]string{{"1", ""}},
|
||||
[][]string{{"1"}},
|
||||
},
|
||||
{
|
||||
[]string{},
|
||||
[]string{"1\n", "2\n"},
|
||||
[][]string{{"1", ""}, {"2", ""}},
|
||||
[][]string{{"1"}, {"2"}},
|
||||
},
|
||||
{
|
||||
[]string{"a"},
|
||||
[]string{"1\n"},
|
||||
[][]string{{"1", ""}},
|
||||
[][]string{{"1"}},
|
||||
},
|
||||
{
|
||||
[]string{"a\x00"},
|
||||
|
|
@ -56,12 +74,12 @@ func TestWriteString(t *testing.T) {
|
|||
{
|
||||
[]string{},
|
||||
[]string{"1\r"},
|
||||
[][]string{{"1", ""}},
|
||||
[][]string{{"1"}},
|
||||
},
|
||||
{
|
||||
[]string{"a"},
|
||||
[]string{"1\r"},
|
||||
[][]string{{"1", ""}},
|
||||
[][]string{{"1"}},
|
||||
},
|
||||
{
|
||||
[]string{"a\x00"},
|
||||
|
|
@ -83,14 +101,14 @@ func TestWriteString(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
v := NewView("name", 0, 0, 10, 10, OutputNormal)
|
||||
for _, l := range test.existingLines {
|
||||
v.lines = append(v.lines, stringToCells(l))
|
||||
v.lines = append(v.lines, lineType{cells: stringToCells(l)})
|
||||
}
|
||||
for _, s := range test.stringsToWrite {
|
||||
v.writeString(s)
|
||||
}
|
||||
var resultingLines [][]string
|
||||
for _, l := range v.lines {
|
||||
resultingLines = append(resultingLines, cellsToStrings(l))
|
||||
resultingLines = append(resultingLines, cellsToStrings(l.cells))
|
||||
}
|
||||
assert.Equal(t, test.expectedLines, resultingLines)
|
||||
}
|
||||
|
|
@ -126,19 +144,19 @@ func TestAutoRenderingHyperlinks(t *testing.T) {
|
|||
|
||||
v.writeString("htt")
|
||||
// No hyperlinks are generated for incomplete URLs
|
||||
assert.Equal(t, "", v.lines[0][0].hyperlink)
|
||||
assert.Equal(t, "", v.lines[0].cells[0].hyperlink)
|
||||
// Writing more characters to the same line makes the link complete (even
|
||||
// though we didn't see a newline yet)
|
||||
v.writeString("ps://example.com")
|
||||
assert.Equal(t, "https://example.com", v.lines[0][0].hyperlink)
|
||||
assert.Equal(t, "https://example.com", v.lines[0].cells[0].hyperlink)
|
||||
|
||||
v.Clear()
|
||||
// Valid but incomplete URL
|
||||
v.writeString("https://exa")
|
||||
assert.Equal(t, "https://exa", v.lines[0][0].hyperlink)
|
||||
assert.Equal(t, "https://exa", v.lines[0].cells[0].hyperlink)
|
||||
// Writing more characters to the same fixes the link
|
||||
v.writeString("mple.com")
|
||||
assert.Equal(t, "https://example.com", v.lines[0][0].hyperlink)
|
||||
assert.Equal(t, "https://example.com", v.lines[0].cells[0].hyperlink)
|
||||
}
|
||||
|
||||
func TestContainsColoredText(t *testing.T) {
|
||||
|
|
@ -211,7 +229,11 @@ func TestContainsColoredText(t *testing.T) {
|
|||
}
|
||||
|
||||
for i, test := range tests {
|
||||
v := &View{lines: test.lines}
|
||||
lines := make([]lineType, len(test.lines))
|
||||
for j, cells := range test.lines {
|
||||
lines[j] = lineType{cells: cells}
|
||||
}
|
||||
v := &View{lines: lines}
|
||||
assert.Equal(t, test.expected, v.ContainsColoredText(test.fgColorStr, test.text), "Test %d failed", i)
|
||||
}
|
||||
}
|
||||
|
|
@ -413,3 +435,147 @@ func TestLineWrap(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewlineTerminatedLineClearsTrailingBg verifies that a '\n' resets
|
||||
// any attributes (e.g. AttrReverse-driven background) past the line's
|
||||
// content, so a reversed cell at the end doesn't bleed into the empty
|
||||
// area to the right.
|
||||
func TestNewlineTerminatedLineClearsTrailingBg(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 5)
|
||||
|
||||
v := NewView("name", 0, 0, 11, 4, OutputNormal)
|
||||
|
||||
// \x1b[7m sets reverse; \x1b[31m sets fg=red. With reverse the cell
|
||||
// renders with bg=red. The trailing area past "foo" must NOT extend
|
||||
// the red bg because '\n' marks the line as cleanly terminated.
|
||||
v.writeString("\x1b[7m\x1b[31mfoo\x1b[0m\n")
|
||||
v.draw()
|
||||
|
||||
// First row: cells 1..3 are "foo" (render with red bg via reverse),
|
||||
// cells 4..10 are trailing and should be plain default.
|
||||
for x := 4; x <= 10; x++ {
|
||||
_, style, _ := Screen.Get(x, 1)
|
||||
assert.Equal(t, tcell.ColorDefault, style.GetForeground(),
|
||||
"trailing cell at (%d, 1) should have default fg", x)
|
||||
assert.False(t, style.HasReverse(),
|
||||
"trailing cell at (%d, 1) should not have reverse attribute", x)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnterminatedReverseLineDoesNotExtend verifies that an unterminated
|
||||
// line ending with an AttrReverse cell does NOT propagate the reversed
|
||||
// background past the line's content — matching real terminal behavior
|
||||
// (try `print '\x1b[7m\x1b[31mfoo'` in a shell). The trailing area
|
||||
// is rendered as plain default.
|
||||
func TestUnterminatedReverseLineDoesNotExtend(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 5)
|
||||
|
||||
v := NewView("name", 0, 0, 11, 4, OutputNormal)
|
||||
|
||||
// Reverse + red fg, "foo", no termination. The trailing cells past
|
||||
// "foo" should be plain default, NOT a continuation of the red bg.
|
||||
v.writeString("\x1b[7m\x1b[31mfoo")
|
||||
v.draw()
|
||||
|
||||
// Cells 4..10 are trailing and should be default with no reverse.
|
||||
for x := 4; x <= 10; x++ {
|
||||
_, style, _ := Screen.Get(x, 1)
|
||||
assert.Equal(t, tcell.ColorDefault, style.GetForeground(),
|
||||
"trailing cell at (%d, 1) should have default fg", x)
|
||||
assert.False(t, style.HasReverse(),
|
||||
"trailing cell at (%d, 1) should not have reverse attribute", x)
|
||||
}
|
||||
}
|
||||
|
||||
// TestShortFilledLineExtendsBgWithoutWrap verifies that '\x1b[K' fills
|
||||
// the rest of the line with the current bg color for a line that's
|
||||
// short enough to fit within the view's inner width.
|
||||
func TestShortFilledLineExtendsBgWithoutWrap(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 5)
|
||||
|
||||
v := NewView("name", 0, 0, 11, 4, OutputNormal)
|
||||
|
||||
// \x1b[41m sets bg=red. "hi" fits within InnerWidth=10; \x1b[K should
|
||||
// fill the remaining 8 cells with red.
|
||||
v.writeString("\x1b[41mhi\x1b[K\x1b[0m\n")
|
||||
v.draw()
|
||||
|
||||
// All ten cells at (1..10, 1) should have red bg.
|
||||
for x := 1; x <= 10; x++ {
|
||||
_, style, _ := Screen.Get(x, 1)
|
||||
assert.Equal(t, color.Maroon, style.GetBackground(),
|
||||
"cell at (%d, 1) should have red bg", x)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// extends the fill background past its content to the right edge.
|
||||
func TestWrappedFilledLineExtendsBgToEdge(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 6)
|
||||
|
||||
// View dimensions: Width=12 (x0=0..x1=11), Height=6; InnerWidth=10,
|
||||
// InnerHeight=4. Frame inset of 1 places content cells at screen
|
||||
// (1..10, 1..4).
|
||||
v := NewView("name", 0, 0, 11, 5, OutputNormal)
|
||||
v.Wrap = true
|
||||
|
||||
// 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
|
||||
// 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()
|
||||
|
||||
// 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 := 1; x <= 10; x++ {
|
||||
_, style, _ := Screen.Get(x, y)
|
||||
assert.Equal(t, color.Maroon, style.GetBackground(),
|
||||
"cell at (%d, %d) should have red bg", x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMulticolorWrappedFillUsesLastCellOfEachSegment demonstrates that
|
||||
// when a wrapped line switches bg color part-way through and ends with
|
||||
// \x1b[K, the trailing area on each wrapped row should match the bg
|
||||
// that was active where that row's content ended — not the \x1b[K bg,
|
||||
// which would bleed the color from the end of the logical line back
|
||||
// into the earlier wrapped rows.
|
||||
func TestMulticolorWrappedFillUsesLastCellOfEachSegment(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 6)
|
||||
|
||||
// View dimensions: Width=12 (x0=0..x1=11), Height=6; InnerWidth=10,
|
||||
// InnerHeight=4. Frame inset of 1 places content cells at screen
|
||||
// (1..10, 1..4).
|
||||
v := NewView("name", 0, 0, 11, 5, OutputNormal)
|
||||
v.Wrap = true
|
||||
|
||||
// Content "aaa bbb ccc" is 11 cells; lineWrap breaks at the space
|
||||
// between "bbb" and "ccc" (index 7) so segment 1 is "aaa bbb" (red,
|
||||
// last cell red) and segment 2 is "ccc" (green, last cell green).
|
||||
// \x1b[K records the green bg on the source line.
|
||||
v.writeString("\x1b[41maaa bbb\x1b[42m ccc\x1b[K\x1b[0m\n")
|
||||
v.draw()
|
||||
|
||||
// Row 1's content ends with a red cell at x=7, so trailing columns
|
||||
// 8..10 should pick up red rather than the \x1b[K's green.
|
||||
for x := 8; x <= 10; x++ {
|
||||
_, style, _ := Screen.Get(x, 1)
|
||||
assert.Equal(t, color.Maroon, style.GetBackground(),
|
||||
"trailing cell at (%d, 1) should have red bg (matching segment's last cell)", x)
|
||||
}
|
||||
|
||||
// Row 2's content ends with a green cell at x=3, so trailing
|
||||
// columns 4..10 should pick up green (matching both the segment's
|
||||
// last cell and the \x1b[K bg — these happen to agree here).
|
||||
for x := 4; x <= 10; x++ {
|
||||
_, style, _ := Screen.Get(x, 2)
|
||||
assert.Equal(t, color.Green, style.GetBackground(),
|
||||
"trailing cell at (%d, 2) should have green bg", x)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue