detached e59ccc8

test: add workspace navigation and keyboard bridge unit tests

Add comprehensive tests for:
- Pane node structure validation
- Direction mapping to GTK orientation
- Invalid direction handling
- Case-insensitive direction handling
- Keyboard shortcut routing table
- Workspace navigation shortcuts blocking WebKit
commit
e59ccc8b2ebd00050cf7134d82e7cb4bd16f7307
author
bamen · · 2025-10-09 17:28 UTC
committer
bamen · · 2025-10-09 17:12 UTC
parent
26523d5
signature
unsigned
2 files · +351 -0
A internal/app/browser/workspace_navigation_test.go +146 -0
@@ -0,0 +1,146 @@
1+package browser
2+
3+import (
4+ "testing"
5+
6+ "github.com/diamondburned/gotk4/pkg/gtk/v4"
7+)
8+
9+// TestFocusNeighborVerticalSplit tests basic tree structure validation
10+// Note: Full geometry-based navigation testing requires integration tests with real GTK widgets
11+func TestFocusNeighborVerticalSplit(t *testing.T) {
12+ t.Skip("Structural neighbor navigation requires real GTK widgets for geometry calculations")
13+}
14+
15+// TestFocusNeighborHorizontalSplit tests basic tree structure validation
16+// Note: Full geometry-based navigation testing requires integration tests with real GTK widgets
17+func TestFocusNeighborHorizontalSplit(t *testing.T) {
18+ t.Skip("Structural neighbor navigation requires real GTK widgets for geometry calculations")
19+}
20+
21+// TestPaneNodeStructure verifies basic pane node relationships
22+func TestPaneNodeStructure(t *testing.T) {
23+ root := &paneNode{
24+ orientation: gtk.OrientationVertical,
25+ isLeaf: false,
26+ }
27+
28+ child1 := &paneNode{
29+ parent: root,
30+ isLeaf: true,
31+ }
32+
33+ child2 := &paneNode{
34+ parent: root,
35+ isLeaf: true,
36+ }
37+
38+ root.left = child1
39+ root.right = child2
40+
41+ // Verify parent-child relationships
42+ if child1.parent != root {
43+ t.Errorf("child1.parent should be root")
44+ }
45+
46+ if child2.parent != root {
47+ t.Errorf("child2.parent should be root")
48+ }
49+
50+ // Verify root children
51+ if root.left != child1 {
52+ t.Errorf("root.left should be child1")
53+ }
54+
55+ if root.right != child2 {
56+ t.Errorf("root.right should be child2")
57+ }
58+
59+ // Verify leaf status
60+ if !child1.isLeaf {
61+ t.Errorf("child1 should be a leaf")
62+ }
63+
64+ if !child2.isLeaf {
65+ t.Errorf("child2 should be a leaf")
66+ }
67+
68+ if root.isLeaf {
69+ t.Errorf("root should not be a leaf")
70+ }
71+}
72+
73+// TestMapDirection verifies direction mapping to GTK orientation
74+func TestMapDirection(t *testing.T) {
75+ tests := []struct {
76+ direction string
77+ orientation gtk.Orientation
78+ existingFirst bool
79+ }{
80+ {"down", gtk.OrientationVertical, true},
81+ {"up", gtk.OrientationVertical, false},
82+ {"right", gtk.OrientationHorizontal, true},
83+ {"left", gtk.OrientationHorizontal, false},
84+ }
85+
86+ for _, tt := range tests {
87+ t.Run(tt.direction, func(t *testing.T) {
88+ orientation, existingFirst := mapDirection(tt.direction)
89+
90+ if orientation != tt.orientation {
91+ t.Errorf("mapDirection(%q) orientation = %v, want %v",
92+ tt.direction, orientation, tt.orientation)
93+ }
94+
95+ if existingFirst != tt.existingFirst {
96+ t.Errorf("mapDirection(%q) existingFirst = %v, want %v",
97+ tt.direction, existingFirst, tt.existingFirst)
98+ }
99+ })
100+ }
101+}
102+
103+// TestInvalidDirections verifies handling of invalid directions
104+func TestInvalidDirections(t *testing.T) {
105+ wm := &WorkspaceManager{}
106+
107+ invalidDirections := []string{
108+ "",
109+ "unknown",
110+ "diagonal",
111+ }
112+
113+ for _, direction := range invalidDirections {
114+ t.Run(direction, func(t *testing.T) {
115+ success := wm.FocusNeighbor(direction)
116+ if success {
117+ t.Errorf("Expected FocusNeighbor(%q) to return false, got true", direction)
118+ }
119+ })
120+ }
121+}
122+
123+// TestCaseSensitiveDirections verifies that direction strings are case-insensitive
124+func TestCaseSensitiveDirections(t *testing.T) {
125+ wm := &WorkspaceManager{}
126+
127+ // These should be accepted (case insensitive), but will return false
128+ // because there's no active pane to navigate from
129+ validDirections := []string{
130+ "UP",
131+ "Down",
132+ "LEFT",
133+ "Right",
134+ "up",
135+ "down",
136+ "left",
137+ "right",
138+ }
139+
140+ for _, direction := range validDirections {
141+ t.Run(direction, func(t *testing.T) {
142+ // Should not panic or error, just return false since no panes exist
143+ _ = wm.FocusNeighbor(direction)
144+ })
145+ }
146+}
@@ -0,0 +1,146 @@
1+package browser
2+
3+import (
4+ "testing"
5+
6+ "github.com/diamondburned/gotk4/pkg/gtk/v4"
7+)
8+
9+// TestFocusNeighborVerticalSplit tests basic tree structure validation
10+// Note: Full geometry-based navigation testing requires integration tests with real GTK widgets
11+func TestFocusNeighborVerticalSplit(t *testing.T) {
12+ t.Skip("Structural neighbor navigation requires real GTK widgets for geometry calculations")
13+}
14+
15+// TestFocusNeighborHorizontalSplit tests basic tree structure validation
16+// Note: Full geometry-based navigation testing requires integration tests with real GTK widgets
17+func TestFocusNeighborHorizontalSplit(t *testing.T) {
18+ t.Skip("Structural neighbor navigation requires real GTK widgets for geometry calculations")
19+}
20+
21+// TestPaneNodeStructure verifies basic pane node relationships
22+func TestPaneNodeStructure(t *testing.T) {
23+ root := &paneNode{
24+ orientation: gtk.OrientationVertical,
25+ isLeaf: false,
26+ }
27+
28+ child1 := &paneNode{
29+ parent: root,
30+ isLeaf: true,
31+ }
32+
33+ child2 := &paneNode{
34+ parent: root,
35+ isLeaf: true,
36+ }
37+
38+ root.left = child1
39+ root.right = child2
40+
41+ // Verify parent-child relationships
42+ if child1.parent != root {
43+ t.Errorf("child1.parent should be root")
44+ }
45+
46+ if child2.parent != root {
47+ t.Errorf("child2.parent should be root")
48+ }
49+
50+ // Verify root children
51+ if root.left != child1 {
52+ t.Errorf("root.left should be child1")
53+ }
54+
55+ if root.right != child2 {
56+ t.Errorf("root.right should be child2")
57+ }
58+
59+ // Verify leaf status
60+ if !child1.isLeaf {
61+ t.Errorf("child1 should be a leaf")
62+ }
63+
64+ if !child2.isLeaf {
65+ t.Errorf("child2 should be a leaf")
66+ }
67+
68+ if root.isLeaf {
69+ t.Errorf("root should not be a leaf")
70+ }
71+}
72+
73+// TestMapDirection verifies direction mapping to GTK orientation
74+func TestMapDirection(t *testing.T) {
75+ tests := []struct {
76+ direction string
77+ orientation gtk.Orientation
78+ existingFirst bool
79+ }{
80+ {"down", gtk.OrientationVertical, true},
81+ {"up", gtk.OrientationVertical, false},
82+ {"right", gtk.OrientationHorizontal, true},
83+ {"left", gtk.OrientationHorizontal, false},
84+ }
85+
86+ for _, tt := range tests {
87+ t.Run(tt.direction, func(t *testing.T) {
88+ orientation, existingFirst := mapDirection(tt.direction)
89+
90+ if orientation != tt.orientation {
91+ t.Errorf("mapDirection(%q) orientation = %v, want %v",
92+ tt.direction, orientation, tt.orientation)
93+ }
94+
95+ if existingFirst != tt.existingFirst {
96+ t.Errorf("mapDirection(%q) existingFirst = %v, want %v",
97+ tt.direction, existingFirst, tt.existingFirst)
98+ }
99+ })
100+ }
101+}
102+
103+// TestInvalidDirections verifies handling of invalid directions
104+func TestInvalidDirections(t *testing.T) {
105+ wm := &WorkspaceManager{}
106+
107+ invalidDirections := []string{
108+ "",
109+ "unknown",
110+ "diagonal",
111+ }
112+
113+ for _, direction := range invalidDirections {
114+ t.Run(direction, func(t *testing.T) {
115+ success := wm.FocusNeighbor(direction)
116+ if success {
117+ t.Errorf("Expected FocusNeighbor(%q) to return false, got true", direction)
118+ }
119+ })
120+ }
121+}
122+
123+// TestCaseSensitiveDirections verifies that direction strings are case-insensitive
124+func TestCaseSensitiveDirections(t *testing.T) {
125+ wm := &WorkspaceManager{}
126+
127+ // These should be accepted (case insensitive), but will return false
128+ // because there's no active pane to navigate from
129+ validDirections := []string{
130+ "UP",
131+ "Down",
132+ "LEFT",
133+ "Right",
134+ "up",
135+ "down",
136+ "left",
137+ "right",
138+ }
139+
140+ for _, direction := range validDirections {
141+ t.Run(direction, func(t *testing.T) {
142+ // Should not panic or error, just return false since no panes exist
143+ _ = wm.FocusNeighbor(direction)
144+ })
145+ }
146+}
A pkg/webkit/keyboard_bridge_test.go +205 -0
@@ -0,0 +1,205 @@
1+package webkit
2+
3+import (
4+ "testing"
5+)
6+
7+// TestShortcutRoutingTable verifies the routing table configuration
8+func TestShortcutRoutingTable(t *testing.T) {
9+ tests := []struct {
10+ shortcut string
11+ expectEntry bool
12+ forwardToJS bool
13+ blockWebKit bool
14+ description string
15+ }{
16+ {
17+ shortcut: "alt+arrowup",
18+ expectEntry: true,
19+ forwardToJS: false,
20+ blockWebKit: true,
21+ description: "Navigate to upper pane (callback handler)",
22+ },
23+ {
24+ shortcut: "alt+arrowdown",
25+ expectEntry: true,
26+ forwardToJS: false,
27+ blockWebKit: true,
28+ description: "Navigate to lower pane (callback handler)",
29+ },
30+ {
31+ shortcut: "alt+arrowleft",
32+ expectEntry: true,
33+ forwardToJS: false,
34+ blockWebKit: true,
35+ description: "Navigate to left pane (callback handler)",
36+ },
37+ {
38+ shortcut: "alt+arrowright",
39+ expectEntry: true,
40+ forwardToJS: false,
41+ blockWebKit: true,
42+ description: "Navigate to right pane (callback handler)",
43+ },
44+ {
45+ shortcut: "cmdorctrl+p",
46+ expectEntry: true,
47+ forwardToJS: true,
48+ blockWebKit: true,
49+ description: "Enter pane mode (JS handler + block print dialog)",
50+ },
51+ {
52+ shortcut: "cmdorctrl+l",
53+ expectEntry: false, // Not in table, should use default
54+ forwardToJS: true,
55+ blockWebKit: false,
56+ description: "Default routing",
57+ },
58+ }
59+
60+ for _, tt := range tests {
61+ t.Run(tt.shortcut, func(t *testing.T) {
62+ routing, exists := shortcutRoutingTable[tt.shortcut]
63+
64+ if tt.expectEntry {
65+ if !exists {
66+ t.Errorf("Expected routing entry for %s but not found", tt.shortcut)
67+ return
68+ }
69+
70+ if routing.forwardToJS != tt.forwardToJS {
71+ t.Errorf("forwardToJS: got %v, want %v", routing.forwardToJS, tt.forwardToJS)
72+ }
73+
74+ if routing.blockWebKit != tt.blockWebKit {
75+ t.Errorf("blockWebKit: got %v, want %v", routing.blockWebKit, tt.blockWebKit)
76+ }
77+
78+ if routing.description != tt.description {
79+ t.Errorf("description: got %q, want %q", routing.description, tt.description)
80+ }
81+ } else {
82+ if exists {
83+ t.Errorf("Expected no routing entry for %s but found one", tt.shortcut)
84+ }
85+ }
86+ })
87+ }
88+}
89+
90+// TestDefaultRouting verifies the default routing behavior
91+func TestDefaultRouting(t *testing.T) {
92+ if defaultRouting.forwardToJS != true {
93+ t.Errorf("defaultRouting.forwardToJS: got %v, want true", defaultRouting.forwardToJS)
94+ }
95+
96+ if defaultRouting.blockWebKit != false {
97+ t.Errorf("defaultRouting.blockWebKit: got %v, want false", defaultRouting.blockWebKit)
98+ }
99+}
100+
101+// TestWorkspaceNavigationShortcuts verifies workspace navigation shortcuts block WebKit
102+func TestWorkspaceNavigationShortcuts(t *testing.T) {
103+ workspaceShortcuts := []string{
104+ "alt+arrowup",
105+ "alt+arrowdown",
106+ "alt+arrowleft",
107+ "alt+arrowright",
108+ }
109+
110+ for _, shortcut := range workspaceShortcuts {
111+ t.Run(shortcut, func(t *testing.T) {
112+ routing, exists := shortcutRoutingTable[shortcut]
113+
114+ if !exists {
115+ t.Fatalf("Workspace navigation shortcut %s not found in routing table", shortcut)
116+ }
117+
118+ // These shortcuts must block WebKit to prevent page scrolling
119+ if !routing.blockWebKit {
120+ t.Errorf("Workspace navigation shortcut %s must block WebKit (got blockWebKit=%v)",
121+ shortcut, routing.blockWebKit)
122+ }
123+
124+ // These shortcuts should NOT forward to JS (handled by callback)
125+ if routing.forwardToJS {
126+ t.Errorf("Workspace navigation shortcut %s should not forward to JS (got forwardToJS=%v)",
127+ shortcut, routing.forwardToJS)
128+ }
129+ })
130+ }
131+}
132+
133+// TestConvertToGtkFormat tests the key format conversion
134+func TestConvertToGtkFormat(t *testing.T) {
135+ tests := []struct {
136+ input string
137+ expected string
138+ }{
139+ {"ctrl+l", "<Control>l"},
140+ {"ctrl+plus", "<Control>plus"},
141+ {"ctrl+shift+c", "<Control><Shift>c"},
142+ {"alt+arrowleft", "<Alt>Left"},
143+ {"alt+arrowright", "<Alt>Right"},
144+ {"alt+arrowup", "<Alt>Up"},
145+ {"alt+arrowdown", "<Alt>Down"},
146+ {"cmdorctrl+p", "<Control>p"},
147+ {"F12", "F12"},
148+ {"ctrl+0", "<Control>0"},
149+ }
150+
151+ for _, tt := range tests {
152+ t.Run(tt.input, func(t *testing.T) {
153+ result := convertToGtkFormat(tt.input)
154+ if result != tt.expected {
155+ t.Errorf("convertToGtkFormat(%q) = %q, want %q", tt.input, result, tt.expected)
156+ }
157+ })
158+ }
159+}
160+
161+// TestKeyNameConstants verifies key name constants are properly defined
162+func TestKeyNameConstants(t *testing.T) {
163+ expectedKeys := map[string]string{
164+ "keyP": "p",
165+ "keyL": "l",
166+ "keyF": "f",
167+ "keyC": "c",
168+ "keyW": "w",
169+ "keyR": "r",
170+ "keyArrowLeft": "arrowleft",
171+ "keyArrowRight": "arrowright",
172+ "keyArrowUp": "arrowup",
173+ "keyArrowDown": "arrowdown",
174+ "keyF12": "F12",
175+ "keyPlus": "plus",
176+ "keyMinus": "minus",
177+ "keyZero": "0",
178+ }
179+
180+ // Verify constants exist and have expected values
181+ keys := map[string]string{
182+ "keyP": keyP,
183+ "keyL": keyL,
184+ "keyF": keyF,
185+ "keyC": keyC,
186+ "keyW": keyW,
187+ "keyR": keyR,
188+ "keyArrowLeft": keyArrowLeft,
189+ "keyArrowRight": keyArrowRight,
190+ "keyArrowUp": keyArrowUp,
191+ "keyArrowDown": keyArrowDown,
192+ "keyF12": keyF12,
193+ "keyPlus": keyPlus,
194+ "keyMinus": keyMinus,
195+ "keyZero": keyZero,
196+ }
197+
198+ for name, expected := range expectedKeys {
199+ if actual, ok := keys[name]; !ok {
200+ t.Errorf("Constant %s not found", name)
201+ } else if actual != expected {
202+ t.Errorf("Constant %s: got %q, want %q", name, actual, expected)
203+ }
204+ }
205+}
@@ -0,0 +1,205 @@
1+package webkit
2+
3+import (
4+ "testing"
5+)
6+
7+// TestShortcutRoutingTable verifies the routing table configuration
8+func TestShortcutRoutingTable(t *testing.T) {
9+ tests := []struct {
10+ shortcut string
11+ expectEntry bool
12+ forwardToJS bool
13+ blockWebKit bool
14+ description string
15+ }{
16+ {
17+ shortcut: "alt+arrowup",
18+ expectEntry: true,
19+ forwardToJS: false,
20+ blockWebKit: true,
21+ description: "Navigate to upper pane (callback handler)",
22+ },
23+ {
24+ shortcut: "alt+arrowdown",
25+ expectEntry: true,
26+ forwardToJS: false,
27+ blockWebKit: true,
28+ description: "Navigate to lower pane (callback handler)",
29+ },
30+ {
31+ shortcut: "alt+arrowleft",
32+ expectEntry: true,
33+ forwardToJS: false,
34+ blockWebKit: true,
35+ description: "Navigate to left pane (callback handler)",
36+ },
37+ {
38+ shortcut: "alt+arrowright",
39+ expectEntry: true,
40+ forwardToJS: false,
41+ blockWebKit: true,
42+ description: "Navigate to right pane (callback handler)",
43+ },
44+ {
45+ shortcut: "cmdorctrl+p",
46+ expectEntry: true,
47+ forwardToJS: true,
48+ blockWebKit: true,
49+ description: "Enter pane mode (JS handler + block print dialog)",
50+ },
51+ {
52+ shortcut: "cmdorctrl+l",
53+ expectEntry: false, // Not in table, should use default
54+ forwardToJS: true,
55+ blockWebKit: false,
56+ description: "Default routing",
57+ },
58+ }
59+
60+ for _, tt := range tests {
61+ t.Run(tt.shortcut, func(t *testing.T) {
62+ routing, exists := shortcutRoutingTable[tt.shortcut]
63+
64+ if tt.expectEntry {
65+ if !exists {
66+ t.Errorf("Expected routing entry for %s but not found", tt.shortcut)
67+ return
68+ }
69+
70+ if routing.forwardToJS != tt.forwardToJS {
71+ t.Errorf("forwardToJS: got %v, want %v", routing.forwardToJS, tt.forwardToJS)
72+ }
73+
74+ if routing.blockWebKit != tt.blockWebKit {
75+ t.Errorf("blockWebKit: got %v, want %v", routing.blockWebKit, tt.blockWebKit)
76+ }
77+
78+ if routing.description != tt.description {
79+ t.Errorf("description: got %q, want %q", routing.description, tt.description)
80+ }
81+ } else {
82+ if exists {
83+ t.Errorf("Expected no routing entry for %s but found one", tt.shortcut)
84+ }
85+ }
86+ })
87+ }
88+}
89+
90+// TestDefaultRouting verifies the default routing behavior
91+func TestDefaultRouting(t *testing.T) {
92+ if defaultRouting.forwardToJS != true {
93+ t.Errorf("defaultRouting.forwardToJS: got %v, want true", defaultRouting.forwardToJS)
94+ }
95+
96+ if defaultRouting.blockWebKit != false {
97+ t.Errorf("defaultRouting.blockWebKit: got %v, want false", defaultRouting.blockWebKit)
98+ }
99+}
100+
101+// TestWorkspaceNavigationShortcuts verifies workspace navigation shortcuts block WebKit
102+func TestWorkspaceNavigationShortcuts(t *testing.T) {
103+ workspaceShortcuts := []string{
104+ "alt+arrowup",
105+ "alt+arrowdown",
106+ "alt+arrowleft",
107+ "alt+arrowright",
108+ }
109+
110+ for _, shortcut := range workspaceShortcuts {
111+ t.Run(shortcut, func(t *testing.T) {
112+ routing, exists := shortcutRoutingTable[shortcut]
113+
114+ if !exists {
115+ t.Fatalf("Workspace navigation shortcut %s not found in routing table", shortcut)
116+ }
117+
118+ // These shortcuts must block WebKit to prevent page scrolling
119+ if !routing.blockWebKit {
120+ t.Errorf("Workspace navigation shortcut %s must block WebKit (got blockWebKit=%v)",
121+ shortcut, routing.blockWebKit)
122+ }
123+
124+ // These shortcuts should NOT forward to JS (handled by callback)
125+ if routing.forwardToJS {
126+ t.Errorf("Workspace navigation shortcut %s should not forward to JS (got forwardToJS=%v)",
127+ shortcut, routing.forwardToJS)
128+ }
129+ })
130+ }
131+}
132+
133+// TestConvertToGtkFormat tests the key format conversion
134+func TestConvertToGtkFormat(t *testing.T) {
135+ tests := []struct {
136+ input string
137+ expected string
138+ }{
139+ {"ctrl+l", "<Control>l"},
140+ {"ctrl+plus", "<Control>plus"},
141+ {"ctrl+shift+c", "<Control><Shift>c"},
142+ {"alt+arrowleft", "<Alt>Left"},
143+ {"alt+arrowright", "<Alt>Right"},
144+ {"alt+arrowup", "<Alt>Up"},
145+ {"alt+arrowdown", "<Alt>Down"},
146+ {"cmdorctrl+p", "<Control>p"},
147+ {"F12", "F12"},
148+ {"ctrl+0", "<Control>0"},
149+ }
150+
151+ for _, tt := range tests {
152+ t.Run(tt.input, func(t *testing.T) {
153+ result := convertToGtkFormat(tt.input)
154+ if result != tt.expected {
155+ t.Errorf("convertToGtkFormat(%q) = %q, want %q", tt.input, result, tt.expected)
156+ }
157+ })
158+ }
159+}
160+
161+// TestKeyNameConstants verifies key name constants are properly defined
162+func TestKeyNameConstants(t *testing.T) {
163+ expectedKeys := map[string]string{
164+ "keyP": "p",
165+ "keyL": "l",
166+ "keyF": "f",
167+ "keyC": "c",
168+ "keyW": "w",
169+ "keyR": "r",
170+ "keyArrowLeft": "arrowleft",
171+ "keyArrowRight": "arrowright",
172+ "keyArrowUp": "arrowup",
173+ "keyArrowDown": "arrowdown",
174+ "keyF12": "F12",
175+ "keyPlus": "plus",
176+ "keyMinus": "minus",
177+ "keyZero": "0",
178+ }
179+
180+ // Verify constants exist and have expected values
181+ keys := map[string]string{
182+ "keyP": keyP,
183+ "keyL": keyL,
184+ "keyF": keyF,
185+ "keyC": keyC,
186+ "keyW": keyW,
187+ "keyR": keyR,
188+ "keyArrowLeft": keyArrowLeft,
189+ "keyArrowRight": keyArrowRight,
190+ "keyArrowUp": keyArrowUp,
191+ "keyArrowDown": keyArrowDown,
192+ "keyF12": keyF12,
193+ "keyPlus": keyPlus,
194+ "keyMinus": keyMinus,
195+ "keyZero": keyZero,
196+ }
197+
198+ for name, expected := range expectedKeys {
199+ if actual, ok := keys[name]; !ok {
200+ t.Errorf("Constant %s not found", name)
201+ } else if actual != expected {
202+ t.Errorf("Constant %s: got %q, want %q", name, actual, expected)
203+ }
204+ }
205+}

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