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. It
67// lets viewport renderers avoid materializing ranges for off-screen rows.
68func (s Selection) RangeForRow(doc *Document, row int) (CellRange, bool) {
69 bounds, ok := s.bounds(doc)
70 if !ok {
71 return CellRange{}, false
72 }
73 return bounds.rangeForRow(doc, row)
74}
75
76type selectionBounds struct {
77 start, end Pos
78 linewise bool
79}
80
81func (s Selection) bounds(doc *Document) (selectionBounds, bool) {
82 if !s.Enabled || doc == nil {
83 return selectionBounds{}, false
84 }
85 anchor, ok := doc.Normalize(s.Anchor)
86 if !ok {
87 return selectionBounds{}, false
88 }
89 active, ok := doc.Normalize(s.Active)
90 if !ok {
91 return selectionBounds{}, false
92 }
93
94 switch s.Granularity {
95 case Line:
96 start, end := order(anchor, active)
97 return selectionBounds{start: start, end: end, linewise: true}, true
98 case Character:
99 start, end := order(anchor, active)
100 return selectionBounds{start: start, end: end}, true
101 case Word:
102 anchorStart, anchorEnd, ok := doc.WordBounds(anchor)
103 if !ok {
104 return selectionBounds{}, false
105 }
106 activeStart, activeEnd, ok := doc.WordBounds(active)
107 if !ok {
108 return selectionBounds{}, false
109 }
110 start, _ := order(anchorStart, activeStart)
111 _, end := order(anchorEnd, activeEnd)
112 return selectionBounds{start: start, end: end}, true
113 default:
114 return selectionBounds{}, false
115 }
116}
117
118func (b selectionBounds) rangeForRow(doc *Document, row int) (CellRange, bool) {
119 if row < b.start.Row || row > b.end.Row {
120 return CellRange{}, false
121 }
122 cells := doc.Row(row)
123 if len(cells) == 0 {
124 return CellRange{Row: row}, true
125 }
126 if b.linewise {
127 return CellRange{Row: row, End: len(cells) - 1}, true
128 }
129
130 start, end := 0, len(cells)-1
131 if row == b.start.Row {
132 start = b.start.Col
133 }
134 if row == b.end.Row {
135 end = glyphEnd(cells, b.end.Col)
136 }
137 return CellRange{Row: row, Start: start, End: end}, true
138}
139
140// Text extracts the selection's ranges. The granularity is already encoded in
141// those ranges, so extraction applies the same padding and wrap rules to all.
142func (s Selection) Text(doc *Document) string {
143 if doc == nil {
144 return ""
145 }
146 return doc.Extract(s.Ranges(doc))
147}
148
149// Extend moves the active endpoint. Document normalization occurs when ranges
150// are requested so callers may pass raw pointer coordinates.
151func (s *Selection) Extend(at Pos) {
152 s.Active = at
153}
154
155// AsCharacter changes an active selection to stream selection without moving
156// either endpoint.
157func (s *Selection) AsCharacter() {
158 s.Granularity = Character
159}
160
161func before(left, right Pos) bool {
162 return left.Row < right.Row || left.Row == right.Row && left.Col < right.Col
163}
164
165func order(left, right Pos) (Pos, Pos) {
166 if before(right, left) {
167 return right, left
168 }
169 return left, right
170}
171
172// glyphEnd expands a glyph-head endpoint over all of its continuation cells.
173func glyphEnd(cells []renderer.Cell, col int) int {
174 for col+1 < len(cells) && cells[col+1].Continuation {
175 col++
176 }
177 return col
178}