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
70func (m *Model) Up() {
71	if m != nil && m.selected > 0 {
72		m.selected--
73		m.clamp()
74		m.ensureVisible(defaultVisibleRows)
75	}
76}
77
78func (m *Model) Down() {
79	if m != nil && m.selected+1 < len(m.matches) {
80		m.selected++
81		m.clamp()
82		m.ensureVisible(defaultVisibleRows)
83	}
84}
85
86func (m *Model) Selected() (scopy.SearchMatch, bool) {
87	idx := m.SelectedIndex()
88	if idx < 0 {
89		return scopy.SearchMatch{}, false
90	}
91	return m.matches[idx], true
92}
93
94func (m *Model) SelectedIndex() int {
95	if m == nil || m.selected < 0 || m.selected >= len(m.matches) {
96		return -1
97	}
98	return m.selected
99}
100
101func (m *Model) Matches() []scopy.SearchMatch {
102	if m == nil {
103		return nil
104	}
105	return append([]scopy.SearchMatch(nil), m.matches...)
106}
107
108func (m *Model) refresh() {
109	m.matches = scopy.FindMatches(scopy.NewDocument(m.snapshot, ""), m.input.Value())
110	m.clamp()
111	m.ensureVisible(defaultVisibleRows)
112}
113
114func (m *Model) clamp() {
115	if len(m.matches) == 0 {
116		m.selected = -1
117		m.scroll = 0
118		return
119	}
120	if m.selected < 0 {
121		m.selected = 0
122	}
123	if m.selected >= len(m.matches) {
124		m.selected = len(m.matches) - 1
125	}
126	if m.scroll < 0 {
127		m.scroll = 0
128	}
129	if m.scroll > m.selected {
130		m.scroll = m.selected
131	}
132}
133
134const defaultVisibleRows = 10
135
136// RenderStyles contains the base chrome surface and active selection role.
137type RenderStyles struct {
138	Base      renderer.Style
139	Selection renderer.Style
140}
141
142func (m *Model) ensureVisible(visible int) {
143	if m == nil || visible <= 0 || len(m.matches) == 0 {
144		return
145	}
146	if m.selected < m.scroll {
147		m.scroll = m.selected
148	}
149	if m.selected >= m.scroll+visible {
150		m.scroll = m.selected - visible + 1
151	}
152}
153
154func (m *Model) Render(inner domain.Size, selectedStyle ...renderer.Style) renderer.Frame {
155	base := renderer.DefaultStyle()
156	selection := base
157	selection.Inverse = true
158	if len(selectedStyle) > 0 {
159		selection = selectedStyle[0]
160	}
161	return m.RenderStyled(inner, RenderStyles{Base: base, Selection: selection})
162}
163
164// RenderStyled renders with explicit chrome roles captured by the daemon.
165func (m *Model) RenderStyled(inner domain.Size, styles RenderStyles) renderer.Frame {
166	frame := renderer.NewFrame(max(inner.Cols, 0), max(inner.Rows, 0))
167	if frame.Width == 0 || frame.Height == 0 {
168		return frame
169	}
170	base, selection := styles.Base, styles.Selection
171	ui.FillRect(frame, domain.Rect{Width: frame.Width, Height: frame.Height}, renderer.Cell{Rune: ' ', Style: base})
172	ui.DrawInputLine(frame, 0, "/", m.Query(), base, selection)
173	visible := frame.Height - 1
174	if visible <= 0 || len(m.matches) == 0 {
175		return frame
176	}
177	scroll := m.scroll
178	if m.selected < scroll {
179		scroll = m.selected
180	}
181	if m.selected >= scroll+visible {
182		scroll = m.selected - visible + 1
183	}
184	for y := range visible {
185		idx := scroll + y
186		if idx >= len(m.matches) {
187			break
188		}
189		match := m.matches[idx]
190		style := base
191		if idx == m.selected {
192			style = selection
193		}
194		ui.FillRect(frame, domain.Rect{Y: y + 1, Width: frame.Width, Height: 1}, renderer.Cell{Rune: ' ', Style: style})
195		label := strconv.Itoa(match.Row+1) + ":" + strconv.Itoa(match.Start+1) + "  " + match.Text
196		ui.DrawText(frame, 0, y+1, frame.Width, label, style)
197	}
198	return frame
199}

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