main ece7682

model.go RawHistoryFolder

1package visualsearch
2
3import (
4	"strconv"
5
6	renderer "github.com/bnema/vev-vt"
7	"github.com/bnema/vev/internal/domain"
8	scopy "github.com/bnema/vev/internal/usecase/copy"
9	"github.com/bnema/vev/internal/usecase/ui"
10)
11
12type Model struct {
13	snapshot scopy.Snapshot
14	input    ui.TextInput
15	matches  []scopy.SearchMatch
16	selected int
17	scroll   int
18}
19
20func New(snapshot scopy.Snapshot) *Model {
21	m := &Model{snapshot: snapshot, selected: -1}
22	m.refresh()
23	return m
24}
25
26func (m *Model) Clone() *Model {
27	if m == nil {
28		return nil
29	}
30	clone := *m
31	clone.input.SetValue(m.input.Value())
32	clone.matches = append([]scopy.SearchMatch(nil), m.matches...)
33	return &clone
34}
35
36func (m *Model) Snapshot() scopy.Snapshot {
37	if m == nil {
38		return scopy.NewSnapshotFromRows(nil, 0, 0)
39	}
40	return m.snapshot
41}
42
43func (m *Model) Insert(r rune) {
44	if m == nil {
45		return
46	}
47	m.input.Insert(r)
48	m.selected = 0
49	m.scroll = 0
50	m.refresh()
51}
52
53func (m *Model) Backspace() {
54	if m == nil || m.input.Value() == "" {
55		return
56	}
57	m.input.Backspace()
58	m.selected = 0
59	m.scroll = 0
60	m.refresh()
61}
62
63func (m *Model) Query() string {
64	if m == nil {
65		return ""
66	}
67	return m.input.Value()
68}
69
70// searchInputPrefix leads the query line on row 0 of the rendered frame.
71const searchInputPrefix = "/"
72
73// Caret is the query caret column on row 0 of a Render frame of size inner
74// (see ui.InputCaret).
75func (m *Model) Caret(inner domain.Size) (int, bool) {
76	return ui.InputCaret(inner, searchInputPrefix, m.Query())
77}
78
79func (m *Model) Up() {
80	if m != nil && m.selected > 0 {
81		m.selected--
82		m.clamp()
83		m.ensureVisible(defaultVisibleRows)
84	}
85}
86
87func (m *Model) Down() {
88	if m != nil && m.selected+1 < len(m.matches) {
89		m.selected++
90		m.clamp()
91		m.ensureVisible(defaultVisibleRows)
92	}
93}
94
95func (m *Model) Selected() (scopy.SearchMatch, bool) {
96	idx := m.SelectedIndex()
97	if idx < 0 {
98		return scopy.SearchMatch{}, false
99	}
100	return m.matches[idx], true
101}
102
103func (m *Model) SelectedIndex() int {
104	if m == nil || m.selected < 0 || m.selected >= len(m.matches) {
105		return -1
106	}
107	return m.selected
108}
109
110func (m *Model) Matches() []scopy.SearchMatch {
111	if m == nil {
112		return nil
113	}
114	return append([]scopy.SearchMatch(nil), m.matches...)
115}
116
117func (m *Model) refresh() {
118	m.matches = scopy.FindMatches(scopy.NewDocument(m.snapshot, ""), m.input.Value())
119	m.clamp()
120	m.ensureVisible(defaultVisibleRows)
121}
122
123func (m *Model) clamp() {
124	if len(m.matches) == 0 {
125		m.selected = -1
126		m.scroll = 0
127		return
128	}
129	if m.selected < 0 {
130		m.selected = 0
131	}
132	if m.selected >= len(m.matches) {
133		m.selected = len(m.matches) - 1
134	}
135	if m.scroll < 0 {
136		m.scroll = 0
137	}
138	if m.scroll > m.selected {
139		m.scroll = m.selected
140	}
141}
142
143const defaultVisibleRows = 10
144
145// RenderStyles contains the base chrome surface and active selection role.
146type RenderStyles struct {
147	Base      renderer.Style
148	Selection renderer.Style
149}
150
151func (m *Model) ensureVisible(visible int) {
152	if m == nil || visible <= 0 || len(m.matches) == 0 {
153		return
154	}
155	if m.selected < m.scroll {
156		m.scroll = m.selected
157	}
158	if m.selected >= m.scroll+visible {
159		m.scroll = m.selected - visible + 1
160	}
161}
162
163func (m *Model) Render(inner domain.Size, selectedStyle ...renderer.Style) renderer.Frame {
164	base := renderer.DefaultStyle()
165	selection := base
166	selection.Inverse = true
167	if len(selectedStyle) > 0 {
168		selection = selectedStyle[0]
169	}
170	return m.RenderStyled(inner, RenderStyles{Base: base, Selection: selection})
171}
172
173// RenderStyled renders with explicit chrome roles captured by the daemon.
174func (m *Model) RenderStyled(inner domain.Size, styles RenderStyles) renderer.Frame {
175	frame := renderer.NewFrame(max(inner.Cols, 0), max(inner.Rows, 0))
176	if frame.Width == 0 || frame.Height == 0 {
177		return frame
178	}
179	base, selection := styles.Base, styles.Selection
180	ui.FillRect(frame, domain.Rect{Width: frame.Width, Height: frame.Height}, renderer.Cell{Rune: ' ', Style: base})
181	ui.DrawInputLine(frame, 0, searchInputPrefix, m.Query(), base)
182	visible := frame.Height - 1
183	if visible <= 0 || len(m.matches) == 0 {
184		return frame
185	}
186	scroll := m.scroll
187	if m.selected < scroll {
188		scroll = m.selected
189	}
190	if m.selected >= scroll+visible {
191		scroll = m.selected - visible + 1
192	}
193	for y := range visible {
194		idx := scroll + y
195		if idx >= len(m.matches) {
196			break
197		}
198		match := m.matches[idx]
199		style := base
200		if idx == m.selected {
201			style = selection
202		}
203		ui.FillRect(frame, domain.Rect{Y: y + 1, Width: frame.Width, Height: 1}, renderer.Cell{Rune: ' ', Style: style})
204		label := strconv.Itoa(match.Row+1) + ":" + strconv.Itoa(match.Start+1) + "  " + match.Text
205		ui.DrawText(frame, 0, y+1, frame.Width, label, style)
206	}
207	return frame
208}

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