v0.2.0 4859eb8

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

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