detached f2aa780

test(core): harden switcher tests and refresh preview comments

commit
f2aa780fdcc027d607bfa2849cd7dab25092b30c
author
brice · · 2026-10-08 19:17 UTC
parent
79b4f2d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +77 -21
M internal/core/publish_alloc_test.go +7 -9
@@ -247,11 +247,10 @@ func TestPublishOverviewAllocations(t *testing.T) {
247247 })
248248 }
249249 }
250- // The column switcher's settled cards are previews too: a held switch
251- // shows them, and a publish while they stay costs a plain frame.
250+ // The column switcher's cards are previews too: a held switch shows
251+ // them, and a publish while their motions run costs a plain frame.
252252 t.Run("switcher", func(t *testing.T) {
253253 c := publishRig(t, 1)
254- c.cfg.Animations.Slowdown = 1 // settle fast: only the settled frame is measured
255254 c.cmdMod = ports.ModSuper
256255 c.mods = ports.ModSuper
257256 sc := c.cur()
@@ -266,14 +265,10 @@ func TestPublishOverviewAllocations(t *testing.T) {
266265 if !c.switching() || !c.switcherTick() || !m.switcherShown() {
267266 t.Fatalf("setup: switching %v, shown %v", c.switching(), m.switcherShown())
268267 }
269- // Let the card motions settle (the rig slows every spring).
268+ // The rig slows every spring: the card motions run for the whole measure.
270269 step := publishStep(t, c, nil)
271270 step()
272- for start := time.Now(); c.animating() && time.Since(start) < 30*time.Second; {
273- time.Sleep(50 * time.Millisecond)
274- step()
275- }
276- if c.animating() || len(sc.rects) != 0 || len(c.overviewReal) == 0 {
271+ if len(sc.rects) == 0 || !c.animating() || len(c.overviewReal) == 0 {
277272 t.Fatalf("setup: %d rect motions, animating %v, real layouts %d", len(sc.rects), c.animating(), len(c.overviewReal))
278273 }
279274 publish := func() {
@@ -288,6 +283,9 @@ func TestPublishOverviewAllocations(t *testing.T) {
288283 }
289284 n := testing.AllocsPerRun(50, publish)
290285 t.Logf("switcher allocs per publish = %v", n)
286+ if len(sc.rects) == 0 {
287+ t.Fatal("card motions settled during the measure")
288+ }
291289 if n > publishAllocBudget {
292290 t.Errorf("switcher publish allocs per frame = %v, budget %d", n, publishAllocBudget)
293291 }
@@ -247,11 +247,10 @@ func TestPublishOverviewAllocations(t *testing.T) {
247 })247 })
248 }248 }
249 }249 }
250- // The column switcher's settled cards are previews too: a held switch250+ // The column switcher's cards are previews too: a held switch shows
251- // shows them, and a publish while they stay costs a plain frame.251+ // them, and a publish while their motions run costs a plain frame.
252 t.Run("switcher", func(t *testing.T) {252 t.Run("switcher", func(t *testing.T) {
253 c := publishRig(t, 1)253 c := publishRig(t, 1)
254- c.cfg.Animations.Slowdown = 1 // settle fast: only the settled frame is measured
255 c.cmdMod = ports.ModSuper254 c.cmdMod = ports.ModSuper
256 c.mods = ports.ModSuper255 c.mods = ports.ModSuper
257 sc := c.cur()256 sc := c.cur()
@@ -266,14 +265,10 @@ func TestPublishOverviewAllocations(t *testing.T) {
266 if !c.switching() || !c.switcherTick() || !m.switcherShown() {265 if !c.switching() || !c.switcherTick() || !m.switcherShown() {
267 t.Fatalf("setup: switching %v, shown %v", c.switching(), m.switcherShown())266 t.Fatalf("setup: switching %v, shown %v", c.switching(), m.switcherShown())
268 }267 }
269- // Let the card motions settle (the rig slows every spring).268+ // The rig slows every spring: the card motions run for the whole measure.
270 step := publishStep(t, c, nil)269 step := publishStep(t, c, nil)
271 step()270 step()
272- for start := time.Now(); c.animating() && time.Since(start) < 30*time.Second; {271+ if len(sc.rects) == 0 || !c.animating() || len(c.overviewReal) == 0 {
273- time.Sleep(50 * time.Millisecond)
274- step()
275- }
276- if c.animating() || len(sc.rects) != 0 || len(c.overviewReal) == 0 {
277 t.Fatalf("setup: %d rect motions, animating %v, real layouts %d", len(sc.rects), c.animating(), len(c.overviewReal))272 t.Fatalf("setup: %d rect motions, animating %v, real layouts %d", len(sc.rects), c.animating(), len(c.overviewReal))
278 }273 }
279 publish := func() {274 publish := func() {
@@ -288,6 +283,9 @@ func TestPublishOverviewAllocations(t *testing.T) {
288 }283 }
289 n := testing.AllocsPerRun(50, publish)284 n := testing.AllocsPerRun(50, publish)
290 t.Logf("switcher allocs per publish = %v", n)285 t.Logf("switcher allocs per publish = %v", n)
286+ if len(sc.rects) == 0 {
287+ t.Fatal("card motions settled during the measure")
288+ }
291 if n > publishAllocBudget {289 if n > publishAllocBudget {
292 t.Errorf("switcher publish allocs per frame = %v, budget %d", n, publishAllocBudget)290 t.Errorf("switcher publish allocs per frame = %v, budget %d", n, publishAllocBudget)
293 }291 }
M internal/core/screens.go +2 -2
@@ -64,8 +64,8 @@ func (s *screen) settled() bool { return s.mon.settled() && len(s.rects) == 0 }
6464 func (s *screen) stopRects() { clear(s.rects); s.rectsWS = nil }
6565
6666 // rectsOwner is what the screen's rect motions belong to: its workspace on
67-// screen, or nil in a preview (the overview or the column switcher), where
68-// the cards change under the windows.
67+// screen, or nil in a preview (the overview or the column switcher), where the
68+// workspace changes under the cards.
6969 func (s *screen) rectsOwner() *Workspace {
7070 if s.mon.previewing() {
7171 return nil
@@ -64,8 +64,8 @@ func (s *screen) settled() bool { return s.mon.settled() && len(s.rects) == 0 }
64 func (s *screen) stopRects() { clear(s.rects); s.rectsWS = nil }64 func (s *screen) stopRects() { clear(s.rects); s.rectsWS = nil }
65 65
66 // rectsOwner is what the screen's rect motions belong to: its workspace on66 // rectsOwner is what the screen's rect motions belong to: its workspace on
67-// screen, or nil in a preview (the overview or the column switcher), where67+// screen, or nil in a preview (the overview or the column switcher), where the
68-// the cards change under the windows.68+// workspace changes under the cards.
69 func (s *screen) rectsOwner() *Workspace {69 func (s *screen) rectsOwner() *Workspace {
70 if s.mon.previewing() {70 if s.mon.previewing() {
71 return nil71 return nil
M internal/core/switcher_test.go +61 -3
@@ -144,13 +144,13 @@ func TestMRUNoteFocusDoesNotAllocateWhenSettled(t *testing.T) {
144144 }
145145 n := len(w.recent)
146146 col := w.Columns[w.Focus]
147+ if len(col.Windows) < 2 {
148+ t.Fatalf("setup: column %v has one window", col.Windows)
149+ }
147150 other := col.Windows[0]
148151 if other == 2 {
149152 other = col.Windows[1]
150153 }
151- if len(col.Windows) < 2 {
152- t.Fatalf("setup: column %v has one window", col.Windows)
153- }
154154 w.FocusID(other)
155155 if got := testing.AllocsPerRun(100, w.noteFocus); got != 0 {
156156 t.Fatalf("noteFocus allocates %v after a same-column change", got)
@@ -925,6 +925,9 @@ func TestSwitcherShowDropsMotionsOfHiddenWindows(t *testing.T) {
925925 }
926926 c.mods = ports.ModSuper
927927 bind(t, c, ActionSwitchColumnNext)
928+ if _, ok := sc.rects[4]; !ok {
929+ t.Fatal("setup: motion dropped by the bind")
930+ }
928931 c.switcherTick()
929932 if _, ok := sc.rects[4]; ok {
930933 t.Fatalf("a hidden window keeps its motion: %+v", sc.rects[4])
@@ -987,6 +990,61 @@ func TestSwitcherClickOnCardCommits(t *testing.T) {
987990 }
988991 }
989992
993+// A click lands on a card where it is drawn, while its motion still runs.
994+func TestSwitcherClickOnMovingCardCommits(t *testing.T) {
995+ c, ic, _ := switcherCore(t, 3)
996+ c.cfg.Animations.On = true
997+ sc := c.cur()
998+ sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true))
999+ c.switcherTick()
1000+ indicatorScene(t, c)
1001+ ic.now = ic.now.Add(16 * time.Millisecond)
1002+ if err := c.step(context.Background(), nil); err != nil {
1003+ t.Fatal(err)
1004+ }
1005+ lastScene(t, c)
1006+ target := sc.mon.sw.order[2]
1007+ if len(sc.rects) == 0 || !c.animating() {
1008+ t.Fatalf("setup: %d card motions, animating %v", len(sc.rects), c.animating())
1009+ }
1010+ var settled, shown Rect
1011+ for _, p := range sc.settledLayout {
1012+ if p.ID == target {
1013+ settled = p.Rect
1014+ }
1015+ }
1016+ for _, p := range sc.shownLayout() {
1017+ if p.ID == target {
1018+ shown = p.Rect
1019+ }
1020+ }
1021+ if settled == shown {
1022+ t.Fatalf("setup: card %d shown at its settled rect %+v", target, shown)
1023+ }
1024+ c.cursorX, c.cursorY = cardCenter(t, c, target)
1025+ if err := c.pointerButton(context.Background(), ports.PointerButton{Button: 0x110, Pressed: true}); err != nil {
1026+ t.Fatal(err)
1027+ }
1028+ requireClosed(t, c)
1029+ if switcherFocus(c) != target {
1030+ t.Fatalf("focus %d, want %d", switcherFocus(c), target)
1031+ }
1032+}
1033+
1034+// Cancelling while the switch is held leaves nothing for the delayed show.
1035+func TestSwitcherTickAfterCancelDoesNothing(t *testing.T) {
1036+ c, _, _ := switcherCore(t, 3)
1037+ sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true))
1038+ if !c.switching() {
1039+ t.Fatal("setup: no switcher open")
1040+ }
1041+ c.cancelSwitcher()
1042+ if c.switcherTick() {
1043+ t.Fatal("switcherTick reports a switcher after the cancel")
1044+ }
1045+ requireClosed(t, c)
1046+}
1047+
9901048 func TestSwitcherClickElsewhereCancels(t *testing.T) {
9911049 c, _, _ := switcherCore(t, 3)
9921050 start := switcherFocus(c)
@@ -144,13 +144,13 @@ func TestMRUNoteFocusDoesNotAllocateWhenSettled(t *testing.T) {
144 }144 }
145 n := len(w.recent)145 n := len(w.recent)
146 col := w.Columns[w.Focus]146 col := w.Columns[w.Focus]
147+ if len(col.Windows) < 2 {
148+ t.Fatalf("setup: column %v has one window", col.Windows)
149+ }
147 other := col.Windows[0]150 other := col.Windows[0]
148 if other == 2 {151 if other == 2 {
149 other = col.Windows[1]152 other = col.Windows[1]
150 }153 }
151- if len(col.Windows) < 2 {
152- t.Fatalf("setup: column %v has one window", col.Windows)
153- }
154 w.FocusID(other)154 w.FocusID(other)
155 if got := testing.AllocsPerRun(100, w.noteFocus); got != 0 {155 if got := testing.AllocsPerRun(100, w.noteFocus); got != 0 {
156 t.Fatalf("noteFocus allocates %v after a same-column change", got)156 t.Fatalf("noteFocus allocates %v after a same-column change", got)
@@ -925,6 +925,9 @@ func TestSwitcherShowDropsMotionsOfHiddenWindows(t *testing.T) {
925 }925 }
926 c.mods = ports.ModSuper926 c.mods = ports.ModSuper
927 bind(t, c, ActionSwitchColumnNext)927 bind(t, c, ActionSwitchColumnNext)
928+ if _, ok := sc.rects[4]; !ok {
929+ t.Fatal("setup: motion dropped by the bind")
930+ }
928 c.switcherTick()931 c.switcherTick()
929 if _, ok := sc.rects[4]; ok {932 if _, ok := sc.rects[4]; ok {
930 t.Fatalf("a hidden window keeps its motion: %+v", sc.rects[4])933 t.Fatalf("a hidden window keeps its motion: %+v", sc.rects[4])
@@ -987,6 +990,61 @@ func TestSwitcherClickOnCardCommits(t *testing.T) {
987 }990 }
988 }991 }
989 992
993+// A click lands on a card where it is drawn, while its motion still runs.
994+func TestSwitcherClickOnMovingCardCommits(t *testing.T) {
995+ c, ic, _ := switcherCore(t, 3)
996+ c.cfg.Animations.On = true
997+ sc := c.cur()
998+ sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true))
999+ c.switcherTick()
1000+ indicatorScene(t, c)
1001+ ic.now = ic.now.Add(16 * time.Millisecond)
1002+ if err := c.step(context.Background(), nil); err != nil {
1003+ t.Fatal(err)
1004+ }
1005+ lastScene(t, c)
1006+ target := sc.mon.sw.order[2]
1007+ if len(sc.rects) == 0 || !c.animating() {
1008+ t.Fatalf("setup: %d card motions, animating %v", len(sc.rects), c.animating())
1009+ }
1010+ var settled, shown Rect
1011+ for _, p := range sc.settledLayout {
1012+ if p.ID == target {
1013+ settled = p.Rect
1014+ }
1015+ }
1016+ for _, p := range sc.shownLayout() {
1017+ if p.ID == target {
1018+ shown = p.Rect
1019+ }
1020+ }
1021+ if settled == shown {
1022+ t.Fatalf("setup: card %d shown at its settled rect %+v", target, shown)
1023+ }
1024+ c.cursorX, c.cursorY = cardCenter(t, c, target)
1025+ if err := c.pointerButton(context.Background(), ports.PointerButton{Button: 0x110, Pressed: true}); err != nil {
1026+ t.Fatal(err)
1027+ }
1028+ requireClosed(t, c)
1029+ if switcherFocus(c) != target {
1030+ t.Fatalf("focus %d, want %d", switcherFocus(c), target)
1031+ }
1032+}
1033+
1034+// Cancelling while the switch is held leaves nothing for the delayed show.
1035+func TestSwitcherTickAfterCancelDoesNothing(t *testing.T) {
1036+ c, _, _ := switcherCore(t, 3)
1037+ sendKeys(t, c, superKey(ports.ModSuper, true), tab(ports.ModSuper, true))
1038+ if !c.switching() {
1039+ t.Fatal("setup: no switcher open")
1040+ }
1041+ c.cancelSwitcher()
1042+ if c.switcherTick() {
1043+ t.Fatal("switcherTick reports a switcher after the cancel")
1044+ }
1045+ requireClosed(t, c)
1046+}
1047+
990 func TestSwitcherClickElsewhereCancels(t *testing.T) {1048 func TestSwitcherClickElsewhereCancels(t *testing.T) {
991 c, _, _ := switcherCore(t, 3)1049 c, _, _ := switcherCore(t, 3)
992 start := switcherFocus(c)1050 start := switcherFocus(c)
M internal/core/transition.go +7 -7
@@ -51,7 +51,7 @@ type viewShot struct {
5151 list []*Workspace
5252 pos float64
5353 ok bool
54- // rects are the windows the screen drew (cards in the overview),
54+ // rects are the windows the screen drew (the cards in a preview),
5555 // hidden windows left out. The slice is reused like list.
5656 rects []rectShot
5757 // stash holds the settled placements of the stash windows the screen
@@ -71,7 +71,7 @@ type rectShot struct {
7171 rect Rect
7272 off, vel rectOffsets
7373 // fade and fadeV are the fade offset of a running fade motion and its
74- // speed, so an overview toggle carries it over (transitionOverview).
74+ // speed, so a preview toggle carries it over (transitionOverview).
7575 fade, fadeV float64
7676 // dim and dimV are the dim offset of a running dim motion and its
7777 // speed; veil is the veil the window's settled placement drew
@@ -92,7 +92,7 @@ type rectShot struct {
9292 // The walk shrinks content toward the content rect's top-left, so this
9393 // suits aspect-preserving changes: the zoom follows the smaller of the width
9494 // and height ratios, so content never exceeds its frame when the aspect
95-// differs a little (rounding) or a lot (the frame is then letterboxed). An overview card's settled zoom is
95+// differs a little (rounding) or a lot (the frame is then letterboxed). A card's settled zoom is
9696 // its Preview: a window that opens into a card goes from 1 to Preview with
9797 // its width, and back.
9898 //
@@ -228,7 +228,7 @@ func (c *Core) retargetWith(m *motion, cur *float64, s spring, now time.Time) {
228228 // refreshShown builds the layouts of every screen once per publish: the
229229 // settled one (what configures are sized from) and the shown one (the same
230230 // with the rect motions applied: what is drawn and hit-tested). Rect motions
231-// belong to the workspace they began on, or to the open overview (rectsOwner):
231+// belong to the workspace they began on, or to the open preview (the overview or the switcher's cards, rectsOwner):
232232 // any other change drops them.
233233 //
234234 // Leaving motions add an entry for a window the layout no longer shows:
@@ -623,7 +623,7 @@ func (c *Core) snapshot(now time.Time) []viewShot {
623623 w := m.Current()
624624 s.ws, s.preview = w, m.previewing()
625625 if !m.previewing() {
626- // The overview does not scroll or slide: no camera.
626+ // A preview (the overview or the switcher's cards) does not scroll or slide: no camera.
627627 s.settledView, s.view = w.View, float64(w.View)+w.view.off
628628 s.switchAxis = w.policy().workspace
629629 if m.switchView.busy() {
@@ -642,7 +642,7 @@ func (c *Core) snapshot(now time.Time) []viewShot {
642642 }
643643 // settledLayout and shown are index-aligned and differ only by
644644 // the offsets of sc.rects: the shot keeps both unrounded. The
645- // overview's cards are recorded like windows.
645+ // cards of a preview are recorded like windows.
646646 for _, p := range sc.settledLayout {
647647 if p.Hidden {
648648 continue
@@ -687,7 +687,7 @@ func (c *Core) snapshot(now time.Time) []viewShot {
687687 // transition starts the springs that take each screen from what before
688688 // showed to what the action left, at now. It does nothing with animations
689689 // off, or while a swipe follows the fingers on that screen. Per-window rect
690-// motions join the camera here; with the overview open before or after
690+// motions join the camera here; with a preview open before or after
691691 // there is no camera, only the cards' (transitionOverview).
692692 //
693693 // A workspace moved to another monitor (move-workspace-to-monitor-*, or a
@@ -51,7 +51,7 @@ type viewShot struct {
51 list []*Workspace51 list []*Workspace
52 pos float6452 pos float64
53 ok bool53 ok bool
54- // rects are the windows the screen drew (cards in the overview),54+ // rects are the windows the screen drew (the cards in a preview),
55 // hidden windows left out. The slice is reused like list.55 // hidden windows left out. The slice is reused like list.
56 rects []rectShot56 rects []rectShot
57 // stash holds the settled placements of the stash windows the screen57 // stash holds the settled placements of the stash windows the screen
@@ -71,7 +71,7 @@ type rectShot struct {
71 rect Rect71 rect Rect
72 off, vel rectOffsets72 off, vel rectOffsets
73 // fade and fadeV are the fade offset of a running fade motion and its73 // fade and fadeV are the fade offset of a running fade motion and its
74- // speed, so an overview toggle carries it over (transitionOverview).74+ // speed, so a preview toggle carries it over (transitionOverview).
75 fade, fadeV float6475 fade, fadeV float64
76 // dim and dimV are the dim offset of a running dim motion and its76 // dim and dimV are the dim offset of a running dim motion and its
77 // speed; veil is the veil the window's settled placement drew77 // speed; veil is the veil the window's settled placement drew
@@ -92,7 +92,7 @@ type rectShot struct {
92 // The walk shrinks content toward the content rect's top-left, so this92 // The walk shrinks content toward the content rect's top-left, so this
93 // suits aspect-preserving changes: the zoom follows the smaller of the width93 // suits aspect-preserving changes: the zoom follows the smaller of the width
94 // and height ratios, so content never exceeds its frame when the aspect94 // and height ratios, so content never exceeds its frame when the aspect
95-// differs a little (rounding) or a lot (the frame is then letterboxed). An overview card's settled zoom is95+// differs a little (rounding) or a lot (the frame is then letterboxed). A card's settled zoom is
96 // its Preview: a window that opens into a card goes from 1 to Preview with96 // its Preview: a window that opens into a card goes from 1 to Preview with
97 // its width, and back.97 // its width, and back.
98 //98 //
@@ -228,7 +228,7 @@ func (c *Core) retargetWith(m *motion, cur *float64, s spring, now time.Time) {
228 // refreshShown builds the layouts of every screen once per publish: the228 // refreshShown builds the layouts of every screen once per publish: the
229 // settled one (what configures are sized from) and the shown one (the same229 // settled one (what configures are sized from) and the shown one (the same
230 // with the rect motions applied: what is drawn and hit-tested). Rect motions230 // with the rect motions applied: what is drawn and hit-tested). Rect motions
231-// belong to the workspace they began on, or to the open overview (rectsOwner):231+// belong to the workspace they began on, or to the open preview (the overview or the switcher's cards, rectsOwner):
232 // any other change drops them.232 // any other change drops them.
233 //233 //
234 // Leaving motions add an entry for a window the layout no longer shows:234 // Leaving motions add an entry for a window the layout no longer shows:
@@ -623,7 +623,7 @@ func (c *Core) snapshot(now time.Time) []viewShot {
623 w := m.Current()623 w := m.Current()
624 s.ws, s.preview = w, m.previewing()624 s.ws, s.preview = w, m.previewing()
625 if !m.previewing() {625 if !m.previewing() {
626- // The overview does not scroll or slide: no camera.626+ // A preview (the overview or the switcher's cards) does not scroll or slide: no camera.
627 s.settledView, s.view = w.View, float64(w.View)+w.view.off627 s.settledView, s.view = w.View, float64(w.View)+w.view.off
628 s.switchAxis = w.policy().workspace628 s.switchAxis = w.policy().workspace
629 if m.switchView.busy() {629 if m.switchView.busy() {
@@ -642,7 +642,7 @@ func (c *Core) snapshot(now time.Time) []viewShot {
642 }642 }
643 // settledLayout and shown are index-aligned and differ only by643 // settledLayout and shown are index-aligned and differ only by
644 // the offsets of sc.rects: the shot keeps both unrounded. The644 // the offsets of sc.rects: the shot keeps both unrounded. The
645- // overview's cards are recorded like windows.645+ // cards of a preview are recorded like windows.
646 for _, p := range sc.settledLayout {646 for _, p := range sc.settledLayout {
647 if p.Hidden {647 if p.Hidden {
648 continue648 continue
@@ -687,7 +687,7 @@ func (c *Core) snapshot(now time.Time) []viewShot {
687 // transition starts the springs that take each screen from what before687 // transition starts the springs that take each screen from what before
688 // showed to what the action left, at now. It does nothing with animations688 // showed to what the action left, at now. It does nothing with animations
689 // off, or while a swipe follows the fingers on that screen. Per-window rect689 // off, or while a swipe follows the fingers on that screen. Per-window rect
690-// motions join the camera here; with the overview open before or after690+// motions join the camera here; with a preview open before or after
691 // there is no camera, only the cards' (transitionOverview).691 // there is no camera, only the cards' (transitionOverview).
692 //692 //
693 // A workspace moved to another monitor (move-workspace-to-monitor-*, or a693 // A workspace moved to another monitor (move-workspace-to-monitor-*, or a

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 26

Tags 10

↑↓ moveenter openesc close