v0.27.1 c78ce83

workspace_view_test.go RawHistoryFolder

1package component_test
2
3import (
4	"context"
5	"testing"
6
7	"github.com/jwijenbergh/puregotk/v4/gtk"
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/mock"
10	"github.com/stretchr/testify/require"
11
12	"github.com/bnema/dumber/internal/domain/entity"
13	"github.com/bnema/dumber/internal/ui/component"
14	"github.com/bnema/dumber/internal/ui/layout"
15	"github.com/bnema/dumber/internal/ui/layout/mocks"
16)
17
18func setupLoadingSkeletonMocksWorkspace(
19	t *testing.T,
20	mockFactory *mocks.MockWidgetFactory,
21	mockOverlay *mocks.MockOverlayWidget,
22	mockLoadingContainer *mocks.MockBoxWidget,
23	mockLoadingContent *mocks.MockBoxWidget,
24	mockLoadingSpinner *mocks.MockSpinnerWidget,
25	mockLoadingLogo *mocks.MockImageWidget,
26) {
27	mockLoadingVersion := mocks.NewMockLabelWidget(t)
28
29	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockLoadingContainer).Once()
30	mockLoadingContainer.EXPECT().SetHexpand(true).Maybe()
31	mockLoadingContainer.EXPECT().SetVexpand(true).Maybe()
32	mockLoadingContainer.EXPECT().SetHalign(mock.Anything).Maybe()
33	mockLoadingContainer.EXPECT().SetValign(mock.Anything).Maybe()
34	mockLoadingContainer.EXPECT().SetCanFocus(false).Maybe()
35	mockLoadingContainer.EXPECT().SetCanTarget(false).Maybe()
36	mockLoadingContainer.EXPECT().AddCssClass("loading-skeleton").Maybe()
37	mockLoadingContainer.EXPECT().SetVisible(true).Maybe()
38
39	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 6).Return(mockLoadingContent).Once()
40	mockLoadingContent.EXPECT().SetHalign(mock.Anything).Maybe()
41	mockLoadingContent.EXPECT().SetValign(mock.Anything).Maybe()
42	mockLoadingContent.EXPECT().SetCanFocus(false).Maybe()
43	mockLoadingContent.EXPECT().SetCanTarget(false).Maybe()
44	mockLoadingContent.EXPECT().AddCssClass("loading-skeleton-content").Maybe()
45
46	mockFactory.EXPECT().NewImage().Return(mockLoadingLogo).Once()
47	mockLoadingLogo.EXPECT().SetHalign(mock.Anything).Maybe()
48	mockLoadingLogo.EXPECT().SetValign(mock.Anything).Maybe()
49	mockLoadingLogo.EXPECT().SetCanFocus(false).Maybe()
50	mockLoadingLogo.EXPECT().SetCanTarget(false).Maybe()
51	mockLoadingLogo.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Maybe()
52	mockLoadingLogo.EXPECT().SetPixelSize(mock.Anything).Maybe()
53	mockLoadingLogo.EXPECT().AddCssClass("loading-skeleton-logo").Maybe()
54	mockLoadingLogo.EXPECT().SetFromPaintable(mock.Anything).Maybe()
55
56	mockFactory.EXPECT().NewSpinner().Return(mockLoadingSpinner).Once()
57	mockLoadingSpinner.EXPECT().SetHalign(mock.Anything).Maybe()
58	mockLoadingSpinner.EXPECT().SetValign(mock.Anything).Maybe()
59	mockLoadingSpinner.EXPECT().SetCanFocus(false).Maybe()
60	mockLoadingSpinner.EXPECT().SetCanTarget(false).Maybe()
61	mockLoadingSpinner.EXPECT().SetSizeRequest(mock.Anything, mock.Anything).Maybe()
62	mockLoadingSpinner.EXPECT().AddCssClass("loading-skeleton-spinner").Maybe()
63	mockLoadingSpinner.EXPECT().Start().Maybe()
64
65	mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLoadingVersion).Once()
66	mockLoadingVersion.EXPECT().SetHalign(mock.Anything).Maybe()
67	mockLoadingVersion.EXPECT().SetValign(mock.Anything).Maybe()
68	mockLoadingVersion.EXPECT().SetCanFocus(false).Maybe()
69	mockLoadingVersion.EXPECT().SetCanTarget(false).Maybe()
70	mockLoadingVersion.EXPECT().SetMaxWidthChars(mock.Anything).Maybe()
71	mockLoadingVersion.EXPECT().SetEllipsize(mock.Anything).Maybe()
72	mockLoadingVersion.EXPECT().AddCssClass("loading-skeleton-version").Maybe()
73
74	mockLoadingContent.EXPECT().Append(mockLoadingLogo).Maybe()
75	mockLoadingContent.EXPECT().Append(mockLoadingSpinner).Maybe()
76	mockLoadingContent.EXPECT().Append(mockLoadingVersion).Maybe()
77	mockLoadingContainer.EXPECT().Append(mockLoadingContent).Maybe()
78
79	mockOverlay.EXPECT().AddOverlay(mockLoadingContainer).Once()
80	mockOverlay.EXPECT().SetClipOverlay(mockLoadingContainer, false).Once()
81	mockOverlay.EXPECT().SetMeasureOverlay(mockLoadingContainer, false).Once()
82}
83
84func setupWorkspaceViewBase(t *testing.T, mockFactory *mocks.MockWidgetFactory) (context.Context, *mocks.MockBoxWidget) {
85	ctx := context.Background()
86	mockContainer := mocks.NewMockBoxWidget(t)
87	mockOverlay := mocks.NewMockOverlayWidget(t)
88
89	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
90	mockContainer.EXPECT().SetHexpand(true).Once()
91	mockContainer.EXPECT().SetVexpand(true).Once()
92	mockContainer.EXPECT().SetVisible(true).Once()
93
94	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
95	mockOverlay.EXPECT().SetHexpand(true).Once()
96	mockOverlay.EXPECT().SetVexpand(true).Once()
97	mockOverlay.EXPECT().SetChild(mockContainer).Once()
98	mockOverlay.EXPECT().SetVisible(true).Once()
99
100	return ctx, mockContainer
101}
102
103func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory) (*mocks.MockOverlayWidget, *mocks.MockBoxWidget) {
104	mockOverlay := mocks.NewMockOverlayWidget(t)
105	mockBorderBox := mocks.NewMockBoxWidget(t)
106
107	mockLoadingContainer := mocks.NewMockBoxWidget(t)
108	mockLoadingContent := mocks.NewMockBoxWidget(t)
109	mockLoadingSpinner := mocks.NewMockSpinnerWidget(t)
110	mockLoadingLogo := mocks.NewMockImageWidget(t)
111
112	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
113	mockOverlay.EXPECT().SetHexpand(true).Once()
114	mockOverlay.EXPECT().SetVexpand(true).Once()
115	mockOverlay.EXPECT().SetVisible(true).Once()
116	mockOverlay.EXPECT().AddCssClass("pane-overlay").Once()
117
118	setupLoadingSkeletonMocksWorkspace(t, mockFactory, mockOverlay, mockLoadingContainer, mockLoadingContent, mockLoadingSpinner, mockLoadingLogo)
119
120	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
121	mockBorderBox.EXPECT().SetCanFocus(false).Once()
122	mockBorderBox.EXPECT().SetCanTarget(false).Once()
123	mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
124	mockBorderBox.EXPECT().SetHexpand(true).Once()
125	mockBorderBox.EXPECT().SetVexpand(true).Once()
126	mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
127	mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
128	mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
129
130	// GtkWidget is called when attaching hover handler - return nil for tests
131	mockOverlay.EXPECT().GtkWidget().Return(nil).Once()
132
133	return mockOverlay, mockBorderBox
134}
135
136func setupStackedLeafMocks(
137	t *testing.T,
138	mockFactory *mocks.MockWidgetFactory,
139	container *mocks.MockOverlayWidget,
140) *mocks.MockBoxWidget {
141	mockStackBox := mocks.NewMockBoxWidget(t)
142	mockTitleBar := mocks.NewMockBoxWidget(t)
143	mockFavicon := mocks.NewMockImageWidget(t)
144	mockLabel := mocks.NewMockLabelWidget(t)
145	mockCloseButton := mocks.NewMockButtonWidget(t)
146
147	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockStackBox).Once()
148	mockStackBox.EXPECT().SetHexpand(true).Once()
149	mockStackBox.EXPECT().SetVexpand(true).Once()
150
151	// Title bar creation - now directly used without button wrapper
152	mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
153	mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
154	mockTitleBar.EXPECT().AddCssClass("stacked-pane-title-clickable").Once()
155	mockTitleBar.EXPECT().SetVexpand(false).Once()
156	mockTitleBar.EXPECT().SetHexpand(true).Once()
157
158	mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
159	mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
160	mockFavicon.EXPECT().SetPixelSize(16).Once()
161	mockTitleBar.EXPECT().Append(mockFavicon).Once()
162
163	mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once()
164	mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once()
165	mockLabel.EXPECT().SetMaxWidthChars(30).Once()
166	mockLabel.EXPECT().SetHexpand(true).Once()
167	mockLabel.EXPECT().SetXalign(float32(0.0)).Once()
168	mockTitleBar.EXPECT().Append(mockLabel).Once()
169
170	// Close button (uses SetIconName directly instead of child image)
171	mockFactory.EXPECT().NewButton().Return(mockCloseButton).Once()
172	mockCloseButton.EXPECT().SetIconName("window-close-symbolic").Once()
173	mockCloseButton.EXPECT().AddCssClass("stacked-pane-close-button").Once()
174	mockCloseButton.EXPECT().SetFocusOnClick(false).Once()
175	mockCloseButton.EXPECT().SetVexpand(false).Once()
176	mockCloseButton.EXPECT().SetHexpand(false).Once()
177	mockTitleBar.EXPECT().Append(mockCloseButton).Once()
178
179	// GestureClick is added to titleBar via AddController
180	mockTitleBar.EXPECT().AddController(mock.Anything).Once()
181
182	// Close button click handler
183	mockCloseButton.EXPECT().ConnectClicked(mock.Anything).Return(uint(2)).Once()
184
185	// Signal disconnection calls GtkWidget() - return nil to skip actual GTK operations in tests
186	mockCloseButton.EXPECT().GtkWidget().Return(nil).Maybe()
187
188	// Adding to main box - now titleBar is added directly (no button wrapper)
189	mockStackBox.EXPECT().Append(mockTitleBar).Once()
190	mockStackBox.EXPECT().Append(container).Once()
191
192	// Visibility updates for active pane - titleBar is now directly in box, SetVisible called on it
193	mockTitleBar.EXPECT().SetVisible(false).Once() // Active pane hides its title bar
194	container.EXPECT().SetVisible(true).Once()
195	mockTitleBar.EXPECT().AddCssClass("active").Once()
196
197	return mockStackBox
198}
199
200func TestNewWorkspaceView_CreatesEmptyContainer(t *testing.T) {
201	// Arrange
202	mockFactory := mocks.NewMockWidgetFactory(t)
203	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
204
205	// Act
206	wv := component.NewWorkspaceView(ctx, mockFactory)
207
208	// Assert
209	require.NotNil(t, wv)
210	assert.Equal(t, 0, wv.PaneCount())
211	assert.Equal(t, mockBox, wv.Container())
212}
213
214func TestSetWorkspace_NilWorkspace_ReturnsError(t *testing.T) {
215	// Arrange
216	mockFactory := mocks.NewMockWidgetFactory(t)
217	ctx, _ := setupWorkspaceViewBase(t, mockFactory)
218
219	wv := component.NewWorkspaceView(ctx, mockFactory)
220
221	// Act
222	err := wv.SetWorkspace(ctx, nil)
223
224	// Assert
225	assert.ErrorIs(t, err, component.ErrNilWorkspace)
226}
227
228func TestSetWorkspace_SinglePane_CreatesPaneView(t *testing.T) {
229	// Arrange
230	mockFactory := mocks.NewMockWidgetFactory(t)
231	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
232
233	wv := component.NewWorkspaceView(ctx, mockFactory)
234
235	mockOverlay, mockBorderBox := setupWorkspacePaneViewMocks(t, mockFactory)
236	mockStackBox := setupStackedLeafMocks(t, mockFactory, mockOverlay)
237
238	mockStackBox.EXPECT().SetVisible(true).Once()
239	mockBox.EXPECT().Append(mockStackBox).Once()
240	mockBox.EXPECT().AddCssClass("single-pane").Once()
241	mockBorderBox.EXPECT().AddCssClass("pane-active").Once()
242
243	pane := entity.NewPane(entity.PaneID("pane-1"))
244	node := &entity.PaneNode{ID: "node-1", Pane: pane}
245
246	ws := &entity.Workspace{
247		ID:           "ws-1",
248		Root:         node,
249		ActivePaneID: pane.ID,
250	}
251
252	// Act
253	err := wv.SetWorkspace(ctx, ws)
254
255	// Assert
256	require.NoError(t, err)
257	assert.Equal(t, 1, wv.PaneCount())
258	assert.Equal(t, pane.ID, wv.GetActivePaneID())
259	assert.NotNil(t, wv.GetPaneView(pane.ID))
260}
261
262func TestWorkspaceView_HoverFocusLockState(t *testing.T) {
263	// Intentional zero-value check: hoverFocusLock is an atomic.Bool and should
264	// be safe with its language-level zero value before constructor wiring.
265	var wv component.WorkspaceView
266
267	assert.False(t, wv.IsHoverFocusLocked())
268	wv.SetHoverFocusLocked(true)
269	assert.True(t, wv.IsHoverFocusLocked())
270
271	wv.SetHoverFocusLocked(false)
272	assert.False(t, wv.IsHoverFocusLocked())
273}
274
275func TestAddWorkspaceOverlayWidget_DoesNotReaddWhenAlreadyAttached(t *testing.T) {
276	mockFactory := mocks.NewMockWidgetFactory(t)
277	ctx := context.Background()
278
279	mockContainer := mocks.NewMockBoxWidget(t)
280	mockOverlay := mocks.NewMockOverlayWidget(t)
281	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
282	mockContainer.EXPECT().SetHexpand(true).Once()
283	mockContainer.EXPECT().SetVexpand(true).Once()
284	mockContainer.EXPECT().SetVisible(true).Once()
285	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
286	mockOverlay.EXPECT().SetHexpand(true).Once()
287	mockOverlay.EXPECT().SetVexpand(true).Once()
288	mockOverlay.EXPECT().SetChild(mockContainer).Once()
289	mockOverlay.EXPECT().SetVisible(true).Once()
290
291	wv := component.NewWorkspaceView(ctx, mockFactory)
292
293	widget := mocks.NewMockWidget(t)
294	widget.EXPECT().GetParent().Return(mockOverlay).Once()
295
296	wv.AddWorkspaceOverlayWidget(widget)
297}
298
299func TestAddWorkspaceOverlayWidget_UnparentsBeforeAdding(t *testing.T) {
300	mockFactory := mocks.NewMockWidgetFactory(t)
301	ctx := context.Background()
302
303	mockContainer := mocks.NewMockBoxWidget(t)
304	mockOverlay := mocks.NewMockOverlayWidget(t)
305	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
306	mockContainer.EXPECT().SetHexpand(true).Once()
307	mockContainer.EXPECT().SetVexpand(true).Once()
308	mockContainer.EXPECT().SetVisible(true).Once()
309	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
310	mockOverlay.EXPECT().SetHexpand(true).Once()
311	mockOverlay.EXPECT().SetVexpand(true).Once()
312	mockOverlay.EXPECT().SetChild(mockContainer).Once()
313	mockOverlay.EXPECT().SetVisible(true).Once()
314
315	wv := component.NewWorkspaceView(ctx, mockFactory)
316
317	widget := mocks.NewMockWidget(t)
318	otherParent := mocks.NewMockWidget(t)
319	widget.EXPECT().GetParent().Return(otherParent).Once()
320	otherParent.EXPECT().GtkWidget().Return(nil).Once()
321	mockOverlay.EXPECT().GtkWidget().Return(nil).Once()
322	widget.EXPECT().Unparent().Once()
323	mockOverlay.EXPECT().AddOverlay(widget).Once()
324	mockOverlay.EXPECT().SetClipOverlay(widget, false).Once()
325	mockOverlay.EXPECT().SetMeasureOverlay(widget, false).Once()
326
327	wv.AddWorkspaceOverlayWidget(widget)
328}
329
330func TestAddWorkspaceOverlayWidget_DoesNotReaddWhenParentGTKMatchesOverlay(t *testing.T) {
331	mockFactory := mocks.NewMockWidgetFactory(t)
332	ctx := context.Background()
333
334	mockContainer := mocks.NewMockBoxWidget(t)
335	mockOverlay := mocks.NewMockOverlayWidget(t)
336	mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
337	mockContainer.EXPECT().SetHexpand(true).Once()
338	mockContainer.EXPECT().SetVexpand(true).Once()
339	mockContainer.EXPECT().SetVisible(true).Once()
340	mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
341	mockOverlay.EXPECT().SetHexpand(true).Once()
342	mockOverlay.EXPECT().SetVexpand(true).Once()
343	mockOverlay.EXPECT().SetChild(mockContainer).Once()
344	mockOverlay.EXPECT().SetVisible(true).Once()
345
346	wv := component.NewWorkspaceView(ctx, mockFactory)
347
348	widget := mocks.NewMockWidget(t)
349	otherParent := mocks.NewMockWidget(t)
350	gtkWidget := &gtk.Widget{}
351
352	widget.EXPECT().GetParent().Return(otherParent).Once()
353	otherParent.EXPECT().GtkWidget().Return(gtkWidget).Once()
354	mockOverlay.EXPECT().GtkWidget().Return(gtkWidget).Once()
355
356	wv.AddWorkspaceOverlayWidget(widget)
357}
358
359func TestSetActivePaneID_UpdatesStyling(t *testing.T) {
360	// Arrange
361	mockFactory := mocks.NewMockWidgetFactory(t)
362	ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
363
364	wv := component.NewWorkspaceView(ctx, mockFactory)
365
366	// Setup two panes
367	pane1 := entity.NewPane(entity.PaneID("pane-1"))
368	pane2 := entity.NewPane(entity.PaneID("pane-2"))
369	node1 := &entity.PaneNode{ID: "node-1", Pane: pane1}
370	node2 := &entity.PaneNode{ID: "node-2", Pane: pane2}
371
372	// Create mock widgets for split
373	mockPaned := mocks.NewMockPanedWidget(t)
374	mockOverlay1, mockBorderBox1 := setupWorkspacePaneViewMocks(t, mockFactory)
375	mockStackBox1 := setupStackedLeafMocks(t, mockFactory, mockOverlay1)
376	mockOverlay2, mockBorderBox2 := setupWorkspacePaneViewMocks(t, mockFactory)
377	mockStackBox2 := setupStackedLeafMocks(t, mockFactory, mockOverlay2)
378
379	// Split view creation
380	mockFactory.EXPECT().NewPaned(layout.OrientationHorizontal).Return(mockPaned).Once()
381	mockPaned.EXPECT().SetResizeStartChild(true).Once()
382	mockPaned.EXPECT().SetResizeEndChild(true).Once()
383	mockPaned.EXPECT().SetVisible(true).Twice()
384	mockStackBox1.EXPECT().SetVisible(true).Once()
385	mockStackBox2.EXPECT().SetVisible(true).Once()
386	mockPaned.EXPECT().SetStartChild(mockStackBox1).Once()
387	mockPaned.EXPECT().SetEndChild(mockStackBox2).Once()
388	mockPaned.EXPECT().ConnectNotifyPosition(mock.Anything).Return(uint(0)).Once()
389	mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once()
390	mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint(0)).Once()
391	mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once()
392
393	mockBox.EXPECT().Append(mockPaned).Once()
394
395	// Initial active pane is pane-1
396	mockBox.EXPECT().RemoveCssClass("single-pane").Once()
397	mockBorderBox1.EXPECT().AddCssClass("pane-active").Once()
398
399	splitNode := &entity.PaneNode{
400		ID:         "split",
401		SplitDir:   entity.SplitHorizontal,
402		SplitRatio: 0.5,
403		Children:   []*entity.PaneNode{node1, node2},
404	}
405
406	ws := &entity.Workspace{
407		ID:           "ws-1",
408		Root:         splitNode,
409		ActivePaneID: pane1.ID,
410	}
411
412	err := wv.SetWorkspace(ctx, ws)
413	require.NoError(t, err)
414
415	// Now change active pane
416	mockBorderBox1.EXPECT().RemoveCssClass("pane-active").Once()
417	mockBorderBox2.EXPECT().AddCssClass("pane-active").Once()
418
419	// Act
420	err = wv.SetActivePaneID(pane2.ID)
421
422	// Assert
423	require.NoError(t, err)
424	assert.Equal(t, pane2.ID, wv.GetActivePaneID())
425}
426
427func TestSetActivePaneID_InvalidID_ReturnsError(t *testing.T) {
428	// Arrange
429	mockFactory := mocks.NewMockWidgetFactory(t)
430	ctx, _ := setupWorkspaceViewBase(t, mockFactory)
431
432	wv := component.NewWorkspaceView(ctx, mockFactory)
433
434	// Act
435	err := wv.SetActivePaneID(entity.PaneID("non-existent"))
436
437	// Assert
438	assert.ErrorIs(t, err, component.ErrPaneNotFound)
439}

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