v0.1.0 7deb448

history.go RawHistoryFolder

1package vt
2
3import (
4	"errors"
5	"math"
6
7	"github.com/bnema/vev/pkg/renderer"
8)
9
10// ErrHistoryRowTooWide is returned when a row cannot fit within the configured
11// cell budget. The history is not modified when this error is returned.
12var ErrHistoryRowTooWide = errors.New("history row exceeds cell capacity")
13
14// HistoryConfig controls the bounded terminal history retained by a Screen.
15type HistoryConfig struct {
16	MaxRows   int
17	MaxCells  int
18	ChunkRows int
19}
20
21// History stores terminal rows in immutable chunks. It is intended to be
22// mutated by the owner of a Screen; views are safe to retain after later
23// appends.
24type History struct {
25	maxRows    int
26	maxCells   int
27	chunkRows  int
28	chunks     []*HistoryChunk
29	tail       [][]renderer.Cell
30	tailBounds []LineBound
31	rows       int
32	cells      int
33}
34
35// HistoryChunk is an immutable group of history rows. Its identity is stable
36// and can be used by consumers to avoid copying unchanged sealed chunks.
37// bounds is parallel to rows and always has the same length.
38type HistoryChunk struct {
39	rows   [][]renderer.Cell
40	bounds []LineBound
41}
42
43// HistoryView is an immutable snapshot of history. Row returns a copy so the
44// storage behind a sealed chunk remains owned by VT.
45type HistoryView struct {
46	chunks []*HistoryChunk
47	rows   int
48	cells  int
49}
50
51// HistorySnapshotView captures sealed history chunks and the mutable tail
52// independently. Sealed chunks are shared by identity; Tail is owned by the
53// view and can be serialized without rotating the live tail into a chunk.
54type HistorySnapshotView struct {
55	chunks     []*HistoryChunk
56	tail       [][]renderer.Cell
57	tailBounds []LineBound
58	rows       int
59	cells      int
60}
61
62func NewHistory(config HistoryConfig) *History {
63	if config.MaxRows <= 0 {
64		return &History{}
65	}
66	chunkRows := config.ChunkRows
67	if chunkRows <= 0 {
68		chunkRows = 256
69	}
70	chunkRows = min(chunkRows, 256)
71	chunkRows = min(chunkRows, config.MaxRows)
72	maxCells := config.MaxCells
73	if maxCells <= 0 {
74		maxCells = defaultHistoryMaxCells(config.MaxRows)
75	}
76	return &History{maxRows: config.MaxRows, maxCells: maxCells, chunkRows: chunkRows}
77}
78
79func defaultHistoryMaxCells(maxRows int) int {
80	if maxRows > math.MaxInt/160 {
81		return math.MaxInt
82	}
83	return maxRows * 160
84}
85
86// Append records a copy of row along with its logical extent. Once a chunk is
87// full it is sealed forever. Rows wider than the total cell capacity are
88// rejected without mutation.
89func (h *History) Append(row []renderer.Cell, bound LineBound) error {
90	if h == nil || h.maxRows == 0 {
91		return nil
92	}
93	if len(row) > h.maxCells {
94		return ErrHistoryRowTooWide
95	}
96	// Make room before adding so Cells cannot overflow even when the default
97	// capacity is MaxInt.
98	h.evictFor(len(row))
99	h.tail = append(h.tail, append([]renderer.Cell(nil), row...))
100	h.tailBounds = append(h.tailBounds, clampBound(bound, len(row)))
101	h.rows++
102	h.cells += len(row)
103	if len(h.tail) == h.chunkRows {
104		h.sealTail()
105	}
106	return nil
107}
108
109// clampBound keeps End inside the row it describes so a persisted bound can be
110// validated without consulting its row again.
111func clampBound(b LineBound, cells int) LineBound {
112	b.End = min(max(b.End, 0), cells)
113	return b
114}
115
116// growBounds returns bounds sized to rows, padding with zero values. It keeps
117// the parallel-slice invariant when a chunk arrives with fewer bounds than rows.
118func growBounds(bounds []LineBound, rows int) []LineBound {
119	if len(bounds) == rows {
120		return bounds
121	}
122	out := make([]LineBound, rows)
123	copy(out, bounds)
124	return out
125}
126
127func (h *History) sealTail() {
128	if len(h.tail) == 0 {
129		return
130	}
131	h.chunks = append(h.chunks, &HistoryChunk{rows: h.tail, bounds: growBounds(h.tailBounds, len(h.tail))})
132	h.tail = nil
133	h.tailBounds = nil
134}
135
136// normalizeTail seals complete chunks so the mutable tail remains shorter than
137// chunkRows. Restored snapshots may have been written with a different chunk
138// size than the current history configuration.
139func (h *History) normalizeTail() {
140	for h.chunkRows > 0 && len(h.tail) >= h.chunkRows {
141		h.chunks = append(h.chunks, &HistoryChunk{
142			rows:   h.tail[:h.chunkRows],
143			bounds: growBounds(h.tailBounds, len(h.tail))[:h.chunkRows],
144		})
145		h.tail = h.tail[h.chunkRows:]
146		h.tailBounds = growBounds(h.tailBounds, len(h.tail)+h.chunkRows)[h.chunkRows:]
147	}
148}
149
150func (h *History) evict() {
151	h.evictUntil(0, 0)
152}
153
154// evictFor discards oldest rows until another row using cellCount cells can fit.
155func (h *History) evictFor(cellCount int) {
156	h.evictUntil(1, cellCount)
157}
158
159// evictUntil makes room for rowCount rows and cellCount cells without
160// overflowing either accounting total.
161func (h *History) evictUntil(rowCount, cellCount int) {
162	for h.rows > h.maxRows-rowCount || h.cells > h.maxCells-cellCount {
163		if len(h.chunks) > 0 {
164			chunk := h.chunks[0]
165			row := chunk.rows[0]
166			h.rows--
167			h.cells -= len(row)
168			if len(chunk.rows) == 1 {
169				copy(h.chunks, h.chunks[1:])
170				h.chunks[len(h.chunks)-1] = nil
171				h.chunks = h.chunks[:len(h.chunks)-1]
172			} else {
173				// Preserve cell storage while replacing only the chunk wrapper: a
174				// retained view may still refer to the original immutable chunk.
175				h.chunks[0] = &HistoryChunk{rows: chunk.rows[1:], bounds: growBounds(chunk.bounds, len(chunk.rows))[1:]}
176			}
177			continue
178		}
179		if len(h.tail) == 0 {
180			return
181		}
182		row := h.tail[0]
183		h.rows--
184		h.cells -= len(row)
185		h.tail[0] = nil
186		h.tail = h.tail[1:]
187		h.tailBounds = growBounds(h.tailBounds, len(h.tail)+1)[1:]
188	}
189}
190
191// SealAndView rotates the mutable tail into an immutable chunk, then captures
192// the sealed chunks by identity. Callers must synchronize access to History.
193func (h *History) SealAndView() HistoryView {
194	if h != nil {
195		h.sealTail()
196	}
197	return h.View()
198}
199
200// View captures the current history. Sealed chunks are shared by identity; a
201// partially-filled tail is copied into a new immutable chunk for this view.
202func (h *History) View() HistoryView {
203	if h == nil || h.rows == 0 {
204		return HistoryView{}
205	}
206	chunks := append([]*HistoryChunk(nil), h.chunks...)
207	if len(h.tail) > 0 {
208		chunks = append(chunks, &HistoryChunk{
209			rows:   cloneHistoryRows(h.tail),
210			bounds: append([]LineBound(nil), growBounds(h.tailBounds, len(h.tail))...),
211		})
212	}
213	return HistoryView{chunks: chunks, rows: h.rows, cells: h.cells}
214}
215
216// SnapshotView captures history for persistence without sealing the mutable
217// tail. Sealed chunks are shared by identity and the tail is deeply copied.
218func (h *History) SnapshotView() HistorySnapshotView {
219	if h == nil || h.rows == 0 {
220		return HistorySnapshotView{}
221	}
222	return HistorySnapshotView{
223		chunks:     append([]*HistoryChunk(nil), h.chunks...),
224		tail:       cloneHistoryRows(h.tail),
225		tailBounds: append([]LineBound(nil), growBounds(h.tailBounds, len(h.tail))...),
226		rows:       h.rows,
227		cells:      h.cells,
228	}
229}
230
231func cloneHistoryRows(rows [][]renderer.Cell) [][]renderer.Cell {
232	if len(rows) == 0 {
233		return nil
234	}
235	copyRows := make([][]renderer.Cell, len(rows))
236	for i, row := range rows {
237		copyRows[i] = append([]renderer.Cell(nil), row...)
238	}
239	return copyRows
240}
241
242// Len returns the currently retained history row count.
243func (h *History) Len() int {
244	if h == nil {
245		return 0
246	}
247	return h.rows
248}
249
250// Cells returns the currently retained history cell count.
251func (h *History) Cells() int {
252	if h == nil {
253		return 0
254	}
255	return h.cells
256}
257
258// Cap returns the configured bounded row capacity.
259func (h *History) Cap() int {
260	if h == nil {
261		return 0
262	}
263	return h.maxRows
264}
265
266// CellCap returns the configured bounded cell capacity.
267func (h *History) CellCap() int {
268	if h == nil {
269		return 0
270	}
271	return h.maxCells
272}
273
274func (v HistoryView) Len() int        { return v.rows }
275func (v HistoryView) Cells() int      { return v.cells }
276func (v HistoryView) ChunkCount() int { return len(v.chunks) }
277
278// Chunk returns the immutable chunk at i, or nil when i is out of range.
279func (v HistoryView) Chunk(i int) *HistoryChunk {
280	if i < 0 || i >= len(v.chunks) {
281		return nil
282	}
283	return v.chunks[i]
284}
285
286// Range calls yield for each row in oldest-first order. The row is borrowed
287// immutable storage: yield must not mutate or retain it after returning.
288func (v HistoryView) Range(yield func([]renderer.Cell) bool) {
289	for _, chunk := range v.chunks {
290		for _, row := range chunk.rows {
291			if !yield(row) {
292				return
293			}
294		}
295	}
296}
297
298// Row returns a copy of the row at i, or nil when i is out of range.
299func (v HistoryView) Row(i int) []renderer.Cell {
300	return append([]renderer.Cell(nil), v.BorrowedRow(i)...)
301}
302
303// BorrowedRow returns immutable storage for the row at i, or nil when i is out
304// of range. The caller must not mutate or retain the result after it no longer
305// retains v. Consumers that need ownership should use Row instead.
306func (v HistoryView) BorrowedRow(i int) []renderer.Cell {
307	if i < 0 {
308		return nil
309	}
310	for _, chunk := range v.chunks {
311		if i < len(chunk.rows) {
312			return chunk.rows[i]
313		}
314		i -= len(chunk.rows)
315	}
316	return nil
317}
318
319// Bound returns the logical extent of the row at i, or the zero value when i is
320// out of range. A zero value describes a hard row whose End is not meaningful.
321func (v HistoryView) Bound(i int) LineBound {
322	if i < 0 {
323		return LineBound{}
324	}
325	for _, chunk := range v.chunks {
326		if i < len(chunk.rows) {
327			if i < len(chunk.bounds) {
328				return chunk.bounds[i]
329			}
330			return LineBound{}
331		}
332		i -= len(chunk.rows)
333	}
334	return LineBound{}
335}
336
337func (v HistorySnapshotView) Len() int        { return v.rows }
338func (v HistorySnapshotView) Cells() int      { return v.cells }
339func (v HistorySnapshotView) ChunkCount() int { return len(v.chunks) }
340
341// Chunk returns a sealed immutable chunk at i, or nil when i is out of range.
342func (v HistorySnapshotView) Chunk(i int) *HistoryChunk {
343	if i < 0 || i >= len(v.chunks) {
344		return nil
345	}
346	return v.chunks[i]
347}
348
349// Tail returns an immutable view of the copied mutable tail.
350func (v HistorySnapshotView) Tail() HistoryView {
351	if len(v.tail) == 0 {
352		return HistoryView{}
353	}
354	return HistoryView{
355		chunks: []*HistoryChunk{{rows: v.tail, bounds: growBounds(v.tailBounds, len(v.tail))}},
356		rows:   len(v.tail),
357		cells:  historyRowsCells(v.tail),
358	}
359}
360
361func historyRowsCells(rows [][]renderer.Cell) int {
362	cells := 0
363	for _, row := range rows {
364		cells += len(row)
365	}
366	return cells
367}

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 12

Tags 11

↑↓ moveenter openesc close