v0.7.1 ff50ee4

screen_scroll.go RawHistoryFolder

1package vt
2
3import (
4	"errors"
5	"fmt"
6	"math"
7
8	renderer "github.com/bnema/vev-vt/core"
9	"github.com/bnema/vev-vt/graphics"
10)
11
12// index moves to the next physical row, scrolling the region when the cursor
13// already sits on its last row. It reports whether the row the cursor was on
14// survived that scroll by moving up to s.Row-1, which only a deferred wrap
15// cares about: a wrap must relink to the row it flowed out of, and
16// buffer.scrollUp severs that link on the way. Callers that merely advance,
17// such as LF and ESC D, ignore the result.
18func (s *Screen) index() (movedUp bool) {
19	if s.frame.Height == 0 {
20		return false
21	}
22	if s.Row == s.scrollBottom && s.Row >= s.scrollTop {
23		return s.scrollUpRegion(s.scrollTop, s.scrollBottom, 1)
24	}
25	if s.Row+1 < s.frame.Height {
26		s.Row++
27		return false
28	}
29	s.Row = s.frame.Height - 1
30	return false
31}
32
33func (s *Screen) nextLine() {
34	s.buffer.hard(s.Row)
35	s.Col = 0
36	s.index()
37}
38
39func (s *Screen) reverseIndex() {
40	if s.frame.Height == 0 {
41		return
42	}
43	if s.Row == s.scrollTop && s.Row <= s.scrollBottom {
44		s.scrollDownRegion(s.scrollTop, s.scrollBottom, 1)
45		return
46	}
47	if s.Row > 0 {
48		s.Row--
49	}
50}
51
52// scrollUpBy scrolls the current scroll region up by n lines (CSI S / SU).
53// The cursor position is preserved.
54func (s *Screen) scrollUpBy(n int) {
55	if n <= 0 {
56		return
57	}
58	s.scrollUpRegion(s.scrollTop, s.scrollBottom, n)
59}
60
61func (s *Screen) scrollDownBy(n int) {
62	if n <= 0 {
63		return
64	}
65	s.scrollDownRegion(s.scrollTop, s.scrollBottom, n)
66}
67
68// scrollUpRegion scrolls [top,bottom] up by n. It reports whether any row
69// survived by moving up: a region exactly n rows tall is blanked in place, so
70// the receiving range [top,bottom-n] is empty and nothing survives. Reporting
71// what the scroll observed keeps callers from re-deriving a predicate that this
72// function may have declined to act on at all.
73func (s *Screen) scrollUpRegion(top, bottom, n int) (shifted bool) {
74	if s.frame.Width == 0 || s.frame.Height == 0 || n <= 0 {
75		return false
76	}
77	top, bottom, ok := s.normalizedRegion(top, bottom)
78	if !ok {
79		return false
80	}
81	w := s.frame.Width
82	height := bottom - top + 1
83	graphicsRows := n
84	if n > height {
85		n = height
86	}
87	s.scrollGraphicsRegion(top, bottom, -graphicsRows)
88	// VT scroll regions always span the full frame width, so we rotate the
89	// frame's line offsets (recycling and blanking the evicted rows in place)
90	// instead of copying cells. See renderer.Frame.ScrollUp.
91	s.emitLineEvicted(top, n)
92	s.frame.ScrollUp(top, bottom, n)
93	s.buffer.scrollUp(top, bottom, n)
94	s.fillMissingRowIDs(s.buffer)
95	s.record(renderer.Damage{Kind: renderer.DamageScrollUp, X: 0, Y: top, Width: w, Height: height, Count: n})
96	s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: bottom - n + 1, Width: w, Height: n, Count: 1})
97	return bottom-n >= top
98}
99
100func (s *Screen) emitLineEvicted(top, n int) {
101	// Only rows leaving the top edge of the primary screen belong to global
102	// scrollback. Interior DECSTBM scroll regions are local mutations.
103	if s.alternate != nil || top != 0 {
104		return
105	}
106	// Without a history or an observer nothing consumes the evicted rows, so
107	// skip materialising them.
108	if s.history == nil && s.OnLineEvicted == nil {
109		return
110	}
111	// Drop scratch sized for a much wider past geometry.
112	if cap(s.evictScratch) > 2*s.frame.Width {
113		s.evictScratch = nil
114	}
115	// Read boundaries and IDs before the caller rotates the frame: a soft link
116	// belongs to the row it follows, and rotation reassigns row indices.
117	for y := top; y < top+n; y++ {
118		// The scratch row is only valid for this call: History copies appended
119		// rows and OnLineEvicted receives its own copy (see recordEvicted).
120		row := s.evictScratch[:0]
121		for x := range s.frame.Width {
122			row = append(row, s.frame.Cell(x, y))
123		}
124		s.evictScratch = row
125		s.recordEvicted(row, s.buffer.bound(y), s.buffer.rowIDs[y])
126		clear(s.evictScratch)
127	}
128}
129
130func (s *Screen) recordEvicted(row []renderer.Cell, bound LineBound, id RowID) {
131	if s.history != nil {
132		err := s.history.AppendWithID(row, bound, id)
133		if err != nil && !errors.Is(err, ErrHistoryRowTooLarge) {
134			panic(err)
135		}
136		// A nil error records the row; ErrHistoryRowTooLarge explicitly leaves it
137		// unrecorded. Both cases continue to the eviction observer below.
138	}
139	if s.OnLineEvicted != nil {
140		s.OnLineEvicted(append([]renderer.Cell(nil), row...))
141	}
142}
143
144func (s *Screen) scrollDownRegion(top, bottom, n int) {
145	if s.frame.Width == 0 || s.frame.Height == 0 || n <= 0 {
146		return
147	}
148	top, bottom, ok := s.normalizedRegion(top, bottom)
149	if !ok {
150		return
151	}
152	w := s.frame.Width
153	height := bottom - top + 1
154	graphicsRows := n
155	if n > height {
156		n = height
157	}
158	s.scrollGraphicsRegion(top, bottom, graphicsRows)
159	// Full-width region: rotate line offsets instead of copying cells.
160	s.frame.ScrollDown(top, bottom, n)
161	s.buffer.scrollDown(top, bottom, n)
162	s.fillMissingRowIDs(s.buffer)
163	s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: top, Width: w, Height: height, Count: 1})
164}
165
166func (s *Screen) scrollGraphicsRegion(top, bottom, rows int) {
167	if rows == 0 || s.graphics == nil || s.graphics.scene == nil {
168		return
169	}
170	_, cellHeight, ok := s.kittyCellPixels()
171	if !ok || cellHeight <= 0 {
172		return
173	}
174	delta := int64(rows) * int64(cellHeight)
175	fullScreen := top == 0 && bottom == s.frame.Height-1
176	snapshot := s.graphics.scene.Snapshot()
177	placements := snapshot.Placements()
178	operations := make([]graphics.Operation, 0, len(placements))
179	for _, placement := range placements {
180		destination := placement.Destination()
181		if !fullScreen {
182			end, valid := destination.Bottom()
183			if !valid || destination.Y < 0 || end <= destination.Y {
184				continue
185			}
186			startRow := int(destination.Y / int64(cellHeight))
187			endRow := int((end - 1) / int64(cellHeight))
188			if startRow < top || endRow > bottom {
189				continue
190			}
191		}
192		if delta > 0 && destination.Y > math.MaxInt64-delta || delta < 0 && destination.Y < math.MinInt64-delta {
193			continue
194		}
195		destination.Y += delta
196		if _, valid := destination.Bottom(); !valid {
197			continue
198		}
199		spec := placement.Spec()
200		spec.Destination = destination
201		operations = append(operations, graphics.Operation{Kind: graphics.OperationUpdatePlacement, PlacementID: placement.ID(), Placement: spec})
202	}
203	if len(operations) != 0 {
204		if err := s.graphics.scene.Apply(operations...); err != nil {
205			panic(fmt.Errorf("scroll graphics placements: %w", err))
206		}
207	}
208}
209
210func (s *Screen) normalizedRegion(top, bottom int) (int, int, bool) {
211	if s.frame.Height == 0 || top > bottom {
212		return 0, 0, false
213	}
214	top = clamp(top, 0, s.frame.Height-1)
215	bottom = clamp(bottom, 0, s.frame.Height-1)
216	return top, bottom, top <= bottom
217}

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