v0.7.0 36c429d

frame_style_reuse_test.go RawHistoryFolder

1package core
2
3import (
4	"reflect"
5	"testing"
6)
7
8func TestSetStyleReuse(t *testing.T) {
9	for _, rotated := range []bool{false, true} {
10		f := NewFrame(3, 2)
11		if rotated {
12			f.ScrollUp(0, 1, 1)
13		}
14		styles := []Style{DefaultStyle(), {Bold: true}, {Italic: true}, {Bold: true}, DefaultStyle()}
15		for _, style := range styles {
16			for repeat := range 3 {
17				for x := range f.Width {
18					cell := Cell{Rune: rune('a' + repeat), Style: style}
19					// Inactive RGB fields differ without changing canonical style.
20					cell.Style.ForegroundRGB = RGB{R: uint8(repeat + 1)}
21					index := f.offset(x, 0)
22					oldID := f.page.cells[index].styleID
23					sameStyle := f.page.styles[oldID].style == style.Canonical()
24					f.Set(x, 0, cell)
25					if got := f.page.cells[index].styleID; sameStyle && got != oldID {
26						t.Fatalf("equivalent style changed ID from %d to %d", oldID, got)
27					}
28					if got := f.Cell(x, 0); !got.Equal(cell) {
29						t.Fatalf("cell = %+v, want %+v", got, cell)
30					}
31					if err := f.CheckInvariants(); err != nil {
32						t.Fatal(err)
33					}
34				}
35			}
36		}
37	}
38}
39
40func BenchmarkFrameRewriteSameStyle(b *testing.B) {
41	f := NewFrame(182, 53)
42	row := make([]Cell, f.Width)
43	for x := range row {
44		row[x] = Cell{Rune: 'x', Style: Style{Bold: true}}
45	}
46	for y := range f.Height {
47		f.WriteRow(y, 0, row)
48	}
49	b.ReportAllocs()
50	for b.Loop() {
51		for y := range f.Height {
52			f.WriteRow(y, 0, row)
53		}
54	}
55}
56
57func TestStyleCacheSurvivesSlotFreeAndReuse(t *testing.T) {
58	a := Style{Foreground: 1}
59	b := Style{Foreground: 2, Bold: true}
60	f := NewFrame(4, 2)
61	check := func(step string) {
62		t.Helper()
63		if err := f.CheckInvariants(); err != nil {
64			t.Fatalf("%s: %v", step, err)
65		}
66	}
67
68	f.Set(0, 0, Cell{Rune: 'a', Style: a})
69	check("set a")
70	idA := f.page.cells[f.offset(0, 0)].styleID
71	if !f.page.styleCacheOK || f.page.styleCacheID != idA {
72		t.Fatalf("cache = (%v,%d), want live slot %d", f.page.styleCacheOK, f.page.styleCacheID, idA)
73	}
74
75	// Overwrite the only A cell so its slot is freed; the cache must not keep it.
76	f.Set(0, 0, BlankCell())
77	check("free a")
78	if f.page.styleCacheOK && !f.page.styles[f.page.styleCacheID].used {
79		t.Fatal("cache points at a freed style slot")
80	}
81
82	// Reuse the freed slot for B, then request A again: it must not alias B.
83	f.Set(1, 0, Cell{Rune: 'b', Style: b})
84	check("reuse slot for b")
85	if got := f.page.cells[f.offset(1, 0)].styleID; got != idA {
86		t.Fatalf("expected freed slot %d to be reused, got %d", idA, got)
87	}
88	f.Set(2, 0, Cell{Rune: 'a', Style: a})
89	check("set a again")
90	if got := f.Cell(2, 0).Style; got != a {
91		t.Fatalf("style after reuse = %+v, want %+v", got, a)
92	}
93	if got := f.Cell(1, 0).Style; got != b {
94		t.Fatalf("style b = %+v, want %+v", got, b)
95	}
96	if f.StyleCount() != 3 {
97		t.Fatalf("StyleCount = %d, want 3", f.StyleCount())
98	}
99
100	// Repeating the cached raw style must bump references exactly once per cell.
101	f.Set(3, 0, Cell{Rune: 'a', Style: a})
102	f.Set(0, 1, Cell{Rune: 'a', Style: a})
103	check("repeat a")
104	f.Set(2, 0, BlankCell())
105	f.Set(3, 0, BlankCell())
106	f.Set(0, 1, BlankCell())
107	check("release a")
108	if f.StyleCount() != 2 {
109		t.Fatalf("StyleCount = %d, want 2", f.StyleCount())
110	}
111}
112
113func TestStyleCacheNonCanonicalInput(t *testing.T) {
114	f := NewFrame(3, 1)
115	rgbWithStaleIndex := Style{Foreground: 7, HasForegroundRGB: true, ForegroundRGB: RGB{R: 1}, Background: -1}
116	canonical := rgbWithStaleIndex.Canonical()
117	f.Set(0, 0, Cell{Rune: 'x', Style: rgbWithStaleIndex})
118	f.Set(1, 0, Cell{Rune: 'y', Style: canonical})
119	f.Set(2, 0, Cell{Rune: 'z', Style: rgbWithStaleIndex})
120	if err := f.CheckInvariants(); err != nil {
121		t.Fatal(err)
122	}
123	if f.StyleCount() != 2 {
124		t.Fatalf("StyleCount = %d, want 2", f.StyleCount())
125	}
126	for x := range 3 {
127		if got := f.Cell(x, 0).Style; got != canonical {
128			t.Fatalf("cell %d style = %+v, want %+v", x, got, canonical)
129		}
130	}
131	// Rewriting with the non-canonical form must keep reference counts exact.
132	f.Set(0, 0, Cell{Rune: 'x', Style: rgbWithStaleIndex})
133	if err := f.CheckInvariants(); err != nil {
134		t.Fatal(err)
135	}
136}
137
138func TestStyleCacheCloneAndReplaceIndependence(t *testing.T) {
139	a := Style{Foreground: 1}
140	b := Style{Foreground: 2}
141	src := NewFrame(2, 1)
142	src.Set(0, 0, Cell{Rune: 'a', Style: a})
143	clone := src.Clone()
144	if !reflect.DeepEqual(src, clone) {
145		t.Fatal("clone is not DeepEqual to its source")
146	}
147
148	// Free A in the source and reuse its slot for B; the clone keeps A.
149	src.Set(0, 0, BlankCell())
150	src.Set(1, 0, Cell{Rune: 'b', Style: b})
151	clone.Set(1, 0, Cell{Rune: 'a', Style: a})
152	for name, f := range map[string]Frame{"src": src, "clone": clone} {
153		if err := f.CheckInvariants(); err != nil {
154			t.Fatalf("%s: %v", name, err)
155		}
156	}
157	if got := clone.Cell(0, 0).Style; got != a {
158		t.Fatalf("clone cell 0 = %+v, want %+v", got, a)
159	}
160	if got := clone.Cell(1, 0).Style; got != a {
161		t.Fatalf("clone cell 1 = %+v, want %+v", got, a)
162	}
163	if got := src.Cell(1, 0).Style; got != b {
164		t.Fatalf("src cell 1 = %+v, want %+v", got, b)
165	}
166
167	replaced := NewFrame(2, 1)
168	replaced.Set(0, 0, Cell{Rune: 'q', Style: b})
169	replaced.Replace(clone)
170	replaced.Set(0, 0, Cell{Rune: 'q', Style: b})
171	replaced.Set(1, 0, Cell{Rune: 'q', Style: b})
172	if err := replaced.CheckInvariants(); err != nil {
173		t.Fatal(err)
174	}
175	if err := clone.CheckInvariants(); err != nil || clone.Cell(0, 0).Style != a {
176		t.Fatalf("clone changed by replaced frame: %v", err)
177	}
178}
179
180func TestBlankPhysicalRowReleasesStylesAndPayloads(t *testing.T) {
181	const width = 7
182	f := NewFrame(width, 3)
183	payload, err := NewCellPayload("é", "")
184	if err != nil {
185		t.Fatal(err)
186	}
187	for x := range width {
188		f.Set(x, 0, Cell{Rune: 'x', Style: Style{Foreground: x % 3}, Payload: payload})
189		f.Set(x, 1, Cell{Rune: 'y', Style: Style{Foreground: x % 3}})
190	}
191	f.ScrollUp(0, 2, 1)
192	if err := f.CheckInvariants(); err != nil {
193		t.Fatal(err)
194	}
195	for x := range width {
196		if got := f.Cell(x, 2); got != (Cell{Rune: ' ', Style: DefaultStyle()}) {
197			t.Fatalf("blank row cell %d = %+v", x, got)
198		}
199		if got := f.Cell(x, 0); got.Rune != 'y' || !got.Payload.Empty() {
200			t.Fatalf("shifted row cell %d = %+v", x, got)
201		}
202	}
203	f.ScrollDown(0, 2, 3)
204	if err := f.CheckInvariants(); err != nil {
205		t.Fatal(err)
206	}
207	if f.StyleCount() != 1 {
208		t.Fatalf("StyleCount = %d, want 1", f.StyleCount())
209	}
210}

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 2

Tags 13

↑↓ moveenter openesc close