v0.5.1 8e4df5c

delta_plan.go RawHistoryFolder

1package ansi
2
3// Span identifies a changed horizontal range in a logical frame row.
4type Span struct {
5	Y     int
6	X     int
7	Width int
8}
9
10// Scroll identifies a safe full-width scroll.
11type Scroll struct {
12	Y      int
13	Height int
14	Count  int
15	Down   bool
16}
17
18// DeltaPlan describes how to advance a committed frame to the candidate frame.
19// Snapshot requests replacement of the committed frame; Spans, when present,
20// are the canonical ranges to encode before that replacement.
21type DeltaPlan struct {
22	Snapshot bool
23	Scroll   Scroll
24	Spans    []Span
25}
26
27// DeltaCandidate holds a plan and owns a source frame snapshot when the plan
28// has work to commit. Empty candidates are true no-ops.
29type DeltaCandidate struct {
30	Plan  DeltaPlan
31	frame Frame
32}
33
34func newDeltaCandidate(frame CellSource, plan DeltaPlan) DeltaCandidate {
35	candidate := DeltaCandidate{Plan: plan}
36	if plan.Snapshot || plan.Scroll.Height != 0 || len(plan.Spans) != 0 {
37		candidate.frame = cloneCellSource(frame)
38	}
39	return candidate
40}
41
42// PlanDelta prepares a non-mutating update from committed to frame.
43func PlanDelta(frame CellSource, damage []Damage, committed Frame, reset bool) (DeltaCandidate, error) {
44	if err := validateCellSource(frame); err != nil {
45		return DeltaCandidate{}, err
46	}
47
48	if reset || frame.Columns() != committed.Columns() || frame.Rows() != committed.Rows() {
49		return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}), nil
50	}
51	if err := committed.Validate(); err != nil {
52		return DeltaCandidate{}, err
53	}
54
55	if len(damage) == 1 && (damage[0].Kind == DamageText || damage[0].Kind == DamageClear) {
56		return newDeltaCandidate(frame, planSingleDamage(frame, damage[0])), nil
57	}
58
59	if needsFull(damage) {
60		dirty, full := buildDirtyLinePlan(frame, committed)
61		if len(dirty) > 0 || full {
62			return newDeltaCandidate(frame, DeltaPlan{Snapshot: true, Spans: dirty}), nil
63		}
64		return DeltaCandidate{}, nil
65	}
66
67	var plan DeltaPlan
68	var skip *Damage
69	if scroll, ok := findSafeScroll(frame, damage); ok && canApplyScrollAgainst(frame, scroll, damage, committed) {
70		plan.Scroll = Scroll{Y: scroll.Y, Height: scroll.Height, Count: scroll.Count, Down: scroll.Kind == DamageScrollDown}
71		skip = &scroll
72	} else if hasScrollDamage(damage) {
73		return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}), nil
74	}
75
76	var spans []Span
77	var full bool
78	if len(damage) == 0 {
79		spans, full = buildDirtyLinePlan(frame, committed)
80	} else {
81		spans, full = buildDamagePlan(frame, damage, skip)
82	}
83	if full {
84		return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}), nil
85	}
86	if deltaCostsSnapshot(frame, spans, plan.Scroll.Height != 0) {
87		if plan.Scroll.Height != 0 {
88			return newDeltaCandidate(frame, DeltaPlan{Snapshot: true}), nil
89		}
90		return newDeltaCandidate(frame, DeltaPlan{Snapshot: true, Spans: spans}), nil
91	}
92	plan.Spans = spans
93	return newDeltaCandidate(frame, plan), nil
94}
95
96func planSingleDamage(frame CellSource, d Damage) DeltaPlan {
97	spans, full := buildSingleDamageSpans(frame, d)
98	if full {
99		return DeltaPlan{Snapshot: true}
100	}
101	plan := DeltaPlan{Spans: spans}
102	if deltaCostsSnapshot(frame, spans, false) {
103		plan.Snapshot = true
104	}
105	return plan
106}
107
108func buildDirtyLinePlan(frame, committed CellSource) ([]Span, bool) {
109	var spans []Span
110	for y := range frame.Rows() {
111		dirty := false
112		for x := range frame.Columns() {
113			if !frame.Cell(x, y).Equal(committed.Cell(x, y)) {
114				dirty = true
115				break
116			}
117		}
118		if dirty {
119			if len(spans) == maxPlannedDamageSpans {
120				return nil, true
121			}
122			spans = append(spans, Span{Y: y, Width: frame.Columns()})
123		}
124	}
125	return spans, false
126}
127
128func deltaCostsSnapshot(frame CellSource, spans []Span, hasScroll bool) bool {
129	if !hasScroll && frame.Rows() > 1 && len(spans) == 1 {
130		return false
131	}
132
133	var deltaCost int64
134	if hasScroll {
135		deltaCost = 6
136	}
137	for _, span := range spans {
138		deltaCost += 12 + int64(span.Width)
139	}
140
141	minimumSnapshotCost := int64(frame.Rows()) * (12 + int64(frame.Columns()))
142	if deltaCost < minimumSnapshotCost {
143		return false
144	}
145	maximumSnapshotCost := int64(frame.Rows()) * (8 + 5*int64(frame.Columns()))
146	if deltaCost >= maximumSnapshotCost {
147		return true
148	}
149	if !hasScroll && fullWidthRows(frame, spans) {
150		return true
151	}
152
153	for _, span := range spans {
154		deltaCost += 4 * int64(styleRunCountIn(frame, span.Y, span.X, span.Width)-1)
155	}
156	var snapshotCost int64
157	for y := range frame.Rows() {
158		snapshotCost += spanCost(frame, y, 0, frame.Columns())
159	}
160	return deltaCost >= snapshotCost
161}
162
163func fullWidthRows(frame CellSource, spans []Span) bool {
164	if len(spans) != frame.Rows() {
165		return false
166	}
167	for y, span := range spans {
168		if span.Y != y || span.X != 0 || span.Width != frame.Columns() {
169			return false
170		}
171	}
172	return true
173}
174
175func spanCost(frame CellSource, y, x, width int) int64 {
176	return 8 + int64(width) + 4*int64(styleRunCountIn(frame, y, x, width))
177}
178
179func styleRunCountIn(frame CellSource, y, x, width int) int {
180	if width == 0 {
181		return 0
182	}
183	runs := 1
184	previous := frame.Cell(x, y).Style
185	for column := x + 1; column < x+width; column++ {
186		style := frame.Cell(column, y).Style
187		if previous != style && !previous.Equal(style) {
188			runs++
189		}
190		previous = style
191	}
192	return runs
193}
194
195// Commit advances dst to the frame snapshot owned by the candidate.
196func (c DeltaCandidate) Commit(dst *Frame) {
197	if !c.Plan.Snapshot && c.Plan.Scroll.Height == 0 && len(c.Plan.Spans) == 0 {
198		return
199	}
200	if c.Plan.Snapshot || dst.Width != c.frame.Width || dst.Height != c.frame.Height {
201		replaceFrame(dst, c.frame)
202		return
203	}
204	if c.Plan.Scroll.Height != 0 {
205		scroll := c.Plan.Scroll
206		if scroll.Down {
207			dst.ScrollDown(scroll.Y, scroll.Y+scroll.Height-1, scroll.Count)
208		} else {
209			dst.ScrollUp(scroll.Y, scroll.Y+scroll.Height-1, scroll.Count)
210		}
211	}
212	for _, span := range c.Plan.Spans {
213		for x := span.X; x < span.X+span.Width; x++ {
214			dst.Set(x, span.Y, c.frame.Cell(x, span.Y))
215		}
216	}
217}
218
219func replaceFrame(dst *Frame, src Frame) {
220	dst.Replace(src)
221}

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