detached a123cbe

test(history): preserve screen-owned fixtures

commit
a123cbeda4a30aa08afd8025016583600ba5417e
author
brice · · 2026-07-12 21:03 UTC
parent
e17db81
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +39 -21
M internal/usecase/copy/mode.go +2 -1
@@ -38,7 +38,8 @@ func NewSnapshot(historySource *vt.History, screen renderer.Frame) Snapshot {
3838
3939 // NewSnapshotFromRows constructs a snapshot that owns explicit caller rows.
4040 // It is intended for callers that already have a complete document rather than
41-// a scrollback and visible frame.
41+// a scrollback and visible frame. Height is viewport geometry, not a request to
42+// append blank document rows.
4243 func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot {
4344 history := vt.NewHistory(vt.HistoryConfig{MaxRows: len(rows), ChunkRows: 256})
4445 for _, row := range rows {
@@ -38,7 +38,8 @@ func NewSnapshot(historySource *vt.History, screen renderer.Frame) Snapshot {
38 38
39 // NewSnapshotFromRows constructs a snapshot that owns explicit caller rows.39 // NewSnapshotFromRows constructs a snapshot that owns explicit caller rows.
40 // It is intended for callers that already have a complete document rather than40 // It is intended for callers that already have a complete document rather than
41-// a scrollback and visible frame.41+// a scrollback and visible frame. Height is viewport geometry, not a request to
42+// append blank document rows.
42 func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot {43 func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot {
43 history := vt.NewHistory(vt.HistoryConfig{MaxRows: len(rows), ChunkRows: 256})44 history := vt.NewHistory(vt.HistoryConfig{MaxRows: len(rows), ChunkRows: 256})
44 for _, row := range rows {45 for _, row := range rows {
M internal/usecase/copy/mode_test.go +11 -0
@@ -539,6 +539,17 @@ func TestNewSnapshotFromRowsOwnsCallerRows(t *testing.T) {
539539 require.Equal(t, "one", rowText(document.Row(0)))
540540 }
541541
542+func TestNewSnapshotFromRowsKeepsDocumentAndViewportDimensionsIndependent(t *testing.T) {
543+ document := NewSnapshotFromRows([][]renderer.Cell{row("one"), row("two")}, 7, 5)
544+
545+ require.Equal(t, 2, document.Len())
546+ require.Equal(t, 7, document.Width)
547+ require.Equal(t, 5, document.Height)
548+ require.Equal(t, "one", rowText(document.Row(0)))
549+ require.Equal(t, "two", rowText(document.Row(1)))
550+ require.Nil(t, document.Row(2))
551+}
552+
542553 // This scaling gate deliberately compares equal row counts rather than an
543554 // absolute budget, which varies between Go releases. A HistoryView shares
544555 // sealed chunks, so its allocation must not grow with history-row width.
@@ -539,6 +539,17 @@ func TestNewSnapshotFromRowsOwnsCallerRows(t *testing.T) {
539 require.Equal(t, "one", rowText(document.Row(0)))539 require.Equal(t, "one", rowText(document.Row(0)))
540 }540 }
541 541
542+func TestNewSnapshotFromRowsKeepsDocumentAndViewportDimensionsIndependent(t *testing.T) {
543+ document := NewSnapshotFromRows([][]renderer.Cell{row("one"), row("two")}, 7, 5)
544+
545+ require.Equal(t, 2, document.Len())
546+ require.Equal(t, 7, document.Width)
547+ require.Equal(t, 5, document.Height)
548+ require.Equal(t, "one", rowText(document.Row(0)))
549+ require.Equal(t, "two", rowText(document.Row(1)))
550+ require.Nil(t, document.Row(2))
551+}
552+
542 // This scaling gate deliberately compares equal row counts rather than an553 // This scaling gate deliberately compares equal row counts rather than an
543 // absolute budget, which varies between Go releases. A HistoryView shares554 // absolute budget, which varies between Go releases. A HistoryView shares
544 // sealed chunks, so its allocation must not grow with history-row width.555 // sealed chunks, so its allocation must not grow with history-row width.
M internal/usecase/daemon/copymode_test.go +10 -10
@@ -166,7 +166,7 @@ func TestCopyModePaletteCommandEntersAndDoesNotForward(t *testing.T) {
166166 writes := make(chan []byte, 1)
167167 p, _ := newBlockingPTYWithWrites(t, writes)
168168 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
169- sess.tabs[0].focusedPane().history = newTestHistory(4)
169+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
170170 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
171171
172172 d.handleInput(sess, ac, []byte("\x1b "))
@@ -293,7 +293,7 @@ func TestCopyModeSearchModalCapturesMouseAndClearsOnExit(t *testing.T) {
293293 p, _ := newBlockingPTY(t)
294294 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
295295 pane := sess.tabs[0].focusedPane()
296- pane.history = newTestHistory(4)
296+ installTestHistory(pane, vt.HistoryConfig{MaxRows: 4})
297297 pane.history.Append(testRow("old alpha"))
298298 copy(pane.screen.Frame.Row(0), testRow("live alpha"))
299299
@@ -322,7 +322,7 @@ func TestCopyModeInputNotForwardedAndOSC52Copy(t *testing.T) {
322322 writes := make(chan []byte, 1)
323323 p, _ := newBlockingPTYWithWrites(t, writes)
324324 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
325- sess.tabs[0].focusedPane().history = newTestHistory(4)
325+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
326326 sess.tabs[0].focusedPane().history.Append(testRow("old1 "))
327327 sess.tabs[0].focusedPane().history.Append(testRow("old2 "))
328328 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
@@ -481,7 +481,7 @@ func TestScrollbackEvictionFeedsCopyModeYank(t *testing.T) {
481481 func TestCopyModeEscapeRestoresLiveFullRepaint(t *testing.T) {
482482 p, _ := newBlockingPTY(t)
483483 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
484- sess.tabs[0].focusedPane().history = newTestHistory(4)
484+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
485485 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
486486
487487 d.enterCopyMode(sess, ac)
@@ -515,7 +515,7 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) {
515515 t.Run(tc.name, func(t *testing.T) {
516516 p, _ := newBlockingPTY(t)
517517 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
518- sess.tabs[0].focusedPane().history = newTestHistory(4)
518+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
519519 sess.tabs[0].focusedPane().history.Append(testRow("old1 "))
520520 sess.tabs[0].focusedPane().history.Append(testRow("old2 "))
521521 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
@@ -535,7 +535,7 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) {
535535 func TestCopyModeOversizedYankShowsTooLargeFeedback(t *testing.T) {
536536 p, _ := newBlockingPTY(t)
537537 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
538- sess.tabs[0].focusedPane().history = newTestHistory(1)
538+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 1})
539539 longLine := strings.Repeat("x", scopy.OSC52MaxPayloadBytes+1)
540540 sess.tabs[0].focusedPane().screen.Frame = renderer.NewFrame(len(longLine), 1)
541541 copy(sess.tabs[0].focusedPane().screen.Frame.Row(0), testRow(longLine))
@@ -560,7 +560,7 @@ func TestCopyModeLoneEscapeExitsAfterDelay(t *testing.T) {
560560 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
561561 clk := &signalClock{timers: make(chan *signalTimer, 1)}
562562 d.clock = clk
563- sess.tabs[0].focusedPane().history = newTestHistory(4)
563+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
564564 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
565565
566566 d.enterCopyMode(sess, ac)
@@ -584,7 +584,7 @@ func TestCopyModePendingEscapeDoesNotCloseNewMode(t *testing.T) {
584584 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
585585 clk := &signalClock{timers: make(chan *signalTimer, 1)}
586586 d.clock = clk
587- sess.tabs[0].focusedPane().history = newTestHistory(4)
587+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
588588 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
589589
590590 d.enterCopyMode(sess, ac)
@@ -627,7 +627,7 @@ func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) {
627627 t.Run(tc.name, func(t *testing.T) {
628628 p, _ := newBlockingPTY(t)
629629 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
630- sess.tabs[0].focusedPane().history = newTestHistory(4)
630+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
631631 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
632632
633633 d.enterCopyMode(sess, ac)
@@ -676,7 +676,7 @@ func TestHandleCopyInputUsesImmutableSnapshotWithoutPaneLock(t *testing.T) {
676676 func TestCopyModeEnterExitConcurrentWithPaintRace(t *testing.T) {
677677 p, _ := newBlockingPTY(t)
678678 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
679- sess.tabs[0].focusedPane().history = newTestHistory(4)
679+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
680680 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
681681
682682 done := make(chan struct{})
@@ -166,7 +166,7 @@ func TestCopyModePaletteCommandEntersAndDoesNotForward(t *testing.T) {
166 writes := make(chan []byte, 1)166 writes := make(chan []byte, 1)
167 p, _ := newBlockingPTYWithWrites(t, writes)167 p, _ := newBlockingPTYWithWrites(t, writes)
168 d, sess, ac, sends := newManualSessionWithPTYs(t, p)168 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
169- sess.tabs[0].focusedPane().history = newTestHistory(4)169+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
170 sess.tabs[0].focusedPane().screen.Write([]byte("live"))170 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
171 171
172 d.handleInput(sess, ac, []byte("\x1b "))172 d.handleInput(sess, ac, []byte("\x1b "))
@@ -293,7 +293,7 @@ func TestCopyModeSearchModalCapturesMouseAndClearsOnExit(t *testing.T) {
293 p, _ := newBlockingPTY(t)293 p, _ := newBlockingPTY(t)
294 d, sess, ac, sends := newManualSessionWithPTYs(t, p)294 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
295 pane := sess.tabs[0].focusedPane()295 pane := sess.tabs[0].focusedPane()
296- pane.history = newTestHistory(4)296+ installTestHistory(pane, vt.HistoryConfig{MaxRows: 4})
297 pane.history.Append(testRow("old alpha"))297 pane.history.Append(testRow("old alpha"))
298 copy(pane.screen.Frame.Row(0), testRow("live alpha"))298 copy(pane.screen.Frame.Row(0), testRow("live alpha"))
299 299
@@ -322,7 +322,7 @@ func TestCopyModeInputNotForwardedAndOSC52Copy(t *testing.T) {
322 writes := make(chan []byte, 1)322 writes := make(chan []byte, 1)
323 p, _ := newBlockingPTYWithWrites(t, writes)323 p, _ := newBlockingPTYWithWrites(t, writes)
324 d, sess, ac, sends := newManualSessionWithPTYs(t, p)324 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
325- sess.tabs[0].focusedPane().history = newTestHistory(4)325+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
326 sess.tabs[0].focusedPane().history.Append(testRow("old1 "))326 sess.tabs[0].focusedPane().history.Append(testRow("old1 "))
327 sess.tabs[0].focusedPane().history.Append(testRow("old2 "))327 sess.tabs[0].focusedPane().history.Append(testRow("old2 "))
328 sess.tabs[0].focusedPane().screen.Write([]byte("live"))328 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
@@ -481,7 +481,7 @@ func TestScrollbackEvictionFeedsCopyModeYank(t *testing.T) {
481 func TestCopyModeEscapeRestoresLiveFullRepaint(t *testing.T) {481 func TestCopyModeEscapeRestoresLiveFullRepaint(t *testing.T) {
482 p, _ := newBlockingPTY(t)482 p, _ := newBlockingPTY(t)
483 d, sess, ac, sends := newManualSessionWithPTYs(t, p)483 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
484- sess.tabs[0].focusedPane().history = newTestHistory(4)484+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
485 sess.tabs[0].focusedPane().screen.Write([]byte("live"))485 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
486 486
487 d.enterCopyMode(sess, ac)487 d.enterCopyMode(sess, ac)
@@ -515,7 +515,7 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) {
515 t.Run(tc.name, func(t *testing.T) {515 t.Run(tc.name, func(t *testing.T) {
516 p, _ := newBlockingPTY(t)516 p, _ := newBlockingPTY(t)
517 d, sess, ac, sends := newManualSessionWithPTYs(t, p)517 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
518- sess.tabs[0].focusedPane().history = newTestHistory(4)518+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
519 sess.tabs[0].focusedPane().history.Append(testRow("old1 "))519 sess.tabs[0].focusedPane().history.Append(testRow("old1 "))
520 sess.tabs[0].focusedPane().history.Append(testRow("old2 "))520 sess.tabs[0].focusedPane().history.Append(testRow("old2 "))
521 sess.tabs[0].focusedPane().screen.Write([]byte("live"))521 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
@@ -535,7 +535,7 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) {
535 func TestCopyModeOversizedYankShowsTooLargeFeedback(t *testing.T) {535 func TestCopyModeOversizedYankShowsTooLargeFeedback(t *testing.T) {
536 p, _ := newBlockingPTY(t)536 p, _ := newBlockingPTY(t)
537 d, sess, ac, sends := newManualSessionWithPTYs(t, p)537 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
538- sess.tabs[0].focusedPane().history = newTestHistory(1)538+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 1})
539 longLine := strings.Repeat("x", scopy.OSC52MaxPayloadBytes+1)539 longLine := strings.Repeat("x", scopy.OSC52MaxPayloadBytes+1)
540 sess.tabs[0].focusedPane().screen.Frame = renderer.NewFrame(len(longLine), 1)540 sess.tabs[0].focusedPane().screen.Frame = renderer.NewFrame(len(longLine), 1)
541 copy(sess.tabs[0].focusedPane().screen.Frame.Row(0), testRow(longLine))541 copy(sess.tabs[0].focusedPane().screen.Frame.Row(0), testRow(longLine))
@@ -560,7 +560,7 @@ func TestCopyModeLoneEscapeExitsAfterDelay(t *testing.T) {
560 d, sess, ac, sends := newManualSessionWithPTYs(t, p)560 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
561 clk := &signalClock{timers: make(chan *signalTimer, 1)}561 clk := &signalClock{timers: make(chan *signalTimer, 1)}
562 d.clock = clk562 d.clock = clk
563- sess.tabs[0].focusedPane().history = newTestHistory(4)563+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
564 sess.tabs[0].focusedPane().screen.Write([]byte("live"))564 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
565 565
566 d.enterCopyMode(sess, ac)566 d.enterCopyMode(sess, ac)
@@ -584,7 +584,7 @@ func TestCopyModePendingEscapeDoesNotCloseNewMode(t *testing.T) {
584 d, sess, ac, sends := newManualSessionWithPTYs(t, p)584 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
585 clk := &signalClock{timers: make(chan *signalTimer, 1)}585 clk := &signalClock{timers: make(chan *signalTimer, 1)}
586 d.clock = clk586 d.clock = clk
587- sess.tabs[0].focusedPane().history = newTestHistory(4)587+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
588 sess.tabs[0].focusedPane().screen.Write([]byte("live"))588 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
589 589
590 d.enterCopyMode(sess, ac)590 d.enterCopyMode(sess, ac)
@@ -627,7 +627,7 @@ func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) {
627 t.Run(tc.name, func(t *testing.T) {627 t.Run(tc.name, func(t *testing.T) {
628 p, _ := newBlockingPTY(t)628 p, _ := newBlockingPTY(t)
629 d, sess, ac, sends := newManualSessionWithPTYs(t, p)629 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
630- sess.tabs[0].focusedPane().history = newTestHistory(4)630+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
631 sess.tabs[0].focusedPane().screen.Write([]byte("live"))631 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
632 632
633 d.enterCopyMode(sess, ac)633 d.enterCopyMode(sess, ac)
@@ -676,7 +676,7 @@ func TestHandleCopyInputUsesImmutableSnapshotWithoutPaneLock(t *testing.T) {
676 func TestCopyModeEnterExitConcurrentWithPaintRace(t *testing.T) {676 func TestCopyModeEnterExitConcurrentWithPaintRace(t *testing.T) {
677 p, _ := newBlockingPTY(t)677 p, _ := newBlockingPTY(t)
678 d, sess, ac, sends := newManualSessionWithPTYs(t, p)678 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
679- sess.tabs[0].focusedPane().history = newTestHistory(4)679+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
680 sess.tabs[0].focusedPane().screen.Write([]byte("live"))680 sess.tabs[0].focusedPane().screen.Write([]byte("live"))
681 681
682 done := make(chan struct{})682 done := make(chan struct{})
M internal/usecase/daemon/history_test_helper_test.go +5 -0
@@ -3,3 +3,8 @@ package daemon
33 import "github.com/bnema/vev/pkg/vt"
44
55 func newTestHistory(rows int) *vt.History { return vt.NewHistory(vt.HistoryConfig{MaxRows: rows}) }
6+
7+func installTestHistory(p *pane, config vt.HistoryConfig) {
8+ p.screen = vt.NewScreenWithHistory(p.screen.Frame.Width, p.screen.Frame.Height, config)
9+ p.history = p.screen.History()
10+}
@@ -3,3 +3,8 @@ package daemon
3 import "github.com/bnema/vev/pkg/vt"3 import "github.com/bnema/vev/pkg/vt"
4 4
5 func newTestHistory(rows int) *vt.History { return vt.NewHistory(vt.HistoryConfig{MaxRows: rows}) }5 func newTestHistory(rows int) *vt.History { return vt.NewHistory(vt.HistoryConfig{MaxRows: rows}) }
6+
7+func installTestHistory(p *pane, config vt.HistoryConfig) {
8+ p.screen = vt.NewScreenWithHistory(p.screen.Frame.Width, p.screen.Frame.Height, config)
9+ p.history = p.screen.History()
10+}
M internal/usecase/daemon/input_test.go +2 -1
@@ -17,6 +17,7 @@ import (
1717 "github.com/bnema/vev/internal/usecase/keys"
1818 "github.com/bnema/vev/internal/usecase/layout"
1919 "github.com/bnema/vev/internal/usecase/picker"
20+ "github.com/bnema/vev/pkg/vt"
2021 )
2122
2223 // --- test doubles -----------------------------------------------------------
@@ -930,7 +931,7 @@ func TestCopyModeStatusRowPressClearsDragState(t *testing.T) {
930931 func TestMouseNormalScreenDragExtendsToCurrentScrollbackOffset(t *testing.T) {
931932 p, _ := newBlockingPTY(t)
932933 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
933- sess.tabs[0].focusedPane().history = newTestHistory(50)
934+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 50})
934935 require.Equal(t, 23, sess.tabs[0].focusedPane().screen.Frame.Height, "fixture assumption: status row is wire row 24")
935936
936937 // Press on a content row while scrollback is empty: the anchor is
@@ -17,6 +17,7 @@ import (
17 "github.com/bnema/vev/internal/usecase/keys"17 "github.com/bnema/vev/internal/usecase/keys"
18 "github.com/bnema/vev/internal/usecase/layout"18 "github.com/bnema/vev/internal/usecase/layout"
19 "github.com/bnema/vev/internal/usecase/picker"19 "github.com/bnema/vev/internal/usecase/picker"
20+ "github.com/bnema/vev/pkg/vt"
20 )21 )
21 22
22 // --- test doubles -----------------------------------------------------------23 // --- test doubles -----------------------------------------------------------
@@ -930,7 +931,7 @@ func TestCopyModeStatusRowPressClearsDragState(t *testing.T) {
930 func TestMouseNormalScreenDragExtendsToCurrentScrollbackOffset(t *testing.T) {931 func TestMouseNormalScreenDragExtendsToCurrentScrollbackOffset(t *testing.T) {
931 p, _ := newBlockingPTY(t)932 p, _ := newBlockingPTY(t)
932 d, sess, ac, _ := newManualSessionWithPTYs(t, p)933 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
933- sess.tabs[0].focusedPane().history = newTestHistory(50)934+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 50})
934 require.Equal(t, 23, sess.tabs[0].focusedPane().screen.Frame.Height, "fixture assumption: status row is wire row 24")935 require.Equal(t, 23, sess.tabs[0].focusedPane().screen.Frame.Height, "fixture assumption: status row is wire row 24")
935 936
936 // Press on a content row while scrollback is empty: the anchor is937 // Press on a content row while scrollback is empty: the anchor is
M internal/usecase/daemon/render_test.go +2 -2
@@ -911,7 +911,7 @@ func TestOverlayPaintInvalidationShowsAndRestoresBaseFrame(t *testing.T) {
911911 open: func(d *Daemon, sess *session, ac *attachedClient) { d.enterCopyMode(sess, ac) },
912912 close: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("q")) },
913913 prepareScreen: func(sess *session) {
914- sess.tabs[0].focusedPane().history = newTestHistory(4)
914+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
915915 },
916916 visible: "[SCROLL]",
917917 notVisible: "[SCROLL]",
@@ -923,10 +923,10 @@ func TestOverlayPaintInvalidationShowsAndRestoresBaseFrame(t *testing.T) {
923923 p, release := newBlockingPTY(t)
924924 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
925925 defer release()
926- sess.tabs[0].focusedPane().screen.Write([]byte("live"))
927926 if tt.prepareScreen != nil {
928927 tt.prepareScreen(sess)
929928 }
929+ sess.tabs[0].focusedPane().screen.Write([]byte("live"))
930930 if tt.wantRestored == "" {
931931 tt.wantRestored = "live"
932932 }
@@ -911,7 +911,7 @@ func TestOverlayPaintInvalidationShowsAndRestoresBaseFrame(t *testing.T) {
911 open: func(d *Daemon, sess *session, ac *attachedClient) { d.enterCopyMode(sess, ac) },911 open: func(d *Daemon, sess *session, ac *attachedClient) { d.enterCopyMode(sess, ac) },
912 close: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("q")) },912 close: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("q")) },
913 prepareScreen: func(sess *session) {913 prepareScreen: func(sess *session) {
914- sess.tabs[0].focusedPane().history = newTestHistory(4)914+ installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4})
915 },915 },
916 visible: "[SCROLL]",916 visible: "[SCROLL]",
917 notVisible: "[SCROLL]",917 notVisible: "[SCROLL]",
@@ -923,10 +923,10 @@ func TestOverlayPaintInvalidationShowsAndRestoresBaseFrame(t *testing.T) {
923 p, release := newBlockingPTY(t)923 p, release := newBlockingPTY(t)
924 d, sess, ac, sends := newManualSessionWithPTYs(t, p)924 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
925 defer release()925 defer release()
926- sess.tabs[0].focusedPane().screen.Write([]byte("live"))
927 if tt.prepareScreen != nil {926 if tt.prepareScreen != nil {
928 tt.prepareScreen(sess)927 tt.prepareScreen(sess)
929 }928 }
929+ sess.tabs[0].focusedPane().screen.Write([]byte("live"))
930 if tt.wantRestored == "" {930 if tt.wantRestored == "" {
931 tt.wantRestored = "live"931 tt.wantRestored = "live"
932 }932 }
M internal/usecase/daemon/snapshot_acceptance_test.go +4 -4
@@ -100,9 +100,9 @@ func TestSnapshotCaptureRetainsImmutableValuesAfterTailMutationAndEviction(t *te
100100 }
101101 sess := newSnapshotTestSession(t, "frozen", false, "/work")
102102 pane := sess.tabs[0].panes["pane-1"]
103- pane.history = vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
103+ pane.screen = vt.NewScreenWithHistory(8, 3, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
104+ pane.history = pane.screen.History()
104105 pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}})
105- pane.screen = vt.NewScreen(8, 3)
106106 pane.screen.Write([]byte("before"))
107107
108108 require.True(t, d.captureSession(sess))
@@ -127,11 +127,11 @@ func TestSnapshotCaptureRetainsImmutableValuesAfterTailMutationAndEviction(t *te
127127 func TestDaemonHistoryEvictionLeavesCopyAndSearchSnapshotsUsable(t *testing.T) {
128128 sess := newSnapshotTestSession(t, "copy-lifetime", false, "/work")
129129 pane := sess.tabs[0].panes["pane-1"]
130- pane.history = vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
130+ pane.screen = vt.NewScreenWithHistory(8, 1, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
131+ pane.history = pane.screen.History()
131132 for _, text := range []string{"first", "second"} {
132133 pane.history.Append(cells(text))
133134 }
134- pane.screen = vt.NewScreen(8, 1)
135135 pane.screen.Write([]byte("screen"))
136136
137137 document := scopy.NewSnapshot(pane.history, pane.screen.Frame)
@@ -100,9 +100,9 @@ func TestSnapshotCaptureRetainsImmutableValuesAfterTailMutationAndEviction(t *te
100 }100 }
101 sess := newSnapshotTestSession(t, "frozen", false, "/work")101 sess := newSnapshotTestSession(t, "frozen", false, "/work")
102 pane := sess.tabs[0].panes["pane-1"]102 pane := sess.tabs[0].panes["pane-1"]
103- pane.history = vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})103+ pane.screen = vt.NewScreenWithHistory(8, 3, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
104+ pane.history = pane.screen.History()
104 pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}})105 pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}})
105- pane.screen = vt.NewScreen(8, 3)
106 pane.screen.Write([]byte("before"))106 pane.screen.Write([]byte("before"))
107 107
108 require.True(t, d.captureSession(sess))108 require.True(t, d.captureSession(sess))
@@ -127,11 +127,11 @@ func TestSnapshotCaptureRetainsImmutableValuesAfterTailMutationAndEviction(t *te
127 func TestDaemonHistoryEvictionLeavesCopyAndSearchSnapshotsUsable(t *testing.T) {127 func TestDaemonHistoryEvictionLeavesCopyAndSearchSnapshotsUsable(t *testing.T) {
128 sess := newSnapshotTestSession(t, "copy-lifetime", false, "/work")128 sess := newSnapshotTestSession(t, "copy-lifetime", false, "/work")
129 pane := sess.tabs[0].panes["pane-1"]129 pane := sess.tabs[0].panes["pane-1"]
130- pane.history = vt.NewHistory(vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})130+ pane.screen = vt.NewScreenWithHistory(8, 1, vt.HistoryConfig{MaxRows: 2, ChunkRows: 1})
131+ pane.history = pane.screen.History()
131 for _, text := range []string{"first", "second"} {132 for _, text := range []string{"first", "second"} {
132 pane.history.Append(cells(text))133 pane.history.Append(cells(text))
133 }134 }
134- pane.screen = vt.NewScreen(8, 1)
135 pane.screen.Write([]byte("screen"))135 pane.screen.Write([]byte("screen"))
136 136
137 document := scopy.NewSnapshot(pane.history, pane.screen.Frame)137 document := scopy.NewSnapshot(pane.history, pane.screen.Frame)
M internal/usecase/snapshot/codec.go +2 -3
@@ -574,9 +574,8 @@ func validateActive(active uint16, tabCount int) error {
574574 return nil
575575 }
576576
577-// preflightSession first validates the full structural and aggregate resource
578-// shape without materializing strings, maps, or reference lists. Only once
579-// that pass has accepted global budgets does it allocate to check references.
577+// validateTree checks that every pane reference in the layout tree resolves
578+// to a pane declared by the enclosing tab.
580579 func validateTree(t *layout.Tree, ids map[layout.PaneID]struct{}) error {
581580 if t == nil || t.Root == nil {
582581 return nil
@@ -574,9 +574,8 @@ func validateActive(active uint16, tabCount int) error {
574 return nil574 return nil
575 }575 }
576 576
577-// preflightSession first validates the full structural and aggregate resource577+// validateTree checks that every pane reference in the layout tree resolves
578-// shape without materializing strings, maps, or reference lists. Only once578+// to a pane declared by the enclosing tab.
579-// that pass has accepted global budgets does it allocate to check references.
580 func validateTree(t *layout.Tree, ids map[layout.PaneID]struct{}) error {579 func validateTree(t *layout.Tree, ids map[layout.PaneID]struct{}) error {
581 if t == nil || t.Root == nil {580 if t == nil || t.Root == nil {
582 return nil581 return nil
M pkg/vt/history_codec_test.go +1 -0
@@ -205,6 +205,7 @@ func historyPayloadWithDimensions(rowCount, width int) []byte {
205205 data = binary.BigEndian.AppendUint32(data, uint32(chunkRows))
206206 for range chunkRows {
207207 data = binary.BigEndian.AppendUint32(data, uint32(width))
208+ //nolint:makezero // The header prefix is retained while zeroed cell records are appended.
208209 data = append(data, make([]byte, width*historyCellBytes)...)
209210 }
210211 rowCount -= chunkRows
@@ -205,6 +205,7 @@ func historyPayloadWithDimensions(rowCount, width int) []byte {
205 data = binary.BigEndian.AppendUint32(data, uint32(chunkRows))205 data = binary.BigEndian.AppendUint32(data, uint32(chunkRows))
206 for range chunkRows {206 for range chunkRows {
207 data = binary.BigEndian.AppendUint32(data, uint32(width))207 data = binary.BigEndian.AppendUint32(data, uint32(width))
208+ //nolint:makezero // The header prefix is retained while zeroed cell records are appended.
208 data = append(data, make([]byte, width*historyCellBytes)...)209 data = append(data, make([]byte, width*historyCellBytes)...)
209 }210 }
210 rowCount -= chunkRows211 rowCount -= chunkRows

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