v0.3.0 52147d7

screen_scroll.go RawHistoryFolder

1package vt
2
3import (
4	"errors"
5
6	renderer "github.com/bnema/vev-vt/core"
7)
8
9// index moves to the next physical row, scrolling the region when the cursor
10// already sits on its last row. It reports whether the row the cursor was on
11// survived that scroll by moving up to s.Row-1, which only a deferred wrap
12// cares about: a wrap must relink to the row it flowed out of, and
13// buffer.scrollUp severs that link on the way. Callers that merely advance,
14// such as LF and ESC D, ignore the result.
15func (s *Screen) index() (movedUp bool) {
16	if s.Frame.Height == 0 {
17		return false
18	}
19	if s.Row == s.scrollBottom && s.Row >= s.scrollTop {
20		return s.scrollUpRegion(s.scrollTop, s.scrollBottom, 1)
21	}
22	if s.Row+1 < s.Frame.Height {
23		s.Row++
24		return false
25	}
26	s.Row = s.Frame.Height - 1
27	return false
28}
29
30func (s *Screen) nextLine() {
31	s.buffer.hard(s.Row)
32	s.Col = 0
33	s.index()
34}
35
36func (s *Screen) reverseIndex() {
37	if s.Frame.Height == 0 {
38		return
39	}
40	if s.Row == s.scrollTop && s.Row <= s.scrollBottom {
41		s.scrollDownRegion(s.scrollTop, s.scrollBottom, 1)
42		return
43	}
44	if s.Row > 0 {
45		s.Row--
46	}
47}
48
49// scrollUpBy scrolls the current scroll region up by n lines (CSI S / SU).
50// The cursor position is preserved.
51func (s *Screen) scrollUpBy(n int) {
52	if n <= 0 {
53		return
54	}
55	s.scrollUpRegion(s.scrollTop, s.scrollBottom, n)
56}
57
58func (s *Screen) scrollDownBy(n int) {
59	if n <= 0 {
60		return
61	}
62	s.scrollDownRegion(s.scrollTop, s.scrollBottom, n)
63}
64
65// scrollUpRegion scrolls [top,bottom] up by n. It reports whether any row
66// survived by moving up: a region exactly n rows tall is blanked in place, so
67// the receiving range [top,bottom-n] is empty and nothing survives. Reporting
68// what the scroll observed keeps callers from re-deriving a predicate that this
69// function may have declined to act on at all.
70func (s *Screen) scrollUpRegion(top, bottom, n int) (shifted bool) {
71	if s.Frame.Width == 0 || s.Frame.Height == 0 || n <= 0 {
72		return false
73	}
74	top, bottom, ok := s.normalizedRegion(top, bottom)
75	if !ok {
76		return false
77	}
78	w := s.Frame.Width
79	height := bottom - top + 1
80	if n > height {
81		n = height
82	}
83	// VT scroll regions always span the full frame width, so we rotate the
84	// frame's line offsets (recycling and blanking the evicted rows in place)
85	// instead of copying cells. See renderer.Frame.ScrollUp.
86	s.emitLineEvicted(top, n)
87	s.Frame.ScrollUp(top, bottom, n)
88	s.buffer.scrollUp(top, bottom, n)
89	s.fillMissingRowIDs(s.buffer)
90	s.record(renderer.Damage{Kind: renderer.DamageScrollUp, X: 0, Y: top, Width: w, Height: height, Count: n})
91	s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: bottom - n + 1, Width: w, Height: n, Count: 1})
92	return bottom-n >= top
93}
94
95func (s *Screen) emitLineEvicted(top, n int) {
96	// Only rows leaving the top edge of the primary screen belong to global
97	// scrollback. Interior DECSTBM scroll regions are local mutations.
98	if s.alternate != nil || top != 0 {
99		return
100	}
101	// Read boundaries and IDs before the caller rotates the frame: a soft link
102	// belongs to the row it follows, and rotation reassigns row indices.
103	for y := top; y < top+n; y++ {
104		s.recordEvicted(s.Frame.Row(y), s.buffer.bound(y), s.buffer.rowIDs[y])
105	}
106}
107
108func (s *Screen) recordEvicted(row []renderer.Cell, bound LineBound, id RowID) {
109	if s.history != nil {
110		err := s.history.AppendWithID(row, bound, id)
111		if err != nil && !errors.Is(err, ErrHistoryRowTooWide) {
112			panic(err)
113		}
114		// A nil error records the row; ErrHistoryRowTooWide explicitly leaves it
115		// unrecorded. Both cases continue to the eviction observer below.
116	}
117	if s.OnLineEvicted != nil {
118		s.OnLineEvicted(append([]renderer.Cell(nil), row...))
119	}
120}
121
122func (s *Screen) scrollDownRegion(top, bottom, n int) {
123	if s.Frame.Width == 0 || s.Frame.Height == 0 || n <= 0 {
124		return
125	}
126	top, bottom, ok := s.normalizedRegion(top, bottom)
127	if !ok {
128		return
129	}
130	w := s.Frame.Width
131	height := bottom - top + 1
132	if n > height {
133		n = height
134	}
135	// Full-width region: rotate line offsets instead of copying cells.
136	s.Frame.ScrollDown(top, bottom, n)
137	s.buffer.scrollDown(top, bottom, n)
138	s.fillMissingRowIDs(s.buffer)
139	s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: top, Width: w, Height: height, Count: 1})
140}
141
142func (s *Screen) normalizedRegion(top, bottom int) (int, int, bool) {
143	if s.Frame.Height == 0 || top > bottom {
144		return 0, 0, false
145	}
146	top = clamp(top, 0, s.Frame.Height-1)
147	bottom = clamp(bottom, 0, s.Frame.Height-1)
148	return top, bottom, top <= bottom
149}

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