feat(copy): invert selected cells and mark selected newlines
- commit
- 3f3a435518c02857d82bd775dabd9dd5558e2355
- author
- brice · · 2026-10-05 07:17 UTC
- parent
- 9d2dea9
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +110 -3
A internal/usecase/copy/content_render_test.go +87 -0
| @@ -0,0 +1,87 @@ | |||
| 1 | + | package copy | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | vt "github.com/bnema/vev-vt" | |
| 8 | + | "github.com/stretchr/testify/require" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestRenderRowsSelectionHighlightsContentOnly(t *testing.T) { | |
| 12 | + | wide := padRow("", 8) | |
| 13 | + | wide[0] = vt.Cell{Rune: 'a'} | |
| 14 | + | wide[1] = vt.Cell{Rune: '界'} | |
| 15 | + | wide[2] = vt.Cell{Continuation: true} | |
| 16 | + | tests := []struct { | |
| 17 | + | name string | |
| 18 | + | rows [][]vt.Cell | |
| 19 | + | bounds []vt.LineBound | |
| 20 | + | end Pos | |
| 21 | + | granularity Granularity | |
| 22 | + | want [][]int | |
| 23 | + | }{ | |
| 24 | + | {"hard rows", [][]vt.Cell{padRow("ab", 8), padRow("cd", 8)}, nil, Pos{1, 1}, Character, [][]int{{0, 1, 2}, {0, 1}}}, | |
| 25 | + | {"word rows", [][]vt.Cell{padRow("ab", 8), padRow("cd", 8)}, nil, Pos{1, 1}, Word, [][]int{{0, 1, 2}, {0, 1}}}, | |
| 26 | + | {"line mode", [][]vt.Cell{padRow("ab", 8), padRow("", 8), padRow("cd", 8)}, nil, Pos{2, 0}, Line, [][]int{{0, 1, 2}, {}, {0, 1, 2}}}, | |
| 27 | + | {"soft spaces", [][]vt.Cell{padRow("abc ", 8), padRow("de", 8)}, []vt.LineBound{{End: 8, Soft: true}, {End: 2}}, Pos{1, 1}, Character, [][]int{{0, 1, 2, 3, 4, 5, 6, 7}, {0, 1}}}, | |
| 28 | + | {"soft partial", [][]vt.Cell{padRow("abc", 8), padRow("de", 8)}, []vt.LineBound{{End: 5, Soft: true}, {End: 2}}, Pos{1, 1}, Character, [][]int{{0, 1, 2, 3, 4}, {0, 1}}}, | |
| 29 | + | {"full hard row", [][]vt.Cell{padRow("abcdefgh", 8), padRow("ij", 8)}, nil, Pos{1, 0}, Character, [][]int{{0, 1, 2, 3, 4, 5, 6, 7}, {0}}}, | |
| 30 | + | {"wide glyph", [][]vt.Cell{wide}, nil, Pos{0, 0}, Line, [][]int{{0, 1, 2, 3}}}, | |
| 31 | + | {"end in padding", [][]vt.Cell{padRow("ab", 8)}, nil, Pos{0, 6}, Character, [][]int{{0, 1}}}, | |
| 32 | + | } | |
| 33 | + | for _, tt := range tests { | |
| 34 | + | t.Run(tt.name, func(t *testing.T) { | |
| 35 | + | doc := NewDocument(NewSnapshotFromLines(tt.rows, tt.bounds, 8, len(tt.rows)), "") | |
| 36 | + | m := NewMode(doc) | |
| 37 | + | m.selection = Selection{Anchor: Pos{0, 0}, Active: tt.end, Granularity: tt.granularity, Enabled: true} | |
| 38 | + | m.navigator.Pos = Pos{0, 0} | |
| 39 | + | f := m.Render(vt.DefaultStyle(), vt.Style{HasBackgroundRGB: true, BackgroundRGB: vt.RGB{R: 1}}) | |
| 40 | + | var copied strings.Builder | |
| 41 | + | bounds, _ := m.selection.bounds(doc) | |
| 42 | + | for y, source := range tt.rows { | |
| 43 | + | cols := []int{} | |
| 44 | + | marker, hasMarker := bounds.newlineMarker(doc, y) | |
| 45 | + | for x, cell := range source { | |
| 46 | + | if f.At(x, y).Style.Inverse == cell.Style.Inverse { | |
| 47 | + | continue | |
| 48 | + | } | |
| 49 | + | cols = append(cols, x) | |
| 50 | + | if (hasMarker && x == marker) || cell.Continuation { | |
| 51 | + | continue | |
| 52 | + | } | |
| 53 | + | if cell.Rune == 0 { | |
| 54 | + | copied.WriteByte(' ') | |
| 55 | + | } else { | |
| 56 | + | copied.WriteRune(cell.Rune) | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | require.Equal(t, tt.want[y], cols) | |
| 60 | + | if y < len(tt.rows)-1 && !doc.snapshot.Bound(y).Soft { | |
| 61 | + | copied.WriteByte('\n') | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | require.Equal(t, m.SelectedText(), copied.String(), "highlighted glyphs match copied text") | |
| 65 | + | for y, source := range tt.rows { | |
| 66 | + | require.Equal(t, source, doc.Row(y)) | |
| 67 | + | } | |
| 68 | + | }) | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | func TestRenderRowsSelectionPreservesCellColors(t *testing.T) { | |
| 73 | + | styles := []vt.Style{ | |
| 74 | + | {HasForegroundRGB: true, ForegroundRGB: vt.RGB{G: 255, B: 255}, Bold: true}, | |
| 75 | + | {Inverse: true, HasBackgroundRGB: true, BackgroundRGB: vt.RGB{R: 20}, Italic: true}, | |
| 76 | + | } | |
| 77 | + | cells := []vt.Cell{{Rune: 'a', Style: styles[0]}, {Rune: 'b', Style: styles[1]}} | |
| 78 | + | m := NewMode(NewDocument(NewSnapshotFromRows([][]vt.Cell{cells}, 2, 1), "")) | |
| 79 | + | require.True(t, m.StartCharacterSelection(Pos{0, 0})) | |
| 80 | + | require.True(t, m.ExtendCharacterSelection(Pos{0, 1})) | |
| 81 | + | f := m.Render(vt.DefaultStyle(), vt.Style{HasBackgroundRGB: true, BackgroundRGB: vt.RGB{R: 1}}) | |
| 82 | + | for x, style := range styles { | |
| 83 | + | style.Inverse = !style.Inverse | |
| 84 | + | require.Equal(t, style, f.At(x, 0).Style) | |
| 85 | + | } | |
| 86 | + | require.Equal(t, cells, m.Document().Row(0)) | |
| 87 | + | } | |
| @@ -0,0 +1,87 @@ | |||||
| 1 | + | package copy | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strings" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | vt "github.com/bnema/vev-vt" | |||
| 8 | + | "github.com/stretchr/testify/require" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestRenderRowsSelectionHighlightsContentOnly(t *testing.T) { | |||
| 12 | + | wide := padRow("", 8) | |||
| 13 | + | wide[0] = vt.Cell{Rune: 'a'} | |||
| 14 | + | wide[1] = vt.Cell{Rune: '界'} | |||
| 15 | + | wide[2] = vt.Cell{Continuation: true} | |||
| 16 | + | tests := []struct { | |||
| 17 | + | name string | |||
| 18 | + | rows [][]vt.Cell | |||
| 19 | + | bounds []vt.LineBound | |||
| 20 | + | end Pos | |||
| 21 | + | granularity Granularity | |||
| 22 | + | want [][]int | |||
| 23 | + | }{ | |||
| 24 | + | {"hard rows", [][]vt.Cell{padRow("ab", 8), padRow("cd", 8)}, nil, Pos{1, 1}, Character, [][]int{{0, 1, 2}, {0, 1}}}, | |||
| 25 | + | {"word rows", [][]vt.Cell{padRow("ab", 8), padRow("cd", 8)}, nil, Pos{1, 1}, Word, [][]int{{0, 1, 2}, {0, 1}}}, | |||
| 26 | + | {"line mode", [][]vt.Cell{padRow("ab", 8), padRow("", 8), padRow("cd", 8)}, nil, Pos{2, 0}, Line, [][]int{{0, 1, 2}, {}, {0, 1, 2}}}, | |||
| 27 | + | {"soft spaces", [][]vt.Cell{padRow("abc ", 8), padRow("de", 8)}, []vt.LineBound{{End: 8, Soft: true}, {End: 2}}, Pos{1, 1}, Character, [][]int{{0, 1, 2, 3, 4, 5, 6, 7}, {0, 1}}}, | |||
| 28 | + | {"soft partial", [][]vt.Cell{padRow("abc", 8), padRow("de", 8)}, []vt.LineBound{{End: 5, Soft: true}, {End: 2}}, Pos{1, 1}, Character, [][]int{{0, 1, 2, 3, 4}, {0, 1}}}, | |||
| 29 | + | {"full hard row", [][]vt.Cell{padRow("abcdefgh", 8), padRow("ij", 8)}, nil, Pos{1, 0}, Character, [][]int{{0, 1, 2, 3, 4, 5, 6, 7}, {0}}}, | |||
| 30 | + | {"wide glyph", [][]vt.Cell{wide}, nil, Pos{0, 0}, Line, [][]int{{0, 1, 2, 3}}}, | |||
| 31 | + | {"end in padding", [][]vt.Cell{padRow("ab", 8)}, nil, Pos{0, 6}, Character, [][]int{{0, 1}}}, | |||
| 32 | + | } | |||
| 33 | + | for _, tt := range tests { | |||
| 34 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 35 | + | doc := NewDocument(NewSnapshotFromLines(tt.rows, tt.bounds, 8, len(tt.rows)), "") | |||
| 36 | + | m := NewMode(doc) | |||
| 37 | + | m.selection = Selection{Anchor: Pos{0, 0}, Active: tt.end, Granularity: tt.granularity, Enabled: true} | |||
| 38 | + | m.navigator.Pos = Pos{0, 0} | |||
| 39 | + | f := m.Render(vt.DefaultStyle(), vt.Style{HasBackgroundRGB: true, BackgroundRGB: vt.RGB{R: 1}}) | |||
| 40 | + | var copied strings.Builder | |||
| 41 | + | bounds, _ := m.selection.bounds(doc) | |||
| 42 | + | for y, source := range tt.rows { | |||
| 43 | + | cols := []int{} | |||
| 44 | + | marker, hasMarker := bounds.newlineMarker(doc, y) | |||
| 45 | + | for x, cell := range source { | |||
| 46 | + | if f.At(x, y).Style.Inverse == cell.Style.Inverse { | |||
| 47 | + | continue | |||
| 48 | + | } | |||
| 49 | + | cols = append(cols, x) | |||
| 50 | + | if (hasMarker && x == marker) || cell.Continuation { | |||
| 51 | + | continue | |||
| 52 | + | } | |||
| 53 | + | if cell.Rune == 0 { | |||
| 54 | + | copied.WriteByte(' ') | |||
| 55 | + | } else { | |||
| 56 | + | copied.WriteRune(cell.Rune) | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | require.Equal(t, tt.want[y], cols) | |||
| 60 | + | if y < len(tt.rows)-1 && !doc.snapshot.Bound(y).Soft { | |||
| 61 | + | copied.WriteByte('\n') | |||
| 62 | + | } | |||
| 63 | + | } | |||
| 64 | + | require.Equal(t, m.SelectedText(), copied.String(), "highlighted glyphs match copied text") | |||
| 65 | + | for y, source := range tt.rows { | |||
| 66 | + | require.Equal(t, source, doc.Row(y)) | |||
| 67 | + | } | |||
| 68 | + | }) | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | func TestRenderRowsSelectionPreservesCellColors(t *testing.T) { | |||
| 73 | + | styles := []vt.Style{ | |||
| 74 | + | {HasForegroundRGB: true, ForegroundRGB: vt.RGB{G: 255, B: 255}, Bold: true}, | |||
| 75 | + | {Inverse: true, HasBackgroundRGB: true, BackgroundRGB: vt.RGB{R: 20}, Italic: true}, | |||
| 76 | + | } | |||
| 77 | + | cells := []vt.Cell{{Rune: 'a', Style: styles[0]}, {Rune: 'b', Style: styles[1]}} | |||
| 78 | + | m := NewMode(NewDocument(NewSnapshotFromRows([][]vt.Cell{cells}, 2, 1), "")) | |||
| 79 | + | require.True(t, m.StartCharacterSelection(Pos{0, 0})) | |||
| 80 | + | require.True(t, m.ExtendCharacterSelection(Pos{0, 1})) | |||
| 81 | + | f := m.Render(vt.DefaultStyle(), vt.Style{HasBackgroundRGB: true, BackgroundRGB: vt.RGB{R: 1}}) | |||
| 82 | + | for x, style := range styles { | |||
| 83 | + | style.Inverse = !style.Inverse | |||
| 84 | + | require.Equal(t, style, f.At(x, 0).Style) | |||
| 85 | + | } | |||
| 86 | + | require.Equal(t, cells, m.Document().Row(0)) | |||
| 87 | + | } | |||
M internal/usecase/copy/mode.go +9 -1
| @@ -606,7 +606,13 @@ func (m *Mode) RenderRowsRange(start, end int, paint func(int, []renderer.Cell), | |||
| 606 | 606 | if hasSelectionBounds { | |
| 607 | 607 | if r, ok := selectionBounds.rangeForRow(d, src); ok { | |
| 608 | 608 | for x := max(r.Start, 0); x <= min(r.End, len(row)-1); x++ { | |
| 609 | - | applySelectionStyle(&row[x].Style, selection, hasSelection) | |
| 609 | + | toggleInverse(&row[x].Style) | |
| 610 | + | } | |
| 611 | + | if col, ok := selectionBounds.newlineMarker(d, src); ok && col < len(row) { | |
| 612 | + | toggleInverse(&row[col].Style) | |
| 613 | + | if cursorValid && cursor.Row == src && cursor.Col == col { | |
| 614 | + | cursorCovered = true | |
| 615 | + | } | |
| 610 | 616 | } | |
| 611 | 617 | if cursorValid && r.Row == cursor.Row && cursor.Col >= r.Start && cursor.Col <= r.End { | |
| 612 | 618 | cursorCovered = true | |
| @@ -639,6 +645,8 @@ func optionalStyle(styles []renderer.Style, idx int) (renderer.Style, bool) { | |||
| 639 | 645 | } | |
| 640 | 646 | return styles[idx], true | |
| 641 | 647 | } | |
| 648 | + | func toggleInverse(dst *renderer.Style) { dst.Inverse = !dst.Inverse } | |
| 649 | + | ||
| 642 | 650 | func applySelectionStyle(dst *renderer.Style, style renderer.Style, ok bool) { | |
| 643 | 651 | if !ok || style.Equal(inverseStyle()) { | |
| 644 | 652 | dst.Inverse = true | |
| @@ -606,7 +606,13 @@ func (m *Mode) RenderRowsRange(start, end int, paint func(int, []renderer.Cell), | |||||
| 606 | if hasSelectionBounds { | 606 | if hasSelectionBounds { | ||
| 607 | if r, ok := selectionBounds.rangeForRow(d, src); ok { | 607 | if r, ok := selectionBounds.rangeForRow(d, src); ok { | ||
| 608 | for x := max(r.Start, 0); x <= min(r.End, len(row)-1); x++ { | 608 | for x := max(r.Start, 0); x <= min(r.End, len(row)-1); x++ { | ||
| 609 | - | applySelectionStyle(&row[x].Style, selection, hasSelection) | 609 | + | toggleInverse(&row[x].Style) |
| 610 | + | } | |||
| 611 | + | if col, ok := selectionBounds.newlineMarker(d, src); ok && col < len(row) { | |||
| 612 | + | toggleInverse(&row[col].Style) | |||
| 613 | + | if cursorValid && cursor.Row == src && cursor.Col == col { | |||
| 614 | + | cursorCovered = true | |||
| 615 | + | } | |||
| 610 | } | 616 | } | ||
| 611 | if cursorValid && r.Row == cursor.Row && cursor.Col >= r.Start && cursor.Col <= r.End { | 617 | if cursorValid && r.Row == cursor.Row && cursor.Col >= r.Start && cursor.Col <= r.End { | ||
| 612 | cursorCovered = true | 618 | cursorCovered = true | ||
| @@ -639,6 +645,8 @@ func optionalStyle(styles []renderer.Style, idx int) (renderer.Style, bool) { | |||||
| 639 | } | 645 | } | ||
| 640 | return styles[idx], true | 646 | return styles[idx], true | ||
| 641 | } | 647 | } | ||
| 648 | + | func toggleInverse(dst *renderer.Style) { dst.Inverse = !dst.Inverse } | |||
| 649 | + | ||||
| 642 | func applySelectionStyle(dst *renderer.Style, style renderer.Style, ok bool) { | 650 | func applySelectionStyle(dst *renderer.Style, style renderer.Style, ok bool) { | ||
| 643 | if !ok || style.Equal(inverseStyle()) { | 651 | if !ok || style.Equal(inverseStyle()) { | ||
| 644 | dst.Inverse = true | 652 | dst.Inverse = true | ||
M internal/usecase/copy/mode_test.go +5 -2
| @@ -143,7 +143,7 @@ func TestCopyModePreferredColumnAndPartialSpaces(t *testing.T) { | |||
| 143 | 143 | require.Equal(t, "bcde", m.SelectedText()) | |
| 144 | 144 | } | |
| 145 | 145 | ||
| 146 | - | func TestCopyModeRenderSelectionWinsSearchAndWideGlyph(t *testing.T) { | |
| 146 | + | func TestCopyModeRenderSelectionInvertsSearchAndWideGlyph(t *testing.T) { | |
| 147 | 147 | cells := []renderer.Cell{{Rune: '界'}, {Continuation: true}, {Rune: 'a'}} | |
| 148 | 148 | m := NewMode(NewDocument(NewSnapshotFromRows([][]renderer.Cell{cells}, 3, 1), "")) | |
| 149 | 149 | require.True(t, m.SetSearchMatches("界", []SearchMatch{{Row: 0, Start: 0, End: 2}}, 0)) | |
| @@ -151,6 +151,8 @@ func TestCopyModeRenderSelectionWinsSearchAndWideGlyph(t *testing.T) { | |||
| 151 | 151 | f := m.Render(renderer.DefaultStyle(), renderer.Style{HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 1}}) | |
| 152 | 152 | require.True(t, f.At(0, 0).Style.HasBackgroundRGB) | |
| 153 | 153 | require.True(t, f.At(1, 0).Style.HasBackgroundRGB) | |
| 154 | + | require.True(t, f.At(0, 0).Style.Inverse) | |
| 155 | + | require.True(t, f.At(1, 0).Style.Inverse) | |
| 154 | 156 | } | |
| 155 | 157 | ||
| 156 | 158 | func TestCopyModeSearchNavigationExtendsActiveSelection(t *testing.T) { | |
| @@ -202,7 +204,8 @@ func TestCopyModeRenderKeepsPassiveCursorOutsidePartialSameRowSelection(t *testi | |||
| 202 | 204 | selection := renderer.Style{HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 1}} | |
| 203 | 205 | f := m.Render(renderer.DefaultStyle(), selection) | |
| 204 | 206 | require.True(t, f.At(0, 0).Style.HasBackgroundRGB) | |
| 205 | - | require.True(t, f.At(2, 0).Style.HasBackgroundRGB) | |
| 207 | + | require.True(t, f.At(2, 0).Style.Inverse) | |
| 208 | + | require.False(t, f.At(2, 0).Style.HasBackgroundRGB) | |
| 206 | 209 | } | |
| 207 | 210 | ||
| 208 | 211 | func TestFindMatchesUsesExclusiveDisplayCellOffsets(t *testing.T) { | |
| @@ -143,7 +143,7 @@ func TestCopyModePreferredColumnAndPartialSpaces(t *testing.T) { | |||||
| 143 | require.Equal(t, "bcde", m.SelectedText()) | 143 | require.Equal(t, "bcde", m.SelectedText()) | ||
| 144 | } | 144 | } | ||
| 145 | 145 | ||||
| 146 | - | func TestCopyModeRenderSelectionWinsSearchAndWideGlyph(t *testing.T) { | 146 | + | func TestCopyModeRenderSelectionInvertsSearchAndWideGlyph(t *testing.T) { |
| 147 | cells := []renderer.Cell{{Rune: '界'}, {Continuation: true}, {Rune: 'a'}} | 147 | cells := []renderer.Cell{{Rune: '界'}, {Continuation: true}, {Rune: 'a'}} | ||
| 148 | m := NewMode(NewDocument(NewSnapshotFromRows([][]renderer.Cell{cells}, 3, 1), "")) | 148 | m := NewMode(NewDocument(NewSnapshotFromRows([][]renderer.Cell{cells}, 3, 1), "")) | ||
| 149 | require.True(t, m.SetSearchMatches("界", []SearchMatch{{Row: 0, Start: 0, End: 2}}, 0)) | 149 | require.True(t, m.SetSearchMatches("界", []SearchMatch{{Row: 0, Start: 0, End: 2}}, 0)) | ||
| @@ -151,6 +151,8 @@ func TestCopyModeRenderSelectionWinsSearchAndWideGlyph(t *testing.T) { | |||||
| 151 | f := m.Render(renderer.DefaultStyle(), renderer.Style{HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 1}}) | 151 | f := m.Render(renderer.DefaultStyle(), renderer.Style{HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 1}}) | ||
| 152 | require.True(t, f.At(0, 0).Style.HasBackgroundRGB) | 152 | require.True(t, f.At(0, 0).Style.HasBackgroundRGB) | ||
| 153 | require.True(t, f.At(1, 0).Style.HasBackgroundRGB) | 153 | require.True(t, f.At(1, 0).Style.HasBackgroundRGB) | ||
| 154 | + | require.True(t, f.At(0, 0).Style.Inverse) | |||
| 155 | + | require.True(t, f.At(1, 0).Style.Inverse) | |||
| 154 | } | 156 | } | ||
| 155 | 157 | ||||
| 156 | func TestCopyModeSearchNavigationExtendsActiveSelection(t *testing.T) { | 158 | func TestCopyModeSearchNavigationExtendsActiveSelection(t *testing.T) { | ||
| @@ -202,7 +204,8 @@ func TestCopyModeRenderKeepsPassiveCursorOutsidePartialSameRowSelection(t *testi | |||||
| 202 | selection := renderer.Style{HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 1}} | 204 | selection := renderer.Style{HasBackgroundRGB: true, BackgroundRGB: renderer.RGB{R: 1}} | ||
| 203 | f := m.Render(renderer.DefaultStyle(), selection) | 205 | f := m.Render(renderer.DefaultStyle(), selection) | ||
| 204 | require.True(t, f.At(0, 0).Style.HasBackgroundRGB) | 206 | require.True(t, f.At(0, 0).Style.HasBackgroundRGB) | ||
| 205 | - | require.True(t, f.At(2, 0).Style.HasBackgroundRGB) | 207 | + | require.True(t, f.At(2, 0).Style.Inverse) |
| 208 | + | require.False(t, f.At(2, 0).Style.HasBackgroundRGB) | |||
| 206 | } | 209 | } | ||
| 207 | 210 | ||||
| 208 | func TestFindMatchesUsesExclusiveDisplayCellOffsets(t *testing.T) { | 211 | func TestFindMatchesUsesExclusiveDisplayCellOffsets(t *testing.T) { | ||
M internal/usecase/copy/selection.go +9 -0
| @@ -145,6 +145,15 @@ func (b selectionBounds) rangeForRow(doc *Document, row int) (CellRange, bool) { | |||
| 145 | 145 | return CellRange{Row: row, Start: start, End: end}, true | |
| 146 | 146 | } | |
| 147 | 147 | ||
| 148 | + | // newlineMarker reports the column marking a selected hard newline on row. | |
| 149 | + | func (b selectionBounds) newlineMarker(doc *Document, row int) (int, bool) { | |
| 150 | + | if row < b.start.Row || row > b.end.Row || doc.snapshot.Bound(row).Soft || (!b.linewise && row == b.end.Row) { | |
| 151 | + | return 0, false | |
| 152 | + | } | |
| 153 | + | end := doc.contentEnd(row) | |
| 154 | + | return end, end > 0 && end < doc.Width() | |
| 155 | + | } | |
| 156 | + | ||
| 148 | 157 | // Text extracts the selection's ranges. The granularity is already encoded in | |
| 149 | 158 | // those ranges, so extraction applies the same padding and wrap rules to all. | |
| 150 | 159 | func (s Selection) Text(doc *Document) string { | |
| @@ -145,6 +145,15 @@ func (b selectionBounds) rangeForRow(doc *Document, row int) (CellRange, bool) { | |||||
| 145 | return CellRange{Row: row, Start: start, End: end}, true | 145 | return CellRange{Row: row, Start: start, End: end}, true | ||
| 146 | } | 146 | } | ||
| 147 | 147 | ||||
| 148 | + | // newlineMarker reports the column marking a selected hard newline on row. | |||
| 149 | + | func (b selectionBounds) newlineMarker(doc *Document, row int) (int, bool) { | |||
| 150 | + | if row < b.start.Row || row > b.end.Row || doc.snapshot.Bound(row).Soft || (!b.linewise && row == b.end.Row) { | |||
| 151 | + | return 0, false | |||
| 152 | + | } | |||
| 153 | + | end := doc.contentEnd(row) | |||
| 154 | + | return end, end > 0 && end < doc.Width() | |||
| 155 | + | } | |||
| 156 | + | ||||
| 148 | // Text extracts the selection's ranges. The granularity is already encoded in | 157 | // Text extracts the selection's ranges. The granularity is already encoded in | ||
| 149 | // those ranges, so extraction applies the same padding and wrap rules to all. | 158 | // those ranges, so extraction applies the same padding and wrap rules to all. | ||
| 150 | func (s Selection) Text(doc *Document) string { | 159 | func (s Selection) Text(doc *Document) string { | ||