feat(copy): carry stable row identities
- commit
- 7560131e189233c389394b1892b99acb20fe3b2c
- author
- brice · · 2026-08-02 20:15 UTC
- committer
- brice · · 2026-08-03 17:54 UTC
- parent
- 8a3d00a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +100 -5
M internal/usecase/copy/document.go +3 -0
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "unicode" | |
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/vev/pkg/renderer" | |
| 8 | + | "github.com/bnema/vev/pkg/vt" | |
| 8 | 9 | ) | |
| 9 | 10 | ||
| 10 | 11 | // Pos identifies a terminal display cell in a document. Valid positions are | |
| @@ -40,6 +41,8 @@ func (d *Document) Len() int { return d.snapshot.Len() } | |||
| 40 | 41 | func (d *Document) Width() int { return d.snapshot.Width } | |
| 41 | 42 | func (d *Document) Height() int { return d.snapshot.Height } | |
| 42 | 43 | func (d *Document) Row(row int) []renderer.Cell { return d.snapshot.Row(row) } | |
| 44 | + | func (d *Document) RowID(row int) vt.RowID { return d.snapshot.RowID(row) } | |
| 45 | + | func (d *Document) FindRowID(id vt.RowID) int { return d.snapshot.FindRowID(id) } | |
| 43 | 46 | ||
| 44 | 47 | // Normalize returns pos as a valid glyph head. Empty physical rows have the | |
| 45 | 48 | // stable logical position {row, 0}, which is useful to vertical navigation. | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "unicode" | 5 | "unicode" | ||
| 6 | 6 | ||||
| 7 | "github.com/bnema/vev/pkg/renderer" | 7 | "github.com/bnema/vev/pkg/renderer" | ||
| 8 | + | "github.com/bnema/vev/pkg/vt" | |||
| 8 | ) | 9 | ) | ||
| 9 | 10 | ||||
| 10 | // Pos identifies a terminal display cell in a document. Valid positions are | 11 | // Pos identifies a terminal display cell in a document. Valid positions are | ||
| @@ -40,6 +41,8 @@ func (d *Document) Len() int { return d.snapshot.Len() } | |||||
| 40 | func (d *Document) Width() int { return d.snapshot.Width } | 41 | func (d *Document) Width() int { return d.snapshot.Width } | ||
| 41 | func (d *Document) Height() int { return d.snapshot.Height } | 42 | func (d *Document) Height() int { return d.snapshot.Height } | ||
| 42 | func (d *Document) Row(row int) []renderer.Cell { return d.snapshot.Row(row) } | 43 | func (d *Document) Row(row int) []renderer.Cell { return d.snapshot.Row(row) } | ||
| 44 | + | func (d *Document) RowID(row int) vt.RowID { return d.snapshot.RowID(row) } | |||
| 45 | + | func (d *Document) FindRowID(id vt.RowID) int { return d.snapshot.FindRowID(id) } | |||
| 43 | 46 | ||||
| 44 | // Normalize returns pos as a valid glyph head. Empty physical rows have the | 47 | // Normalize returns pos as a valid glyph head. Empty physical rows have the | ||
| 45 | // stable logical position {row, 0}, which is useful to vertical navigation. | 48 | // stable logical position {row, 0}, which is useful to vertical navigation. | ||
M internal/usecase/copy/mode.go +54 -3
| @@ -14,24 +14,30 @@ import ( | |||
| 14 | 14 | const OSC52MaxPayloadBytes = 75_000 | |
| 15 | 15 | ||
| 16 | 16 | // Snapshot is the immutable scrollback document: sealed history plus a cloned | |
| 17 | - | // screen. screenBounds is parallel to screen rows, exactly as the history view's | |
| 18 | - | // own bounds are parallel to its chunk rows. | |
| 17 | + | // screen. screenBounds and screenRowIDs are parallel to screen rows, exactly as | |
| 18 | + | // the history view's own metadata is parallel to its chunk rows. | |
| 19 | 19 | type Snapshot struct { | |
| 20 | 20 | history vt.HistoryView | |
| 21 | 21 | screen renderer.Frame | |
| 22 | 22 | screenBounds []vt.LineBound | |
| 23 | + | screenRowIDs []vt.RowID | |
| 23 | 24 | Width, Height int | |
| 24 | 25 | } | |
| 25 | 26 | ||
| 26 | - | func NewSnapshot(historySource *vt.History, screen renderer.Frame, bounds []vt.LineBound) Snapshot { | |
| 27 | + | func NewSnapshot(historySource *vt.History, screen renderer.Frame, bounds []vt.LineBound, rowIDs ...[]vt.RowID) Snapshot { | |
| 27 | 28 | var history vt.HistoryView | |
| 28 | 29 | if historySource != nil { | |
| 29 | 30 | history = historySource.SealAndView() | |
| 30 | 31 | } | |
| 32 | + | var screenRowIDs []vt.RowID | |
| 33 | + | if len(rowIDs) > 0 { | |
| 34 | + | screenRowIDs = append([]vt.RowID(nil), rowIDs[0]...) | |
| 35 | + | } | |
| 31 | 36 | return Snapshot{ | |
| 32 | 37 | history: history, | |
| 33 | 38 | screen: screen.Clone(), | |
| 34 | 39 | screenBounds: append([]vt.LineBound(nil), bounds...), | |
| 40 | + | screenRowIDs: screenRowIDs, | |
| 35 | 41 | Width: screen.Width, | |
| 36 | 42 | Height: screen.Height, | |
| 37 | 43 | } | |
| @@ -104,6 +110,51 @@ func (s Snapshot) Bound(i int) vt.LineBound { | |||
| 104 | 110 | } | |
| 105 | 111 | return s.screenBounds[i] | |
| 106 | 112 | } | |
| 113 | + | ||
| 114 | + | // RowIDs returns an owned copy of physical row identities in document order. | |
| 115 | + | func (s Snapshot) RowIDs() []vt.RowID { | |
| 116 | + | ids := make([]vt.RowID, s.Len()) | |
| 117 | + | for i := range ids { | |
| 118 | + | ids[i] = s.RowID(i) | |
| 119 | + | } | |
| 120 | + | return ids | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | // RowID returns the identity of document row i, or zero when it is unavailable. | |
| 124 | + | func (s Snapshot) RowID(i int) vt.RowID { | |
| 125 | + | if i < 0 { | |
| 126 | + | return 0 | |
| 127 | + | } | |
| 128 | + | if i < s.history.Len() { | |
| 129 | + | return s.history.RowID(i) | |
| 130 | + | } | |
| 131 | + | i -= s.history.Len() | |
| 132 | + | if i >= s.screen.Height || i >= len(s.screenRowIDs) { | |
| 133 | + | return 0 | |
| 134 | + | } | |
| 135 | + | return s.screenRowIDs[i] | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // FindRowID returns the document row containing id, or -1 when it is absent. | |
| 139 | + | func (s Snapshot) FindRowID(id vt.RowID) int { | |
| 140 | + | if id == 0 { | |
| 141 | + | return -1 | |
| 142 | + | } | |
| 143 | + | if row := s.history.FindRowID(id); row >= 0 { | |
| 144 | + | return row | |
| 145 | + | } | |
| 146 | + | offset := s.history.Len() | |
| 147 | + | for i, candidate := range s.screenRowIDs { | |
| 148 | + | if i >= s.screen.Height { | |
| 149 | + | break | |
| 150 | + | } | |
| 151 | + | if candidate == id { | |
| 152 | + | return offset + i | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | return -1 | |
| 156 | + | } | |
| 157 | + | ||
| 107 | 158 | func (s Snapshot) rangeRows(yield func(int, []renderer.Cell) bool) { | |
| 108 | 159 | i := 0 | |
| 109 | 160 | stopped := false | |
| @@ -14,24 +14,30 @@ import ( | |||||
| 14 | const OSC52MaxPayloadBytes = 75_000 | 14 | const OSC52MaxPayloadBytes = 75_000 | ||
| 15 | 15 | ||||
| 16 | // Snapshot is the immutable scrollback document: sealed history plus a cloned | 16 | // Snapshot is the immutable scrollback document: sealed history plus a cloned | ||
| 17 | - | // screen. screenBounds is parallel to screen rows, exactly as the history view's | 17 | + | // screen. screenBounds and screenRowIDs are parallel to screen rows, exactly as |
| 18 | - | // own bounds are parallel to its chunk rows. | 18 | + | // the history view's own metadata is parallel to its chunk rows. |
| 19 | type Snapshot struct { | 19 | type Snapshot struct { | ||
| 20 | history vt.HistoryView | 20 | history vt.HistoryView | ||
| 21 | screen renderer.Frame | 21 | screen renderer.Frame | ||
| 22 | screenBounds []vt.LineBound | 22 | screenBounds []vt.LineBound | ||
| 23 | + | screenRowIDs []vt.RowID | |||
| 23 | Width, Height int | 24 | Width, Height int | ||
| 24 | } | 25 | } | ||
| 25 | 26 | ||||
| 26 | - | func NewSnapshot(historySource *vt.History, screen renderer.Frame, bounds []vt.LineBound) Snapshot { | 27 | + | func NewSnapshot(historySource *vt.History, screen renderer.Frame, bounds []vt.LineBound, rowIDs ...[]vt.RowID) Snapshot { |
| 27 | var history vt.HistoryView | 28 | var history vt.HistoryView | ||
| 28 | if historySource != nil { | 29 | if historySource != nil { | ||
| 29 | history = historySource.SealAndView() | 30 | history = historySource.SealAndView() | ||
| 30 | } | 31 | } | ||
| 32 | + | var screenRowIDs []vt.RowID | |||
| 33 | + | if len(rowIDs) > 0 { | |||
| 34 | + | screenRowIDs = append([]vt.RowID(nil), rowIDs[0]...) | |||
| 35 | + | } | |||
| 31 | return Snapshot{ | 36 | return Snapshot{ | ||
| 32 | history: history, | 37 | history: history, | ||
| 33 | screen: screen.Clone(), | 38 | screen: screen.Clone(), | ||
| 34 | screenBounds: append([]vt.LineBound(nil), bounds...), | 39 | screenBounds: append([]vt.LineBound(nil), bounds...), | ||
| 40 | + | screenRowIDs: screenRowIDs, | |||
| 35 | Width: screen.Width, | 41 | Width: screen.Width, | ||
| 36 | Height: screen.Height, | 42 | Height: screen.Height, | ||
| 37 | } | 43 | } | ||
| @@ -104,6 +110,51 @@ func (s Snapshot) Bound(i int) vt.LineBound { | |||||
| 104 | } | 110 | } | ||
| 105 | return s.screenBounds[i] | 111 | return s.screenBounds[i] | ||
| 106 | } | 112 | } | ||
| 113 | + | ||||
| 114 | + | // RowIDs returns an owned copy of physical row identities in document order. | |||
| 115 | + | func (s Snapshot) RowIDs() []vt.RowID { | |||
| 116 | + | ids := make([]vt.RowID, s.Len()) | |||
| 117 | + | for i := range ids { | |||
| 118 | + | ids[i] = s.RowID(i) | |||
| 119 | + | } | |||
| 120 | + | return ids | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | // RowID returns the identity of document row i, or zero when it is unavailable. | |||
| 124 | + | func (s Snapshot) RowID(i int) vt.RowID { | |||
| 125 | + | if i < 0 { | |||
| 126 | + | return 0 | |||
| 127 | + | } | |||
| 128 | + | if i < s.history.Len() { | |||
| 129 | + | return s.history.RowID(i) | |||
| 130 | + | } | |||
| 131 | + | i -= s.history.Len() | |||
| 132 | + | if i >= s.screen.Height || i >= len(s.screenRowIDs) { | |||
| 133 | + | return 0 | |||
| 134 | + | } | |||
| 135 | + | return s.screenRowIDs[i] | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // FindRowID returns the document row containing id, or -1 when it is absent. | |||
| 139 | + | func (s Snapshot) FindRowID(id vt.RowID) int { | |||
| 140 | + | if id == 0 { | |||
| 141 | + | return -1 | |||
| 142 | + | } | |||
| 143 | + | if row := s.history.FindRowID(id); row >= 0 { | |||
| 144 | + | return row | |||
| 145 | + | } | |||
| 146 | + | offset := s.history.Len() | |||
| 147 | + | for i, candidate := range s.screenRowIDs { | |||
| 148 | + | if i >= s.screen.Height { | |||
| 149 | + | break | |||
| 150 | + | } | |||
| 151 | + | if candidate == id { | |||
| 152 | + | return offset + i | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | return -1 | |||
| 156 | + | } | |||
| 157 | + | ||||
| 107 | func (s Snapshot) rangeRows(yield func(int, []renderer.Cell) bool) { | 158 | func (s Snapshot) rangeRows(yield func(int, []renderer.Cell) bool) { | ||
| 108 | i := 0 | 159 | i := 0 | ||
| 109 | stopped := false | 160 | stopped := false | ||
M internal/usecase/copy/mode_test.go +23 -0
| @@ -42,6 +42,29 @@ func TestNewSnapshotFromRowsPreservesWideRows(t *testing.T) { | |||
| 42 | 42 | } | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | + | func TestSnapshotCarriesImmutableRowIDsAndDocumentsResolveBookmarks(t *testing.T) { | |
| 46 | + | history := vt.NewHistory(vt.HistoryConfig{MaxRows: 8, MaxCells: 1024}) | |
| 47 | + | require.NoError(t, history.Append(row("hist"), vt.LineBound{End: 4}, vt.RowID(11))) | |
| 48 | + | require.NoError(t, history.Append(row("tail"), vt.LineBound{End: 4}, vt.RowID(12))) | |
| 49 | + | screen := renderer.NewFrame(4, 2) | |
| 50 | + | copy(screen.Row(0), row("live")) | |
| 51 | + | copy(screen.Row(1), row("more")) | |
| 52 | + | rowIDs := []vt.RowID{21, 22} | |
| 53 | + | ||
| 54 | + | snapshot := NewSnapshot(history, screen, []vt.LineBound{{End: 4}, {End: 4}}, rowIDs) | |
| 55 | + | rowIDs[0] = 99 | |
| 56 | + | doc := NewDocument(snapshot, "") | |
| 57 | + | ||
| 58 | + | require.Equal(t, []vt.RowID{11, 12, 21, 22}, snapshot.RowIDs()) | |
| 59 | + | ids := snapshot.RowIDs() | |
| 60 | + | ids[0] = 100 | |
| 61 | + | require.Equal(t, vt.RowID(11), snapshot.RowID(0)) | |
| 62 | + | require.Equal(t, vt.RowID(21), doc.RowID(2)) | |
| 63 | + | require.Equal(t, 2, doc.FindRowID(vt.RowID(21))) | |
| 64 | + | require.Equal(t, 3, doc.FindRowID(vt.RowID(22))) | |
| 65 | + | require.Equal(t, -1, doc.FindRowID(vt.RowID(99))) | |
| 66 | + | } | |
| 67 | + | ||
| 45 | 68 | func TestSnapshotBoundDispatchesLikeRow(t *testing.T) { | |
| 46 | 69 | row := func(s string) []renderer.Cell { | |
| 47 | 70 | cells := make([]renderer.Cell, 0, len(s)) | |
| @@ -42,6 +42,29 @@ func TestNewSnapshotFromRowsPreservesWideRows(t *testing.T) { | |||||
| 42 | } | 42 | } | ||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | + | func TestSnapshotCarriesImmutableRowIDsAndDocumentsResolveBookmarks(t *testing.T) { | |||
| 46 | + | history := vt.NewHistory(vt.HistoryConfig{MaxRows: 8, MaxCells: 1024}) | |||
| 47 | + | require.NoError(t, history.Append(row("hist"), vt.LineBound{End: 4}, vt.RowID(11))) | |||
| 48 | + | require.NoError(t, history.Append(row("tail"), vt.LineBound{End: 4}, vt.RowID(12))) | |||
| 49 | + | screen := renderer.NewFrame(4, 2) | |||
| 50 | + | copy(screen.Row(0), row("live")) | |||
| 51 | + | copy(screen.Row(1), row("more")) | |||
| 52 | + | rowIDs := []vt.RowID{21, 22} | |||
| 53 | + | ||||
| 54 | + | snapshot := NewSnapshot(history, screen, []vt.LineBound{{End: 4}, {End: 4}}, rowIDs) | |||
| 55 | + | rowIDs[0] = 99 | |||
| 56 | + | doc := NewDocument(snapshot, "") | |||
| 57 | + | ||||
| 58 | + | require.Equal(t, []vt.RowID{11, 12, 21, 22}, snapshot.RowIDs()) | |||
| 59 | + | ids := snapshot.RowIDs() | |||
| 60 | + | ids[0] = 100 | |||
| 61 | + | require.Equal(t, vt.RowID(11), snapshot.RowID(0)) | |||
| 62 | + | require.Equal(t, vt.RowID(21), doc.RowID(2)) | |||
| 63 | + | require.Equal(t, 2, doc.FindRowID(vt.RowID(21))) | |||
| 64 | + | require.Equal(t, 3, doc.FindRowID(vt.RowID(22))) | |||
| 65 | + | require.Equal(t, -1, doc.FindRowID(vt.RowID(99))) | |||
| 66 | + | } | |||
| 67 | + | ||||
| 45 | func TestSnapshotBoundDispatchesLikeRow(t *testing.T) { | 68 | func TestSnapshotBoundDispatchesLikeRow(t *testing.T) { | ||
| 46 | row := func(s string) []renderer.Cell { | 69 | row := func(s string) []renderer.Cell { | ||
| 47 | cells := make([]renderer.Cell, 0, len(s)) | 70 | cells := make([]renderer.Cell, 0, len(s)) | ||
M internal/usecase/daemon/copy_mouse_input.go +1 -1
| @@ -68,7 +68,7 @@ func (d *Daemon) handleFreshCopyPress(sess *session, ac *attachedClient, tb *tab | |||
| 68 | 68 | geometry.pane.mu.Lock() | |
| 69 | 69 | mouseMode, _ := geometry.pane.screen.MouseMode() | |
| 70 | 70 | altScreen := geometry.pane.screen.AltScreenActive() | |
| 71 | - | snapshot := scopy.NewSnapshot(geometry.pane.history, geometry.pane.screen.Frame, geometry.pane.screen.LineBounds()) | |
| 71 | + | snapshot := scopy.NewSnapshot(geometry.pane.history, geometry.pane.screen.Frame, geometry.pane.screen.LineBounds(), geometry.pane.screen.RowIDs()) | |
| 72 | 72 | geometry.pane.mu.Unlock() | |
| 73 | 73 | if mouseMode != 0 || altScreen { | |
| 74 | 74 | return false // child forwarding retains its existing raw-byte path. | |
| @@ -68,7 +68,7 @@ func (d *Daemon) handleFreshCopyPress(sess *session, ac *attachedClient, tb *tab | |||||
| 68 | geometry.pane.mu.Lock() | 68 | geometry.pane.mu.Lock() | ||
| 69 | mouseMode, _ := geometry.pane.screen.MouseMode() | 69 | mouseMode, _ := geometry.pane.screen.MouseMode() | ||
| 70 | altScreen := geometry.pane.screen.AltScreenActive() | 70 | altScreen := geometry.pane.screen.AltScreenActive() | ||
| 71 | - | snapshot := scopy.NewSnapshot(geometry.pane.history, geometry.pane.screen.Frame, geometry.pane.screen.LineBounds()) | 71 | + | snapshot := scopy.NewSnapshot(geometry.pane.history, geometry.pane.screen.Frame, geometry.pane.screen.LineBounds(), geometry.pane.screen.RowIDs()) |
| 72 | geometry.pane.mu.Unlock() | 72 | geometry.pane.mu.Unlock() | ||
| 73 | if mouseMode != 0 || altScreen { | 73 | if mouseMode != 0 || altScreen { | ||
| 74 | return false // child forwarding retains its existing raw-byte path. | 74 | return false // child forwarding retains its existing raw-byte path. | ||
M internal/usecase/daemon/copymode.go +1 -1
| @@ -74,7 +74,7 @@ func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |||
| 74 | 74 | return | |
| 75 | 75 | } | |
| 76 | 76 | p.mu.Lock() | |
| 77 | - | snapshot := scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds()) | |
| 77 | + | snapshot := scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), p.screen.RowIDs()) | |
| 78 | 78 | p.mu.Unlock() | |
| 79 | 79 | cfg := d.currentCopyConfig() | |
| 80 | 80 | document := scopy.NewDocument(snapshot, cfg.WordSeparators) | |
| @@ -74,7 +74,7 @@ func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |||||
| 74 | return | 74 | return | ||
| 75 | } | 75 | } | ||
| 76 | p.mu.Lock() | 76 | p.mu.Lock() | ||
| 77 | - | snapshot := scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds()) | 77 | + | snapshot := scopy.NewSnapshot(p.history, p.screen.Frame, p.screen.LineBounds(), p.screen.RowIDs()) |
| 78 | p.mu.Unlock() | 78 | p.mu.Unlock() | ||
| 79 | cfg := d.currentCopyConfig() | 79 | cfg := d.currentCopyConfig() | ||
| 80 | document := scopy.NewDocument(snapshot, cfg.WordSeparators) | 80 | document := scopy.NewDocument(snapshot, cfg.WordSeparators) | ||
M internal/usecase/daemon/copymode_test.go +18 -0
| @@ -105,6 +105,24 @@ func TestCopySearchModalGeometry(t *testing.T) { | |||
| 105 | 105 | require.Equal(t, ui.Margins{Bottom: 1}, copySearchModal.Margins) | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | + | func TestCopyModeDocumentCarriesPaneRowIDs(t *testing.T) { | |
| 109 | + | p, _ := newBlockingPTY(t) | |
| 110 | + | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |
| 111 | + | pane := sess.activeTab().focusedPane() | |
| 112 | + | appendHistoryRow(t, pane.history, testRow("history")) | |
| 113 | + | pane.screen.Write([]byte("live")) | |
| 114 | + | historyID := pane.history.View().RowID(0) | |
| 115 | + | liveID := pane.screen.RowID(0) | |
| 116 | + | ||
| 117 | + | d.enterCopyMode(sess, ac) | |
| 118 | + | awaitFrame(t, sends, ports.MsgOutput) | |
| 119 | + | doc := ac.overlays.copyDocument | |
| 120 | + | ||
| 121 | + | require.Equal(t, 0, doc.FindRowID(historyID)) | |
| 122 | + | require.Equal(t, pane.history.Len(), doc.FindRowID(liveID)) | |
| 123 | + | require.Equal(t, liveID, doc.RowID(pane.history.Len())) | |
| 124 | + | } | |
| 125 | + | ||
| 108 | 126 | func TestComposeCopyClientFrameConcurrentPaneOutput(t *testing.T) { | |
| 109 | 127 | p, release := newBlockingPTY(t) | |
| 110 | 128 | defer release() | |
| @@ -105,6 +105,24 @@ func TestCopySearchModalGeometry(t *testing.T) { | |||||
| 105 | require.Equal(t, ui.Margins{Bottom: 1}, copySearchModal.Margins) | 105 | require.Equal(t, ui.Margins{Bottom: 1}, copySearchModal.Margins) | ||
| 106 | } | 106 | } | ||
| 107 | 107 | ||||
| 108 | + | func TestCopyModeDocumentCarriesPaneRowIDs(t *testing.T) { | |||
| 109 | + | p, _ := newBlockingPTY(t) | |||
| 110 | + | d, sess, ac, sends := newManualSessionWithPTYs(t, p) | |||
| 111 | + | pane := sess.activeTab().focusedPane() | |||
| 112 | + | appendHistoryRow(t, pane.history, testRow("history")) | |||
| 113 | + | pane.screen.Write([]byte("live")) | |||
| 114 | + | historyID := pane.history.View().RowID(0) | |||
| 115 | + | liveID := pane.screen.RowID(0) | |||
| 116 | + | ||||
| 117 | + | d.enterCopyMode(sess, ac) | |||
| 118 | + | awaitFrame(t, sends, ports.MsgOutput) | |||
| 119 | + | doc := ac.overlays.copyDocument | |||
| 120 | + | ||||
| 121 | + | require.Equal(t, 0, doc.FindRowID(historyID)) | |||
| 122 | + | require.Equal(t, pane.history.Len(), doc.FindRowID(liveID)) | |||
| 123 | + | require.Equal(t, liveID, doc.RowID(pane.history.Len())) | |||
| 124 | + | } | |||
| 125 | + | ||||
| 108 | func TestComposeCopyClientFrameConcurrentPaneOutput(t *testing.T) { | 126 | func TestComposeCopyClientFrameConcurrentPaneOutput(t *testing.T) { | ||
| 109 | p, release := newBlockingPTY(t) | 127 | p, release := newBlockingPTY(t) | ||
| 110 | defer release() | 128 | defer release() | ||