v0.7.1 ff50ee4

scroll.go RawHistoryFolder

1package ansi
2
3import (
4	"bytes"
5	"strconv"
6
7	"github.com/bnema/vev-vt/core"
8)
9
10func findSafeScroll(frame CellSource, damage []Damage) (Damage, bool) {
11	for _, d := range damage {
12		if (d.Kind == DamageScrollUp || d.Kind == DamageScrollDown) && isSafeScroll(frame, d) {
13			return d, true
14		}
15	}
16	return Damage{}, false
17}
18
19func isSafeScroll(frame CellSource, d Damage) bool {
20	return d.X == 0 && d.Width == frame.Columns() && d.Y >= 0 && d.Y <= frame.Rows() && d.Height > 0 && d.Height <= frame.Rows()-d.Y && d.Count > 0 && d.Count < d.Height
21}
22
23func emitScroll(out *bytes.Buffer, d Damage) {
24	out.WriteString("\x1b[0m")
25	// \x1b[%d;%dr — set scroll region
26	out.WriteString("\x1b[")
27	var b [16]byte
28	n := strconv.AppendInt(b[:0], int64(d.Y+1), 10)
29	out.Write(n)
30	out.WriteByte(';')
31	n = strconv.AppendInt(b[:0], int64(d.Y+d.Height), 10)
32	out.Write(n)
33	out.WriteByte('r')
34	// SD scrolls down without moving content outside the chosen region.
35	if d.Kind == DamageScrollDown {
36		out.WriteString("\x1b[")
37		n = strconv.AppendInt(b[:0], int64(d.Count), 10)
38		out.Write(n)
39		out.WriteByte('T')
40		out.WriteString("\x1b[r")
41		return
42	}
43	// Position cursor at bottom of scroll region.
44	writeCursor(out, d.Y+d.Height-1, 0)
45	if d.Count == 1 {
46		out.WriteByte('\n')
47	} else {
48		out.WriteString("\x1b[")
49		n = strconv.AppendInt(b[:0], int64(d.Count), 10)
50		out.Write(n)
51		out.WriteByte('S')
52	}
53	out.WriteString("\x1b[r")
54}
55
56func canApplyScrollAgainst(frame CellSource, scroll Damage, damage []Damage, committed Frame) bool {
57	// Scrolling fills exposed rows with default-style blanks. Every target
58	// cell that differs from that fill must be repainted, in either direction.
59	exposed := scroll.Y + scroll.Height - scroll.Count
60	if scroll.Kind == DamageScrollDown {
61		exposed = scroll.Y
62	}
63	blank := BlankCell()
64	for y := exposed; y < exposed+scroll.Count; y++ {
65		for x := range scroll.Width {
66			column := scroll.X + x
67			if !damageCoversCell(damage, column, y) && !frame.Cell(column, y).Equal(blank) {
68				return false
69			}
70		}
71	}
72	// The plan emits one scroll plus text/clear spans. Rows outside the chosen
73	// region must therefore already match the committed shadow wherever damage
74	// does not repaint them; another scroll region in the same batch moves rows
75	// without text damage and must fall back to a snapshot.
76	if !outsideScrollMatches(frame, scroll, damage, committed) {
77		return false
78	}
79	if dense, ok := asFrame(frame); ok {
80		return canApplyDenseScrollAgainst(dense, scroll, damage, committed)
81	}
82	start, end, offset := scrollRetainedRows(scroll)
83	for y := start; y < end; y++ {
84		for x := range scroll.Width {
85			column := scroll.X + x
86			committedCell, frameCell := committed.Cell(column, y+offset), frame.Cell(column, y)
87			if committedCell == frameCell || committedCell.Equal(frameCell) {
88				continue
89			}
90			if !damageCoversCell(damage, column, y) {
91				return false
92			}
93		}
94	}
95	return true
96}
97
98func canApplyDenseScrollAgainst(frame Frame, scroll Damage, damage []Damage, committed Frame) bool {
99	start, end, offset := scrollRetainedRows(scroll)
100	for y := start; y < end; y++ {
101		// isSafeScroll guarantees full-width scrolls, so a retained row that
102		// matches whole needs no per-cell damage lookups.
103		if core.RowsEqualAt(committed, y+offset, frame, y) {
104			continue
105		}
106		for x := range scroll.Width {
107			column := scroll.X + x
108			committedCell, frameCell := committed.At(column, y+offset), frame.At(column, y)
109			if committedCell == frameCell || committedCell.Equal(frameCell) {
110				continue
111			}
112			if !damageCoversCell(damage, column, y) {
113				return false
114			}
115		}
116	}
117	return true
118}
119
120func outsideScrollMatches(frame CellSource, scroll Damage, damage []Damage, committed Frame) bool {
121	dense, isDense := asFrame(frame)
122	columns := frame.Columns()
123	for y := range frame.Rows() {
124		if y >= scroll.Y && y < scroll.Y+scroll.Height {
125			continue
126		}
127		if isDense && core.RowsEqualAt(committed, y, dense, y) {
128			continue
129		}
130		for x := range columns {
131			committedCell, frameCell := committed.Cell(x, y), frame.Cell(x, y)
132			if committedCell == frameCell || committedCell.Equal(frameCell) {
133				continue
134			}
135			if !damageCoversCell(damage, x, y) {
136				return false
137			}
138		}
139	}
140	return true
141}
142
143func scrollRetainedRows(scroll Damage) (start, end, offset int) {
144	if scroll.Kind == DamageScrollDown {
145		return scroll.Y + scroll.Count, scroll.Y + scroll.Height, -scroll.Count
146	}
147	return scroll.Y, scroll.Y + scroll.Height - scroll.Count, scroll.Count
148}
149
150func damageCoversCell(damage []Damage, x, y int) bool {
151	for _, d := range damage {
152		if (d.Kind == DamageText || d.Kind == DamageClear) && x >= d.X && x < d.X+d.Width && y >= d.Y && y < d.Y+d.Height {
153			return true
154		}
155	}
156	return false
157}

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