main ab6686c

delta_plan.go RawHistoryFolder

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

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