mode_frame_test.go RawHistoryFolder

1package ui
2
3import (
4	"testing"
5
6	"github.com/bnema/dumber/internal/domain/entity"
7	"github.com/bnema/dumber/internal/ui/input"
8	"github.com/bnema/dumber/internal/ui/layout"
9	"github.com/bnema/dumber/internal/ui/layout/mocks"
10	"github.com/stretchr/testify/require"
11)
12
13func TestModeFrameTargets(t *testing.T) {
14	f := newVimModeTestFixture(t)
15	tests := []struct {
16		mode input.Mode
17		want layout.Widget
18	}{
19		{input.ModeVim, f.pv1A.Widget()},
20		{input.ModePane, f.pv1A.Widget()},
21		{input.ModeResize, f.pv1A.Widget()},
22		{input.ModeTab, f.app.workspaceViews["tab-1"].Widget()},
23		{input.ModeSession, f.app.workspaceViews["tab-1"].Widget()},
24		{input.ModeNormal, nil},
25	}
26	for _, tt := range tests {
27		t.Run(tt.mode.String(), func(t *testing.T) {
28			require.Equal(t, tt.want, f.app.modeFrameTarget(f.bw1, tt.mode))
29		})
30	}
31	f.bw1.tabs.ActiveTabID = "tab-missing"
32	require.Nil(t, f.app.modeFrameTarget(f.bw1, input.ModePane))
33}
34
35// The mode border is drawn by the frame's own overlay, above web content.
36// Pane widgets are only geometry targets and must never receive mode classes:
37// an inset shadow on them is painted below the web view and is invisible.
38func TestModeFrameSetTargetStylesFrameBorderNotPane(t *testing.T) {
39	first := mocks.NewMockWidget(t)  // no expectations: must stay unstyled
40	second := mocks.NewMockWidget(t) // no expectations: must stay unstyled
41	border := mocks.NewMockWidget(t)
42	f := &modeFrame{borderStyle: border}
43	border.EXPECT().AddCssClass("pane-mode-active").Once()
44	f.setTarget(first, "pane-mode-active")
45	border.EXPECT().RemoveCssClass("pane-mode-active").Once()
46	for _, class := range vimPulseClasses {
47		border.EXPECT().RemoveCssClass(class).Times(2)
48	}
49	border.EXPECT().AddCssClass("resize-mode-active").Once()
50	f.setTarget(second, "resize-mode-active")
51	border.EXPECT().RemoveCssClass("resize-mode-active").Once()
52	f.setTarget(nil, "")
53}
54
55func TestModeFrameVimPulseAlternatesOnBorder(t *testing.T) {
56	pane := mocks.NewMockWidget(t) // no expectations: must stay unstyled
57	target := mocks.NewMockWidget(t)
58	f := &modeFrame{mode: input.ModeVim, target: pane, frameClass: "vim-mode-active", borderStyle: target}
59	for _, pulse := range []struct {
60		fast  bool
61		class string
62		cycle string
63	}{
64		{false, "vim-mode-pulse", "vim-mode-pulse-cycle-a"},
65		{true, "vim-mode-pulse-fast", "vim-mode-pulse-cycle-b"},
66	} {
67		for _, class := range []string{"vim-mode-pulse", "vim-mode-pulse-fast", "vim-mode-pulse-cycle-a", "vim-mode-pulse-cycle-b"} {
68			target.EXPECT().RemoveCssClass(class).Once()
69		}
70		target.EXPECT().AddCssClass(pulse.class).Once()
71		target.EXPECT().AddCssClass(pulse.cycle).Once()
72		f.pulse(pulse.fast)
73	}
74	target.EXPECT().RemoveCssClass("vim-mode-active").Once()
75	for _, class := range []string{"vim-mode-pulse", "vim-mode-pulse-fast", "vim-mode-pulse-cycle-a", "vim-mode-pulse-cycle-b"} {
76		target.EXPECT().RemoveCssClass(class).Once()
77	}
78	f.setTarget(nil, "")
79}
80
81func TestModeLegendActionsUseActiveConfiguration(t *testing.T) {
82	workspace := entity.WorkspaceConfig{
83		PaneMode:   entity.PaneModeConfig{Actions: map[string]entity.ActionBinding{"split-right": {Keys: []string{"r"}}}},
84		ResizeMode: entity.ResizeModeConfig{Actions: map[string]entity.ActionBinding{"resize-increase": {Keys: []string{"+"}}}},
85		VimMode:    entity.VimModeConfig{Actions: map[string]entity.ActionBinding{"focus-input": {Keys: []string{"gi"}}}},
86		TabMode:    entity.TabModeConfig{Actions: map[string]entity.ActionBinding{"next-tab": {Keys: []string{"l"}}}},
87	}
88	session := entity.SessionConfig{SessionMode: entity.SessionModeConfig{Actions: map[string]entity.ActionBinding{
89		"session-manager": {Keys: []string{"s"}},
90	}}}
91	for _, tc := range []struct {
92		mode    input.Mode
93		class   string
94		actions map[string]entity.ActionBinding
95	}{
96		{input.ModePane, "pane-mode-active", workspace.PaneMode.Actions},
97		{input.ModeResize, "resize-mode-active", workspace.ResizeMode.Actions},
98		{input.ModeVim, "vim-mode-active", workspace.VimMode.Actions},
99		{input.ModeTab, "tab-mode-active", workspace.TabMode.Actions},
100		{input.ModeSession, "session-mode-active", session.SessionMode.Actions},
101	} {
102		t.Run(tc.mode.String(), func(t *testing.T) {
103			require.Equal(t, tc.actions, legendActions(tc.mode, workspace, session))
104			require.Equal(t, tc.class, modeFrameClass(tc.mode))
105		})
106	}
107	require.Nil(t, legendActions(input.ModeNormal, workspace, session))
108	require.Empty(t, modeFrameClass(input.ModeNormal))
109	require.Equal(t, "→", displayLegendKey("arrowright"))
110	require.Equal(t, "SPLIT", modeLegendGroup(input.ModePane, "split-right"))
111	require.Equal(t, "JUMP", modeLegendGroup(input.ModeVim, "heading-next"))
112	require.Equal(t, "SELECT", modeLegendGroup(input.ModeVim, "hint-follow-new"))
113	require.Equal(t, "SELECT", modeLegendGroup(input.ModeVim, "yank-code"))
114	require.Equal(t, "SELECT", modeLegendGroup(input.ModeVim, "visual"))
115}
116
117func TestModeFramePendingSequenceFiltersBindings(t *testing.T) {
118	require.True(t, legendSequenceMatches("3]", []string{"]]"}))
119	require.False(t, legendSequenceMatches("3]", []string{"gi"}))
120	require.True(t, legendSequenceMatches("3", []string{"gi"}))
121	require.True(t, legendSequenceMatches("", []string{"gi"}))
122}
123
124func TestModeFrameTargetDoesNotDependOnGlobalFocusedWindow(t *testing.T) {
125	f := newVimModeTestFixture(t)
126	f.app.lastFocusedWindowID = f.bw2.id
127	require.Equal(t, f.pv1A.Widget(), f.app.modeFrameTarget(f.bw1, input.ModeVim))
128	require.Equal(t, f.pv2A.Widget(), f.app.modeFrameTarget(f.bw2, input.ModeVim))
129}
130
131func TestShouldLinger(t *testing.T) {
132	rect := func() (int, int, int, int, bool) { return 10, 20, 30, 40, true }
133	for _, tt := range []struct {
134		name            string
135		enabled, inside bool
136		x, y            float64
137		placement       func() (int, int, int, int, bool)
138		want            bool
139	}{
140		{"inside", true, true, 10, 20, rect, true},
141		{"edge outside", true, true, 40, 20, rect, false},
142		{"outside", true, true, 5, 25, rect, false},
143		{"disabled", false, true, 15, 25, rect, false},
144		{"left window", true, false, 15, 25, rect, false},
145		{"nil placement", true, true, 15, 25, nil, false},
146	} {
147		t.Run(tt.name, func(t *testing.T) {
148			require.Equal(t, tt.want, shouldLinger(tt.enabled, tt.inside, tt.x, tt.y, tt.placement))
149		})
150	}
151}
152
153func TestShouldStartLinger(t *testing.T) {
154	rect := func() (int, int, int, int, bool) { return 10, 20, 30, 40, true }
155	require.True(t, shouldStartLinger(true, false, true, true, 15, 25, rect))
156	require.False(t, shouldStartLinger(true, true, true, true, 15, 25, rect))
157	require.False(t, shouldStartLinger(false, false, true, true, 15, 25, rect))
158}

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