mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 10:13:41 -05:00
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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0c0c50c3f2
commit
2fce9c91b6
+20
-3
@@ -54,6 +54,14 @@ type cursorDown struct{ n int }
|
||||
|
||||
func (self cursorDown) isInstruction() {}
|
||||
|
||||
// cursorForward asks the view to materialize N space cells. Emitted
|
||||
// when CUF advances the cursor right — ConPTY uses CUF (often paired
|
||||
// with ECH) to encode runs of default-colored spaces compactly, so we
|
||||
// have to render the gap, not just bump a counter.
|
||||
type cursorForward struct{ n int }
|
||||
|
||||
func (self cursorForward) isInstruction() {}
|
||||
|
||||
type noInstruction struct{}
|
||||
|
||||
func (self noInstruction) isInstruction() {}
|
||||
@@ -272,10 +280,11 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
|
||||
ei.csiParam = append(ei.csiParam, "0")
|
||||
case characterEquals(ch, 'K'),
|
||||
characterEquals(ch, 'H'), characterEquals(ch, 'f'), characterEquals(ch, 'd'),
|
||||
characterEquals(ch, 'B'), characterEquals(ch, 'E'):
|
||||
characterEquals(ch, 'B'), characterEquals(ch, 'E'),
|
||||
characterEquals(ch, 'C'):
|
||||
// fall through — let stateParams handle these with default
|
||||
// params (CUP/VPA default to row 1, CUD/CNL default to advance
|
||||
// by 1).
|
||||
// params (CUP/VPA default to row 1, CUD/CNL/CUF default to
|
||||
// advance by 1).
|
||||
case characterEquals(ch, ';'):
|
||||
// Empty first param ([;Xm ≡ [0;Xm). Seed a slot for the
|
||||
// empty param; stateParams will append the next one when it
|
||||
@@ -376,6 +385,14 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
|
||||
ei.state = stateNone
|
||||
ei.csiParam = nil
|
||||
return true, nil
|
||||
case characterEquals(ch, 'C'):
|
||||
// CUF — cursor forward N. Emit space cells so the gap
|
||||
// renders. (screenCol is updated by the view via
|
||||
// notifyCellsWritten as those spaces are emitted.)
|
||||
ei.instruction = cursorForward{n: ei.firstParamOrDefault(1)}
|
||||
ei.state = stateNone
|
||||
ei.csiParam = nil
|
||||
return true, nil
|
||||
case len(ch) == 1 && ch[0] >= 0x20 && ch[0] <= 0x2F:
|
||||
// CSI intermediate byte after params. The final byte will
|
||||
// have a semantic we don't implement (e.g. `[0 q` =
|
||||
|
||||
@@ -243,6 +243,29 @@ func TestParseOneCursorHomeReanchors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1003,6 +1003,14 @@ func (v *View) parseInput(ch []byte, width int, x int, _ int) (bool, []cell) {
|
||||
bg: v.ei.curBgColor,
|
||||
}
|
||||
return truncateLine, []cell{}
|
||||
} else if cf, ok := v.ei.instruction.(cursorForward); ok {
|
||||
// emit `n` space cells under the parser-tracked SGR — used
|
||||
// to materialize ConPTY's compressed runs of spaces (which
|
||||
// it emits as ECH+CUF instead of literal whitespace).
|
||||
v.ei.instructionRead()
|
||||
repeatCount = cf.n
|
||||
ch = []byte{' '}
|
||||
width = 1
|
||||
} else if isEscape {
|
||||
// do not output anything
|
||||
return truncateLine, nil
|
||||
|
||||
@@ -297,10 +297,7 @@ func TestWriteCursorForwardEscape(t *testing.T) {
|
||||
got = append(got, cellsToStrings(l.cells))
|
||||
}
|
||||
|
||||
/* EXPECTED:
|
||||
assert.Equal(t, [][]string{{"a", " ", " ", " ", " ", " ", "b"}}, got)
|
||||
ACTUAL: */
|
||||
assert.Equal(t, [][]string{{"a", "b"}}, got)
|
||||
}
|
||||
|
||||
func TestWriteCursorPositionEscapeWithSoftWraps(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user