detached 4ac84f4

test(vt): cover wide-pair wrap and ICH/DCH repair branches

Add five table cases to TestWideAndZeroWidthRunes covering branches added
during self-review that had no direct coverage:

- wrap abandoning a continuation cell: asserts both halves of the pair are
  cleared and the damage rect extends left over the orphaned wide half.
- ICH at a continuation splitting a pair mid-row; ICH pushing a pair's
  continuation off the right edge.
- DCH landing on each half of a pair, asserting repairRow blanks orphaned
  halves and damage extends one column left on a left-half split.

RED verified by temporarily neutralizing the cx damage extension, the
repairRow calls, and the leftSplit dmgX extension: all five cases fail;
restored code passes.
commit
4ac84f440a4482d201a19ff80c21fb229899cf14
author
brice · · 2026-07-02 09:45 UTC
parent
e3beccd
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +130 -0
M pkg/vt/screen_test.go +130 -0
@@ -1462,6 +1462,136 @@ func TestWideAndZeroWidthRunes(t *testing.T) {
14621462 assertCell(t, s, 3, 0, 'B')
14631463 },
14641464 },
1465+ {
1466+ name: "wrap abandoning a continuation cell clears its pair and extends damage",
1467+ run: func(t *testing.T) {
1468+ s := NewScreen(4, 2)
1469+ s.Write([]byte("AB你")) // A(0) B(1) 你(2) cont(3)
1470+ s.Write([]byte("\x1b[1;4H")) // cursor to col 3: the continuation cell
1471+ s.ClearDamage()
1472+ // A wide rune at the last column wraps; the abandoned last cell
1473+ // is a continuation, so its wide left half (col 2) must be
1474+ // cleared too and the damage must span both columns.
1475+ s.Write([]byte("好"))
1476+
1477+ assertCell(t, s, 0, 0, 'A')
1478+ assertCell(t, s, 1, 0, 'B')
1479+ assertBlank(t, s, 2, 0) // orphaned wide left cleared
1480+ assertBlank(t, s, 3, 0) // abandoned continuation cleared
1481+ assertCell(t, s, 0, 1, '好')
1482+ assertContinuation(t, s, 1, 1)
1483+ if s.Col != 2 || s.Row != 1 {
1484+ t.Errorf("cursor at col=%d row=%d, want col=2 row=1", s.Col, s.Row)
1485+ }
1486+
1487+ d := s.Damage()
1488+ if len(d) == 0 {
1489+ t.Fatal("expected damage")
1490+ }
1491+ // First damage item covers the cleared pair on row 0.
1492+ if d[0].X != 2 || d[0].Y != 0 || d[0].Width != 2 {
1493+ t.Errorf("wrap damage = {X:%d Y:%d W:%d}, want {X:2 Y:0 W:2}", d[0].X, d[0].Y, d[0].Width)
1494+ }
1495+ },
1496+ },
1497+ {
1498+ name: "insert chars at a continuation splits the pair and repairs orphans",
1499+ run: func(t *testing.T) {
1500+ s := NewScreen(6, 2)
1501+ s.Write([]byte("你好")) // 你(0) cont(1) 好(2) cont(3)
1502+ s.Write([]byte("\x1b[1;4H")) // cursor to col 3: continuation of 好
1503+ s.ClearDamage()
1504+ s.Write([]byte("\x1b[1@")) // ICH 1: shift right from col 3
1505+
1506+ // The shift splits 好/cont: 好 stays at col 2, its continuation
1507+ // moves to col 4. Both orphans must be repaired to blanks.
1508+ assertCell(t, s, 0, 0, '你')
1509+ assertContinuation(t, s, 1, 0)
1510+ assertBlank(t, s, 2, 0) // 好 orphaned by the split → cleared
1511+ assertBlank(t, s, 3, 0) // inserted blank
1512+ assertBlank(t, s, 4, 0) // shifted continuation orphaned → cleared
1513+ assertBlank(t, s, 5, 0)
1514+
1515+ d := s.Damage()
1516+ if len(d) == 0 {
1517+ t.Fatal("expected damage")
1518+ }
1519+ // Damage must extend one column left to cover the repaired 好.
1520+ if d[0].X != 2 || d[0].Width != 4 {
1521+ t.Errorf("ICH damage = {X:%d W:%d}, want {X:2 W:4}", d[0].X, d[0].Width)
1522+ }
1523+ },
1524+ },
1525+ {
1526+ name: "insert chars shifting a wide pair off the right edge repairs the left half",
1527+ run: func(t *testing.T) {
1528+ s := NewScreen(4, 2)
1529+ s.Write([]byte("你好")) // 你(0) cont(1) 好(2) cont(3)
1530+ s.Write([]byte("\x1b[1;1H")) // cursor home
1531+ s.Write([]byte("\x1b[1@")) // ICH 1: shift the row right by 1
1532+
1533+ // 好 lands on the last column with its continuation pushed off
1534+ // the edge; the orphaned wide left must be blanked.
1535+ assertBlank(t, s, 0, 0)
1536+ assertCell(t, s, 1, 0, '你')
1537+ assertContinuation(t, s, 2, 0)
1538+ assertBlank(t, s, 3, 0) // 好 lost its continuation → cleared
1539+ },
1540+ },
1541+ {
1542+ name: "delete char at the left half of a wide pair repairs the orphan",
1543+ run: func(t *testing.T) {
1544+ s := NewScreen(6, 2)
1545+ s.Write([]byte("你好AB")) // 你(0) cont(1) 好(2) cont(3) A(4) B(5)
1546+ s.Write([]byte("\x1b[1;1H")) // cursor to col 0: wide left of 你
1547+ s.ClearDamage()
1548+ s.Write([]byte("\x1b[1P")) // DCH 1
1549+
1550+ // 你's continuation shifts to col 0 with no left half → repaired.
1551+ assertBlank(t, s, 0, 0)
1552+ assertCell(t, s, 1, 0, '好')
1553+ assertContinuation(t, s, 2, 0)
1554+ assertCell(t, s, 3, 0, 'A')
1555+ assertCell(t, s, 4, 0, 'B')
1556+ assertBlank(t, s, 5, 0)
1557+
1558+ d := s.Damage()
1559+ if len(d) == 0 {
1560+ t.Fatal("expected damage")
1561+ }
1562+ if d[0].X != 0 || d[0].Width != 6 {
1563+ t.Errorf("DCH damage = {X:%d W:%d}, want {X:0 W:6}", d[0].X, d[0].Width)
1564+ }
1565+ },
1566+ },
1567+ {
1568+ name: "delete char at a continuation repairs the wide left and extends damage",
1569+ run: func(t *testing.T) {
1570+ s := NewScreen(6, 2)
1571+ s.Write([]byte("你好AB")) // 你(0) cont(1) 好(2) cont(3) A(4) B(5)
1572+ s.Write([]byte("\x1b[1;2H")) // cursor to col 1: continuation of 你
1573+ s.ClearDamage()
1574+ s.Write([]byte("\x1b[1P")) // DCH 1: delete the continuation
1575+
1576+ // 你 at col 0 loses its continuation → repaired to blank.
1577+ assertBlank(t, s, 0, 0)
1578+ assertCell(t, s, 1, 0, '好')
1579+ assertContinuation(t, s, 2, 0)
1580+ assertCell(t, s, 3, 0, 'A')
1581+ assertCell(t, s, 4, 0, 'B')
1582+ assertBlank(t, s, 5, 0)
1583+
1584+ d := s.Damage()
1585+ if len(d) == 0 {
1586+ t.Fatal("expected damage")
1587+ }
1588+ // Damage must extend one column left (to col 0) to cover the
1589+ // repaired wide left half.
1590+ if d[0].X != 0 || d[0].Width != 6 {
1591+ t.Errorf("DCH damage = {X:%d W:%d}, want {X:0 W:6}", d[0].X, d[0].Width)
1592+ }
1593+ },
1594+ },
14651595 {
14661596 name: "wide char on a width-1 screen degrades to a single cell",
14671597 run: func(t *testing.T) {
@@ -1462,6 +1462,136 @@ func TestWideAndZeroWidthRunes(t *testing.T) {
1462 assertCell(t, s, 3, 0, 'B')1462 assertCell(t, s, 3, 0, 'B')
1463 },1463 },
1464 },1464 },
1465+ {
1466+ name: "wrap abandoning a continuation cell clears its pair and extends damage",
1467+ run: func(t *testing.T) {
1468+ s := NewScreen(4, 2)
1469+ s.Write([]byte("AB你")) // A(0) B(1) 你(2) cont(3)
1470+ s.Write([]byte("\x1b[1;4H")) // cursor to col 3: the continuation cell
1471+ s.ClearDamage()
1472+ // A wide rune at the last column wraps; the abandoned last cell
1473+ // is a continuation, so its wide left half (col 2) must be
1474+ // cleared too and the damage must span both columns.
1475+ s.Write([]byte("好"))
1476+
1477+ assertCell(t, s, 0, 0, 'A')
1478+ assertCell(t, s, 1, 0, 'B')
1479+ assertBlank(t, s, 2, 0) // orphaned wide left cleared
1480+ assertBlank(t, s, 3, 0) // abandoned continuation cleared
1481+ assertCell(t, s, 0, 1, '好')
1482+ assertContinuation(t, s, 1, 1)
1483+ if s.Col != 2 || s.Row != 1 {
1484+ t.Errorf("cursor at col=%d row=%d, want col=2 row=1", s.Col, s.Row)
1485+ }
1486+
1487+ d := s.Damage()
1488+ if len(d) == 0 {
1489+ t.Fatal("expected damage")
1490+ }
1491+ // First damage item covers the cleared pair on row 0.
1492+ if d[0].X != 2 || d[0].Y != 0 || d[0].Width != 2 {
1493+ t.Errorf("wrap damage = {X:%d Y:%d W:%d}, want {X:2 Y:0 W:2}", d[0].X, d[0].Y, d[0].Width)
1494+ }
1495+ },
1496+ },
1497+ {
1498+ name: "insert chars at a continuation splits the pair and repairs orphans",
1499+ run: func(t *testing.T) {
1500+ s := NewScreen(6, 2)
1501+ s.Write([]byte("你好")) // 你(0) cont(1) 好(2) cont(3)
1502+ s.Write([]byte("\x1b[1;4H")) // cursor to col 3: continuation of 好
1503+ s.ClearDamage()
1504+ s.Write([]byte("\x1b[1@")) // ICH 1: shift right from col 3
1505+
1506+ // The shift splits 好/cont: 好 stays at col 2, its continuation
1507+ // moves to col 4. Both orphans must be repaired to blanks.
1508+ assertCell(t, s, 0, 0, '你')
1509+ assertContinuation(t, s, 1, 0)
1510+ assertBlank(t, s, 2, 0) // 好 orphaned by the split → cleared
1511+ assertBlank(t, s, 3, 0) // inserted blank
1512+ assertBlank(t, s, 4, 0) // shifted continuation orphaned → cleared
1513+ assertBlank(t, s, 5, 0)
1514+
1515+ d := s.Damage()
1516+ if len(d) == 0 {
1517+ t.Fatal("expected damage")
1518+ }
1519+ // Damage must extend one column left to cover the repaired 好.
1520+ if d[0].X != 2 || d[0].Width != 4 {
1521+ t.Errorf("ICH damage = {X:%d W:%d}, want {X:2 W:4}", d[0].X, d[0].Width)
1522+ }
1523+ },
1524+ },
1525+ {
1526+ name: "insert chars shifting a wide pair off the right edge repairs the left half",
1527+ run: func(t *testing.T) {
1528+ s := NewScreen(4, 2)
1529+ s.Write([]byte("你好")) // 你(0) cont(1) 好(2) cont(3)
1530+ s.Write([]byte("\x1b[1;1H")) // cursor home
1531+ s.Write([]byte("\x1b[1@")) // ICH 1: shift the row right by 1
1532+
1533+ // 好 lands on the last column with its continuation pushed off
1534+ // the edge; the orphaned wide left must be blanked.
1535+ assertBlank(t, s, 0, 0)
1536+ assertCell(t, s, 1, 0, '你')
1537+ assertContinuation(t, s, 2, 0)
1538+ assertBlank(t, s, 3, 0) // 好 lost its continuation → cleared
1539+ },
1540+ },
1541+ {
1542+ name: "delete char at the left half of a wide pair repairs the orphan",
1543+ run: func(t *testing.T) {
1544+ s := NewScreen(6, 2)
1545+ s.Write([]byte("你好AB")) // 你(0) cont(1) 好(2) cont(3) A(4) B(5)
1546+ s.Write([]byte("\x1b[1;1H")) // cursor to col 0: wide left of 你
1547+ s.ClearDamage()
1548+ s.Write([]byte("\x1b[1P")) // DCH 1
1549+
1550+ // 你's continuation shifts to col 0 with no left half → repaired.
1551+ assertBlank(t, s, 0, 0)
1552+ assertCell(t, s, 1, 0, '好')
1553+ assertContinuation(t, s, 2, 0)
1554+ assertCell(t, s, 3, 0, 'A')
1555+ assertCell(t, s, 4, 0, 'B')
1556+ assertBlank(t, s, 5, 0)
1557+
1558+ d := s.Damage()
1559+ if len(d) == 0 {
1560+ t.Fatal("expected damage")
1561+ }
1562+ if d[0].X != 0 || d[0].Width != 6 {
1563+ t.Errorf("DCH damage = {X:%d W:%d}, want {X:0 W:6}", d[0].X, d[0].Width)
1564+ }
1565+ },
1566+ },
1567+ {
1568+ name: "delete char at a continuation repairs the wide left and extends damage",
1569+ run: func(t *testing.T) {
1570+ s := NewScreen(6, 2)
1571+ s.Write([]byte("你好AB")) // 你(0) cont(1) 好(2) cont(3) A(4) B(5)
1572+ s.Write([]byte("\x1b[1;2H")) // cursor to col 1: continuation of 你
1573+ s.ClearDamage()
1574+ s.Write([]byte("\x1b[1P")) // DCH 1: delete the continuation
1575+
1576+ // 你 at col 0 loses its continuation → repaired to blank.
1577+ assertBlank(t, s, 0, 0)
1578+ assertCell(t, s, 1, 0, '好')
1579+ assertContinuation(t, s, 2, 0)
1580+ assertCell(t, s, 3, 0, 'A')
1581+ assertCell(t, s, 4, 0, 'B')
1582+ assertBlank(t, s, 5, 0)
1583+
1584+ d := s.Damage()
1585+ if len(d) == 0 {
1586+ t.Fatal("expected damage")
1587+ }
1588+ // Damage must extend one column left (to col 0) to cover the
1589+ // repaired wide left half.
1590+ if d[0].X != 0 || d[0].Width != 6 {
1591+ t.Errorf("DCH damage = {X:%d W:%d}, want {X:0 W:6}", d[0].X, d[0].Width)
1592+ }
1593+ },
1594+ },
1465 {1595 {
1466 name: "wide char on a width-1 screen degrades to a single cell",1596 name: "wide char on a width-1 screen degrades to a single cell",
1467 run: func(t *testing.T) {1597 run: func(t *testing.T) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close