Files
lazygit/pkg/gocui/escape_test.go
Stefan HallerandClaude Opus 4.7 2fce9c91b6 Materialize cursor-forward escapes as space runs
ConPTY compresses runs of default-colored spaces into ECH + CUF
(\x1b[NX\x1b[NC) instead of emitting them literally. ECH is still a
no-op for us — our buffer is built sequentially and has nothing to
erase — but CUF has to materialize as N visible space cells so the
gap actually appears, otherwise content the child wrote with leading
indentation slides left against the preceding cell.

The view's cursorForward branch reuses the same machinery as tab
expansion: substitute the trigger byte for a space and let the
repeatCount path emit the cells under the parser-tracked SGR. The
existing notifyCellsWritten plumbing then advances screenCol over
the gap, keeping subsequent CUP targets aligned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00

277 lines
10 KiB
Go

package gocui
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseOne(t *testing.T) {
var ei *escapeInterpreter
ei = newEscapeInterpreter(OutputNormal)
isEscape, err := ei.parseOne([]byte{'a'})
assert.Equal(t, false, isEscape)
assert.NoError(t, err)
ei = newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, "\x1b[0K")
_, ok := ei.instruction.(eraseInLineFromCursor)
assert.Equal(t, true, ok)
ei = newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, "\x1b[K")
_, ok = ei.instruction.(eraseInLineFromCursor)
assert.Equal(t, true, ok)
ei = newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, "\x1b[1K")
_, ok = ei.instruction.(noInstruction)
assert.Equal(t, true, ok)
ei = newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, "\x1b(B")
_, ok = ei.instruction.(noInstruction)
assert.Equal(t, true, ok)
ei = newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, "\x1b)0")
_, ok = ei.instruction.(noInstruction)
assert.Equal(t, true, ok)
ei = newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, "\x1b*A")
_, ok = ei.instruction.(noInstruction)
assert.Equal(t, true, ok)
ei = newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, "\x1b+K")
_, ok = ei.instruction.(noInstruction)
assert.Equal(t, true, ok)
}
func TestParseOneColours(t *testing.T) {
scenarios := []struct {
outputMode OutputMode
input string
expectedFg Attribute
expectedBg Attribute
}{
{OutputNormal, "\x1b[30m", ColorBlack, ColorDefault},
{OutputNormal, "\x1b[31m", ColorRed, ColorDefault},
{OutputNormal, "\x1b[32m", ColorGreen, ColorDefault},
{OutputNormal, "\x1b[33m", ColorYellow, ColorDefault},
{OutputNormal, "\x1b[34m", ColorBlue, ColorDefault},
{OutputNormal, "\x1b[35m", ColorMagenta, ColorDefault},
{OutputNormal, "\x1b[36m", ColorCyan, ColorDefault},
{OutputNormal, "\x1b[37m", ColorWhite, ColorDefault},
{OutputNormal, "\x1b[40m", ColorDefault, ColorBlack},
{OutputNormal, "\x1b[41m", ColorDefault, ColorRed},
{OutputNormal, "\x1b[42m", ColorDefault, ColorGreen},
{OutputNormal, "\x1b[43m", ColorDefault, ColorYellow},
{OutputNormal, "\x1b[44m", ColorDefault, ColorBlue},
{OutputNormal, "\x1b[45m", ColorDefault, ColorMagenta},
{OutputNormal, "\x1b[46m", ColorDefault, ColorCyan},
{OutputNormal, "\x1b[47m", ColorDefault, ColorWhite},
{OutputNormal, "\x1b[47;31m", ColorRed, ColorWhite},
{OutputNormal, "\x1b[90m", Get256Color(8), ColorDefault},
{OutputNormal, "\x1b[91m", Get256Color(9), ColorDefault},
{OutputNormal, "\x1b[92m", Get256Color(10), ColorDefault},
{OutputNormal, "\x1b[93m", Get256Color(11), ColorDefault},
{OutputNormal, "\x1b[94m", Get256Color(12), ColorDefault},
{OutputNormal, "\x1b[95m", Get256Color(13), ColorDefault},
{OutputNormal, "\x1b[96m", Get256Color(14), ColorDefault},
{OutputNormal, "\x1b[97m", Get256Color(15), ColorDefault},
{OutputNormal, "\x1b[100m", ColorDefault, Get256Color(8)},
{OutputNormal, "\x1b[101m", ColorDefault, Get256Color(9)},
{OutputNormal, "\x1b[102m", ColorDefault, Get256Color(10)},
{OutputNormal, "\x1b[103m", ColorDefault, Get256Color(11)},
{OutputNormal, "\x1b[104m", ColorDefault, Get256Color(12)},
{OutputNormal, "\x1b[105m", ColorDefault, Get256Color(13)},
{OutputNormal, "\x1b[106m", ColorDefault, Get256Color(14)},
{OutputNormal, "\x1b[107m", ColorDefault, Get256Color(15)},
{Output256, "\x1b[38;5;32m", Get256Color(32), ColorDefault},
{OutputTrue, "\x1b[38;5;32m", Get256Color(32), ColorDefault},
{OutputTrue, "\x1b[38;2;50;103;205m", NewRGBColor(50, 103, 205), ColorDefault},
{Output256, "\x1b[48;5;32m", ColorDefault, Get256Color(32)},
{OutputTrue, "\x1b[48;5;32m", ColorDefault, Get256Color(32)},
{OutputTrue, "\x1b[48;2;50;103;205m", ColorDefault, NewRGBColor(50, 103, 205)},
{OutputTrue, "\x1b[1;95;48;2;255;224;224m", Get256Color(13), NewRGBColor(255, 224, 224)},
}
for _, scenario := range scenarios {
ei := newEscapeInterpreter(scenario.outputMode)
parseEscRunes(t, ei, scenario.input)
assert.Equal(t, scenario.expectedFg, ei.curFgColor&AttrColorBits)
assert.Equal(t, scenario.expectedBg, ei.curBgColor)
}
// resetting colours
scenarios = []struct {
outputMode OutputMode
input string
expectedFg Attribute
expectedBg Attribute
}{
{OutputNormal, "\x1b[39m", ColorDefault, ColorRed},
{OutputNormal, "\x1b[49m", ColorRed, ColorDefault},
{OutputNormal, "\x1b[0m", ColorDefault, ColorDefault},
}
for _, scenario := range scenarios {
ei := newEscapeInterpreter(scenario.outputMode)
ei.curFgColor = ColorRed
ei.curBgColor = ColorRed
parseEscRunes(t, ei, scenario.input)
assert.Equal(t, scenario.expectedFg, ei.curFgColor)
assert.Equal(t, scenario.expectedBg, ei.curBgColor)
}
// setting attributes
attrScenarios := []struct {
outputMode OutputMode
input string
expectedAttr Attribute
}{
{OutputNormal, "\x1b[1m", AttrBold},
{OutputNormal, "\x1b[2m", AttrDim},
{OutputNormal, "\x1b[3m", AttrItalic},
{OutputNormal, "\x1b[4m", AttrUnderline},
{OutputNormal, "\x1b[5m", AttrBlink},
{OutputNormal, "\x1b[7m", AttrReverse},
{OutputNormal, "\x1b[9m", AttrStrikeThrough},
}
for _, scenario := range attrScenarios {
ei := newEscapeInterpreter(scenario.outputMode)
parseEscRunes(t, ei, scenario.input)
style := ei.curFgColor & AttrStyleBits
assert.Equal(t, scenario.expectedAttr, style)
}
}
func TestParseOneIgnoresUnknownSequences(t *testing.T) {
// Escape sequences the interpreter doesn't implement -- whether well-formed-but-unsupported
// (private modes, DECSCUSR, …) or outright malformed -- must be silently
// consumed rather than leaked into the view as literal text.
scenarios := []string{
"\x1b[?9001h", // DEC private-mode set (?-prefix)
"\x1b[?25l", // hide cursor
"\x1b[?25h", // show cursor
"\x1b[2;J", // erase display (unusual 2;J variant)
"\x1b[H", // cursor home — re-anchors to row 1 (no-op when already there)
"\x1bc", // RIS — single-char ESC sequence
"\x1b[;5H", // empty first param — defaults to row 1, no-op
"\x1b[ q", // intermediate byte with no params (DECSCUSR family)
"\x1b[0 q", // intermediate byte after a param
"\x1b[1;;m", // malformed SGR: empty middle param
"\x1b]8bogus\x07", // OSC 8 missing ';'
"\x1b[" + strings.Repeat("0", 300) + "m", // single param overflows length cap
"\x1b[" + strings.Repeat("1;", 25) + "1m", // too many params
}
for _, input := range scenarios {
ei := newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, input)
// An unimplemented/malformed sequence must leave no trace: no
// pending instruction, no color change.
_, noop := ei.instruction.(noInstruction)
assert.True(t, noop, "input %q left a pending instruction", input)
assert.Equal(t, ColorDefault, ei.curFgColor, "input %q mutated fg color", input)
assert.Equal(t, ColorDefault, ei.curBgColor, "input %q mutated bg color", input)
}
}
func TestParseOneCursorPositioning(t *testing.T) {
// Cursor-positioning escapes that advance the row forward emit a
// cursorDown instruction; backward / same-row moves are ignored
// because the view's buffer is line-based.
scenarios := []struct {
input string
startRow int // parser's screenRow before parsing
wantAdvance int // 0 means "no instruction emitted"
}{
{"\x1b[5;1H", 1, 4}, // CUP — absolute row 5 from row 1
{"\x1b[5H", 1, 4}, // CUP with only the row param
{"\x1b[5;1H", 5, 0}, // CUP to the same row we're on — no-op
{"\x1b[2;1H", 5, 0}, // CUP backward — ignored
{"\x1b[5;1f", 1, 4}, // HVP alias for CUP
{"\x1b[5d", 1, 4}, // VPA — absolute row
{"\x1b[2d", 5, 0}, // VPA backward — ignored
{"\x1b[3B", 1, 3}, // CUD — relative
{"\x1b[B", 1, 1}, // CUD with default param of 1
{"\x1b[2E", 1, 2}, // CNL — relative
}
for _, s := range scenarios {
ei := newEscapeInterpreter(OutputNormal)
ei.screenRow = s.startRow
parseEscRunes(t, ei, s.input)
if s.wantAdvance == 0 {
_, noop := ei.instruction.(noInstruction)
assert.True(t, noop, "input %q at row %d should be a no-op", s.input, s.startRow)
} else {
cd, ok := ei.instruction.(cursorDown)
if assert.True(t, ok, "input %q at row %d should emit cursorDown", s.input, s.startRow) {
assert.Equal(t, s.wantAdvance, cd.n, "input %q at row %d", s.input, s.startRow)
}
}
}
}
func TestParseOneCursorHomeReanchors(t *testing.T) {
// ConPTY emits cursor-home ([H) after [2J at the start of every screen.
// In a view that isn't rewound in lockstep with ConPTY (the command log)
// screenRow has drifted, so home must re-anchor it to the current write
// position rather than be dropped as a backward move — otherwise the
// absolute CUPs that follow compute negative, dropped advances and the
// rows ConPTY positioned with collapse together.
ei := newEscapeInterpreter(OutputNormal)
ei.screenRow = 12 // accumulated drift from earlier command-log output
parseEscRunes(t, ei, "\x1b[H")
assert.Equal(t, 1, ei.screenRow, "home should re-anchor screenRow")
_, noop := ei.instruction.(noInstruction)
assert.True(t, noop, "home should not emit an instruction")
// A subsequent CUP now advances relative to the re-anchored origin.
parseEscRunes(t, ei, "\x1b[3;1H")
cd, ok := ei.instruction.(cursorDown)
if assert.True(t, ok, "CUP after home should emit cursorDown") {
assert.Equal(t, 2, cd.n)
}
}
func TestParseOneCursorForward(t *testing.T) {
// CUF (\x1b[NC) emits a cursorForward instruction so the view can
// materialize the N-cell gap as spaces. ConPTY uses this (often
// paired with ECH) to encode runs of default-colored spaces.
scenarios := []struct {
input string
wantN int
}{
{"\x1b[5C", 5},
{"\x1b[1C", 1},
{"\x1b[C", 1}, // no param defaults to 1
}
for _, s := range scenarios {
ei := newEscapeInterpreter(OutputNormal)
parseEscRunes(t, ei, s.input)
cf, ok := ei.instruction.(cursorForward)
if assert.True(t, ok, "input %q should emit cursorForward", s.input) {
assert.Equal(t, s.wantN, cf.n, "input %q", s.input)
}
}
}
func parseEscRunes(t *testing.T, ei *escapeInterpreter, runes string) {
t.Helper()
for _, b := range []byte(runes) {
isEscape, err := ei.parseOne([]byte{b})
assert.Equal(t, true, isEscape)
assert.NoError(t, err)
}
}