v0.33.1 9577fee

matcher.go RawHistoryFolder

1package vimkeys
2
3import "strconv"
4
5// ResultKind classifies matcher feed outcomes.
6type ResultKind int
7
8const (
9	ResultPending ResultKind = iota
10	ResultComplete
11	ResultInvalid
12)
13
14// Result is the outcome of feeding a key into the matcher.
15type Result struct {
16	Kind    ResultKind
17	Count   int
18	Action  string
19	Pending string
20}
21
22// Matcher incrementally matches key sequences against a trie.
23type Matcher struct {
24	trie      *Trie
25	node      *trieNode
26	pending   Sequence
27	count     int
28	inCount   bool
29	ambiguous bool
30}
31
32// NewMatcher creates a matcher backed by trie.
33func NewMatcher(trie *Trie) *Matcher {
34	return &Matcher{trie: trie}
35}
36
37// Reset clears matcher state.
38func (m *Matcher) Reset() {
39	if m == nil {
40		return
41	}
42	m.node = nil
43	m.pending = nil
44	m.count = 0
45	m.inCount = false
46	m.ambiguous = false
47}
48
49// Pending returns the count prefix and keys matched so far.
50func (m *Matcher) Pending() string {
51	if !m.hasTrie() {
52		return ""
53	}
54	return formatPending(m.count, m.pending)
55}
56
57// Ambiguous reports whether the current prefix is both exact and extendable.
58func (m *Matcher) Ambiguous() bool {
59	return m.hasTrie() && m.ambiguous
60}
61
62// Feed consumes one key and returns the current match state.
63func (m *Matcher) Feed(key Key) Result {
64	if !m.hasTrie() {
65		m.Reset()
66		return Result{Kind: ResultInvalid}
67	}
68
69	if m.node == nil && len(m.pending) == 0 && m.count == 0 && !m.inCount {
70		m.beginSequence()
71	}
72
73	if m.inCount {
74		return m.feedCountDigit(key)
75	}
76
77	if len(m.pending) == 0 && isCountStartDigit(key) {
78		m.inCount = true
79		return m.feedCountDigit(key)
80	}
81
82	return m.feedSequenceKey(key)
83}
84
85func (m *Matcher) hasTrie() bool {
86	return m != nil && m.trie != nil && m.trie.root != nil
87}
88
89func (m *Matcher) beginSequence() {
90	if !m.hasTrie() {
91		m.Reset()
92		return
93	}
94	m.node = m.trie.root
95	m.pending = nil
96	m.count = 0
97	m.inCount = false
98	m.ambiguous = false
99}
100
101func (m *Matcher) feedCountDigit(key Key) Result {
102	if !isCountDigit(key) {
103		m.inCount = false
104		m.node = m.trie.root
105		return m.feedSequenceKey(key)
106	}
107
108	digit := int(key.Sym[0] - '0')
109	next := m.count*10 + digit
110	if next > maxCount {
111		next = maxCount
112	}
113	m.count = next
114	return Result{Kind: ResultPending, Count: normalizedCount(m.count), Pending: m.Pending()}
115}
116
117func (m *Matcher) feedSequenceKey(key Key) Result {
118	if m.node == nil {
119		result := Result{Kind: ResultInvalid, Count: normalizedCount(m.count)}
120		m.Reset()
121		return result
122	}
123
124	child, ok := m.node.children[key]
125	if !ok {
126		result := Result{Kind: ResultInvalid, Count: normalizedCount(m.count)}
127		m.Reset()
128		return result
129	}
130
131	m.pending = append(m.pending, key)
132	m.node = child
133
134	exact := m.node.hasAction
135	children := len(m.node.children) > 0
136	m.ambiguous = exact && children
137
138	switch {
139	case m.ambiguous:
140		return Result{Kind: ResultPending, Count: normalizedCount(m.count), Pending: m.Pending()}
141	case exact:
142		result := Result{
143			Kind:   ResultComplete,
144			Count:  normalizedCount(m.count),
145			Action: m.node.action,
146		}
147		m.Reset()
148		return result
149	case children:
150		return Result{Kind: ResultPending, Count: normalizedCount(m.count), Pending: m.Pending()}
151	default:
152		result := Result{Kind: ResultInvalid, Count: normalizedCount(m.count)}
153		m.Reset()
154		return result
155	}
156}
157
158// ResolveAmbiguity completes the current short exact binding.
159func (m *Matcher) ResolveAmbiguity() (Result, bool) {
160	if !m.hasTrie() || !m.ambiguous || m.node == nil || !m.node.hasAction {
161		return Result{}, false
162	}
163
164	result := Result{
165		Kind:   ResultComplete,
166		Count:  normalizedCount(m.count),
167		Action: m.node.action,
168	}
169	m.Reset()
170	return result, true
171}
172
173func isCountStartDigit(key Key) bool {
174	return key.Mods == 0 && len(key.Sym) == 1 && key.Sym[0] >= '1' && key.Sym[0] <= '9'
175}
176
177func isCountDigit(key Key) bool {
178	return key.Mods == 0 && len(key.Sym) == 1 && key.Sym[0] >= '0' && key.Sym[0] <= '9'
179}
180
181func normalizedCount(count int) int {
182	if count <= 0 {
183		return 0
184	}
185	return count
186}
187
188func formatPending(count int, pending Sequence) string {
189	var out string
190	if count > 0 {
191		out = strconv.Itoa(count)
192	}
193	if len(pending) > 0 {
194		out += pending.String()
195	}
196	return out
197}

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 8

Tags 42

↑↓ moveenter openesc close