v0.1.0 af61ae3

damage_plan.go RawHistoryFolder

1package ansi
2
3import (
4	"cmp"
5	"slices"
6)
7
8const maxPlannedDamageSpans = 4096
9
10// buildDamagePlan creates the bounded, canonical view of text and clear damage
11// used for terminal emission. The source damage remains untouched because the
12// scroll and shadow paths need its original structural information.
13func buildDamagePlan(frame Frame, damage []Damage, skip *Damage) ([]Span, bool) {
14	if len(damage) == 1 {
15		d := damage[0]
16		if skip != nil && sameDamage(d, *skip) {
17			return nil, false
18		}
19		if d.Kind != DamageText && d.Kind != DamageClear {
20			return nil, false
21		}
22		return buildSingleDamageSpans(frame, d)
23	}
24
25	spans := make([]Span, 0)
26	for _, d := range damage {
27		if skip != nil && sameDamage(d, *skip) {
28			continue
29		}
30		if d.Kind != DamageText && d.Kind != DamageClear {
31			continue
32		}
33		x, y, width, height, ok := clampRect(frame, d.X, d.Y, d.Width, d.Height)
34		if !ok {
35			continue
36		}
37		for row := y; row < y+height; row++ {
38			if len(spans) == maxPlannedDamageSpans {
39				return nil, true
40			}
41			spans = append(spans, Span{Y: row, X: x, Width: width})
42		}
43	}
44
45	slices.SortFunc(spans, func(a, b Span) int {
46		if c := cmp.Compare(a.Y, b.Y); c != 0 {
47			return c
48		}
49		return cmp.Compare(a.X, b.X)
50	})
51
52	return mergeDamageSpans(spans), false
53}
54
55func buildSingleDamageSpans(frame Frame, d Damage) ([]Span, bool) {
56	x, y, width, height, ok := clampRect(frame, d.X, d.Y, d.Width, d.Height)
57	if !ok {
58		return nil, false
59	}
60	if height > maxPlannedDamageSpans {
61		return nil, true
62	}
63	spans := make([]Span, height)
64	for row := range height {
65		spans[row] = Span{Y: y + row, X: x, Width: width}
66	}
67	return spans, false
68}
69
70func mergeDamageSpans(spans []Span) []Span {
71	if len(spans) < 2 {
72		return spans
73	}
74
75	out := spans[:0]
76	for _, span := range spans {
77		if len(out) == 0 {
78			out = append(out, span)
79			continue
80		}
81		last := &out[len(out)-1]
82		// Both endpoints are bounded by frame.Width after clampRect, so these
83		// additions cannot overflow. The comparison includes adjacency.
84		lastEnd := last.X + last.Width
85		spanEnd := span.X + span.Width
86		if span.Y != last.Y || span.X > lastEnd {
87			out = append(out, span)
88			continue
89		}
90		if spanEnd > lastEnd {
91			last.Width = spanEnd - last.X
92		}
93	}
94	return out
95}

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