main 5041353

clipboard_text_orchestrator_test.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5	"errors"
6	"sync"
7	"testing"
8	"time"
9
10	"github.com/bnema/dumber/internal/application/dto"
11	portmocks "github.com/bnema/dumber/internal/application/port/mocks"
12	"github.com/bnema/dumber/internal/domain/entity"
13)
14
15type blockingClipboard struct {
16	started chan struct{}
17	release chan struct{}
18	mu      sync.Mutex
19	writes  []string
20}
21
22type countingClipboard struct {
23	mu     sync.Mutex
24	writes int
25}
26
27type staticAutoCopyConfig struct {
28	enabled bool
29}
30
31func (s *staticAutoCopyConfig) IsAutoCopyEnabled() bool { return s.enabled }
32
33func newBlockingClipboard() *blockingClipboard {
34	return &blockingClipboard{
35		started: make(chan struct{}, 2),
36		release: make(chan struct{}),
37	}
38}
39
40func (b *blockingClipboard) WriteText(_ context.Context, text string) error {
41	b.mu.Lock()
42	b.writes = append(b.writes, text)
43	b.mu.Unlock()
44	b.started <- struct{}{}
45	<-b.release
46	return nil
47}
48
49func (*blockingClipboard) WriteImage(context.Context, entity.ImageData) error { return nil }
50func (*blockingClipboard) ReadText(context.Context) (string, error)           { return "", nil }
51func (*blockingClipboard) Clear(context.Context) error                        { return nil }
52func (*blockingClipboard) HasText(context.Context) (bool, error)              { return false, nil }
53
54func (b *blockingClipboard) writeCount() int {
55	b.mu.Lock()
56	defer b.mu.Unlock()
57	return len(b.writes)
58}
59
60func (c *countingClipboard) WriteText(_ context.Context, _ string) error {
61	c.mu.Lock()
62	c.writes++
63	c.mu.Unlock()
64	return nil
65}
66
67func (*countingClipboard) WriteImage(context.Context, entity.ImageData) error { return nil }
68func (*countingClipboard) ReadText(context.Context) (string, error)           { return "", nil }
69func (*countingClipboard) Clear(context.Context) error                        { return nil }
70func (*countingClipboard) HasText(context.Context) (bool, error)              { return false, nil }
71
72func (c *countingClipboard) writeCount() int {
73	c.mu.Lock()
74	defer c.mu.Unlock()
75	return c.writes
76}
77
78func TestClipboardTextOrchestrator_HandleSelectionUpdate_AutoCopyDisabledDoesNothing(t *testing.T) {
79	ctx := context.Background()
80	clipboard := portmocks.NewMockClipboard(t)
81	autoCopyConfig := &staticAutoCopyConfig{enabled: false}
82	var toastCalls []int
83
84	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, func(textLen int) {
85		toastCalls = append(toastCalls, textLen)
86	})
87
88	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "hello", SourceEngine: dto.ClipboardSourceCEF}); err != nil {
89		t.Fatalf("HandleSelectionUpdate() error = %v, want nil", err)
90	}
91
92	if len(toastCalls) != 0 {
93		t.Fatalf("toast calls = %v, want none", toastCalls)
94	}
95}
96
97func TestClipboardTextOrchestrator_HandleSelectionUpdate_WritesOncePerUniqueSelectionAndResetsAfterEmptySelection(t *testing.T) {
98	ctx := context.Background()
99	clipboard := portmocks.NewMockClipboard(t)
100	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
101	var toastCalls []int
102
103	clipboard.EXPECT().WriteText(ctx, "éé").Return(nil).Times(2)
104
105	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, func(textLen int) {
106		toastCalls = append(toastCalls, textLen)
107	})
108
109	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "é", SourceEngine: dto.ClipboardSourceCEF}); err != nil {
110		t.Fatalf("HandleSelectionUpdate(short) error = %v, want nil", err)
111	}
112	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}); err != nil {
113		t.Fatalf("HandleSelectionUpdate(first) error = %v, want nil", err)
114	}
115	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}); err != nil {
116		t.Fatalf("HandleSelectionUpdate(duplicate) error = %v, want nil", err)
117	}
118	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "", SourceEngine: dto.ClipboardSourceCEF}); err != nil {
119		t.Fatalf("HandleSelectionUpdate(reset) error = %v, want nil", err)
120	}
121	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF}); err != nil {
122		t.Fatalf("HandleSelectionUpdate(after reset) error = %v, want nil", err)
123	}
124
125	if len(toastCalls) != 2 || toastCalls[0] != 2 || toastCalls[1] != 2 {
126		t.Fatalf("toast calls = %v, want [2 2]", toastCalls)
127	}
128}
129
130func TestClipboardTextOrchestrator_HandleSelectionUpdate_DoesNotDedupAcrossEngines(t *testing.T) {
131	ctx := context.Background()
132	clipboard := portmocks.NewMockClipboard(t)
133	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
134
135	clipboard.EXPECT().WriteText(ctx, "shared").Return(nil).Times(2)
136
137	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil)
138
139	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceWebKit}); err != nil {
140		t.Fatalf("HandleSelectionUpdate(webkit) error = %v, want nil", err)
141	}
142	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF}); err != nil {
143		t.Fatalf("HandleSelectionUpdate(cef) error = %v, want nil", err)
144	}
145}
146
147func TestClipboardTextOrchestrator_HandleSelectionUpdate_DoesNotDedupAcrossViews(t *testing.T) {
148	ctx := context.Background()
149	clipboard := portmocks.NewMockClipboard(t)
150	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
151
152	clipboard.EXPECT().WriteText(ctx, "shared").Return(nil).Times(2)
153
154	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil)
155
156	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil {
157		t.Fatalf("HandleSelectionUpdate(view 1) error = %v, want nil", err)
158	}
159	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 2}); err != nil {
160		t.Fatalf("HandleSelectionUpdate(view 2) error = %v, want nil", err)
161	}
162}
163
164func TestClipboardTextOrchestrator_HandleExplicitCopy_DeduplicatesIdenticalPayloadsWithinWindow(t *testing.T) {
165	ctx := context.Background()
166	clipboard := portmocks.NewMockClipboard(t)
167	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
168	var toastCalls []int
169
170	clipboard.EXPECT().WriteText(ctx, "éé").Return(nil).Times(2)
171
172	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, func(textLen int) {
173		toastCalls = append(toastCalls, textLen)
174	})
175
176	now := time.Unix(0, 0)
177	uc.now = func() time.Time { return now }
178
179	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil {
180		t.Fatalf("HandleExplicitCopy(first) error = %v, want nil", err)
181	}
182	now = now.Add(100 * time.Millisecond)
183	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil {
184		t.Fatalf("HandleExplicitCopy(duplicate) error = %v, want nil", err)
185	}
186	now = now.Add(151 * time.Millisecond)
187	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil {
188		t.Fatalf("HandleExplicitCopy(after window) error = %v, want nil", err)
189	}
190
191	if len(toastCalls) != 2 || toastCalls[0] != 2 || toastCalls[1] != 2 {
192		t.Fatalf("toast calls = %v, want [2 2]", toastCalls)
193	}
194}
195
196func TestClipboardTextOrchestrator_HandleExplicitCopy_NativeHandledSkipsBackendWrite(t *testing.T) {
197	ctx := context.Background()
198	clipboard := &countingClipboard{}
199	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
200	var toastCalls []int
201
202	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, func(textLen int) {
203		toastCalls = append(toastCalls, textLen)
204	})
205
206	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy", NativeHandled: true}); err != nil {
207		t.Fatalf("HandleExplicitCopy() error = %v, want nil", err)
208	}
209
210	if got := clipboard.writeCount(); got != 0 {
211		t.Fatalf("clipboard writes = %d, want 0", got)
212	}
213	if len(toastCalls) != 1 || toastCalls[0] != 6 {
214		t.Fatalf("toast calls = %v, want [6]", toastCalls)
215	}
216	if got := len(uc.lastExplicit); got != 1 {
217		t.Fatalf("lastExplicit entries = %d, want 1", got)
218	}
219}
220
221func TestClipboardTextOrchestrator_HandleExplicitCopy_DoesNotDedupDifferentActionsWithinWindow(t *testing.T) {
222	ctx := context.Background()
223	clipboard := portmocks.NewMockClipboard(t)
224	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
225
226	clipboard.EXPECT().WriteText(ctx, "shared").Return(nil).Times(2)
227
228	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil)
229	now := time.Unix(0, 0)
230	uc.now = func() time.Time { return now }
231
232	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil {
233		t.Fatalf("HandleExplicitCopy(copy) error = %v, want nil", err)
234	}
235	now = now.Add(100 * time.Millisecond)
236	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "cut"}); err != nil {
237		t.Fatalf("HandleExplicitCopy(cut) error = %v, want nil", err)
238	}
239}
240
241func TestClipboardTextOrchestrator_HandleExplicitCopy_DeduplicatesRecentAutoCopySelection(t *testing.T) {
242	ctx := context.Background()
243	clipboard := portmocks.NewMockClipboard(t)
244	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
245	var toastCalls []int
246
247	clipboard.EXPECT().WriteText(ctx, "shared").Return(nil).Once()
248
249	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, func(textLen int) {
250		toastCalls = append(toastCalls, textLen)
251	})
252
253	now := time.Unix(0, 0)
254	uc.now = func() time.Time { return now }
255
256	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil {
257		t.Fatalf("HandleSelectionUpdate() error = %v, want nil", err)
258	}
259	now = now.Add(100 * time.Millisecond)
260	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil {
261		t.Fatalf("HandleExplicitCopy() error = %v, want nil", err)
262	}
263
264	if len(toastCalls) != 1 || toastCalls[0] != 6 {
265		t.Fatalf("toast calls = %v, want [6]", toastCalls)
266	}
267	if got := len(uc.lastExplicit); got != 1 {
268		t.Fatalf("lastExplicit entries = %d, want 1", got)
269	}
270}
271
272func TestClipboardTextOrchestrator_HandleExplicitCopy_DoesNotDedupAcrossEngines(t *testing.T) {
273	ctx := context.Background()
274	clipboard := portmocks.NewMockClipboard(t)
275	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
276
277	clipboard.EXPECT().WriteText(ctx, "shared").Return(nil).Times(2)
278
279	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil)
280	uc.now = func() time.Time { return time.Unix(0, 0) }
281
282	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"}); err != nil {
283		t.Fatalf("HandleExplicitCopy(webkit) error = %v, want nil", err)
284	}
285	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, Action: "copy"}); err != nil {
286		t.Fatalf("HandleExplicitCopy(cef) error = %v, want nil", err)
287	}
288}
289
290func TestClipboardTextOrchestrator_HandleExplicitCopy_DoesNotDedupAcrossViews(t *testing.T) {
291	ctx := context.Background()
292	clipboard := portmocks.NewMockClipboard(t)
293	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
294
295	clipboard.EXPECT().WriteText(ctx, "shared").Return(nil).Times(2)
296
297	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil)
298	uc.now = func() time.Time { return time.Unix(0, 0) }
299
300	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1, Action: "copy"}); err != nil {
301		t.Fatalf("HandleExplicitCopy(view 1) error = %v, want nil", err)
302	}
303	if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "shared", SourceEngine: dto.ClipboardSourceCEF, ViewID: 2, Action: "copy"}); err != nil {
304		t.Fatalf("HandleExplicitCopy(view 2) error = %v, want nil", err)
305	}
306}
307
308func TestClipboardTextOrchestrator_HandleExplicitCopy_KeepsBoundedStatePerScope(t *testing.T) {
309	ctx := context.Background()
310	clipboard := portmocks.NewMockClipboard(t)
311	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
312
313	clipboard.EXPECT().WriteText(ctx, "one").Return(nil).Once()
314	clipboard.EXPECT().WriteText(ctx, "two").Return(nil).Once()
315	clipboard.EXPECT().WriteText(ctx, "three").Return(nil).Once()
316
317	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil)
318
319	for i, text := range []string{"one", "two", "three"} {
320		advancedTime := time.Unix(0, 0).Add(time.Duration(i) * time.Second)
321		uc.now = func() time.Time { return advancedTime }
322		if err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: text, SourceEngine: dto.ClipboardSourceCEF, ViewID: 7, Action: "copy"}); err != nil {
323			t.Fatalf("HandleExplicitCopy(%q) error = %v, want nil", text, err)
324		}
325	}
326
327	if got := len(uc.lastExplicit); got != 1 {
328		t.Fatalf("lastExplicit entries = %d, want 1", got)
329	}
330}
331
332func TestClipboardTextOrchestrator_HandleSelectionUpdate_ReenableClearsDedupState(t *testing.T) {
333	ctx := context.Background()
334	clipboard := portmocks.NewMockClipboard(t)
335	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
336
337	clipboard.EXPECT().WriteText(ctx, "toggle").Return(nil).Times(2)
338
339	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, nil)
340
341	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "toggle", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil {
342		t.Fatalf("HandleSelectionUpdate(enabled) error = %v, want nil", err)
343	}
344	autoCopyConfig.enabled = false
345	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "toggle", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil {
346		t.Fatalf("HandleSelectionUpdate(disabled) error = %v, want nil", err)
347	}
348	autoCopyConfig.enabled = true
349	if err := uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "toggle", SourceEngine: dto.ClipboardSourceCEF, ViewID: 1}); err != nil {
350		t.Fatalf("HandleSelectionUpdate(reenabled) error = %v, want nil", err)
351	}
352}
353
354func TestClipboardTextOrchestrator_HandleExplicitCopy_ClipboardFailureDoesNotToast(t *testing.T) {
355	ctx := context.Background()
356	clipboard := portmocks.NewMockClipboard(t)
357	clipboard.EXPECT().WriteText(ctx, "éé").Return(errors.New("clipboard failed")).Once()
358	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
359
360	var toastCalls []int
361	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, func(textLen int) {
362		toastCalls = append(toastCalls, textLen)
363	})
364
365	err := uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF, Action: "copy"})
366	if err == nil {
367		t.Fatal("HandleExplicitCopy() error = nil, want non-nil")
368	}
369	if len(toastCalls) != 0 {
370		t.Fatalf("toast calls = %v, want none", toastCalls)
371	}
372}
373
374func TestClipboardTextOrchestrator_HandleSelectionUpdate_DedupIsAtomic(t *testing.T) {
375	ctx := context.Background()
376	clipboard := newBlockingClipboard()
377	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
378	var (
379		toastCalls []int
380		toastMu    sync.Mutex
381	)
382	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, func(textLen int) {
383		toastMu.Lock()
384		defer toastMu.Unlock()
385		toastCalls = append(toastCalls, textLen)
386	})
387
388	done1 := make(chan error, 1)
389	go func() {
390		done1 <- uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF})
391	}()
392	<-clipboard.started
393
394	done2 := make(chan error, 1)
395	go func() {
396		done2 <- uc.HandleSelectionUpdate(ctx, dto.SelectionClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceCEF})
397	}()
398
399	select {
400	case <-clipboard.started:
401		t.Fatal("second selection write started before first completed")
402	default:
403	}
404
405	close(clipboard.release)
406
407	if err := <-done1; err != nil {
408		t.Fatalf("first HandleSelectionUpdate() error = %v, want nil", err)
409	}
410	if err := <-done2; err != nil {
411		t.Fatalf("second HandleSelectionUpdate() error = %v, want nil", err)
412	}
413
414	if got := clipboard.writeCount(); got != 1 {
415		t.Fatalf("clipboard writes = %d, want 1", got)
416	}
417	toastMu.Lock()
418	defer toastMu.Unlock()
419	if len(toastCalls) != 1 || toastCalls[0] != 2 {
420		t.Fatalf("toast calls = %v, want [2]", toastCalls)
421	}
422}
423
424func TestClipboardTextOrchestrator_HandleExplicitCopy_DedupIsAtomic(t *testing.T) {
425	ctx := context.Background()
426	clipboard := newBlockingClipboard()
427	autoCopyConfig := &staticAutoCopyConfig{enabled: true}
428	var (
429		toastCalls []int
430		toastMu    sync.Mutex
431	)
432	uc := NewClipboardTextOrchestrator(clipboard, autoCopyConfig, func(textLen int) {
433		toastMu.Lock()
434		defer toastMu.Unlock()
435		toastCalls = append(toastCalls, textLen)
436	})
437	uc.now = func() time.Time { return time.Unix(0, 0) }
438
439	done1 := make(chan error, 1)
440	go func() {
441		done1 <- uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"})
442	}()
443	<-clipboard.started
444
445	done2 := make(chan error, 1)
446	go func() {
447		done2 <- uc.HandleExplicitCopy(ctx, dto.ExplicitClipboardInput{Text: "éé", SourceEngine: dto.ClipboardSourceWebKit, Action: "copy"})
448	}()
449
450	select {
451	case <-clipboard.started:
452		t.Fatal("second explicit write started before first completed")
453	default:
454	}
455
456	close(clipboard.release)
457
458	if err := <-done1; err != nil {
459		t.Fatalf("first HandleExplicitCopy() error = %v, want nil", err)
460	}
461	if err := <-done2; err != nil {
462		t.Fatalf("second HandleExplicitCopy() error = %v, want nil", err)
463	}
464
465	if got := clipboard.writeCount(); got != 1 {
466		t.Fatalf("clipboard writes = %d, want 1", got)
467	}
468	toastMu.Lock()
469	defer toastMu.Unlock()
470	if len(toastCalls) != 1 || toastCalls[0] != 2 {
471		t.Fatalf("toast calls = %v, want [2]", toastCalls)
472	}
473}

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