v0.6.0 b250c5e

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

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