turbo-editors/turbo-corepublic Fork 0
v1.0.3
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

input_test.go · 194 lines · 6.4 KBGo Blame HistoryRaw
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 7h ago1package acp_test
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/gdamore/tcell/v2"
8
9 "rickub.com/turbo-editors/turbo-core/acp"
10 "rickub.com/turbo-editors/turbo-core/theme"
11 "rickub.com/turbo-editors/turbo-core/ui"
12)
13
14// drawWithCursor paints a focused view and returns the rows and where the
15// terminal's cursor was put — or -1, -1 when it was hidden, which is what
16// happened to a cursor that ran off the edge of the box.
17func drawWithCursor(t *testing.T, view *acp.View, width, height int) (rows []string, x, y int) {
18 t.Helper()
19
20 screen := tcell.NewSimulationScreen("UTF-8")
21 if err := screen.Init(); err != nil {
22 t.Fatalf("starting the screen: %v", err)
23 }
24 defer screen.Fini()
25 screen.SetSize(width, height)
26
27 th, err := theme.Load(theme.DefaultName, "")
28 if err != nil {
29 t.Fatalf("loading the theme: %v", err)
30 }
31
32 view.SetFocused(true)
33 view.Draw(ui.NewPainter(screen), th)
34 screen.Show()
35
36 cells, w, h := screen.GetContents()
37 rows = make([]string, h)
38 for row := range h {
39 var text strings.Builder
40 for col := range w {
41 text.WriteString(string(cells[row*w+col].Runes))
42 }
43 rows[row] = strings.TrimRight(text.String(), " ")
44 }
45
46 x, y, visible := screen.GetCursor()
47 if !visible {
48 return rows, -1, -1
49 }
50 return rows, x, y
51}
52
53// inputRows returns the rows of the box you type in: everything below the
54// rule.
55func inputRows(rows []string) []string {
56 for i, row := range rows {
57 if strings.Contains(row, "─") {
58 return rows[i+1:]
59 }
60 }
61 return nil
62}
63
64func typeInto(view *acp.View, text string) {
65 for _, r := range text {
66 view.HandleKey(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))
67 }
68}
69
70func TestALongPromptWrapsAtSpacesInsideTheBox(t *testing.T) {
71 // This is the feature: before, the line was cut off with an ellipsis at
72 // the edge and the rest of what you typed was invisible.
73 view, _ := newView(t, 30, 12)
74 typeInto(view, "please explain how the wrapping of a long prompt works")
75
76 rows, _, _ := drawWithCursor(t, view, 30, 12)
77 box := inputRows(rows)
78
79 if !strings.HasPrefix(box[0], "> please explain how the") || box[1] != " wrapping of a long prompt" || box[2] != " works" {
80 t.Errorf("the box is not wrapped at spaces:\n%s", strings.Join(box, "\n"))
81 }
82 if strings.Contains(strings.Join(box, "\n"), "…") {
83 t.Errorf("the box still cuts the prompt off:\n%s", strings.Join(box, "\n"))
84 }
85}
86
87func TestTheCursorFollowsTheTextOntoTheWrappedRow(t *testing.T) {
88 view, _ := newView(t, 30, 12)
89 typeInto(view, "please explain how the wrapping")
90
91 _, x, y := drawWithCursor(t, view, 30, 12)
92
93 // Row 0 of the box is screen row 9 (12 rows: 8 of conversation, the rule,
94 // then 3 of box); "wrapping" is 8 runes on the second row, after "> " and
95 // its own indent.
96 if x != 2+len("wrapping") || y != 10 {
97 t.Errorf("cursor at (%d, %d), want it after %q on the second row (%d, %d)", x, y, "wrapping", 2+len("wrapping"), 10)
98 }
99}
100
101func TestTheCursorAtTheEndOfAFullRowStaysInsideThePane(t *testing.T) {
102 // A row holds width-3 runes: prompt, space, and a column for exactly
103 // this cursor. Without it the cursor sat on the frame and tcell hid it.
104 view, _ := newView(t, 30, 12)
105 typeInto(view, strings.Repeat("x", 27))
106
107 _, x, y := drawWithCursor(t, view, 30, 12)
108
109 if x != 29 || y != 9 {
110 t.Errorf("cursor at (%d, %d), want (29, 9): the last column of the pane, first row of the box", x, y)
111 }
112}
113
114func TestAWordLongerThanTheBoxBreaksAtTheEdgeRatherThanVanishing(t *testing.T) {
115 view, _ := newView(t, 30, 12)
116 typeInto(view, strings.Repeat("a", 40))
117
118 rows, _, _ := drawWithCursor(t, view, 30, 12)
119 box := inputRows(rows)
120
121 if box[0] != "> "+strings.Repeat("a", 27) || box[1] != " "+strings.Repeat("a", 13) {
122 t.Errorf("a long word is not broken at the edge:\n%s", strings.Join(box, "\n"))
123 }
124}
125
126func TestUpAndDownMoveByRowInsideAWrappedPrompt(t *testing.T) {
127 // Three rows to the eye are three rows to the arrow keys. Up from the last
128 // row lands on the middle one, not on the conversation.
129 view, _ := newView(t, 30, 12)
130 typeInto(view, "please explain how the wrapping of a long prompt works")
131
132 if !view.HandleKey(tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)) {
133 t.Fatal("Up was not taken by the box, though there is a row above the cursor")
134 }
135 _, x, y := drawWithCursor(t, view, 30, 12)
136 if y != 10 || x != 2+len("works") {
137 t.Errorf("after Up the cursor is at (%d, %d), want the middle row at the same column (%d, 10)", x, y, 2+len("works"))
138 }
139
140 view.HandleKey(tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone))
141 view.HandleKey(tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone))
142 _, x, y = drawWithCursor(t, view, 30, 12)
143 if y != 10 || x != 2+len("works") {
144 t.Errorf("Up then Down did not come back: cursor at (%d, %d)", x, y)
145 }
146
147 // The column is kept, not the word: five in on the middle row is between
148 // "wrapp" and "ing", which is where a key pressed now goes.
149 typeInto(view, "!")
150 if got := view.Input(); got != "please explain how the wrapp!ing of a long prompt works" {
151 t.Errorf("typing after the moves went to the wrong place: %q", got)
152 }
153}
154
155func TestUpFromTheFirstRowOfTheBoxIsLeftForTheConversation(t *testing.T) {
156 view, _ := newView(t, 30, 12)
157 typeInto(view, "short")
158
159 if view.HandleKey(tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)) && view.Input() != "short" {
160 t.Error("Up on a one-row prompt changed the input")
161 }
162}
163
164func TestAPromptTallerThanTheBoxScrollsToKeepTheCursorInSight(t *testing.T) {
165 // Three rows of box, five rows of prompt: the first two go up under the
166 // rule, and the prompt marker goes with them.
167 view, _ := newView(t, 30, 12)
168 typeInto(view, "one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty")
169
170 rows, _, y := drawWithCursor(t, view, 30, 12)
171 box := inputRows(rows)
172
173 if y != 11 {
174 t.Errorf("the cursor is on screen row %d, want the last row of the box, 11", y)
175 }
176 if strings.HasPrefix(box[0], ">") {
177 t.Errorf("the prompt marker is beside a row that is not the first:\n%s", strings.Join(box, "\n"))
178 }
179 if !strings.HasSuffix(box[2], "twenty") {
180 t.Errorf("the last row does not end with what was typed last:\n%s", strings.Join(box, "\n"))
181 }
182}
183
184func TestATypedTrailingSpaceAtTheEdgePutsTheCursorOnTheNextRow(t *testing.T) {
185 // 27 runes fill a row; the space after them is the break, not drawn, and
186 // the cursor is at the start of an empty row below rather than on the frame.
187 view, _ := newView(t, 30, 12)
188 typeInto(view, strings.Repeat("x", 27)+" ")
189
190 _, x, y := drawWithCursor(t, view, 30, 12)
191 if x != 2 || y != 10 {
192 t.Errorf("cursor at (%d, %d), want (2, 10)", x, y)
193 }
194}