v0.2.0 4859eb8

frame.go RawHistoryFolder

1package core
2
3import "fmt"
4
5// Frame is a fixed-size grid of cells. Logical rows are decoupled from their
6// physical storage through lineOffset: logical row y lives at
7// Cells[lineOffset[y] : lineOffset[y]+Width]. This indirection lets full-width
8// scrolls rotate offsets instead of copying cell memory (NeoVim's grid
9// technique; cf. nvim grid.c line_offset[]). All cell access must go through
10// the accessors (At/Set/Row) so callers never assume a physical layout.
11type Frame struct {
12	Width  int
13	Height int
14	Cells  []Cell
15	// lineOffset maps a logical row index to its physical base index into
16	// Cells. It is always a permutation of the canonical offsets y*Width.
17	lineOffset []int
18}
19
20func NewFrame(width, height int) Frame {
21	cells := make([]Cell, width*height)
22	for i := range cells {
23		cells[i] = BlankCell()
24	}
25	lineOffset := make([]int, height)
26	for y := range lineOffset {
27		lineOffset[y] = y * width
28	}
29	return Frame{Width: width, Height: height, Cells: cells, lineOffset: lineOffset}
30}
31
32// Clone returns an independent copy preserving logical row contents. The clone
33// uses canonical row storage so later scrolls or writes to the source frame do
34// not affect it.
35func (f Frame) Clone() Frame {
36	clone := NewFrame(f.Width, f.Height)
37	for y := range f.Height {
38		copy(clone.Row(y), f.Row(y))
39	}
40	return clone
41}
42
43// Replace copies src into f while reusing canonical storage when dimensions
44// permit. src is read through logical rows so its physical row rotation is
45// preserved in the resulting contents without exposing layout internals.
46func (f *Frame) Replace(src Frame) {
47	if f == nil {
48		return
49	}
50	if src.Width <= 0 || src.Height <= 0 || len(src.Cells) != src.Width*src.Height {
51		*f = Frame{}
52		return
53	}
54
55	sameDimensions := f.Width == src.Width && f.Height == src.Height
56	cellCount := src.Width * src.Height
57	if sameDimensions && cap(f.Cells) >= cellCount {
58		f.Cells = f.Cells[:cellCount]
59	} else {
60		f.Cells = make([]Cell, cellCount)
61	}
62	if sameDimensions && cap(f.lineOffset) >= src.Height {
63		f.lineOffset = f.lineOffset[:src.Height]
64	} else {
65		f.lineOffset = make([]int, src.Height)
66	}
67	f.Width = src.Width
68	f.Height = src.Height
69	for y := range f.lineOffset {
70		f.lineOffset[y] = y * f.Width
71	}
72	for y := range src.Height {
73		copy(f.Row(y), src.Row(y))
74	}
75}
76
77func (f Frame) Validate() error {
78	if f.Width <= 0 || f.Height <= 0 {
79		return fmt.Errorf("invalid frame size %dx%d", f.Width, f.Height)
80	}
81	if len(f.Cells) != f.Width*f.Height {
82		return fmt.Errorf("invalid cell count: got %d want %d", len(f.Cells), f.Width*f.Height)
83	}
84	if len(f.lineOffset) != f.Height {
85		return fmt.Errorf("invalid lineOffset length: got %d want %d", len(f.lineOffset), f.Height)
86	}
87	return nil
88}
89
90// CheckInvariants verifies that lineOffset is a permutation of the canonical
91// row offsets: every entry is a non-negative multiple of Width within bounds,
92// and every logical row maps to a distinct physical row. It is used by tests
93// and assertions; the hot path uses the lighter Validate.
94func (f Frame) CheckInvariants() error {
95	if f.Width <= 0 {
96		return fmt.Errorf("invalid width %d", f.Width)
97	}
98	if len(f.lineOffset) != f.Height {
99		return fmt.Errorf("lineOffset length %d != height %d", len(f.lineOffset), f.Height)
100	}
101	seen := make([]bool, f.Height)
102	for y, off := range f.lineOffset {
103		if off < 0 || off%f.Width != 0 {
104			return fmt.Errorf("row %d: offset %d is not a non-negative multiple of width %d", y, off, f.Width)
105		}
106		phys := off / f.Width
107		if phys >= f.Height {
108			return fmt.Errorf("row %d: offset %d maps to physical row %d out of range [0,%d)", y, off, phys, f.Height)
109		}
110		if seen[phys] {
111			return fmt.Errorf("row %d: physical row %d already mapped by another logical row", y, phys)
112		}
113		seen[phys] = true
114	}
115	return nil
116}
117
118func (f Frame) At(x, y int) Cell {
119	return f.Cells[f.lineOffset[y]+x]
120}
121
122func (f Frame) Set(x, y int, cell Cell) {
123	f.Cells[f.lineOffset[y]+x] = cell
124}
125
126// Row returns the backing slice for logical row y. Mutating the returned slice
127// mutates the frame in place. The slice is valid until the next scroll or
128// resize that reassigns offsets.
129func (f Frame) Row(y int) []Cell {
130	base := f.lineOffset[y]
131	return f.Cells[base : base+f.Width]
132}
133
134// ScrollUp scrolls the logical rows in [top,bottom] up by n lines by rotating
135// their physical offsets: no per-cell copying. Rows scrolled off the top are
136// recycled to the bottom and blanked in place. It assumes the full frame width
137// and 0 <= top <= bottom < Height and 0 < n <= bottom-top+1 (the caller
138// clamps). The receiver is a value, but lineOffset and Cells alias the caller's
139// backing arrays, so in-place mutations are visible to the caller.
140func (f Frame) ScrollUp(top, bottom, n int) {
141	for ; n > 0; n-- {
142		recycled := f.lineOffset[top]
143		for y := top; y < bottom; y++ {
144			f.lineOffset[y] = f.lineOffset[y+1]
145		}
146		f.lineOffset[bottom] = recycled
147		blankRow(f.Cells[recycled : recycled+f.Width])
148	}
149}
150
151// ScrollDown scrolls the logical rows in [top,bottom] down by n lines by
152// rotating their physical offsets. Rows scrolled off the bottom are recycled
153// to the top and blanked in place. See ScrollUp for preconditions.
154func (f Frame) ScrollDown(top, bottom, n int) {
155	for ; n > 0; n-- {
156		recycled := f.lineOffset[bottom]
157		for y := bottom; y > top; y-- {
158			f.lineOffset[y] = f.lineOffset[y-1]
159		}
160		f.lineOffset[top] = recycled
161		blankRow(f.Cells[recycled : recycled+f.Width])
162	}
163}
164
165func blankRow(row []Cell) {
166	for i := range row {
167		row[i] = BlankCell()
168	}
169}

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