detached 024f793

perf(core): skip publishing border resize motions that change nothing

commit
024f793cb04630cab3ba40950c354b21bd11eba1
author
brice · · 2026-10-21 08:06 UTC
committer
brice · · 2026-10-21 08:07 UTC
parent
04334a4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +80 -14
M internal/core/border.go +22 -13
@@ -77,23 +77,26 @@ func pixelShare(pixels, total int) int {
7777 return (pixels*10000 + total - 1) / total
7878 }
7979
80-func (w *Workspace) setBorder(b border, pos int) {
80+// setBorder moves border b to pos and reports whether any span or share
81+// changed (stamping a lane or a column's shares counts as a change).
82+func (w *Workspace) setBorder(b border, pos int) bool {
8183 if b.col < 0 || b.col >= len(w.Columns) || w.fullscreen != 0 {
82- return
84+ return false
8385 }
8486 rs := w.columnRects()
8587 r := rs[b.col]
8688 if b.kind == borderRow {
8789 c := &w.Columns[b.col]
8890 if b.row < 0 || b.row+1 >= len(c.Windows) || len(c.Windows)*minShare > 10000 {
89- return
91+ return false
9092 }
9193 rows := rowRects(r, *c, w.gap())
9294 avail := max(r.H-(len(rows)-1)*w.gap(), 0)
9395 if avail == 0 {
94- return
96+ return false
9597 }
96- if !validShares(*c) {
98+ stamped := !validShares(*c)
99+ if stamped {
97100 c.Shares = make([]int, len(rows))
98101 used := 0
99102 for i := range rows {
@@ -106,18 +109,19 @@ func (w *Workspace) setBorder(b border, pos int) {
106109 }
107110 pair := c.Shares[b.row] + c.Shares[b.row+1]
108111 if pair < 2*minShare {
109- return
112+ return stamped
110113 }
111114 target := pixelShare(pos-w.gap()/2-rows[b.row].Y, avail)
112115 target = min(max(target, minShare), pair-minShare)
113116 if b.row+1 < len(rows)-1 {
114117 target = pairPixelShare(target, pair, avail, rows[b.row].H+rows[b.row+1].H, minShare)
115118 }
119+ changed := stamped || c.Shares[b.row] != target
116120 c.Shares[b.row], c.Shares[b.row+1] = target, pair-target
117- return
121+ return changed
118122 }
119123 if b.kind != borderColumn || b.col+1 >= len(w.Columns) || w.laneOf(b.col) != w.laneOf(b.col+1) || w.Overflow == OverflowFixed && !w.laneRow() || w.Columns[b.col].FullWidth || w.Columns[b.col+1].FullWidth {
120- return
124+ return false
121125 }
122126 first, last := w.laneBounds(w.laneOf(b.col))
123127 avail := max(w.Usable.W-2*w.gap(), 0)
@@ -125,9 +129,10 @@ func (w *Workspace) setBorder(b border, pos int) {
125129 avail = max(w.Usable.W-w.gap()*(last-first+2), 0)
126130 }
127131 if avail == 0 {
128- return
132+ return false
129133 }
130- if !w.validSpans(first, last) {
134+ stamped := !w.validSpans(first, last)
135+ if stamped {
131136 w.spanSeq++
132137 used := 0
133138 for i := first; i <= last; i++ {
@@ -142,18 +147,22 @@ func (w *Workspace) setBorder(b border, pos int) {
142147 minimum := pixelShare(max(avail/20, 4*w.border, borderMinWidth), avail)
143148 target := pixelShare(pos-w.gap()/2-r.X, avail)
144149 if !w.policy().equalCells {
145- w.Columns[b.col].Span = min(max(target, min(minimum, 10000)), 10000)
146- return
150+ span := min(max(target, min(minimum, 10000)), 10000)
151+ changed := stamped || w.Columns[b.col].Span != span
152+ w.Columns[b.col].Span = span
153+ return changed
147154 }
148155 pair := w.Columns[b.col].Span + w.Columns[b.col+1].Span
149156 if pair < 2*minimum {
150- return
157+ return stamped
151158 }
152159 target = min(max(target, minimum), pair-minimum)
153160 if b.col+1 < last {
154161 target = pairPixelShare(target, pair, avail, rs[b.col].W+rs[b.col+1].W, minimum)
155162 }
163+ changed := stamped || w.Columns[b.col].Span != target
156164 w.Columns[b.col].Span, w.Columns[b.col+1].Span = target, pair-target
165+ return changed
157166 }
158167
159168 func (w *Workspace) resetBorder(b border) {
@@ -77,23 +77,26 @@ func pixelShare(pixels, total int) int {
77 return (pixels*10000 + total - 1) / total77 return (pixels*10000 + total - 1) / total
78 }78 }
79 79
80-func (w *Workspace) setBorder(b border, pos int) {80+// setBorder moves border b to pos and reports whether any span or share
81+// changed (stamping a lane or a column's shares counts as a change).
82+func (w *Workspace) setBorder(b border, pos int) bool {
81 if b.col < 0 || b.col >= len(w.Columns) || w.fullscreen != 0 {83 if b.col < 0 || b.col >= len(w.Columns) || w.fullscreen != 0 {
82- return84+ return false
83 }85 }
84 rs := w.columnRects()86 rs := w.columnRects()
85 r := rs[b.col]87 r := rs[b.col]
86 if b.kind == borderRow {88 if b.kind == borderRow {
87 c := &w.Columns[b.col]89 c := &w.Columns[b.col]
88 if b.row < 0 || b.row+1 >= len(c.Windows) || len(c.Windows)*minShare > 10000 {90 if b.row < 0 || b.row+1 >= len(c.Windows) || len(c.Windows)*minShare > 10000 {
89- return91+ return false
90 }92 }
91 rows := rowRects(r, *c, w.gap())93 rows := rowRects(r, *c, w.gap())
92 avail := max(r.H-(len(rows)-1)*w.gap(), 0)94 avail := max(r.H-(len(rows)-1)*w.gap(), 0)
93 if avail == 0 {95 if avail == 0 {
94- return96+ return false
95 }97 }
96- if !validShares(*c) {98+ stamped := !validShares(*c)
99+ if stamped {
97 c.Shares = make([]int, len(rows))100 c.Shares = make([]int, len(rows))
98 used := 0101 used := 0
99 for i := range rows {102 for i := range rows {
@@ -106,18 +109,19 @@ func (w *Workspace) setBorder(b border, pos int) {
106 }109 }
107 pair := c.Shares[b.row] + c.Shares[b.row+1]110 pair := c.Shares[b.row] + c.Shares[b.row+1]
108 if pair < 2*minShare {111 if pair < 2*minShare {
109- return112+ return stamped
110 }113 }
111 target := pixelShare(pos-w.gap()/2-rows[b.row].Y, avail)114 target := pixelShare(pos-w.gap()/2-rows[b.row].Y, avail)
112 target = min(max(target, minShare), pair-minShare)115 target = min(max(target, minShare), pair-minShare)
113 if b.row+1 < len(rows)-1 {116 if b.row+1 < len(rows)-1 {
114 target = pairPixelShare(target, pair, avail, rows[b.row].H+rows[b.row+1].H, minShare)117 target = pairPixelShare(target, pair, avail, rows[b.row].H+rows[b.row+1].H, minShare)
115 }118 }
119+ changed := stamped || c.Shares[b.row] != target
116 c.Shares[b.row], c.Shares[b.row+1] = target, pair-target120 c.Shares[b.row], c.Shares[b.row+1] = target, pair-target
117- return121+ return changed
118 }122 }
119 if b.kind != borderColumn || b.col+1 >= len(w.Columns) || w.laneOf(b.col) != w.laneOf(b.col+1) || w.Overflow == OverflowFixed && !w.laneRow() || w.Columns[b.col].FullWidth || w.Columns[b.col+1].FullWidth {123 if b.kind != borderColumn || b.col+1 >= len(w.Columns) || w.laneOf(b.col) != w.laneOf(b.col+1) || w.Overflow == OverflowFixed && !w.laneRow() || w.Columns[b.col].FullWidth || w.Columns[b.col+1].FullWidth {
120- return124+ return false
121 }125 }
122 first, last := w.laneBounds(w.laneOf(b.col))126 first, last := w.laneBounds(w.laneOf(b.col))
123 avail := max(w.Usable.W-2*w.gap(), 0)127 avail := max(w.Usable.W-2*w.gap(), 0)
@@ -125,9 +129,10 @@ func (w *Workspace) setBorder(b border, pos int) {
125 avail = max(w.Usable.W-w.gap()*(last-first+2), 0)129 avail = max(w.Usable.W-w.gap()*(last-first+2), 0)
126 }130 }
127 if avail == 0 {131 if avail == 0 {
128- return132+ return false
129 }133 }
130- if !w.validSpans(first, last) {134+ stamped := !w.validSpans(first, last)
135+ if stamped {
131 w.spanSeq++136 w.spanSeq++
132 used := 0137 used := 0
133 for i := first; i <= last; i++ {138 for i := first; i <= last; i++ {
@@ -142,18 +147,22 @@ func (w *Workspace) setBorder(b border, pos int) {
142 minimum := pixelShare(max(avail/20, 4*w.border, borderMinWidth), avail)147 minimum := pixelShare(max(avail/20, 4*w.border, borderMinWidth), avail)
143 target := pixelShare(pos-w.gap()/2-r.X, avail)148 target := pixelShare(pos-w.gap()/2-r.X, avail)
144 if !w.policy().equalCells {149 if !w.policy().equalCells {
145- w.Columns[b.col].Span = min(max(target, min(minimum, 10000)), 10000)150+ span := min(max(target, min(minimum, 10000)), 10000)
146- return151+ changed := stamped || w.Columns[b.col].Span != span
152+ w.Columns[b.col].Span = span
153+ return changed
147 }154 }
148 pair := w.Columns[b.col].Span + w.Columns[b.col+1].Span155 pair := w.Columns[b.col].Span + w.Columns[b.col+1].Span
149 if pair < 2*minimum {156 if pair < 2*minimum {
150- return157+ return stamped
151 }158 }
152 target = min(max(target, minimum), pair-minimum)159 target = min(max(target, minimum), pair-minimum)
153 if b.col+1 < last {160 if b.col+1 < last {
154 target = pairPixelShare(target, pair, avail, rs[b.col].W+rs[b.col+1].W, minimum)161 target = pairPixelShare(target, pair, avail, rs[b.col].W+rs[b.col+1].W, minimum)
155 }162 }
163+ changed := stamped || w.Columns[b.col].Span != target
156 w.Columns[b.col].Span, w.Columns[b.col+1].Span = target, pair-target164 w.Columns[b.col].Span, w.Columns[b.col+1].Span = target, pair-target
165+ return changed
157 }166 }
158 167
159 func (w *Workspace) resetBorder(b border) {168 func (w *Workspace) resetBorder(b border) {
M internal/core/border_resize.go +3 -1
@@ -67,7 +67,9 @@ func (g *borderResize) motion(ctx context.Context, _ time.Duration) error {
6767 if g.border.kind == borderRow {
6868 pos = int(c.cursorY) - g.screen.y
6969 }
70- g.ws.setBorder(g.border, pos)
70+ if !g.ws.setBorder(g.border, pos) {
71+ return nil
72+ }
7173 return c.publish(ctx)
7274 }
7375
@@ -67,7 +67,9 @@ func (g *borderResize) motion(ctx context.Context, _ time.Duration) error {
67 if g.border.kind == borderRow {67 if g.border.kind == borderRow {
68 pos = int(c.cursorY) - g.screen.y68 pos = int(c.cursorY) - g.screen.y
69 }69 }
70- g.ws.setBorder(g.border, pos)70+ if !g.ws.setBorder(g.border, pos) {
71+ return nil
72+ }
71 return c.publish(ctx)73 return c.publish(ctx)
72 }74 }
73 75
M internal/core/border_test.go +55 -0
@@ -3,6 +3,7 @@ package core
33 import (
44 "slices"
55 "testing"
6+ "time"
67 )
78
89 func TestBorderAt(t *testing.T) {
@@ -228,3 +229,57 @@ func TestBorderStampIgnoresMaximizedWidth(t *testing.T) {
228229 })
229230 }
230231 }
232+
233+func TestSetBorderReportsChange(t *testing.T) {
234+ for _, mode := range []Overflow{OverflowScroll, OverflowFixed} {
235+ w := spanWorkspace(mode, 2)
236+ b := border{kind: borderColumn, col: 0}
237+ x := w.columnRects()[0]
238+ pos := x.X + x.W + 5 + 40
239+ if !w.setBorder(b, pos) {
240+ t.Fatal(mode, "first move must report a change")
241+ }
242+ if w.setBorder(b, pos) {
243+ t.Fatal(mode, "same position reported a change")
244+ }
245+ w.setBorder(b, -10000)
246+ if w.setBorder(b, -20000) {
247+ t.Fatal(mode, "clamped at the minimum reported a change")
248+ }
249+ }
250+ w := spanWorkspace(OverflowFixed, 2)
251+ w.Columns[0].Windows = []WindowID{1, 3}
252+ b := border{kind: borderRow, col: 0}
253+ r := w.columnRects()[0]
254+ pos := rowRects(r, w.Columns[0], w.gap())[0].Y + 200
255+ if !w.setBorder(b, pos) || w.setBorder(b, pos) {
256+ t.Fatal("row border change reporting")
257+ }
258+ if w.setBorder(border{kind: borderColumn, col: 9}, 0) {
259+ t.Fatal("invalid border reported a change")
260+ }
261+}
262+
263+func TestBorderMotionPublishesOnlyChanges(t *testing.T) {
264+ c, w := borderCore(t, false)
265+ grabButton(t, c, btnRight, true, time.Second)
266+ grabCommands(c)
267+ grabMove(t, c, c.cursorX+150, c.cursorY)
268+ if len(grabCommands(c)) == 0 {
269+ t.Fatal("a resizing motion must publish")
270+ }
271+ spans := []int{w.Columns[0].Span, w.Columns[1].Span}
272+ grabMove(t, c, c.cursorX, c.cursorY)
273+ if cmds := grabCommands(c); len(cmds) != 0 {
274+ t.Fatalf("unchanged motion published %+v", cmds)
275+ }
276+ if spans[0] != w.Columns[0].Span || spans[1] != w.Columns[1].Span {
277+ t.Fatal("spans changed")
278+ }
279+ grabMove(t, c, -10000, c.cursorY)
280+ grabCommands(c)
281+ grabMove(t, c, -20000, c.cursorY)
282+ if cmds := grabCommands(c); len(cmds) != 0 {
283+ t.Fatalf("motion clamped at the minimum published %+v", cmds)
284+ }
285+}
@@ -3,6 +3,7 @@ package core
3 import (3 import (
4 "slices"4 "slices"
5 "testing"5 "testing"
6+ "time"
6 )7 )
7 8
8 func TestBorderAt(t *testing.T) {9 func TestBorderAt(t *testing.T) {
@@ -228,3 +229,57 @@ func TestBorderStampIgnoresMaximizedWidth(t *testing.T) {
228 })229 })
229 }230 }
230 }231 }
232+
233+func TestSetBorderReportsChange(t *testing.T) {
234+ for _, mode := range []Overflow{OverflowScroll, OverflowFixed} {
235+ w := spanWorkspace(mode, 2)
236+ b := border{kind: borderColumn, col: 0}
237+ x := w.columnRects()[0]
238+ pos := x.X + x.W + 5 + 40
239+ if !w.setBorder(b, pos) {
240+ t.Fatal(mode, "first move must report a change")
241+ }
242+ if w.setBorder(b, pos) {
243+ t.Fatal(mode, "same position reported a change")
244+ }
245+ w.setBorder(b, -10000)
246+ if w.setBorder(b, -20000) {
247+ t.Fatal(mode, "clamped at the minimum reported a change")
248+ }
249+ }
250+ w := spanWorkspace(OverflowFixed, 2)
251+ w.Columns[0].Windows = []WindowID{1, 3}
252+ b := border{kind: borderRow, col: 0}
253+ r := w.columnRects()[0]
254+ pos := rowRects(r, w.Columns[0], w.gap())[0].Y + 200
255+ if !w.setBorder(b, pos) || w.setBorder(b, pos) {
256+ t.Fatal("row border change reporting")
257+ }
258+ if w.setBorder(border{kind: borderColumn, col: 9}, 0) {
259+ t.Fatal("invalid border reported a change")
260+ }
261+}
262+
263+func TestBorderMotionPublishesOnlyChanges(t *testing.T) {
264+ c, w := borderCore(t, false)
265+ grabButton(t, c, btnRight, true, time.Second)
266+ grabCommands(c)
267+ grabMove(t, c, c.cursorX+150, c.cursorY)
268+ if len(grabCommands(c)) == 0 {
269+ t.Fatal("a resizing motion must publish")
270+ }
271+ spans := []int{w.Columns[0].Span, w.Columns[1].Span}
272+ grabMove(t, c, c.cursorX, c.cursorY)
273+ if cmds := grabCommands(c); len(cmds) != 0 {
274+ t.Fatalf("unchanged motion published %+v", cmds)
275+ }
276+ if spans[0] != w.Columns[0].Span || spans[1] != w.Columns[1].Span {
277+ t.Fatal("spans changed")
278+ }
279+ grabMove(t, c, -10000, c.cursorY)
280+ grabCommands(c)
281+ grabMove(t, c, -20000, c.cursorY)
282+ if cmds := grabCommands(c); len(cmds) != 0 {
283+ t.Fatalf("motion clamped at the minimum published %+v", cmds)
284+ }
285+}

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 25

Tags 10

↑↓ moveenter openesc close