v0.5.0 31affde

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

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