v0.5.0 31affde

history.go RawHistoryFolder

1package vt
2
3import (
4	"errors"
5	"fmt"
6	"math"
7	"slices"
8
9	renderer "github.com/bnema/vev-vt/core"
10)
11
12// ErrHistoryRowTooLarge is returned when one row cannot fit within the
13// configured logical-byte budget or compact page bounds. History is unchanged.
14var ErrHistoryRowTooLarge = errors.New("history row exceeds logical byte capacity")
15
16var errInvalidHistoryRowID = errors.New("invalid history row ID")
17
18const maxTailPreallocCells = 32 * 1024
19
20// History stores terminal rows in immutable chunks. It is intended to be
21// mutated by the owner of a Screen; views are safe to retain after later
22// appends.
23type History struct {
24	maxRows           int
25	rowCeiling        int
26	maxBytes          uint64
27	byteCeiling       uint64
28	chunkRows         int
29	chunks            []*HistoryChunk
30	tail              [][]renderer.Cell
31	cachedTail        []*HistoryChunk
32	tailCells         []renderer.Cell
33	tailBounds        []LineBound
34	tailIDs           []RowID
35	tailBytes         uint64
36	tailPageWidth     int
37	tailPageRows      int
38	tailPageStyles    map[renderer.Style]struct{}
39	styleScratch      map[renderer.Style]struct{}
40	payloadScratch    map[renderer.CellPayload]struct{}
41	tailPagePayloads  map[renderer.CellPayload]struct{}
42	rows              int
43	cells             int
44	logicalBytes      uint64
45	nextRowID         RowID
46	compressionCursor int
47}
48
49// HistoryChunk is an immutable compact slab of equal-width history rows. Its
50// identity is stable and can be used by consumers to reuse unchanged chunks.
51type HistoryChunk struct {
52	page         *sealedPage
53	start        int
54	count        int
55	width        int
56	bounds       []LineBound
57	rowIDs       []RowID
58	styleDrops   []uint64
59	styleCount   uint64
60	payloadDrops []uint64
61	payloadBytes uint64
62}
63
64func newHistoryChunks(rows [][]renderer.Cell, bounds []LineBound, rowIDs []RowID) []*HistoryChunk {
65	bounds = growBounds(bounds, len(rows))
66	rowIDs = growRowIDs(rowIDs, len(rows))
67	var chunks []*HistoryChunk
68	for start := 0; start < len(rows); {
69		width := len(rows[start])
70		limit := start + min(len(rows)-start, maxHistorySlabRows(width))
71		end := start + 1
72		for end < limit && len(rows[end]) == width {
73			end++
74		}
75		frame := renderer.NewFrame(width, end-start)
76		lastStyleRow := make(map[renderer.Style]int)
77		for i := start; i < end; i++ {
78			frame.WriteRow(i-start, 0, rows[i])
79			for _, cell := range rows[i] {
80				style := cell.Style.Canonical()
81				if style != renderer.DefaultStyle() {
82					lastStyleRow[style] = i - start
83				}
84			}
85		}
86		styleDrops := make([]uint64, end-start)
87		for _, row := range lastStyleRow {
88			styleDrops[row]++
89		}
90		chunks = append(chunks, &HistoryChunk{
91			page:       newSealedPage(frame),
92			count:      end - start,
93			width:      width,
94			bounds:     append([]LineBound(nil), bounds[start:end]...),
95			rowIDs:     append([]RowID(nil), rowIDs[start:end]...),
96			styleDrops: styleDrops,
97			styleCount: uint64(len(lastStyleRow)) + 1,
98		})
99		chunks[len(chunks)-1].recordPayloads()
100		start = end
101	}
102	return chunks
103}
104
105func maxHistorySlabRows(width int) int {
106	if width <= 0 {
107		return math.MaxInt
108	}
109	rows := min(uint64(math.MaxUint32)/uint64(width), uint64(math.MaxInt))
110	return max(int(rows), 1)
111}
112
113func (c *HistoryChunk) len() int {
114	if c == nil {
115		return 0
116	}
117	return c.count
118}
119
120func (c *HistoryChunk) row(i int) []renderer.Cell {
121	if c == nil || i < 0 || i >= c.count {
122		return nil
123	}
124	return c.frameView().Row(c.start + i)
125}
126
127func (c *HistoryChunk) cell(row, column int) renderer.Cell {
128	return c.frameView().Cell(column, c.start+row)
129}
130
131func (c *HistoryChunk) cells() int { return c.count * c.width }
132
133// CheckInvariants validates one compact immutable history slab.
134func (c *HistoryChunk) CheckInvariants() error {
135	if c == nil || c.page == nil {
136		return fmt.Errorf("nil history chunk backing")
137	}
138	frame, err := c.page.readFrame(true)
139	if err != nil {
140		return err
141	}
142	if c.start < 0 || c.count <= 0 || c.width < 0 || c.start > frame.Height-c.count {
143		return fmt.Errorf("invalid history chunk range start=%d count=%d", c.start, c.count)
144	}
145	if frame.Width != c.width || len(c.bounds) != c.count || len(c.rowIDs) != c.count || len(c.styleDrops) != frame.Height {
146		return fmt.Errorf("history chunk metadata is not parallel")
147	}
148	styles := uint64(1)
149	for _, dropped := range c.styleDrops[c.start:] {
150		styles += dropped
151	}
152	if c.styleCount != styles {
153		return fmt.Errorf("history chunk style count = %d want %d", c.styleCount, styles)
154	}
155	var payloadBytes uint64
156	if len(c.payloadDrops) != 0 {
157		if len(c.payloadDrops) != frame.Height {
158			return fmt.Errorf("history payload metadata is not parallel")
159		}
160		for _, dropped := range c.payloadDrops[c.start:] {
161			payloadBytes += dropped
162		}
163	}
164	if payloadBytes != c.payloadBytes {
165		return fmt.Errorf("history payload accounting mismatch")
166	}
167	if err := frame.CheckInvariants(); err != nil {
168		return fmt.Errorf("history chunk frame: %w", err)
169	}
170	for i, id := range c.rowIDs {
171		if id == 0 || id >= ^RowID(0)-1 {
172			return fmt.Errorf("history row %d has invalid ID %d", i, id)
173		}
174		if !validHistoryBound(c.bounds[i], c.width) {
175			return fmt.Errorf("history row %d has invalid bound", i)
176		}
177	}
178	return nil
179}
180
181// HistoryView is an immutable snapshot of history. Row returns a copy so the
182// storage behind a sealed chunk remains owned by VT.
183type HistoryView struct {
184	chunks       []*HistoryChunk
185	rows         int
186	cells        int
187	logicalBytes uint64
188	nextRowID    RowID
189}
190
191// HistorySnapshotView captures sealed history chunks and the mutable tail
192// independently. Sealed chunks are shared by identity; Tail is owned by the
193// view and can be serialized without rotating the live tail into a chunk.
194type HistorySnapshotView struct {
195	chunks       []*HistoryChunk
196	tail         HistoryView
197	rows         int
198	cells        int
199	logicalBytes uint64
200	nextRowID    RowID
201}
202
203func historyRowBaseLogicalBytes(width int) uint64 {
204	return uint64(width)*renderer.StoredCellLogicalBytes + renderer.RowDescriptorLogicalBytes
205}
206
207func addLogicalBytes(total, value uint64) uint64 {
208	if value > math.MaxUint64-total {
209		return math.MaxUint64
210	}
211	return total + value
212}
213
214func historyChunkLogicalBytes(chunk *HistoryChunk) uint64 {
215	if chunk == nil {
216		return 0
217	}
218	return uint64(chunk.len())*historyRowBaseLogicalBytes(chunk.width) + chunk.styleCount*renderer.StyleRecordLogicalBytes + chunk.payloadBytes
219}
220
221func historyChunksLogicalBytes(chunks []*HistoryChunk) uint64 {
222	var total uint64
223	for _, chunk := range chunks {
224		total = addLogicalBytes(total, historyChunkLogicalBytes(chunk))
225	}
226	return total
227}
228
229func (h *History) prepareRowStyles(row []renderer.Cell) uint64 {
230	clear(h.payloadScratch)
231	if h.styleScratch == nil {
232		h.styleScratch = make(map[renderer.Style]struct{})
233	} else {
234		clear(h.styleScratch)
235	}
236	if len(row) > 0 {
237		firstRaw := row[0].Style
238		same := true
239		for _, cell := range row {
240			h.recordPayloadScratch(cell.Payload)
241			if cell.Style != firstRaw {
242				same = false
243			}
244		}
245		if same {
246			first := firstRaw.Canonical()
247			if first != renderer.DefaultStyle() {
248				h.styleScratch[first] = struct{}{}
249			}
250		} else {
251			for _, cell := range row {
252				style := cell.Style.Canonical()
253				if style != renderer.DefaultStyle() {
254					h.styleScratch[style] = struct{}{}
255				}
256			}
257		}
258	}
259	return historyRowBaseLogicalBytes(len(row)) + renderer.StyleRecordLogicalBytes + uint64(len(h.styleScratch))*renderer.StyleRecordLogicalBytes + h.rowPayloadBytes()
260}
261
262func (h *History) prepareChunkRowStyles(chunk *HistoryChunk, row int) uint64 {
263	clear(h.payloadScratch)
264	if h.styleScratch == nil {
265		h.styleScratch = make(map[renderer.Style]struct{})
266	} else {
267		clear(h.styleScratch)
268	}
269	for column := range chunk.width {
270		cell := chunk.cell(row, column)
271		h.recordPayloadScratch(cell.Payload)
272		style := cell.Style.Canonical()
273		if style != renderer.DefaultStyle() {
274			h.styleScratch[style] = struct{}{}
275		}
276	}
277	return historyRowBaseLogicalBytes(chunk.width) + renderer.StyleRecordLogicalBytes + uint64(len(h.styleScratch))*renderer.StyleRecordLogicalBytes + h.rowPayloadBytes()
278}
279
280func (h *History) tailAppendDelta(width int) uint64 {
281	newPage := h.tailPageRows == 0 || h.tailPageWidth != width || h.tailPageRows >= maxHistorySlabRows(width)
282	delta := historyRowBaseLogicalBytes(width)
283	if newPage {
284		return delta + renderer.StyleRecordLogicalBytes + uint64(len(h.styleScratch))*renderer.StyleRecordLogicalBytes + h.rowPayloadBytes()
285	}
286	for style := range h.styleScratch {
287		if _, ok := h.tailPageStyles[style]; !ok {
288			delta += renderer.StyleRecordLogicalBytes
289		}
290	}
291	for p := range h.payloadScratch {
292		if _, ok := h.tailPagePayloads[p]; !ok {
293			delta += p.LogicalBytes()
294		}
295	}
296	return delta
297}
298
299func (h *History) recordTailRowStyles(width int) {
300	if h.tailPageRows == 0 || h.tailPageWidth != width || h.tailPageRows >= maxHistorySlabRows(width) {
301		h.tailPageWidth = width
302		h.tailPageRows = 0
303		h.tailPageStyles = map[renderer.Style]struct{}{renderer.DefaultStyle(): {}}
304		h.tailPagePayloads = nil
305	}
306	for p := range h.payloadScratch {
307		if h.tailPagePayloads == nil {
308			h.tailPagePayloads = make(map[renderer.CellPayload]struct{})
309		}
310		h.tailPagePayloads[p] = struct{}{}
311	}
312	for style := range h.styleScratch {
313		h.tailPageStyles[style] = struct{}{}
314	}
315	h.tailPageRows++
316}
317
318func (h *History) rebuildTailAccounting() {
319	old := h.tailBytes
320	h.tailBytes = 0
321	h.tailPageWidth = 0
322	h.tailPageRows = 0
323	h.tailPageStyles = nil
324	h.tailPagePayloads = nil
325	for _, row := range h.tail {
326		h.prepareRowStyles(row)
327		delta := h.tailAppendDelta(len(row))
328		h.tailBytes += delta
329		h.recordTailRowStyles(len(row))
330	}
331	h.logicalBytes = h.logicalBytes - old + h.tailBytes
332}
333
334// Append records a copy of row along with its logical extent and an
335// automatically allocated nonzero ID. Once a chunk is full it is sealed
336// forever. Rows larger than the logical-byte capacity are rejected without
337// mutation.
338func (h *History) Append(row []renderer.Cell, bound LineBound) error {
339	if h == nil || h.maxRows == 0 {
340		return nil
341	}
342	defer func() { clear(h.payloadScratch) }()
343	if uint64(len(row)) > uint64(math.MaxUint32) || historyRowBaseLogicalBytes(len(row))+renderer.StyleRecordLogicalBytes > h.maxBytes || h.prepareRowStyles(row) > h.maxBytes {
344		return ErrHistoryRowTooLarge
345	}
346	id, err := h.allocateRowID()
347	if err != nil {
348		return err
349	}
350	return h.appendRow(row, bound, id)
351}
352
353// AppendWithID records a copy of row with an explicit persisted identity.
354// Unlike Append, malformed identities are rejected rather than synthesized.
355func (h *History) AppendWithID(row []renderer.Cell, bound LineBound, id RowID) error {
356	if id == 0 || id >= ^RowID(0)-1 {
357		return errInvalidHistoryRowID
358	}
359	if h == nil || h.maxRows == 0 {
360		return nil
361	}
362	defer func() { clear(h.payloadScratch) }()
363	if uint64(len(row)) > uint64(math.MaxUint32) || historyRowBaseLogicalBytes(len(row))+renderer.StyleRecordLogicalBytes > h.maxBytes || h.prepareRowStyles(row) > h.maxBytes {
364		return ErrHistoryRowTooLarge
365	}
366	if h.hasRowID(id) {
367		return errInvalidHistoryRowID
368	}
369	return h.appendRow(row, bound, id)
370}
371
372func (h *History) appendRow(row []renderer.Cell, bound LineBound, id RowID) error {
373	if h.nextRowID == ^RowID(0) {
374		return errInvalidHistoryRowID
375	}
376	if id >= ^RowID(0)-1 {
377		return errInvalidHistoryRowID
378	}
379	if id >= h.nextRowID {
380		h.nextRowID = id + 1
381	}
382	// Make room before adding so accounting cannot overflow even when the
383	// configured capacities use their maximum values.
384	h.evictFor(row)
385	delta := h.tailAppendDelta(len(row))
386	h.appendTailRow(row)
387	h.recordTailRowStyles(len(row))
388	h.tailBytes += delta
389	h.logicalBytes += delta
390	h.tailBounds = append(h.tailBounds, clampBound(bound, len(row)))
391	h.tailIDs = append(h.tailIDs, id)
392	h.rows++
393	h.cells += len(row)
394	if len(h.tail) == h.chunkRows {
395		h.sealTail()
396	}
397	return nil
398}
399
400// allocateRowID relies on appendRow advancing nextRowID above every accepted
401// explicit ID; automatic IDs therefore never need to scan retained rows.
402func (h *History) allocateRowID() (RowID, error) {
403	if h.nextRowID == 0 {
404		h.nextRowID = 1
405	}
406	if h.nextRowID >= ^RowID(0)-1 {
407		return 0, errInvalidHistoryRowID
408	}
409	id := h.nextRowID
410	h.nextRowID++
411	return id, nil
412}
413
414func (h *History) hasRowID(id RowID) bool {
415	for _, chunk := range h.chunks {
416		if slices.Contains(chunk.rowIDs, id) {
417			return true
418		}
419	}
420	return slices.Contains(h.tailIDs, id)
421}
422
423// clampBound keeps End inside the row it describes so a persisted bound can be
424// validated without consulting its row again.
425func clampBound(b LineBound, cells int) LineBound {
426	b.End = min(max(b.End, 0), cells)
427	return b
428}
429
430// growBounds returns bounds sized to rows, padding with zero values. It keeps
431// the parallel-slice invariant when a chunk arrives with fewer bounds than rows.
432func growBounds(bounds []LineBound, rows int) []LineBound {
433	if len(bounds) == rows {
434		return bounds
435	}
436	out := make([]LineBound, rows)
437	copy(out, bounds)
438	return out
439}
440
441func growRowIDs(ids []RowID, rows int) []RowID {
442	if len(ids) == rows {
443		return ids
444	}
445	out := make([]RowID, rows)
446	copy(out, ids)
447	return out
448}
449
450func (h *History) reserveTailRow(width int) (start, end int) {
451	h.cachedTail = nil
452	start = len(h.tailCells)
453	end = start + width
454	if end > cap(h.tailCells) {
455		capacity := end
456		if cap(h.tailCells) <= (math.MaxInt-1)/2 {
457			capacity = max(capacity, cap(h.tailCells)*2)
458		}
459		if len(h.tailCells) == 0 {
460			prealloc := uint64(maxTailPreallocCells)
461			prealloc = min(prealloc, h.maxBytes/renderer.StoredCellLogicalBytes, uint64(width)*uint64(h.chunkRows))
462			capacity = max(capacity, max(width, int(prealloc)))
463		}
464		cells := make([]renderer.Cell, len(h.tailCells), capacity)
465		copy(cells, h.tailCells)
466		h.tailCells = cells
467		offset := 0
468		for i := range h.tail {
469			rowWidth := len(h.tail[i])
470			h.tail[i] = h.tailCells[offset : offset+rowWidth : offset+rowWidth]
471			offset += rowWidth
472		}
473	}
474	h.tailCells = h.tailCells[:end]
475	return start, end
476}
477
478func (h *History) appendTailRow(row []renderer.Cell) {
479	start, end := h.reserveTailRow(len(row))
480	copy(h.tailCells[start:end], row)
481	h.tail = append(h.tail, h.tailCells[start:end:end])
482}
483
484func (h *History) appendTailChunkRow(chunk *HistoryChunk, row int) {
485	start, end := h.reserveTailRow(chunk.width)
486	for x := range chunk.width {
487		h.tailCells[start+x] = chunk.cell(row, x)
488	}
489	h.tail = append(h.tail, h.tailCells[start:end:end])
490}
491
492func (h *History) sealTail() {
493	if len(h.tail) == 0 {
494		return
495	}
496	chunks := h.tailViewChunks()
497	h.chunks = append(h.chunks, chunks...)
498	h.logicalBytes = h.logicalBytes - h.tailBytes + historyChunksLogicalBytes(chunks)
499	h.tail = nil
500	h.cachedTail = nil
501	h.tailCells = nil
502	h.tailBounds = nil
503	h.tailIDs = nil
504	h.tailBytes = 0
505	h.tailPageWidth = 0
506	h.tailPageRows = 0
507	h.tailPageStyles = nil
508	h.tailPagePayloads = nil
509}
510
511// evictFor discards oldest rows until row can fit both retention budgets.
512func (h *History) evictFor(row []renderer.Cell) {
513	for {
514		delta := h.tailAppendDelta(len(row))
515		if h.rows <= h.maxRows-1 && h.logicalBytes <= h.maxBytes-delta {
516			return
517		}
518		h.evictOldest()
519		h.prepareRowStyles(row)
520	}
521}
522
523func (h *History) evictOldest() {
524	if h.rows == 0 {
525		return
526	}
527	if len(h.chunks) > 0 {
528		chunk := h.chunks[0]
529		oldBytes := historyChunkLogicalBytes(chunk)
530		h.rows--
531		h.cells -= chunk.width
532		if chunk.count == 1 {
533			h.logicalBytes -= oldBytes
534			copy(h.chunks, h.chunks[1:])
535			h.chunks[len(h.chunks)-1] = nil
536			h.chunks = h.chunks[:len(h.chunks)-1]
537		} else {
538			// Preserve cell storage while replacing only the chunk wrapper: a
539			// retained view may still refer to the original immutable chunk. The
540			// wrapper tracks only styles still used by its retained suffix.
541			h.chunks[0] = chunk.withoutFirstRow()
542			h.logicalBytes -= oldBytes - historyChunkLogicalBytes(h.chunks[0])
543		}
544		return
545	}
546	if len(h.tail) == 0 {
547		return
548	}
549	h.cachedTail = nil
550	row := h.tail[0]
551	h.rows--
552	h.cells -= len(row)
553	h.tail[0] = nil
554	h.tail = h.tail[1:]
555	h.tailCells = h.tailCells[len(row):]
556	h.tailBounds = growBounds(h.tailBounds, len(h.tail)+1)[1:]
557	h.tailIDs = growRowIDs(h.tailIDs, len(h.tail)+1)[1:]
558	h.rebuildTailAccounting()
559}
560
561// SealAndView rotates the mutable tail into an immutable chunk, then captures
562// the sealed chunks by identity. Callers must synchronize access to History.
563func (h *History) SealAndView() HistoryView {
564	if h != nil {
565		h.sealTail()
566	}
567	return h.View()
568}
569
570func (h *History) tailViewChunks() []*HistoryChunk {
571	if h.cachedTail == nil && len(h.tail) > 0 {
572		h.cachedTail = newHistoryChunks(h.tail, h.tailBounds, h.tailIDs)
573	}
574	return h.cachedTail
575}
576
577// View captures the current history. Sealed chunks are shared by identity;
578// the immutable tail copy is reused until the mutable tail changes.
579func (h *History) View() HistoryView {
580	if h == nil {
581		return HistoryView{}
582	}
583	if h.rows == 0 {
584		return HistoryView{nextRowID: h.nextRowID}
585	}
586	chunks := make([]*HistoryChunk, len(h.chunks), len(h.chunks)+1)
587	copy(chunks, h.chunks)
588	if len(h.tail) > 0 {
589		chunks = append(chunks, h.tailViewChunks()...)
590	}
591	return HistoryView{chunks: chunks, rows: h.rows, cells: h.cells, logicalBytes: h.logicalBytes, nextRowID: h.nextRowID}
592}
593
594// SnapshotView captures history for persistence without sealing the mutable
595// tail. Sealed chunks are shared by identity and the tail is deeply copied.
596func (h *History) SnapshotView() HistorySnapshotView {
597	if h == nil {
598		return HistorySnapshotView{}
599	}
600	if h.rows == 0 {
601		return HistorySnapshotView{nextRowID: h.nextRowID}
602	}
603	tailChunks := newHistoryChunks(h.tail, h.tailBounds, h.tailIDs)
604	return HistorySnapshotView{
605		chunks: append([]*HistoryChunk(nil), h.chunks...),
606		tail: HistoryView{
607			chunks:       tailChunks,
608			rows:         len(h.tail),
609			cells:        historyRowsCells(h.tail),
610			logicalBytes: historyChunksLogicalBytes(tailChunks),
611			nextRowID:    h.nextRowID,
612		},
613		rows:         h.rows,
614		cells:        h.cells,
615		logicalBytes: h.logicalBytes,
616		nextRowID:    h.nextRowID,
617	}
618}
619
620// Len returns the currently retained history row count.
621func (h *History) Len() int {
622	if h == nil {
623		return 0
624	}
625	return h.rows
626}
627
628// Cells returns the currently retained history cell count.
629func (h *History) Cells() int {
630	if h == nil {
631		return 0
632	}
633	return h.cells
634}
635
636// LogicalBytes returns deterministic uncompressed bytes retained by history.
637// It excludes the live primary and alternate screens and has no page allowance.
638func (h *History) LogicalBytes() uint64 {
639	if h == nil {
640		return 0
641	}
642	return h.logicalBytes
643}
644
645// NextRowID returns the next identity allocated by this history.
646func (h *History) NextRowID() RowID {
647	if h == nil || h.nextRowID == 0 {
648		return 1
649	}
650	return h.nextRowID
651}
652
653// Cap returns the configured row ceiling; zero means no independent row
654// ceiling, or disabled history when ByteCap is also zero.
655func (h *History) Cap() int {
656	if h == nil {
657		return 0
658	}
659	return h.rowCeiling
660}
661
662// ByteCap returns the configured uncompressed byte ceiling. Zero means no
663// independent byte ceiling, or disabled history when Cap is also zero.
664func (h *History) ByteCap() uint64 {
665	if h == nil {
666		return 0
667	}
668	return h.byteCeiling
669}
670
671func (v HistoryView) Len() int   { return v.rows }
672func (v HistoryView) Cells() int { return v.cells }
673func (v HistoryView) LogicalBytes() uint64 {
674	if v.logicalBytes != 0 || len(v.chunks) == 0 {
675		return v.logicalBytes
676	}
677	return historyChunksLogicalBytes(v.chunks)
678}
679func (v HistoryView) ChunkCount() int { return len(v.chunks) }
680
681// NextRowID returns the next identity recorded by this view.
682func (v HistoryView) NextRowID() RowID {
683	if v.nextRowID == 0 {
684		next, ok := historyViewNextRowID(v)
685		if !ok {
686			return 1
687		}
688		return next
689	}
690	return v.nextRowID
691}
692
693// Chunk returns the immutable chunk at i, or nil when i is out of range.
694func (v HistoryView) Chunk(i int) *HistoryChunk {
695	if i < 0 || i >= len(v.chunks) {
696		return nil
697	}
698	return v.chunks[i]
699}
700
701// Range calls yield with a decoded owned row in oldest-first order. Returning
702// false stops iteration successfully; a backing restore failure returns an error.
703func (v HistoryView) Range(yield func([]renderer.Cell) bool) error {
704	for _, chunk := range v.chunks {
705		// Streaming search restores at most one transient page at a time and
706		// does not populate the random-access cache of every cold page.
707		frame, err := chunk.page.readFrame(false)
708		if err != nil {
709			return err
710		}
711		for i := range chunk.len() {
712			if !yield(frame.Row(chunk.start + i)) {
713				return nil
714			}
715		}
716	}
717	return nil
718}
719
720// Row returns a decoded owned copy of the row at i, or nil when out of range.
721func (v HistoryView) Row(i int) []renderer.Cell {
722	chunk, row := v.locateRow(i)
723	if chunk == nil {
724		return nil
725	}
726	return chunk.row(row)
727}
728
729// RowID returns the identity of the row at i, or zero when i is out of range.
730func (v HistoryView) RowID(i int) RowID {
731	if i < 0 {
732		return 0
733	}
734	for _, chunk := range v.chunks {
735		if i < chunk.len() {
736			if i < len(chunk.rowIDs) {
737				return chunk.rowIDs[i]
738			}
739			return 0
740		}
741		i -= chunk.len()
742	}
743	return 0
744}
745
746// FindRowID returns the oldest-first index of id, or -1 when id is absent.
747func (v HistoryView) FindRowID(id RowID) int {
748	if id == 0 {
749		return -1
750	}
751	index := 0
752	for _, chunk := range v.chunks {
753		for row := range chunk.len() {
754			if row < len(chunk.rowIDs) && chunk.rowIDs[row] == id {
755				return index
756			}
757			index++
758		}
759	}
760	return -1
761}
762
763// RowID returns the identity of the row within an immutable chunk.
764func (c *HistoryChunk) RowID(i int) RowID {
765	if c == nil || i < 0 || i >= len(c.rowIDs) {
766		return 0
767	}
768	return c.rowIDs[i]
769}
770
771// Bound returns the logical extent of the row at i, or the zero value when i is
772// out of range. A zero value describes a hard row whose End is not meaningful.
773func (v HistoryView) Bound(i int) LineBound {
774	if i < 0 {
775		return LineBound{}
776	}
777	for _, chunk := range v.chunks {
778		if i < chunk.len() {
779			if i < len(chunk.bounds) {
780				return chunk.bounds[i]
781			}
782			return LineBound{}
783		}
784		i -= chunk.len()
785	}
786	return LineBound{}
787}
788
789func (v HistorySnapshotView) Len() int   { return v.rows }
790func (v HistorySnapshotView) Cells() int { return v.cells }
791func (v HistorySnapshotView) LogicalBytes() uint64 {
792	if v.logicalBytes != 0 || v.rows == 0 {
793		return v.logicalBytes
794	}
795	return addLogicalBytes(historyChunksLogicalBytes(v.chunks), v.tail.LogicalBytes())
796}
797func (v HistorySnapshotView) ChunkCount() int { return len(v.chunks) }
798
799// NextRowID returns the next identity recorded by this snapshot.
800func (v HistorySnapshotView) NextRowID() RowID {
801	if v.nextRowID == 0 {
802		return v.Tail().NextRowID()
803	}
804	return v.nextRowID
805}
806
807// Chunk returns a sealed immutable chunk at i, or nil when i is out of range.
808func (v HistorySnapshotView) Chunk(i int) *HistoryChunk {
809	if i < 0 || i >= len(v.chunks) {
810		return nil
811	}
812	return v.chunks[i]
813}
814
815// Tail returns an immutable view of the copied mutable tail.
816func (v HistorySnapshotView) Tail() HistoryView {
817	tail := v.tail
818	tail.nextRowID = v.nextRowID
819	return tail
820}
821
822func historyRowsCells(rows [][]renderer.Cell) int {
823	cells := 0
824	for _, row := range rows {
825		cells += len(row)
826	}
827	return cells
828}

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