v0.2.0 4859eb8

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 Frame, plan DeltaPlan) DeltaCandidate {
34	candidate := DeltaCandidate{Plan: plan}
35	if plan.Snapshot || plan.Scroll.Height != 0 || len(plan.Spans) != 0 {
36		candidate.frame = frame.Clone()
37	}
38	return candidate
39}
40
41// PlanDelta prepares a non-mutating update from committed to frame.
42func PlanDelta(frame Frame, damage []Damage, committed Frame, reset bool) (DeltaCandidate, error) {
43	if err := frame.Validate(); err != nil {
44		return DeltaCandidate{}, err
45	}
46
47	if reset || frame.Width != committed.Width || frame.Height != committed.Height {
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 Frame, 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 Frame) ([]Span, bool) {
108	var spans []Span
109	for y := range frame.Height {
110		frameRow := frame.Row(y)
111		committedRow := committed.Row(y)
112		dirty := false
113		for x := range frame.Width {
114			if !frameRow[x].Equal(committedRow[x]) {
115				dirty = true
116				break
117			}
118		}
119		if dirty {
120			if len(spans) == maxPlannedDamageSpans {
121				return nil, true
122			}
123			spans = append(spans, Span{Y: y, Width: frame.Width})
124		}
125	}
126	return spans, false
127}
128
129func deltaCostsSnapshot(frame Frame, spans []Span, hasScroll bool) bool {
130	if !hasScroll && frame.Height > 1 && len(spans) == 1 {
131		return false
132	}
133
134	var deltaCost int64
135	if hasScroll {
136		deltaCost = 6
137	}
138	for _, span := range spans {
139		deltaCost += 12 + int64(span.Width)
140	}
141
142	minimumSnapshotCost := int64(frame.Height) * (12 + int64(frame.Width))
143	if deltaCost < minimumSnapshotCost {
144		return false
145	}
146	maximumSnapshotCost := int64(frame.Height) * (8 + 5*int64(frame.Width))
147	if deltaCost >= maximumSnapshotCost {
148		return true
149	}
150	if !hasScroll && fullWidthRows(frame, spans) {
151		return true
152	}
153
154	for _, span := range spans {
155		deltaCost += 4 * int64(styleRunCount(frame.Row(span.Y)[span.X:span.X+span.Width])-1)
156	}
157	var snapshotCost int64
158	for y := range frame.Height {
159		snapshotCost += spanCost(frame, y, 0, frame.Width)
160	}
161	return deltaCost >= snapshotCost
162}
163
164func fullWidthRows(frame Frame, spans []Span) bool {
165	if len(spans) != frame.Height {
166		return false
167	}
168	for y, span := range spans {
169		if span.Y != y || span.X != 0 || span.Width != frame.Width {
170			return false
171		}
172	}
173	return true
174}
175
176func spanCost(frame Frame, y, x, width int) int64 {
177	return 8 + int64(width) + 4*int64(styleRunCount(frame.Row(y)[x:x+width]))
178}
179
180func styleRunCount(cells []Cell) int {
181	if len(cells) == 0 {
182		return 0
183	}
184	runs := 1
185	for i := 1; i < len(cells); i++ {
186		if cells[i-1].Style != cells[i].Style && !cells[i-1].Style.Equal(cells[i].Style) {
187			runs++
188		}
189	}
190	return runs
191}
192
193// Commit advances dst to the frame snapshot owned by the candidate.
194func (c DeltaCandidate) Commit(dst *Frame) {
195	if !c.Plan.Snapshot && c.Plan.Scroll.Height == 0 && len(c.Plan.Spans) == 0 {
196		return
197	}
198	if c.Plan.Snapshot || dst.Width != c.frame.Width || dst.Height != c.frame.Height {
199		replaceFrame(dst, c.frame)
200		return
201	}
202	if c.Plan.Scroll.Height != 0 {
203		scroll := c.Plan.Scroll
204		dst.ScrollUp(scroll.Y, scroll.Y+scroll.Height-1, scroll.Count)
205	}
206	for _, span := range c.Plan.Spans {
207		dstRow := dst.Row(span.Y)
208		srcRow := c.frame.Row(span.Y)
209		copy(dstRow[span.X:span.X+span.Width], srcRow[span.X:span.X+span.Width])
210	}
211}
212
213func replaceFrame(dst *Frame, src Frame) {
214	dst.Replace(src)
215}

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