detached ab6686c

extraction_contract_external_test.go RawHistoryFolder

1package vt_test
2
3import (
4	"reflect"
5	"testing"
6
7	vt "github.com/bnema/vev-vt"
8	"github.com/bnema/vev-vt/core"
9)
10
11func TestExternalScreenParserPreservesPublicTerminalModel(t *testing.T) {
12	screen := vt.NewScreen(6, 2)
13	screen.Write([]byte("A界"))
14
15	var source vt.CellSource = screen
16	if source.Columns() != 6 || source.Rows() != 2 {
17		t.Fatalf("screen dimensions = %dx%d, want 6x2", source.Columns(), source.Rows())
18	}
19	if got := source.Cell(0, 0); got.Rune != 'A' {
20		t.Fatalf("ASCII cell = %#v, want A", got)
21	}
22	if got := source.Cell(1, 0); got.Rune != '界' || got.Continuation {
23		t.Fatalf("wide-rune lead cell = %#v, want 界 lead", got)
24	}
25	if got := source.Cell(2, 0); !got.Continuation {
26		t.Fatalf("wide-rune continuation = %#v, want continuation", got)
27	}
28	owned := screen.RowCells(0)
29	owned[0] = core.BlankCell()
30	if got := source.Cell(0, 0).Rune; got != 'A' {
31		t.Fatalf("mutating RowCells result changed live screen cell to %q", got)
32	}
33	if got := vt.RuneWidth('界'); got != 2 {
34		t.Fatalf("RuneWidth(界) = %d, want 2", got)
35	}
36
37	// The parser owns incomplete escape sequences across writes, so callers can
38	// provide arbitrary PTY chunk boundaries.
39	screen.Write([]byte("\x1b[2;2H\x1b[3"))
40	screen.Write([]byte("1mX"))
41	cell := screen.Cell(1, 1)
42	if cell.Rune != 'X' || cell.Style.Foreground != 1 {
43		t.Fatalf("split ANSI write = %#v, want red X", cell)
44	}
45
46	screen.Write([]byte("\x1b]2;public title\a"))
47	if got := screen.Snapshot().Title(); got != "public title" {
48		t.Fatalf("terminal title = %q, want public title", got)
49	}
50}
51
52func TestExternalCallbacksAreSynchronousAndOrdered(t *testing.T) {
53	screen := vt.NewScreen(8, 2)
54	inWrite := false
55	var events []string
56	assertInWrite := func(name string) {
57		if !inWrite {
58			t.Errorf("%s callback ran after Write returned", name)
59		}
60	}
61
62	screen.OnBell = func() {
63		assertInWrite("bell")
64		events = append(events, "bell")
65	}
66	screen.OnResponse = func(response []byte) {
67		assertInWrite("response")
68		events = append(events, "response:"+string(response))
69	}
70	screen.OnNotify = func(title, body string) {
71		assertInWrite("notify")
72		events = append(events, "notify:"+title+":"+body)
73	}
74	screen.OnProgress = func(errored bool) {
75		assertInWrite("progress")
76		if errored {
77			events = append(events, "progress:error")
78		} else {
79			events = append(events, "progress:clear")
80		}
81	}
82	screen.OnClipboard = func(data string) {
83		assertInWrite("clipboard")
84		events = append(events, "clipboard:"+data)
85	}
86
87	inWrite = true
88	screen.Write([]byte("\a\x1b[5n\x1b]9;body\a\x1b]777;notify;title;body\a" +
89		"\x1b]9;4;1\a\x1b]9;4;0\a\x1b]9;4;2\a" +
90		"\x1b]52;c;SGVsbG8=\a"))
91	inWrite = false
92
93	want := []string{
94		"bell",
95		"response:\x1b[0n",
96		"notify::body",
97		"notify:title:body",
98		"progress:clear",
99		"progress:error",
100		"clipboard:SGVsbG8=",
101	}
102	if !reflect.DeepEqual(events, want) {
103		t.Fatalf("callback events = %#v, want %#v", events, want)
104	}
105}
106
107func TestExternalSnapshotsAndHistoryRespectOwnershipContracts(t *testing.T) {
108	screen := vt.NewScreenWithHistory(4, 2, vt.HistoryConfig{MaxRows: 8, ChunkRows: 2})
109	screen.Write([]byte("A"))
110	snapshot := screen.Snapshot()
111
112	owned := snapshot.Row(0)
113	owned[0] = core.Cell{Rune: 'X'}
114	if got := snapshot.Row(0)[0].Rune; got != 'A' {
115		t.Fatalf("owned snapshot row mutation changed snapshot to %q", got)
116	}
117
118	screen.Write([]byte("\rX"))
119	if got := snapshot.Row(0)[0].Rune; got != 'A' {
120		t.Fatalf("snapshot changed after later screen write: %q", got)
121	}
122	if got := snapshot.RowIDs(); len(got) != 2 || got[0] == 0 || got[1] == 0 {
123		t.Fatalf("snapshot row IDs = %#v, want stable nonzero IDs", got)
124	}
125	rowIDs := snapshot.RowIDs()
126	rowIDs[0] = 0
127	if snapshot.RowID(0) == 0 {
128		t.Fatal("RowIDs did not return an owned copy")
129	}
130
131	history := vt.NewHistory(vt.HistoryConfig{MaxRows: 8, ChunkRows: 2})
132	appendHistoryRow(t, history, 'a', 10)
133	appendHistoryRow(t, history, 'b', 11)
134	before := history.View()
135	chunk := before.Chunk(0)
136	if chunk == nil || before.FindRowID(11) != 1 {
137		t.Fatalf("history view = len %d chunk %#v IDs %d/%d", before.Len(), chunk, before.RowID(0), before.RowID(1))
138	}
139
140	copyOfRow := before.Row(0)
141	copyOfRow[0] = core.Cell{Rune: 'x'}
142	if got := before.Row(0)[0].Rune; got != 'a' {
143		t.Fatalf("owned history row mutation changed view to %q", got)
144	}
145
146	appendHistoryRow(t, history, 'c', 12)
147	after := history.View()
148	if after.Chunk(0) != chunk {
149		t.Fatal("sealed history chunk identity changed after an append")
150	}
151	if before.Len() != 2 || before.RowID(0) != 10 || before.RowID(1) != 11 {
152		t.Fatalf("retained history view changed after append: len=%d IDs=%d/%d", before.Len(), before.RowID(0), before.RowID(1))
153	}
154
155	persistenceView := history.SnapshotView()
156	appendHistoryRow(t, history, 'd', 13)
157	if got := persistenceView.Tail().Row(0)[0].Rune; got != 'c' {
158		t.Fatalf("snapshot tail changed after history append to %q", got)
159	}
160}
161
162func TestExternalSingleOwnerUsageSerializesMutationAndCapture(t *testing.T) {
163	screen := vt.NewScreen(4, 1)
164	operations := make(chan func(), 3)
165	done := make(chan struct{})
166	go func() {
167		defer close(done)
168		for operation := range operations {
169			operation()
170		}
171	}()
172
173	operations <- func() { screen.Write([]byte("A")) }
174	captured := make(chan vt.ScreenSnapshot, 1)
175	operations <- func() { captured <- screen.Snapshot() }
176	close(operations)
177	<-done
178
179	if got := (<-captured).Row(0)[0].Rune; got != 'A' {
180		t.Fatalf("single-owner capture = %q, want A", got)
181	}
182}
183
184func appendHistoryRow(t *testing.T, history *vt.History, runeValue rune, id vt.RowID) {
185	t.Helper()
186	if err := history.AppendWithID([]core.Cell{{Rune: runeValue}}, vt.LineBound{End: 1}, id); err != nil {
187		t.Fatalf("AppendWithID(%d): %v", id, err)
188	}
189}

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