v0.3.0 99dc266

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. 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}

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