fix(copy): retain content bounds for resized history rows
- commit
- cb18da6f5e345386b65715c5d12e8b1e565b9836
- author
- brice · · 2026-10-05 07:29 UTC
- parent
- 1324577
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +24 -4
M internal/usecase/copy/content_render_test.go +16 -0
| @@ -69,6 +69,22 @@ func TestRenderRowsSelectionHighlightsContentOnly(t *testing.T) { | |||
| 69 | 69 | } | |
| 70 | 70 | } | |
| 71 | 71 | ||
| 72 | + | func TestRenderRowsSelectionAfterHistoryResize(t *testing.T) { | |
| 73 | + | screen := vt.NewScreenWithHistory(12, 2, vt.HistoryConfig{MaxRows: 64}) | |
| 74 | + | screen.Write([]byte("abc xyz\r\nq\r\nr\r\ns")) | |
| 75 | + | screen.Resize(8, 2) | |
| 76 | + | doc := NewDocument(NewSnapshot(screen.History(), screen, screen.LineBounds(), nil), "") | |
| 77 | + | m := NewMode(doc) | |
| 78 | + | m.navigator.Pos = Pos{0, 0} | |
| 79 | + | m.ViewportTop = 0 | |
| 80 | + | m.selection = Selection{Anchor: Pos{0, 0}, Active: Pos{1, 0}, Granularity: Character, Enabled: true} | |
| 81 | + | f := m.Render(vt.DefaultStyle(), vt.Style{HasBackgroundRGB: true}) | |
| 82 | + | require.Equal(t, "abc xyz\nq", m.SelectedText()) | |
| 83 | + | for x := 0; x < 8; x++ { | |
| 84 | + | require.True(t, f.At(x, 0).Style.Inverse, "visible content column %d", x) | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | ||
| 72 | 88 | func TestRenderRowsSelectionPreservesCellColors(t *testing.T) { | |
| 73 | 89 | styles := []vt.Style{ | |
| 74 | 90 | {HasForegroundRGB: true, ForegroundRGB: vt.RGB{G: 255, B: 255}, Bold: true}, | |
| @@ -69,6 +69,22 @@ func TestRenderRowsSelectionHighlightsContentOnly(t *testing.T) { | |||||
| 69 | } | 69 | } | ||
| 70 | } | 70 | } | ||
| 71 | 71 | ||||
| 72 | + | func TestRenderRowsSelectionAfterHistoryResize(t *testing.T) { | |||
| 73 | + | screen := vt.NewScreenWithHistory(12, 2, vt.HistoryConfig{MaxRows: 64}) | |||
| 74 | + | screen.Write([]byte("abc xyz\r\nq\r\nr\r\ns")) | |||
| 75 | + | screen.Resize(8, 2) | |||
| 76 | + | doc := NewDocument(NewSnapshot(screen.History(), screen, screen.LineBounds(), nil), "") | |||
| 77 | + | m := NewMode(doc) | |||
| 78 | + | m.navigator.Pos = Pos{0, 0} | |||
| 79 | + | m.ViewportTop = 0 | |||
| 80 | + | m.selection = Selection{Anchor: Pos{0, 0}, Active: Pos{1, 0}, Granularity: Character, Enabled: true} | |||
| 81 | + | f := m.Render(vt.DefaultStyle(), vt.Style{HasBackgroundRGB: true}) | |||
| 82 | + | require.Equal(t, "abc xyz\nq", m.SelectedText()) | |||
| 83 | + | for x := 0; x < 8; x++ { | |||
| 84 | + | require.True(t, f.At(x, 0).Style.Inverse, "visible content column %d", x) | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | ||||
| 72 | func TestRenderRowsSelectionPreservesCellColors(t *testing.T) { | 88 | func TestRenderRowsSelectionPreservesCellColors(t *testing.T) { | ||
| 73 | styles := []vt.Style{ | 89 | styles := []vt.Style{ | ||
| 74 | {HasForegroundRGB: true, ForegroundRGB: vt.RGB{G: 255, B: 255}, Bold: true}, | 90 | {HasForegroundRGB: true, ForegroundRGB: vt.RGB{G: 255, B: 255}, Bold: true}, | ||
M internal/usecase/copy/document.go +2 -1
| @@ -16,7 +16,8 @@ type Pos struct { | |||
| 16 | 16 | } | |
| 17 | 17 | ||
| 18 | 18 | // CellRange is an inclusive display-cell range on one physical document row. | |
| 19 | - | // End == Start-1 with an in-bounds Start denotes a selected row with no cells. | |
| 19 | + | // End == Start-1 with an in-bounds Start denotes a selected row with no cells; | |
| 20 | + | // this adjacent pair is never interpreted as a reversed range. | |
| 20 | 21 | type CellRange struct { | |
| 21 | 22 | Row int | |
| 22 | 23 | Start int | |
| @@ -16,7 +16,8 @@ type Pos struct { | |||||
| 16 | } | 16 | } | ||
| 17 | 17 | ||||
| 18 | // CellRange is an inclusive display-cell range on one physical document row. | 18 | // CellRange is an inclusive display-cell range on one physical document row. | ||
| 19 | - | // End == Start-1 with an in-bounds Start denotes a selected row with no cells. | 19 | + | // End == Start-1 with an in-bounds Start denotes a selected row with no cells; |
| 20 | + | // this adjacent pair is never interpreted as a reversed range. | |||
| 20 | type CellRange struct { | 21 | type CellRange struct { | ||
| 21 | Row int | 22 | Row int | ||
| 22 | Start int | 23 | Start int | ||
M internal/usecase/copy/mode.go +6 -3
| @@ -596,7 +596,7 @@ func (m *Mode) RenderRowsRange(start, end int, paint func(int, []renderer.Cell), | |||
| 596 | 596 | paint(y, row) | |
| 597 | 597 | continue | |
| 598 | 598 | } | |
| 599 | - | d.snapshot.CopyRow(src, row) | |
| 599 | + | n := d.snapshot.CopyRow(src, row) | |
| 600 | 600 | if match, ok := m.currentSearchMatchForRow(src); ok { | |
| 601 | 601 | for x := max(match.Start, 0); x < min(match.End, len(row)); x++ { | |
| 602 | 602 | applySelectionStyle(&row[x].Style, selection, hasSelection) | |
| @@ -605,12 +605,15 @@ func (m *Mode) RenderRowsRange(start, end int, paint func(int, []renderer.Cell), | |||
| 605 | 605 | cursorCovered := false | |
| 606 | 606 | if hasSelectionBounds { | |
| 607 | 607 | bound := d.snapshot.Bound(src) | |
| 608 | - | limit := contentEndOf(row, bound) | |
| 608 | + | limit := contentEndOf(row[:n], bound) | |
| 609 | + | if n < d.RowWidth(src) { | |
| 610 | + | limit = d.contentEnd(src) | |
| 611 | + | } | |
| 609 | 612 | if r, ok := selectionBounds.rangeForCells(src, row, limit); ok { | |
| 610 | 613 | for x := max(r.Start, 0); x <= min(r.End, len(row)-1); x++ { | |
| 611 | 614 | toggleInverse(&row[x].Style) | |
| 612 | 615 | } | |
| 613 | - | if col, ok := selectionBounds.newlineMarkerAt(src, limit, d.Width(), bound.Soft); ok && col < len(row) { | |
| 616 | + | if col, ok := selectionBounds.newlineMarkerAt(src, limit, d.RowWidth(src), bound.Soft); ok && col < len(row) { | |
| 614 | 617 | toggleInverse(&row[col].Style) | |
| 615 | 618 | if cursorValid && cursor.Row == src && cursor.Col == col { | |
| 616 | 619 | cursorCovered = true | |
| @@ -596,7 +596,7 @@ func (m *Mode) RenderRowsRange(start, end int, paint func(int, []renderer.Cell), | |||||
| 596 | paint(y, row) | 596 | paint(y, row) | ||
| 597 | continue | 597 | continue | ||
| 598 | } | 598 | } | ||
| 599 | - | d.snapshot.CopyRow(src, row) | 599 | + | n := d.snapshot.CopyRow(src, row) |
| 600 | if match, ok := m.currentSearchMatchForRow(src); ok { | 600 | if match, ok := m.currentSearchMatchForRow(src); ok { | ||
| 601 | for x := max(match.Start, 0); x < min(match.End, len(row)); x++ { | 601 | for x := max(match.Start, 0); x < min(match.End, len(row)); x++ { | ||
| 602 | applySelectionStyle(&row[x].Style, selection, hasSelection) | 602 | applySelectionStyle(&row[x].Style, selection, hasSelection) | ||
| @@ -605,12 +605,15 @@ func (m *Mode) RenderRowsRange(start, end int, paint func(int, []renderer.Cell), | |||||
| 605 | cursorCovered := false | 605 | cursorCovered := false | ||
| 606 | if hasSelectionBounds { | 606 | if hasSelectionBounds { | ||
| 607 | bound := d.snapshot.Bound(src) | 607 | bound := d.snapshot.Bound(src) | ||
| 608 | - | limit := contentEndOf(row, bound) | 608 | + | limit := contentEndOf(row[:n], bound) |
| 609 | + | if n < d.RowWidth(src) { | |||
| 610 | + | limit = d.contentEnd(src) | |||
| 611 | + | } | |||
| 609 | if r, ok := selectionBounds.rangeForCells(src, row, limit); ok { | 612 | if r, ok := selectionBounds.rangeForCells(src, row, limit); ok { | ||
| 610 | for x := max(r.Start, 0); x <= min(r.End, len(row)-1); x++ { | 613 | for x := max(r.Start, 0); x <= min(r.End, len(row)-1); x++ { | ||
| 611 | toggleInverse(&row[x].Style) | 614 | toggleInverse(&row[x].Style) | ||
| 612 | } | 615 | } | ||
| 613 | - | if col, ok := selectionBounds.newlineMarkerAt(src, limit, d.Width(), bound.Soft); ok && col < len(row) { | 616 | + | if col, ok := selectionBounds.newlineMarkerAt(src, limit, d.RowWidth(src), bound.Soft); ok && col < len(row) { |
| 614 | toggleInverse(&row[col].Style) | 617 | toggleInverse(&row[col].Style) | ||
| 615 | if cursorValid && cursor.Row == src && cursor.Col == col { | 618 | if cursorValid && cursor.Row == src && cursor.Col == col { | ||
| 616 | cursorCovered = true | 619 | cursorCovered = true | ||