key.go RawHistoryFolder

1package vimkeys
2
3import (
4	"errors"
5	"strings"
6)
7
8// Mods holds keyboard modifier flags for a parsed key.
9type Mods uint8
10
11const (
12	ModCtrl Mods = 1 << iota
13	ModShift
14	ModAlt
15)
16
17const (
18	symCR  = "CR"
19	symEsc = "Esc"
20)
21
22// Key is a single key event with symbol and modifiers.
23type Key struct {
24	Sym  string
25	Mods Mods
26}
27
28// Sequence is an ordered list of keys forming a Vim-style binding.
29type Sequence []Key
30
31var (
32	ErrEmptyBinding = errors.New("vimkeys: empty binding")
33	ErrBadBinding   = errors.New("vimkeys: malformed binding")
34)
35
36// String renders a key in canonical form.
37func (k Key) String() string {
38	if k.Sym == "" {
39		return ""
40	}
41
42	if k.Mods == ModShift && len(k.Sym) == 1 {
43		if c := k.Sym[0]; c >= 'a' && c <= 'z' {
44			return strings.ToUpper(k.Sym)
45		}
46	}
47
48	if k.Mods == 0 {
49		if k.Sym == "+" {
50			return "<Plus>"
51		}
52		if alias := angleAliasForSym(k.Sym); alias != "" {
53			return "<" + alias + ">"
54		}
55		return k.Sym
56	}
57
58	return formatAngleKey(k)
59}
60
61// String renders the full binding sequence in canonical form.
62// When the naive concatenation would case-insensitively equal an exact legacy
63// atom (enter/return/escape/esc), the first key is forced to an angle literal
64// so ParseBinding cannot reabsorb the sequence as that atom.
65func (s Sequence) String() string {
66	rendered := s.render(false)
67	if collidesWithLegacyAtom(rendered) {
68		return s.render(true)
69	}
70	return rendered
71}
72
73func (s Sequence) render(forceAngleFirst bool) string {
74	var b strings.Builder
75	for i, key := range s {
76		if forceAngleFirst && i == 0 {
77			b.WriteString(key.angleLiteralString())
78			continue
79		}
80		b.WriteString(key.String())
81	}
82	return b.String()
83}
84
85func collidesWithLegacyAtom(s string) bool {
86	switch strings.ToLower(s) {
87	case "enter", "return", "escape", "esc":
88		return true
89	default:
90		return false
91	}
92}
93
94// angleLiteralString forces angle-bracket form for keys that Key.String would
95// otherwise emit as bare or shifted letters (e.g. "e" / "E").
96func (k Key) angleLiteralString() string {
97	if len(k.Sym) == 1 {
98		c := k.Sym[0]
99		if c >= 'a' && c <= 'z' {
100			switch k.Mods {
101			case 0:
102				return "<" + k.Sym + ">"
103			case ModShift:
104				return formatAngleKey(k)
105			}
106		}
107	}
108	return k.String()
109}
110
111// Equal reports whether two sequences contain the same keys.
112func (s Sequence) Equal(other Sequence) bool {
113	if len(s) != len(other) {
114		return false
115	}
116	for i := range s {
117		if s[i] != other[i] {
118			return false
119		}
120	}
121	return true
122}
123
124func angleAliasForSym(sym string) string {
125	switch sym {
126	case symCR:
127		return "Return"
128	case symEsc:
129		return "Escape"
130	case "Space", "Tab", "BackSpace", "Delete",
131		"Left", "Right", "Up", "Down",
132		"Home", "End", "PageUp", "PageDown":
133		return sym
134	case "<":
135		return "lt"
136	case ">":
137		return "gt"
138	default:
139		return ""
140	}
141}
142
143func formatAngleKey(k Key) string {
144	var b strings.Builder
145	b.WriteByte('<')
146	if k.Mods&ModCtrl != 0 {
147		b.WriteByte('C')
148	}
149	if k.Mods&ModShift != 0 {
150		if b.Len() > 1 {
151			b.WriteByte('-')
152		}
153		b.WriteByte('S')
154	}
155	if k.Mods&ModAlt != 0 {
156		if b.Len() > 1 {
157			b.WriteByte('-')
158		}
159		b.WriteByte('A')
160	}
161	b.WriteByte('-')
162	if alias := angleAliasForSym(k.Sym); alias != "" {
163		b.WriteString(alias)
164	} else {
165		b.WriteString(k.Sym)
166	}
167	b.WriteByte('>')
168	return b.String()
169}

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