perf(copy): retain one immutable copy document
- commit
- a628e5995e9de02c1e9c074870baa253db2fbd02
- author
- brice · · 2026-07-11 06:07 UTC
- committer
- brice · · 2026-07-11 06:16 UTC
- parent
- 2f5b567
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +83 -43
M internal/usecase/daemon/copymode.go +42 -29
| @@ -61,16 +61,21 @@ func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | |||
| 61 | 61 | p.mu.Unlock() | |
| 62 | 62 | return | |
| 63 | 63 | } | |
| 64 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 65 | - | if delta > 0 && rt.copyMode.AtBottom(snap) { | |
| 64 | + | if rt.copySnapshot == nil { | |
| 65 | + | rt.copyMu.Unlock() | |
| 66 | + | p.mu.Unlock() | |
| 67 | + | return | |
| 68 | + | } | |
| 69 | + | document := rt.copySnapshot | |
| 70 | + | if delta > 0 && rt.copyMode.AtBottom(*document) { | |
| 66 | 71 | rt.clearCopyModeLocked() | |
| 67 | 72 | rt.copyMu.Unlock() | |
| 68 | 73 | p.mu.Unlock() | |
| 69 | 74 | d.paint(sess, ac, true) | |
| 70 | 75 | return | |
| 71 | 76 | } | |
| 72 | - | rt.copyMode.Move(snap, delta) | |
| 73 | - | exit := delta > 0 && rt.copyMode.AtBottom(snap) | |
| 77 | + | rt.copyMode.Move(*document, delta) | |
| 78 | + | exit := delta > 0 && rt.copyMode.AtBottom(*document) | |
| 74 | 79 | if exit { | |
| 75 | 80 | rt.clearCopyModeLocked() | |
| 76 | 81 | } | |
| @@ -94,10 +99,10 @@ func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |||
| 94 | 99 | return | |
| 95 | 100 | } | |
| 96 | 101 | p.mu.Lock() | |
| 97 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 98 | - | p.mu.Unlock() | |
| 102 | + | document := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 99 | 103 | rt.copyMu.Lock() | |
| 100 | - | rt.copyMode = scopy.NewMode(snap) | |
| 104 | + | rt.copyMode = scopy.NewMode(document) | |
| 105 | + | rt.copySnapshot = &document | |
| 101 | 106 | rt.copyPane = p | |
| 102 | 107 | rt.copySearch = nil | |
| 103 | 108 | rt.copySearchPending = nil | |
| @@ -105,6 +110,7 @@ func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |||
| 105 | 110 | rt.copyDragging = false | |
| 106 | 111 | rt.normalMousePressValid = false | |
| 107 | 112 | rt.copyMu.Unlock() | |
| 113 | + | p.mu.Unlock() | |
| 108 | 114 | if floatingSource { | |
| 109 | 115 | tb.mu.Lock() | |
| 110 | 116 | stillVisible := tb.floating.state == floatingVisible && tb.floating.pane == p | |
| @@ -142,12 +148,17 @@ func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |||
| 142 | 148 | p.mu.Unlock() | |
| 143 | 149 | return | |
| 144 | 150 | } | |
| 145 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 151 | + | if rt.copySnapshot == nil { | |
| 152 | + | rt.copyMu.Unlock() | |
| 153 | + | p.mu.Unlock() | |
| 154 | + | return | |
| 155 | + | } | |
| 156 | + | document := rt.copySnapshot | |
| 146 | 157 | absRow := rt.copyMode.ViewportTop + ev.Row | |
| 147 | 158 | changed := false | |
| 148 | 159 | switch ev.Type { | |
| 149 | 160 | case mouse.Press: | |
| 150 | - | rt.copyMode.SetCursor(snap, absRow) | |
| 161 | + | rt.copyMode.SetCursor(*document, absRow) | |
| 151 | 162 | rt.copyPressRow = rt.copyMode.Cursor | |
| 152 | 163 | rt.copyPressRowValid = true | |
| 153 | 164 | rt.copyDragging = false | |
| @@ -157,10 +168,10 @@ func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |||
| 157 | 168 | break | |
| 158 | 169 | } | |
| 159 | 170 | if !rt.copyDragging { | |
| 160 | - | rt.copyMode.StartSelectionAt(snap, rt.copyPressRow) | |
| 171 | + | rt.copyMode.StartSelectionAt(*document, rt.copyPressRow) | |
| 161 | 172 | rt.copyDragging = true | |
| 162 | 173 | } | |
| 163 | - | rt.copyMode.ExtendTo(snap, absRow) | |
| 174 | + | rt.copyMode.ExtendTo(*document, absRow) | |
| 164 | 175 | changed = true | |
| 165 | 176 | case mouse.Release: | |
| 166 | 177 | // Button release intentionally has no visual effect. | |
| @@ -200,9 +211,14 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 200 | 211 | data = combined | |
| 201 | 212 | rt.copyPending = nil | |
| 202 | 213 | } | |
| 203 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 214 | + | if rt.copySnapshot == nil { | |
| 215 | + | rt.copyMu.Unlock() | |
| 216 | + | p.mu.Unlock() | |
| 217 | + | return | |
| 218 | + | } | |
| 219 | + | document := rt.copySnapshot | |
| 204 | 220 | if rt.copySearch != nil { | |
| 205 | - | changed, closeSearch, accepted := d.routeCopySearchInputLocked(rt, snap, data) | |
| 221 | + | changed, closeSearch, accepted := d.routeCopySearchInputLocked(rt, *document, data) | |
| 206 | 222 | rt.copyMu.Unlock() | |
| 207 | 223 | p.mu.Unlock() | |
| 208 | 224 | if changed || closeSearch || accepted { | |
| @@ -216,40 +232,40 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 216 | 232 | for i := 0; i < len(data); i++ { | |
| 217 | 233 | switch data[i] { | |
| 218 | 234 | case 'j': | |
| 219 | - | rt.copyMode.Move(snap, 1) | |
| 235 | + | rt.copyMode.Move(*document, 1) | |
| 220 | 236 | changed = true | |
| 221 | 237 | case 'k': | |
| 222 | - | rt.copyMode.Move(snap, -1) | |
| 238 | + | rt.copyMode.Move(*document, -1) | |
| 223 | 239 | changed = true | |
| 224 | 240 | case 'g': | |
| 225 | - | rt.copyMode.Top(snap) | |
| 241 | + | rt.copyMode.Top(*document) | |
| 226 | 242 | changed = true | |
| 227 | 243 | case 'G': | |
| 228 | - | rt.copyMode.Bottom(snap) | |
| 244 | + | rt.copyMode.Bottom(*document) | |
| 229 | 245 | changed = true | |
| 230 | 246 | case ' ', 'v': | |
| 231 | 247 | rt.copyMode.ToggleSelection() | |
| 232 | 248 | changed = true | |
| 233 | 249 | case '/': | |
| 234 | - | rt.copySearch = visualsearch.New(snap) | |
| 250 | + | rt.copySearch = visualsearch.New(*document) | |
| 235 | 251 | rt.copySearchPending = nil | |
| 236 | 252 | changed = true | |
| 237 | 253 | if i+1 < len(data) { | |
| 238 | - | searchChanged, _, accepted := d.routeCopySearchInputLocked(rt, snap, data[i+1:]) | |
| 254 | + | searchChanged, _, accepted := d.routeCopySearchInputLocked(rt, *document, data[i+1:]) | |
| 239 | 255 | changed = changed || searchChanged || accepted | |
| 240 | 256 | } | |
| 241 | 257 | i = len(data) | |
| 242 | 258 | case 'n': | |
| 243 | - | changed = rt.copyMode.NextSearchMatch(snap, 1) || changed | |
| 259 | + | changed = rt.copyMode.NextSearchMatch(*document, 1) || changed | |
| 244 | 260 | case 'N': | |
| 245 | - | changed = rt.copyMode.NextSearchMatch(snap, -1) || changed | |
| 261 | + | changed = rt.copyMode.NextSearchMatch(*document, -1) || changed | |
| 246 | 262 | case '\r', '\n', 'y': | |
| 247 | 263 | copyOut = true | |
| 248 | 264 | exit = true | |
| 249 | 265 | case 'q', 0x03, 0x1b: | |
| 250 | 266 | if data[i] == 0x1b { | |
| 251 | 267 | tail := data[i:] | |
| 252 | - | consumed, ok := routeCopyEscape(rt.copyMode, snap, tail) | |
| 268 | + | consumed, ok := routeCopyEscape(rt.copyMode, *document, tail) | |
| 253 | 269 | if ok { | |
| 254 | 270 | i += consumed - 1 | |
| 255 | 271 | changed = true | |
| @@ -269,7 +285,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||
| 269 | 285 | } | |
| 270 | 286 | text := "" | |
| 271 | 287 | if copyOut { | |
| 272 | - | text = rt.copyMode.SelectedText(snap) | |
| 288 | + | text = rt.copyMode.SelectedText(*document) | |
| 273 | 289 | } | |
| 274 | 290 | if exit { | |
| 275 | 291 | d.stopCopyPendingTimerLocked(ac) | |
| @@ -417,15 +433,12 @@ func composeCopySearchClientFrame(model *visualsearch.Model, base renderer.Frame | |||
| 417 | 433 | return composeModalClientFrame(base, copySearchModal, styleSet, styleSet.selection, model.Render) | |
| 418 | 434 | } | |
| 419 | 435 | ||
| 420 | - | func composeCopyClientFrame(mode *scopy.Mode, p *pane, target domain.Rect, frame renderer.Frame, bars barState) (renderer.Frame, []renderer.Damage) { | |
| 421 | - | if mode == nil || p == nil || target.Width <= 0 || target.Height <= 0 || frame.Width <= 0 || frame.Height <= 0 { | |
| 436 | + | func composeCopyClientFrame(mode *scopy.Mode, document *scopy.Snapshot, target domain.Rect, frame renderer.Frame, bars barState) (renderer.Frame, []renderer.Damage) { | |
| 437 | + | if mode == nil || document == nil || target.Width <= 0 || target.Height <= 0 || frame.Width <= 0 || frame.Height <= 0 { | |
| 422 | 438 | return frame, nil | |
| 423 | 439 | } | |
| 424 | 440 | styles := newThemeStyles(bars.theme) | |
| 425 | - | p.mu.Lock() | |
| 426 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 427 | - | p.mu.Unlock() | |
| 428 | - | copyFrame := mode.Render(snap, styles.copyStatus, styles.selection) | |
| 441 | + | copyFrame := mode.Render(*document, styles.copyStatus, styles.selection) | |
| 429 | 442 | bodyRows := max(copyFrame.Height-1, 0) | |
| 430 | 443 | for y := 0; y < target.Height && y < bodyRows && target.Y+y < frame.Height-1; y++ { | |
| 431 | 444 | dstX := max(target.X, 0) | |
| @@ -61,16 +61,21 @@ func (d *Daemon) copyWheel(sess *session, ac *attachedClient, delta int) { | |||||
| 61 | p.mu.Unlock() | 61 | p.mu.Unlock() | ||
| 62 | return | 62 | return | ||
| 63 | } | 63 | } | ||
| 64 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | 64 | + | if rt.copySnapshot == nil { |
| 65 | - | if delta > 0 && rt.copyMode.AtBottom(snap) { | 65 | + | rt.copyMu.Unlock() |
| 66 | + | p.mu.Unlock() | |||
| 67 | + | return | |||
| 68 | + | } | |||
| 69 | + | document := rt.copySnapshot | |||
| 70 | + | if delta > 0 && rt.copyMode.AtBottom(*document) { | |||
| 66 | rt.clearCopyModeLocked() | 71 | rt.clearCopyModeLocked() | ||
| 67 | rt.copyMu.Unlock() | 72 | rt.copyMu.Unlock() | ||
| 68 | p.mu.Unlock() | 73 | p.mu.Unlock() | ||
| 69 | d.paint(sess, ac, true) | 74 | d.paint(sess, ac, true) | ||
| 70 | return | 75 | return | ||
| 71 | } | 76 | } | ||
| 72 | - | rt.copyMode.Move(snap, delta) | 77 | + | rt.copyMode.Move(*document, delta) |
| 73 | - | exit := delta > 0 && rt.copyMode.AtBottom(snap) | 78 | + | exit := delta > 0 && rt.copyMode.AtBottom(*document) |
| 74 | if exit { | 79 | if exit { | ||
| 75 | rt.clearCopyModeLocked() | 80 | rt.clearCopyModeLocked() | ||
| 76 | } | 81 | } | ||
| @@ -94,10 +99,10 @@ func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |||||
| 94 | return | 99 | return | ||
| 95 | } | 100 | } | ||
| 96 | p.mu.Lock() | 101 | p.mu.Lock() | ||
| 97 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | 102 | + | document := scopy.NewSnapshot(p.scrollback, p.screen.Frame) |
| 98 | - | p.mu.Unlock() | |||
| 99 | rt.copyMu.Lock() | 103 | rt.copyMu.Lock() | ||
| 100 | - | rt.copyMode = scopy.NewMode(snap) | 104 | + | rt.copyMode = scopy.NewMode(document) |
| 105 | + | rt.copySnapshot = &document | |||
| 101 | rt.copyPane = p | 106 | rt.copyPane = p | ||
| 102 | rt.copySearch = nil | 107 | rt.copySearch = nil | ||
| 103 | rt.copySearchPending = nil | 108 | rt.copySearchPending = nil | ||
| @@ -105,6 +110,7 @@ func (d *Daemon) enterCopyMode(sess *session, ac *attachedClient) { | |||||
| 105 | rt.copyDragging = false | 110 | rt.copyDragging = false | ||
| 106 | rt.normalMousePressValid = false | 111 | rt.normalMousePressValid = false | ||
| 107 | rt.copyMu.Unlock() | 112 | rt.copyMu.Unlock() | ||
| 113 | + | p.mu.Unlock() | |||
| 108 | if floatingSource { | 114 | if floatingSource { | ||
| 109 | tb.mu.Lock() | 115 | tb.mu.Lock() | ||
| 110 | stillVisible := tb.floating.state == floatingVisible && tb.floating.pane == p | 116 | stillVisible := tb.floating.state == floatingVisible && tb.floating.pane == p | ||
| @@ -142,12 +148,17 @@ func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |||||
| 142 | p.mu.Unlock() | 148 | p.mu.Unlock() | ||
| 143 | return | 149 | return | ||
| 144 | } | 150 | } | ||
| 145 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | 151 | + | if rt.copySnapshot == nil { |
| 152 | + | rt.copyMu.Unlock() | |||
| 153 | + | p.mu.Unlock() | |||
| 154 | + | return | |||
| 155 | + | } | |||
| 156 | + | document := rt.copySnapshot | |||
| 146 | absRow := rt.copyMode.ViewportTop + ev.Row | 157 | absRow := rt.copyMode.ViewportTop + ev.Row | ||
| 147 | changed := false | 158 | changed := false | ||
| 148 | switch ev.Type { | 159 | switch ev.Type { | ||
| 149 | case mouse.Press: | 160 | case mouse.Press: | ||
| 150 | - | rt.copyMode.SetCursor(snap, absRow) | 161 | + | rt.copyMode.SetCursor(*document, absRow) |
| 151 | rt.copyPressRow = rt.copyMode.Cursor | 162 | rt.copyPressRow = rt.copyMode.Cursor | ||
| 152 | rt.copyPressRowValid = true | 163 | rt.copyPressRowValid = true | ||
| 153 | rt.copyDragging = false | 164 | rt.copyDragging = false | ||
| @@ -157,10 +168,10 @@ func (d *Daemon) copyMouse(sess *session, ac *attachedClient, ev mouse.Event) { | |||||
| 157 | break | 168 | break | ||
| 158 | } | 169 | } | ||
| 159 | if !rt.copyDragging { | 170 | if !rt.copyDragging { | ||
| 160 | - | rt.copyMode.StartSelectionAt(snap, rt.copyPressRow) | 171 | + | rt.copyMode.StartSelectionAt(*document, rt.copyPressRow) |
| 161 | rt.copyDragging = true | 172 | rt.copyDragging = true | ||
| 162 | } | 173 | } | ||
| 163 | - | rt.copyMode.ExtendTo(snap, absRow) | 174 | + | rt.copyMode.ExtendTo(*document, absRow) |
| 164 | changed = true | 175 | changed = true | ||
| 165 | case mouse.Release: | 176 | case mouse.Release: | ||
| 166 | // Button release intentionally has no visual effect. | 177 | // Button release intentionally has no visual effect. | ||
| @@ -200,9 +211,14 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 200 | data = combined | 211 | data = combined | ||
| 201 | rt.copyPending = nil | 212 | rt.copyPending = nil | ||
| 202 | } | 213 | } | ||
| 203 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | 214 | + | if rt.copySnapshot == nil { |
| 215 | + | rt.copyMu.Unlock() | |||
| 216 | + | p.mu.Unlock() | |||
| 217 | + | return | |||
| 218 | + | } | |||
| 219 | + | document := rt.copySnapshot | |||
| 204 | if rt.copySearch != nil { | 220 | if rt.copySearch != nil { | ||
| 205 | - | changed, closeSearch, accepted := d.routeCopySearchInputLocked(rt, snap, data) | 221 | + | changed, closeSearch, accepted := d.routeCopySearchInputLocked(rt, *document, data) |
| 206 | rt.copyMu.Unlock() | 222 | rt.copyMu.Unlock() | ||
| 207 | p.mu.Unlock() | 223 | p.mu.Unlock() | ||
| 208 | if changed || closeSearch || accepted { | 224 | if changed || closeSearch || accepted { | ||
| @@ -216,40 +232,40 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 216 | for i := 0; i < len(data); i++ { | 232 | for i := 0; i < len(data); i++ { | ||
| 217 | switch data[i] { | 233 | switch data[i] { | ||
| 218 | case 'j': | 234 | case 'j': | ||
| 219 | - | rt.copyMode.Move(snap, 1) | 235 | + | rt.copyMode.Move(*document, 1) |
| 220 | changed = true | 236 | changed = true | ||
| 221 | case 'k': | 237 | case 'k': | ||
| 222 | - | rt.copyMode.Move(snap, -1) | 238 | + | rt.copyMode.Move(*document, -1) |
| 223 | changed = true | 239 | changed = true | ||
| 224 | case 'g': | 240 | case 'g': | ||
| 225 | - | rt.copyMode.Top(snap) | 241 | + | rt.copyMode.Top(*document) |
| 226 | changed = true | 242 | changed = true | ||
| 227 | case 'G': | 243 | case 'G': | ||
| 228 | - | rt.copyMode.Bottom(snap) | 244 | + | rt.copyMode.Bottom(*document) |
| 229 | changed = true | 245 | changed = true | ||
| 230 | case ' ', 'v': | 246 | case ' ', 'v': | ||
| 231 | rt.copyMode.ToggleSelection() | 247 | rt.copyMode.ToggleSelection() | ||
| 232 | changed = true | 248 | changed = true | ||
| 233 | case '/': | 249 | case '/': | ||
| 234 | - | rt.copySearch = visualsearch.New(snap) | 250 | + | rt.copySearch = visualsearch.New(*document) |
| 235 | rt.copySearchPending = nil | 251 | rt.copySearchPending = nil | ||
| 236 | changed = true | 252 | changed = true | ||
| 237 | if i+1 < len(data) { | 253 | if i+1 < len(data) { | ||
| 238 | - | searchChanged, _, accepted := d.routeCopySearchInputLocked(rt, snap, data[i+1:]) | 254 | + | searchChanged, _, accepted := d.routeCopySearchInputLocked(rt, *document, data[i+1:]) |
| 239 | changed = changed || searchChanged || accepted | 255 | changed = changed || searchChanged || accepted | ||
| 240 | } | 256 | } | ||
| 241 | i = len(data) | 257 | i = len(data) | ||
| 242 | case 'n': | 258 | case 'n': | ||
| 243 | - | changed = rt.copyMode.NextSearchMatch(snap, 1) || changed | 259 | + | changed = rt.copyMode.NextSearchMatch(*document, 1) || changed |
| 244 | case 'N': | 260 | case 'N': | ||
| 245 | - | changed = rt.copyMode.NextSearchMatch(snap, -1) || changed | 261 | + | changed = rt.copyMode.NextSearchMatch(*document, -1) || changed |
| 246 | case '\r', '\n', 'y': | 262 | case '\r', '\n', 'y': | ||
| 247 | copyOut = true | 263 | copyOut = true | ||
| 248 | exit = true | 264 | exit = true | ||
| 249 | case 'q', 0x03, 0x1b: | 265 | case 'q', 0x03, 0x1b: | ||
| 250 | if data[i] == 0x1b { | 266 | if data[i] == 0x1b { | ||
| 251 | tail := data[i:] | 267 | tail := data[i:] | ||
| 252 | - | consumed, ok := routeCopyEscape(rt.copyMode, snap, tail) | 268 | + | consumed, ok := routeCopyEscape(rt.copyMode, *document, tail) |
| 253 | if ok { | 269 | if ok { | ||
| 254 | i += consumed - 1 | 270 | i += consumed - 1 | ||
| 255 | changed = true | 271 | changed = true | ||
| @@ -269,7 +285,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 269 | } | 285 | } | ||
| 270 | text := "" | 286 | text := "" | ||
| 271 | if copyOut { | 287 | if copyOut { | ||
| 272 | - | text = rt.copyMode.SelectedText(snap) | 288 | + | text = rt.copyMode.SelectedText(*document) |
| 273 | } | 289 | } | ||
| 274 | if exit { | 290 | if exit { | ||
| 275 | d.stopCopyPendingTimerLocked(ac) | 291 | d.stopCopyPendingTimerLocked(ac) | ||
| @@ -417,15 +433,12 @@ func composeCopySearchClientFrame(model *visualsearch.Model, base renderer.Frame | |||||
| 417 | return composeModalClientFrame(base, copySearchModal, styleSet, styleSet.selection, model.Render) | 433 | return composeModalClientFrame(base, copySearchModal, styleSet, styleSet.selection, model.Render) | ||
| 418 | } | 434 | } | ||
| 419 | 435 | ||||
| 420 | - | func composeCopyClientFrame(mode *scopy.Mode, p *pane, target domain.Rect, frame renderer.Frame, bars barState) (renderer.Frame, []renderer.Damage) { | 436 | + | func composeCopyClientFrame(mode *scopy.Mode, document *scopy.Snapshot, target domain.Rect, frame renderer.Frame, bars barState) (renderer.Frame, []renderer.Damage) { |
| 421 | - | if mode == nil || p == nil || target.Width <= 0 || target.Height <= 0 || frame.Width <= 0 || frame.Height <= 0 { | 437 | + | if mode == nil || document == nil || target.Width <= 0 || target.Height <= 0 || frame.Width <= 0 || frame.Height <= 0 { |
| 422 | return frame, nil | 438 | return frame, nil | ||
| 423 | } | 439 | } | ||
| 424 | styles := newThemeStyles(bars.theme) | 440 | styles := newThemeStyles(bars.theme) | ||
| 425 | - | p.mu.Lock() | 441 | + | copyFrame := mode.Render(*document, styles.copyStatus, styles.selection) |
| 426 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |||
| 427 | - | p.mu.Unlock() | |||
| 428 | - | copyFrame := mode.Render(snap, styles.copyStatus, styles.selection) | |||
| 429 | bodyRows := max(copyFrame.Height-1, 0) | 442 | bodyRows := max(copyFrame.Height-1, 0) | ||
| 430 | for y := 0; y < target.Height && y < bodyRows && target.Y+y < frame.Height-1; y++ { | 443 | for y := 0; y < target.Height && y < bodyRows && target.Y+y < frame.Height-1; y++ { | ||
| 431 | dstX := max(target.X, 0) | 444 | dstX := max(target.X, 0) | ||
M internal/usecase/daemon/copymode_test.go +2 -2
| @@ -124,7 +124,7 @@ func TestComposeCopyClientFrameConcurrentPaneOutput(t *testing.T) { | |||
| 124 | 124 | go func() { | |
| 125 | 125 | defer wg.Done() | |
| 126 | 126 | for range 200 { | |
| 127 | - | _, _ = composeCopyClientFrame(mode, pane, target, base, bars) | |
| 127 | + | _, _ = composeCopyClientFrame(mode, &snap, target, base, bars) | |
| 128 | 128 | } | |
| 129 | 129 | }() | |
| 130 | 130 | wg.Wait() | |
| @@ -143,7 +143,7 @@ func TestCopyModeFrameIncludesTopAndBottomChrome(t *testing.T) { | |||
| 143 | 143 | ||
| 144 | 144 | bars := barState{status: sess.statusSegments()} | |
| 145 | 145 | base, _ := composeClientFrameWithState(bars, tb, true) | |
| 146 | - | frame, damage := composeCopyClientFrame(mode, tb.focusedPane(), domain.Rect{X: 0, Y: 1, Width: 12, Height: 3}, base, bars) | |
| 146 | + | frame, damage := composeCopyClientFrame(mode, &snap, domain.Rect{X: 0, Y: 1, Width: 12, Height: 3}, base, bars) | |
| 147 | 147 | ||
| 148 | 148 | require.Equal(t, 80, frame.Width) | |
| 149 | 149 | require.Equal(t, 25, frame.Height) | |
| @@ -124,7 +124,7 @@ func TestComposeCopyClientFrameConcurrentPaneOutput(t *testing.T) { | |||||
| 124 | go func() { | 124 | go func() { | ||
| 125 | defer wg.Done() | 125 | defer wg.Done() | ||
| 126 | for range 200 { | 126 | for range 200 { | ||
| 127 | - | _, _ = composeCopyClientFrame(mode, pane, target, base, bars) | 127 | + | _, _ = composeCopyClientFrame(mode, &snap, target, base, bars) |
| 128 | } | 128 | } | ||
| 129 | }() | 129 | }() | ||
| 130 | wg.Wait() | 130 | wg.Wait() | ||
| @@ -143,7 +143,7 @@ func TestCopyModeFrameIncludesTopAndBottomChrome(t *testing.T) { | |||||
| 143 | 143 | ||||
| 144 | bars := barState{status: sess.statusSegments()} | 144 | bars := barState{status: sess.statusSegments()} | ||
| 145 | base, _ := composeClientFrameWithState(bars, tb, true) | 145 | base, _ := composeClientFrameWithState(bars, tb, true) | ||
| 146 | - | frame, damage := composeCopyClientFrame(mode, tb.focusedPane(), domain.Rect{X: 0, Y: 1, Width: 12, Height: 3}, base, bars) | 146 | + | frame, damage := composeCopyClientFrame(mode, &snap, domain.Rect{X: 0, Y: 1, Width: 12, Height: 3}, base, bars) |
| 147 | 147 | ||||
| 148 | require.Equal(t, 80, frame.Width) | 148 | require.Equal(t, 80, frame.Width) | ||
| 149 | require.Equal(t, 25, frame.Height) | 149 | require.Equal(t, 25, frame.Height) | ||
M internal/usecase/daemon/input.go +6 -6
| @@ -271,20 +271,20 @@ func (d *Daemon) handleTerminalMouse(sess *session, ac *attachedClient, p *pane, | |||
| 271 | 271 | } | |
| 272 | 272 | ||
| 273 | 273 | p.mu.Lock() | |
| 274 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 275 | - | p.mu.Unlock() | |
| 276 | - | ||
| 274 | + | document := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 277 | 275 | rt.copyMu.Lock() | |
| 278 | - | mode := scopy.NewMode(snap) | |
| 279 | - | mode.StartSelectionAt(snap, pressTop+pressRow) | |
| 280 | - | mode.ExtendTo(snap, snap.Len()-snap.Height+ev.Row) | |
| 276 | + | mode := scopy.NewMode(document) | |
| 277 | + | mode.StartSelectionAt(document, pressTop+pressRow) | |
| 278 | + | mode.ExtendTo(document, document.Len()-document.Height+ev.Row) | |
| 281 | 279 | rt.copyMode = mode | |
| 280 | + | rt.copySnapshot = &document | |
| 282 | 281 | rt.copyPane = p | |
| 283 | 282 | rt.copyPressRow = pressTop + pressRow | |
| 284 | 283 | rt.copyPressRowValid = true | |
| 285 | 284 | rt.copyDragging = true | |
| 286 | 285 | rt.normalMousePressValid = false | |
| 287 | 286 | rt.copyMu.Unlock() | |
| 287 | + | p.mu.Unlock() | |
| 288 | 288 | d.paint(sess, ac, true) | |
| 289 | 289 | case mouse.Release: | |
| 290 | 290 | rt.copyMu.Lock() | |
| @@ -271,20 +271,20 @@ func (d *Daemon) handleTerminalMouse(sess *session, ac *attachedClient, p *pane, | |||||
| 271 | } | 271 | } | ||
| 272 | 272 | ||||
| 273 | p.mu.Lock() | 273 | p.mu.Lock() | ||
| 274 | - | snap := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | 274 | + | document := scopy.NewSnapshot(p.scrollback, p.screen.Frame) |
| 275 | - | p.mu.Unlock() | |||
| 276 | - | ||||
| 277 | rt.copyMu.Lock() | 275 | rt.copyMu.Lock() | ||
| 278 | - | mode := scopy.NewMode(snap) | 276 | + | mode := scopy.NewMode(document) |
| 279 | - | mode.StartSelectionAt(snap, pressTop+pressRow) | 277 | + | mode.StartSelectionAt(document, pressTop+pressRow) |
| 280 | - | mode.ExtendTo(snap, snap.Len()-snap.Height+ev.Row) | 278 | + | mode.ExtendTo(document, document.Len()-document.Height+ev.Row) |
| 281 | rt.copyMode = mode | 279 | rt.copyMode = mode | ||
| 280 | + | rt.copySnapshot = &document | |||
| 282 | rt.copyPane = p | 281 | rt.copyPane = p | ||
| 283 | rt.copyPressRow = pressTop + pressRow | 282 | rt.copyPressRow = pressTop + pressRow | ||
| 284 | rt.copyPressRowValid = true | 283 | rt.copyPressRowValid = true | ||
| 285 | rt.copyDragging = true | 284 | rt.copyDragging = true | ||
| 286 | rt.normalMousePressValid = false | 285 | rt.normalMousePressValid = false | ||
| 287 | rt.copyMu.Unlock() | 286 | rt.copyMu.Unlock() | ||
| 287 | + | p.mu.Unlock() | |||
| 288 | d.paint(sess, ac, true) | 288 | d.paint(sess, ac, true) | ||
| 289 | case mouse.Release: | 289 | case mouse.Release: | ||
| 290 | rt.copyMu.Lock() | 290 | rt.copyMu.Lock() | ||
M internal/usecase/daemon/overlay_runtime.go +4 -0
| @@ -30,6 +30,7 @@ type overlayRuntime struct { | |||
| 30 | 30 | ||
| 31 | 31 | copyMu sync.Mutex | |
| 32 | 32 | copyMode *scopy.Mode | |
| 33 | + | copySnapshot *scopy.Snapshot | |
| 33 | 34 | copyPane *pane | |
| 34 | 35 | copyPending []byte | |
| 35 | 36 | copyESC pendingByteTimer | |
| @@ -102,6 +103,7 @@ func (rt *overlayRuntime) copySearchActive() bool { | |||
| 102 | 103 | ||
| 103 | 104 | func (rt *overlayRuntime) clearCopyModeLocked() { | |
| 104 | 105 | rt.copyMode = nil | |
| 106 | + | rt.copySnapshot = nil | |
| 105 | 107 | rt.copyPane = nil | |
| 106 | 108 | rt.copySearch = nil | |
| 107 | 109 | rt.copySearchPending = nil | |
| @@ -149,6 +151,7 @@ type overlayRenderSnapshot struct { | |||
| 149 | 151 | ||
| 150 | 152 | copyActive bool | |
| 151 | 153 | copyMode *scopy.Mode | |
| 154 | + | copySnapshot *scopy.Snapshot | |
| 152 | 155 | copyPane *pane | |
| 153 | 156 | copySearchModel *visualsearch.Model | |
| 154 | 157 | copyFeedback string | |
| @@ -181,6 +184,7 @@ func (rt *overlayRuntime) SnapshotForRender() *overlayRenderSnapshot { | |||
| 181 | 184 | rt.copyMu.Lock() | |
| 182 | 185 | snap.copyActive = rt.copyMode != nil | |
| 183 | 186 | snap.copyPane = rt.copyPane | |
| 187 | + | snap.copySnapshot = rt.copySnapshot | |
| 184 | 188 | if rt.copyMode != nil { | |
| 185 | 189 | copyModeValue := *rt.copyMode | |
| 186 | 190 | copyModeValue.Searches = append([]scopy.SearchMatch(nil), rt.copyMode.Searches...) | |
| @@ -30,6 +30,7 @@ type overlayRuntime struct { | |||||
| 30 | 30 | ||||
| 31 | copyMu sync.Mutex | 31 | copyMu sync.Mutex | ||
| 32 | copyMode *scopy.Mode | 32 | copyMode *scopy.Mode | ||
| 33 | + | copySnapshot *scopy.Snapshot | |||
| 33 | copyPane *pane | 34 | copyPane *pane | ||
| 34 | copyPending []byte | 35 | copyPending []byte | ||
| 35 | copyESC pendingByteTimer | 36 | copyESC pendingByteTimer | ||
| @@ -102,6 +103,7 @@ func (rt *overlayRuntime) copySearchActive() bool { | |||||
| 102 | 103 | ||||
| 103 | func (rt *overlayRuntime) clearCopyModeLocked() { | 104 | func (rt *overlayRuntime) clearCopyModeLocked() { | ||
| 104 | rt.copyMode = nil | 105 | rt.copyMode = nil | ||
| 106 | + | rt.copySnapshot = nil | |||
| 105 | rt.copyPane = nil | 107 | rt.copyPane = nil | ||
| 106 | rt.copySearch = nil | 108 | rt.copySearch = nil | ||
| 107 | rt.copySearchPending = nil | 109 | rt.copySearchPending = nil | ||
| @@ -149,6 +151,7 @@ type overlayRenderSnapshot struct { | |||||
| 149 | 151 | ||||
| 150 | copyActive bool | 152 | copyActive bool | ||
| 151 | copyMode *scopy.Mode | 153 | copyMode *scopy.Mode | ||
| 154 | + | copySnapshot *scopy.Snapshot | |||
| 152 | copyPane *pane | 155 | copyPane *pane | ||
| 153 | copySearchModel *visualsearch.Model | 156 | copySearchModel *visualsearch.Model | ||
| 154 | copyFeedback string | 157 | copyFeedback string | ||
| @@ -181,6 +184,7 @@ func (rt *overlayRuntime) SnapshotForRender() *overlayRenderSnapshot { | |||||
| 181 | rt.copyMu.Lock() | 184 | rt.copyMu.Lock() | ||
| 182 | snap.copyActive = rt.copyMode != nil | 185 | snap.copyActive = rt.copyMode != nil | ||
| 183 | snap.copyPane = rt.copyPane | 186 | snap.copyPane = rt.copyPane | ||
| 187 | + | snap.copySnapshot = rt.copySnapshot | |||
| 184 | if rt.copyMode != nil { | 188 | if rt.copyMode != nil { | ||
| 185 | copyModeValue := *rt.copyMode | 189 | copyModeValue := *rt.copyMode | ||
| 186 | copyModeValue.Searches = append([]scopy.SearchMatch(nil), rt.copyMode.Searches...) | 190 | copyModeValue.Searches = append([]scopy.SearchMatch(nil), rt.copyMode.Searches...) | ||
M internal/usecase/daemon/overlay_runtime_test.go +25 -3
| @@ -145,24 +145,46 @@ func TestOverlayRuntimeHandleInputInactive(t *testing.T) { | |||
| 145 | 145 | require.False(t, ac.overlays.HandleInput(&Daemon{}, []byte("x"))) | |
| 146 | 146 | } | |
| 147 | 147 | ||
| 148 | - | func TestOverlayRuntimeSnapshotCarriesCopyPaneAndClearReleasesIt(t *testing.T) { | |
| 148 | + | func TestOverlayRuntimeSnapshotCarriesCopyDocumentAndClearReleasesIt(t *testing.T) { | |
| 149 | 149 | ac := &attachedClient{} | |
| 150 | 150 | ac.initOverlays() | |
| 151 | 151 | p := newPane("floating", nil, domain.Size{Cols: 4, Rows: 2}) | |
| 152 | - | snap := scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("row")}, 4, 2) | |
| 152 | + | document := scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("row")}, 4, 2) | |
| 153 | 153 | ||
| 154 | 154 | ac.overlays.copyMu.Lock() | |
| 155 | - | ac.overlays.copyMode = scopy.NewMode(snap) | |
| 155 | + | ac.overlays.copyMode = scopy.NewMode(document) | |
| 156 | 156 | ac.overlays.copyPane = p | |
| 157 | + | ac.overlays.copySnapshot = &document | |
| 157 | 158 | ac.overlays.copyMu.Unlock() | |
| 158 | 159 | ||
| 159 | 160 | renderSnap := ac.overlays.SnapshotForRender() | |
| 160 | 161 | require.Same(t, p, renderSnap.copyPane) | |
| 162 | + | require.Same(t, &document, renderSnap.copySnapshot) | |
| 161 | 163 | renderSnap.Unlock() | |
| 162 | 164 | ||
| 163 | 165 | ac.overlays.copyMu.Lock() | |
| 164 | 166 | ac.overlays.clearCopyModeLocked() | |
| 165 | 167 | require.Nil(t, ac.overlays.copyPane) | |
| 166 | 168 | require.Nil(t, ac.overlays.copyMode) | |
| 169 | + | require.Nil(t, ac.overlays.copySnapshot) | |
| 167 | 170 | ac.overlays.copyMu.Unlock() | |
| 168 | 171 | } | |
| 172 | + | ||
| 173 | + | func TestOverlayRuntimeCopyDocumentSurvivesRingOverwrite(t *testing.T) { | |
| 174 | + | ac := &attachedClient{} | |
| 175 | + | ac.initOverlays() | |
| 176 | + | history := scopy.NewScrollback(1) | |
| 177 | + | history.Append(testRow("before")) | |
| 178 | + | document := scopy.NewSnapshot(history, renderer.NewFrame(6, 1)) | |
| 179 | + | ||
| 180 | + | ac.overlays.copyMu.Lock() | |
| 181 | + | ac.overlays.copyMode = scopy.NewMode(document) | |
| 182 | + | ac.overlays.copySnapshot = &document | |
| 183 | + | ac.overlays.copyMu.Unlock() | |
| 184 | + | ||
| 185 | + | history.Append(testRow("after")) | |
| 186 | + | renderSnap := ac.overlays.SnapshotForRender() | |
| 187 | + | defer renderSnap.Unlock() | |
| 188 | + | require.Same(t, &document, renderSnap.copySnapshot) | |
| 189 | + | require.Equal(t, "before", rowText(renderSnap.copySnapshot.Row(0))) | |
| 190 | + | } | |
| @@ -145,24 +145,46 @@ func TestOverlayRuntimeHandleInputInactive(t *testing.T) { | |||||
| 145 | require.False(t, ac.overlays.HandleInput(&Daemon{}, []byte("x"))) | 145 | require.False(t, ac.overlays.HandleInput(&Daemon{}, []byte("x"))) | ||
| 146 | } | 146 | } | ||
| 147 | 147 | ||||
| 148 | - | func TestOverlayRuntimeSnapshotCarriesCopyPaneAndClearReleasesIt(t *testing.T) { | 148 | + | func TestOverlayRuntimeSnapshotCarriesCopyDocumentAndClearReleasesIt(t *testing.T) { |
| 149 | ac := &attachedClient{} | 149 | ac := &attachedClient{} | ||
| 150 | ac.initOverlays() | 150 | ac.initOverlays() | ||
| 151 | p := newPane("floating", nil, domain.Size{Cols: 4, Rows: 2}) | 151 | p := newPane("floating", nil, domain.Size{Cols: 4, Rows: 2}) | ||
| 152 | - | snap := scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("row")}, 4, 2) | 152 | + | document := scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("row")}, 4, 2) |
| 153 | 153 | ||||
| 154 | ac.overlays.copyMu.Lock() | 154 | ac.overlays.copyMu.Lock() | ||
| 155 | - | ac.overlays.copyMode = scopy.NewMode(snap) | 155 | + | ac.overlays.copyMode = scopy.NewMode(document) |
| 156 | ac.overlays.copyPane = p | 156 | ac.overlays.copyPane = p | ||
| 157 | + | ac.overlays.copySnapshot = &document | |||
| 157 | ac.overlays.copyMu.Unlock() | 158 | ac.overlays.copyMu.Unlock() | ||
| 158 | 159 | ||||
| 159 | renderSnap := ac.overlays.SnapshotForRender() | 160 | renderSnap := ac.overlays.SnapshotForRender() | ||
| 160 | require.Same(t, p, renderSnap.copyPane) | 161 | require.Same(t, p, renderSnap.copyPane) | ||
| 162 | + | require.Same(t, &document, renderSnap.copySnapshot) | |||
| 161 | renderSnap.Unlock() | 163 | renderSnap.Unlock() | ||
| 162 | 164 | ||||
| 163 | ac.overlays.copyMu.Lock() | 165 | ac.overlays.copyMu.Lock() | ||
| 164 | ac.overlays.clearCopyModeLocked() | 166 | ac.overlays.clearCopyModeLocked() | ||
| 165 | require.Nil(t, ac.overlays.copyPane) | 167 | require.Nil(t, ac.overlays.copyPane) | ||
| 166 | require.Nil(t, ac.overlays.copyMode) | 168 | require.Nil(t, ac.overlays.copyMode) | ||
| 169 | + | require.Nil(t, ac.overlays.copySnapshot) | |||
| 167 | ac.overlays.copyMu.Unlock() | 170 | ac.overlays.copyMu.Unlock() | ||
| 168 | } | 171 | } | ||
| 172 | + | ||||
| 173 | + | func TestOverlayRuntimeCopyDocumentSurvivesRingOverwrite(t *testing.T) { | |||
| 174 | + | ac := &attachedClient{} | |||
| 175 | + | ac.initOverlays() | |||
| 176 | + | history := scopy.NewScrollback(1) | |||
| 177 | + | history.Append(testRow("before")) | |||
| 178 | + | document := scopy.NewSnapshot(history, renderer.NewFrame(6, 1)) | |||
| 179 | + | ||||
| 180 | + | ac.overlays.copyMu.Lock() | |||
| 181 | + | ac.overlays.copyMode = scopy.NewMode(document) | |||
| 182 | + | ac.overlays.copySnapshot = &document | |||
| 183 | + | ac.overlays.copyMu.Unlock() | |||
| 184 | + | ||||
| 185 | + | history.Append(testRow("after")) | |||
| 186 | + | renderSnap := ac.overlays.SnapshotForRender() | |||
| 187 | + | defer renderSnap.Unlock() | |||
| 188 | + | require.Same(t, &document, renderSnap.copySnapshot) | |||
| 189 | + | require.Equal(t, "before", rowText(renderSnap.copySnapshot.Row(0))) | |||
| 190 | + | } | |||
M internal/usecase/daemon/render.go +1 -1
| @@ -406,7 +406,7 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||
| 406 | 406 | copyPane = p | |
| 407 | 407 | } | |
| 408 | 408 | copyTarget := copyTargetRectLocked(layoutSnap, contentArea, copyPane, floating, hasFloating, floatingFrameGeometry) | |
| 409 | - | frame, damage = composeCopyClientFrame(overlays.copyMode, copyPane, copyTarget, frame, bars) | |
| 409 | + | frame, damage = composeCopyClientFrame(overlays.copyMode, overlays.copySnapshot, copyTarget, frame, bars) | |
| 410 | 410 | } | |
| 411 | 411 | // A palette above normal/copy content dims that composed content. When a | |
| 412 | 412 | // floating pane is present its own backdrop already dims normal pane | |
| @@ -406,7 +406,7 @@ func (d *Daemon) paint(sess *session, ac *attachedClient, reset bool) { | |||||
| 406 | copyPane = p | 406 | copyPane = p | ||
| 407 | } | 407 | } | ||
| 408 | copyTarget := copyTargetRectLocked(layoutSnap, contentArea, copyPane, floating, hasFloating, floatingFrameGeometry) | 408 | copyTarget := copyTargetRectLocked(layoutSnap, contentArea, copyPane, floating, hasFloating, floatingFrameGeometry) | ||
| 409 | - | frame, damage = composeCopyClientFrame(overlays.copyMode, copyPane, copyTarget, frame, bars) | 409 | + | frame, damage = composeCopyClientFrame(overlays.copyMode, overlays.copySnapshot, copyTarget, frame, bars) |
| 410 | } | 410 | } | ||
| 411 | // A palette above normal/copy content dims that composed content. When a | 411 | // A palette above normal/copy content dims that composed content. When a | ||
| 412 | // floating pane is present its own backdrop already dims normal pane | 412 | // floating pane is present its own backdrop already dims normal pane | ||
M internal/usecase/daemon/render_test.go +3 -2
| @@ -1419,10 +1419,11 @@ func TestComposeCopyClientFrameOverlaysBaseAtTarget(t *testing.T) { | |||
| 1419 | 1419 | } | |
| 1420 | 1420 | p := newPane("floating", nil, domain.Size{Cols: 18, Rows: 2}) | |
| 1421 | 1421 | p.screen.Write([]byte("ab\r\ncd")) | |
| 1422 | - | mode := scopy.NewMode(scopy.NewSnapshot(p.scrollback, p.screen.Frame)) | |
| 1422 | + | document := scopy.NewSnapshot(p.scrollback, p.screen.Frame) | |
| 1423 | + | mode := scopy.NewMode(document) | |
| 1423 | 1424 | target := domain.Rect{X: 2, Y: 3, Width: 18, Height: 2} | |
| 1424 | 1425 | ||
| 1425 | - | frame, damage := composeCopyClientFrame(mode, p, target, base, barState{}) | |
| 1426 | + | frame, damage := composeCopyClientFrame(mode, &document, target, base, barState{}) | |
| 1426 | 1427 | ||
| 1427 | 1428 | require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, damage) | |
| 1428 | 1429 | require.Equal(t, "##ab ", rowText(frame.Row(3))) | |
| @@ -1419,10 +1419,11 @@ func TestComposeCopyClientFrameOverlaysBaseAtTarget(t *testing.T) { | |||||
| 1419 | } | 1419 | } | ||
| 1420 | p := newPane("floating", nil, domain.Size{Cols: 18, Rows: 2}) | 1420 | p := newPane("floating", nil, domain.Size{Cols: 18, Rows: 2}) | ||
| 1421 | p.screen.Write([]byte("ab\r\ncd")) | 1421 | p.screen.Write([]byte("ab\r\ncd")) | ||
| 1422 | - | mode := scopy.NewMode(scopy.NewSnapshot(p.scrollback, p.screen.Frame)) | 1422 | + | document := scopy.NewSnapshot(p.scrollback, p.screen.Frame) |
| 1423 | + | mode := scopy.NewMode(document) | |||
| 1423 | target := domain.Rect{X: 2, Y: 3, Width: 18, Height: 2} | 1424 | target := domain.Rect{X: 2, Y: 3, Width: 18, Height: 2} | ||
| 1424 | 1425 | ||||
| 1425 | - | frame, damage := composeCopyClientFrame(mode, p, target, base, barState{}) | 1426 | + | frame, damage := composeCopyClientFrame(mode, &document, target, base, barState{}) |
| 1426 | 1427 | ||||
| 1427 | require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, damage) | 1428 | require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, damage) | ||
| 1428 | require.Equal(t, "##ab ", rowText(frame.Row(3))) | 1429 | require.Equal(t, "##ab ", rowText(frame.Row(3))) | ||