v0.50.1 a637010

live_text_test.go RawHistoryFolder

1package tui
2
3import (
4	"strings"
5	"testing"
6)
7
8const liveTextSample = "Intro paragraph with **bold** and `code`.\n\n" +
9	"## Heading\n\n" +
10	"- item one\n- item two\n  - nested\n\n" +
11	"```go\nfunc main() {\n\n\tprintln(\"hi\")\n}\n```\n\n" +
12	"| a | b |\n| --- | ---: |\n| 1 | 2 |\n\n\n" +
13	"> quoted line\n\n" +
14	"1. first\n2. second\n\n" +
15	"A long closing paragraph that should wrap across several rows when the terminal is narrow enough to force it.\n"
16
17// Live rows must equal the finished-message rows for every prefix of a reply,
18// or rows already in terminal scrollback would diverge from the final text.
19func TestLiveTextRowsMatchFinalRendering(t *testing.T) {
20	corpus := map[string]string{
21		"mixed":            liveTextSample,
22		"fence-like table": "| a | b |\n| --- | --- |\n```x | y\n\ntext\n```\n\nm\n\nafter",
23		"crlf":             "one\r\n\r\ntwo **b**\r\n\r\n- item\r\n",
24		"blank spaces":     "para one\n  \n\t\t\npara two\n\n\n\npara three",
25		"leading newlines": "\n\nstarts late\n\nand ends\n\n",
26		"indented fence":   "list:\n\n  ```sh\n  echo hi\n\n  echo bye\n  ```\n\ndone",
27		"trailing table":   "| h | i |\n| - | - |\n| 1 | 2 |\n\n\n",
28	}
29	for name, sample := range corpus {
30		for _, width := range []int{10, 24, 80} {
31			v := &View{Theme: Dark}
32			for n := 1; n <= len(sample); n++ {
33				prefix := sample[:n]
34				got := strings.Join(v.liveTextRows(prefix, width), "\n")
35				want := strings.Join(renderAssistantText(prefix, Dark, width), "\n")
36				if got != want {
37					t.Fatalf("%s, width %d, prefix %d %q:\nlive:\n%q\nfinal:\n%q", name, width, n, prefix, got, want)
38				}
39			}
40		}
41	}
42}
43
44// Width and theme changes must not reuse rows rendered for other settings.
45func TestLiveTextRowsResetOnWidthAndTheme(t *testing.T) {
46	const text = "first **block**\n\n```go\nx := 1\n```\n\ntail"
47	v := &View{Theme: Dark}
48	v.liveTextRows(text, 80)
49	if v.liveText.src == "" {
50		t.Fatal("no finished block was cached")
51	}
52	if got, want := strings.Join(v.liveTextRows(text, 12), "\n"), strings.Join(renderAssistantText(text, Dark, 12), "\n"); got != want {
53		t.Fatalf("width change reused stale rows:\n%q\nwant\n%q", got, want)
54	}
55	v.Theme = Light
56	if got, want := strings.Join(v.liveTextRows(text, 12), "\n"), strings.Join(renderAssistantText(text, Light, 12), "\n"); got != want {
57		t.Fatalf("theme change reused stale rows:\n%q\nwant\n%q", got, want)
58	}
59}
60
61// Render snapshots must not share the live-text cache with the owning view.
62func TestCloneForRenderIsolatesLiveTextCache(t *testing.T) {
63	v := &View{Theme: Dark}
64	v.liveTextRows("block one\n\nblock two\n\n", 80)
65	clone := v.CloneForRender()
66	if clone.liveText.src != "" || clone.liveText.rows != nil {
67		t.Fatal("clone shares the live-text cache")
68	}
69}
70
71// A new reply that does not extend the cached text starts a fresh cache.
72func TestLiveTextRowsResetOnNewReply(t *testing.T) {
73	v := &View{Theme: Dark}
74	v.liveTextRows("first reply\n\nmore", 80)
75	got := strings.Join(v.liveTextRows("second", 80), "\n")
76	want := strings.Join(renderAssistantText("second", Dark, 80), "\n")
77	if got != want {
78		t.Fatalf("stale cache leaked into a new reply:\n%s", got)
79	}
80}
81
82func TestMarkdownBoundariesSkipOpenFence(t *testing.T) {
83	lines := strings.Split("text\n\n```\ncode\n\nmore code\n", "\n")
84	var got []int
85	renderMarkdownRaw(lines, Dark, 80, func(line, _ int) { got = append(got, line) })
86	if len(got) != 1 || got[0] != 2 {
87		t.Fatalf("boundaries = %v, want [2] (none inside an open fence)", got)
88	}
89}
90
91func BenchmarkLiveTextStreaming(b *testing.B) {
92	text := strings.Repeat(liveTextSample, 8)
93	for b.Loop() {
94		v := &View{Theme: Dark}
95		for n := 64; n <= len(text); n += 64 {
96			v.liveTextRows(text[:n], 100)
97		}
98	}
99}
100
101func BenchmarkFullRerenderStreaming(b *testing.B) {
102	text := strings.Repeat(liveTextSample, 8)
103	for b.Loop() {
104		for n := 64; n <= len(text); n += 64 {
105			renderAssistantText(text[:n], Dark, 100)
106		}
107	}
108}

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 1

Tags 476

↑↓ moveenter openesc close