test(history): preserve screen-owned fixtures
- commit
- a123cbeda4a30aa08afd8025016583600ba5417e
- author
- brice · · 2026-07-12 21:03 UTC
- parent
- e17db81
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 38 | 38 | ||
| 39 | 39 | // NewSnapshotFromRows constructs a snapshot that owns explicit caller rows. | |
| 40 | 40 | // 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 | 43 | func NewSnapshotFromRows(rows [][]renderer.Cell, width, height int) Snapshot { | |
| 43 | 44 | history := vt.NewHistory(vt.HistoryConfig{MaxRows: len(rows), ChunkRows: 256}) | |
| 44 | 45 | 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 than | 40 | // 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) { | |||
| 539 | 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 | 553 | // This scaling gate deliberately compares equal row counts rather than an | |
| 543 | 554 | // absolute budget, which varies between Go releases. A HistoryView shares | |
| 544 | 555 | // 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 an | 553 | // This scaling gate deliberately compares equal row counts rather than an | ||
| 543 | // absolute budget, which varies between Go releases. A HistoryView shares | 554 | // 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) { | |||
| 166 | 166 | writes := make(chan []byte, 1) | |
| 167 | 167 | p, _ := newBlockingPTYWithWrites(t, writes) | |
| 168 | 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 | 170 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 171 | 171 | ||
| 172 | 172 | d.handleInput(sess, ac, []byte("\x1b ")) | |
| @@ -293,7 +293,7 @@ func TestCopyModeSearchModalCapturesMouseAndClearsOnExit(t *testing.T) { | |||
| 293 | 293 | p, _ := newBlockingPTY(t) | |
| 294 | 294 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 295 | 295 | pane := sess.tabs[0].focusedPane() | |
| 296 | - | pane.history = newTestHistory(4) | |
| 296 | + | installTestHistory(pane, vt.HistoryConfig{MaxRows: 4}) | |
| 297 | 297 | pane.history.Append(testRow("old alpha")) | |
| 298 | 298 | copy(pane.screen.Frame.Row(0), testRow("live alpha")) | |
| 299 | 299 | ||
| @@ -322,7 +322,7 @@ func TestCopyModeInputNotForwardedAndOSC52Copy(t *testing.T) { | |||
| 322 | 322 | writes := make(chan []byte, 1) | |
| 323 | 323 | p, _ := newBlockingPTYWithWrites(t, writes) | |
| 324 | 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 | 326 | sess.tabs[0].focusedPane().history.Append(testRow("old1 ")) | |
| 327 | 327 | sess.tabs[0].focusedPane().history.Append(testRow("old2 ")) | |
| 328 | 328 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| @@ -481,7 +481,7 @@ func TestScrollbackEvictionFeedsCopyModeYank(t *testing.T) { | |||
| 481 | 481 | func TestCopyModeEscapeRestoresLiveFullRepaint(t *testing.T) { | |
| 482 | 482 | p, _ := newBlockingPTY(t) | |
| 483 | 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 | 485 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 486 | 486 | ||
| 487 | 487 | d.enterCopyMode(sess, ac) | |
| @@ -515,7 +515,7 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) { | |||
| 515 | 515 | t.Run(tc.name, func(t *testing.T) { | |
| 516 | 516 | p, _ := newBlockingPTY(t) | |
| 517 | 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 | 519 | sess.tabs[0].focusedPane().history.Append(testRow("old1 ")) | |
| 520 | 520 | sess.tabs[0].focusedPane().history.Append(testRow("old2 ")) | |
| 521 | 521 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| @@ -535,7 +535,7 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) { | |||
| 535 | 535 | func TestCopyModeOversizedYankShowsTooLargeFeedback(t *testing.T) { | |
| 536 | 536 | p, _ := newBlockingPTY(t) | |
| 537 | 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 | 539 | longLine := strings.Repeat("x", scopy.OSC52MaxPayloadBytes+1) | |
| 540 | 540 | sess.tabs[0].focusedPane().screen.Frame = renderer.NewFrame(len(longLine), 1) | |
| 541 | 541 | copy(sess.tabs[0].focusedPane().screen.Frame.Row(0), testRow(longLine)) | |
| @@ -560,7 +560,7 @@ func TestCopyModeLoneEscapeExitsAfterDelay(t *testing.T) { | |||
| 560 | 560 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 561 | 561 | clk := &signalClock{timers: make(chan *signalTimer, 1)} | |
| 562 | 562 | d.clock = clk | |
| 563 | - | sess.tabs[0].focusedPane().history = newTestHistory(4) | |
| 563 | + | installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4}) | |
| 564 | 564 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 565 | 565 | ||
| 566 | 566 | d.enterCopyMode(sess, ac) | |
| @@ -584,7 +584,7 @@ func TestCopyModePendingEscapeDoesNotCloseNewMode(t *testing.T) { | |||
| 584 | 584 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 585 | 585 | clk := &signalClock{timers: make(chan *signalTimer, 1)} | |
| 586 | 586 | d.clock = clk | |
| 587 | - | sess.tabs[0].focusedPane().history = newTestHistory(4) | |
| 587 | + | installTestHistory(sess.tabs[0].focusedPane(), vt.HistoryConfig{MaxRows: 4}) | |
| 588 | 588 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 589 | 589 | ||
| 590 | 590 | d.enterCopyMode(sess, ac) | |
| @@ -627,7 +627,7 @@ func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) { | |||
| 627 | 627 | t.Run(tc.name, func(t *testing.T) { | |
| 628 | 628 | p, _ := newBlockingPTY(t) | |
| 629 | 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 | 631 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 632 | 632 | ||
| 633 | 633 | d.enterCopyMode(sess, ac) | |
| @@ -676,7 +676,7 @@ func TestHandleCopyInputUsesImmutableSnapshotWithoutPaneLock(t *testing.T) { | |||
| 676 | 676 | func TestCopyModeEnterExitConcurrentWithPaintRace(t *testing.T) { | |
| 677 | 677 | p, _ := newBlockingPTY(t) | |
| 678 | 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 | 680 | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 681 | 681 | ||
| 682 | 682 | 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 = clk | 562 | 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 = clk | 586 | 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 | |||
| 3 | 3 | import "github.com/bnema/vev/pkg/vt" | |
| 4 | 4 | ||
| 5 | 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 | + | } | |
| @@ -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 ( | |||
| 17 | 17 | "github.com/bnema/vev/internal/usecase/keys" | |
| 18 | 18 | "github.com/bnema/vev/internal/usecase/layout" | |
| 19 | 19 | "github.com/bnema/vev/internal/usecase/picker" | |
| 20 | + | "github.com/bnema/vev/pkg/vt" | |
| 20 | 21 | ) | |
| 21 | 22 | ||
| 22 | 23 | // --- test doubles ----------------------------------------------------------- | |
| @@ -930,7 +931,7 @@ func TestCopyModeStatusRowPressClearsDragState(t *testing.T) { | |||
| 930 | 931 | func TestMouseNormalScreenDragExtendsToCurrentScrollbackOffset(t *testing.T) { | |
| 931 | 932 | p, _ := newBlockingPTY(t) | |
| 932 | 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 | 935 | require.Equal(t, 23, sess.tabs[0].focusedPane().screen.Frame.Height, "fixture assumption: status row is wire row 24") | |
| 935 | 936 | ||
| 936 | 937 | // 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 is | 937 | // 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) { | |||
| 911 | 911 | open: func(d *Daemon, sess *session, ac *attachedClient) { d.enterCopyMode(sess, ac) }, | |
| 912 | 912 | close: func(d *Daemon, _ *session, ac *attachedClient) { d.handleCopyInput(ac, []byte("q")) }, | |
| 913 | 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 | 916 | visible: "[SCROLL]", | |
| 917 | 917 | notVisible: "[SCROLL]", | |
| @@ -923,10 +923,10 @@ func TestOverlayPaintInvalidationShowsAndRestoresBaseFrame(t *testing.T) { | |||
| 923 | 923 | p, release := newBlockingPTY(t) | |
| 924 | 924 | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 925 | 925 | defer release() | |
| 926 | - | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 927 | 926 | if tt.prepareScreen != nil { | |
| 928 | 927 | tt.prepareScreen(sess) | |
| 929 | 928 | } | |
| 929 | + | sess.tabs[0].focusedPane().screen.Write([]byte("live")) | |
| 930 | 930 | if tt.wantRestored == "" { | |
| 931 | 931 | tt.wantRestored = "live" | |
| 932 | 932 | } | |
| @@ -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 | |||
| 100 | 100 | } | |
| 101 | 101 | sess := newSnapshotTestSession(t, "frozen", false, "/work") | |
| 102 | 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 | 105 | pane.history.Append([]renderer.Cell{{Rune: 'o'}, {Rune: 'l'}, {Rune: 'd'}}) | |
| 105 | - | pane.screen = vt.NewScreen(8, 3) | |
| 106 | 106 | pane.screen.Write([]byte("before")) | |
| 107 | 107 | ||
| 108 | 108 | require.True(t, d.captureSession(sess)) | |
| @@ -127,11 +127,11 @@ func TestSnapshotCaptureRetainsImmutableValuesAfterTailMutationAndEviction(t *te | |||
| 127 | 127 | func TestDaemonHistoryEvictionLeavesCopyAndSearchSnapshotsUsable(t *testing.T) { | |
| 128 | 128 | sess := newSnapshotTestSession(t, "copy-lifetime", false, "/work") | |
| 129 | 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 | 132 | for _, text := range []string{"first", "second"} { | |
| 132 | 133 | pane.history.Append(cells(text)) | |
| 133 | 134 | } | |
| 134 | - | pane.screen = vt.NewScreen(8, 1) | |
| 135 | 135 | pane.screen.Write([]byte("screen")) | |
| 136 | 136 | ||
| 137 | 137 | 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 { | |||
| 574 | 574 | return nil | |
| 575 | 575 | } | |
| 576 | 576 | ||
| 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. | |
| 580 | 579 | func validateTree(t *layout.Tree, ids map[layout.PaneID]struct{}) error { | |
| 581 | 580 | if t == nil || t.Root == nil { | |
| 582 | 581 | return nil | |
| @@ -574,9 +574,8 @@ func validateActive(active uint16, tabCount int) error { | |||||
| 574 | return nil | 574 | return nil | ||
| 575 | } | 575 | } | ||
| 576 | 576 | ||||
| 577 | - | // preflightSession first validates the full structural and aggregate resource | 577 | + | // validateTree checks that every pane reference in the layout tree resolves |
| 578 | - | // shape without materializing strings, maps, or reference lists. Only once | 578 | + | // 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 nil | 581 | return nil | ||
M pkg/vt/history_codec_test.go +1 -0
| @@ -205,6 +205,7 @@ func historyPayloadWithDimensions(rowCount, width int) []byte { | |||
| 205 | 205 | data = binary.BigEndian.AppendUint32(data, uint32(chunkRows)) | |
| 206 | 206 | for range chunkRows { | |
| 207 | 207 | data = binary.BigEndian.AppendUint32(data, uint32(width)) | |
| 208 | + | //nolint:makezero // The header prefix is retained while zeroed cell records are appended. | |
| 208 | 209 | data = append(data, make([]byte, width*historyCellBytes)...) | |
| 209 | 210 | } | |
| 210 | 211 | 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 -= chunkRows | 211 | rowCount -= chunkRows | ||