1package copy
2
3import renderer "github.com/bnema/vev-vt"
4
5// Granularity controls how a selection's endpoints become display-cell ranges.
6type Granularity uint8
7
8const (
9 Line Granularity = iota
10 Character
11 Word
12)
13
14// Selection is an immutable-document selection. Anchor and Active are glyph
15// head positions whenever they originate from a Document operation.
16type Selection struct {
17 Anchor Pos
18 Active Pos
19 Granularity Granularity
20 Enabled bool
21}
22
23// NewLineSelection starts a complete physical-line selection at at.
24func NewLineSelection(at Pos) Selection {
25 return Selection{Anchor: at, Active: at, Granularity: Line, Enabled: true}
26}
27
28// NewWordSelection selects the complete word containing at. Separators cannot
29// begin a word selection.
30func NewWordSelection(doc *Document, at Pos) (Selection, bool) {
31 if doc == nil {
32 return Selection{}, false
33 }
34 start, end, ok := doc.WordBounds(at)
35 if !ok {
36 return Selection{}, false
37 }
38 return Selection{Anchor: start, Active: end, Granularity: Word, Enabled: true}, true
39}
40
41// Ordered returns the selection endpoints in document order.
42func (s Selection) Ordered() (Pos, Pos) {
43 if before(s.Active, s.Anchor) {
44 return s.Active, s.Anchor
45 }
46 return s.Anchor, s.Active
47}
48
49// Ranges returns the canonical, inclusive per-physical-row display-cell
50// ranges. It returns nil for disabled selections or invalid endpoints.
51func (s Selection) Ranges(doc *Document) []CellRange {
52 bounds, ok := s.bounds(doc)
53 if !ok {
54 return nil
55 }
56 ranges := make([]CellRange, 0, bounds.end.Row-bounds.start.Row+1)
57 for row := bounds.start.Row; row <= bounds.end.Row; row++ {
58 r, ok := bounds.rangeForRow(doc, row)
59 if ok {
60 ranges = append(ranges, r)
61 }
62 }
63 return ranges
64}
65
66// RangeForRow returns the canonical inclusive range for one document row.
67// End == Start-1 denotes a selected row with no cells. It lets viewport
68// renderers avoid materializing ranges for off-screen rows.
69func (s Selection) RangeForRow(doc *Document, row int) (CellRange, bool) {
70 bounds, ok := s.bounds(doc)
71 if !ok {
72 return CellRange{}, false
73 }
74 return bounds.rangeForRow(doc, row)
75}
76
77type selectionBounds struct {
78 start, end Pos
79 linewise bool
80}
81
82func (s Selection) bounds(doc *Document) (selectionBounds, bool) {
83 if !s.Enabled || doc == nil {
84 return selectionBounds{}, false
85 }
86 anchor, ok := doc.Normalize(s.Anchor)
87 if !ok {
88 return selectionBounds{}, false
89 }
90 active, ok := doc.Normalize(s.Active)
91 if !ok {
92 return selectionBounds{}, false
93 }
94
95 switch s.Granularity {
96 case Line:
97 start, end := order(anchor, active)
98 return selectionBounds{start: start, end: end, linewise: true}, true
99 case Character:
100 start, end := order(anchor, active)
101 return selectionBounds{start: start, end: end}, true
102 case Word:
103 anchorStart, anchorEnd, ok := doc.WordBounds(anchor)
104 if !ok {
105 return selectionBounds{}, false
106 }
107 activeStart, activeEnd, ok := doc.WordBounds(active)
108 if !ok {
109 return selectionBounds{}, false
110 }
111 start, _ := order(anchorStart, activeStart)
112 _, end := order(anchorEnd, activeEnd)
113 return selectionBounds{start: start, end: end}, true
114 default:
115 return selectionBounds{}, false
116 }
117}
118
119func (b selectionBounds) rangeForRow(doc *Document, row int) (CellRange, bool) {
120 if row < b.start.Row || row > b.end.Row {
121 return CellRange{}, false
122 }
123 cells := doc.Row(row)
124 return b.rangeForCells(row, cells, contentEndOf(cells, doc.snapshot.Bound(row)))
125}
126
127func (b selectionBounds) rangeForCells(row int, cells []renderer.Cell, limit int) (CellRange, bool) {
128 if row < b.start.Row || row > b.end.Row {
129 return CellRange{}, false
130 }
131 if len(cells) == 0 {
132 return CellRange{Row: row}, true
133 }
134 if limit == 0 {
135 return CellRange{Row: row, End: -1}, true
136 }
137 if b.linewise {
138 return CellRange{Row: row, End: limit - 1}, true
139 }
140
141 start, end := 0, len(cells)-1
142 if row == b.start.Row {
143 start = b.start.Col
144 }
145 if row == b.end.Row {
146 end = glyphEnd(cells, b.end.Col)
147 }
148 end = min(end, limit-1)
149 if start > end {
150 end = start - 1
151 }
152 return CellRange{Row: row, Start: start, End: end}, true
153}
154
155// newlineMarker reports the column marking a selected hard newline on row.
156func (b selectionBounds) newlineMarker(doc *Document, row int) (int, bool) {
157 return b.newlineMarkerAt(row, doc.contentEnd(row), doc.Width(), doc.snapshot.Bound(row).Soft)
158}
159
160func (b selectionBounds) newlineMarkerAt(row, end, width int, soft bool) (int, bool) {
161 if row < b.start.Row || row > b.end.Row || soft || (!b.linewise && row == b.end.Row) {
162 return 0, false
163 }
164 return end, end > 0 && end < width
165}
166
167// Text extracts the selection's ranges. The granularity is already encoded in
168// those ranges, so extraction applies the same padding and wrap rules to all.
169func (s Selection) Text(doc *Document) string {
170 if doc == nil {
171 return ""
172 }
173 return doc.Extract(s.Ranges(doc))
174}
175
176// Extend moves the active endpoint. Document normalization occurs when ranges
177// are requested so callers may pass raw pointer coordinates.
178func (s *Selection) Extend(at Pos) {
179 s.Active = at
180}
181
182// AsCharacter changes an active selection to stream selection without moving
183// either endpoint.
184func (s *Selection) AsCharacter() {
185 s.Granularity = Character
186}
187
188func before(left, right Pos) bool {
189 return left.Row < right.Row || left.Row == right.Row && left.Col < right.Col
190}
191
192func order(left, right Pos) (Pos, Pos) {
193 if before(right, left) {
194 return right, left
195 }
196 return left, right
197}
198
199// glyphEnd expands a glyph-head endpoint over all of its continuation cells.
200func glyphEnd(cells []renderer.Cell, col int) int {
201 for col+1 < len(cells) && cells[col+1].Continuation {
202 col++
203 }
204 return col
205}