selection.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close