turbo-editors/turbo-corepublic Fork 0
main
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
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package acp_test

import (
	"strings"
	"testing"

	"github.com/gdamore/tcell/v2"

	"rickub.com/turbo-editors/turbo-core/acp"
	"rickub.com/turbo-editors/turbo-core/theme"
	"rickub.com/turbo-editors/turbo-core/ui"
)

// drawWithCursor paints a focused view and returns the rows and where the
// terminal's cursor was put — or -1, -1 when it was hidden, which is what
// happened to a cursor that ran off the edge of the box.
func drawWithCursor(t *testing.T, view *acp.View, width, height int) (rows []string, x, y int) {
	t.Helper()

	screen := tcell.NewSimulationScreen("UTF-8")
	if err := screen.Init(); err != nil {
		t.Fatalf("starting the screen: %v", err)
	}
	defer screen.Fini()
	screen.SetSize(width, height)

	th, err := theme.Load(theme.DefaultName, "")
	if err != nil {
		t.Fatalf("loading the theme: %v", err)
	}

	view.SetFocused(true)
	view.Draw(ui.NewPainter(screen), th)
	screen.Show()

	cells, w, h := screen.GetContents()
	rows = make([]string, h)
	for row := range h {
		var text strings.Builder
		for col := range w {
			text.WriteString(string(cells[row*w+col].Runes))
		}
		rows[row] = strings.TrimRight(text.String(), " ")
	}

	x, y, visible := screen.GetCursor()
	if !visible {
		return rows, -1, -1
	}
	return rows, x, y
}

// inputRows returns the rows of the box you type in: everything below the
// rule.
func inputRows(rows []string) []string {
	for i, row := range rows {
		if strings.Contains(row, "─") {
			return rows[i+1:]
		}
	}
	return nil
}

func typeInto(view *acp.View, text string) {
	for _, r := range text {
		view.HandleKey(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))
	}
}

func TestALongPromptWrapsAtSpacesInsideTheBox(t *testing.T) {
	// This is the feature: before, the line was cut off with an ellipsis at
	// the edge and the rest of what you typed was invisible.
	view, _ := newView(t, 30, 12)
	typeInto(view, "please explain how the wrapping of a long prompt works")

	rows, _, _ := drawWithCursor(t, view, 30, 12)
	box := inputRows(rows)

	if !strings.HasPrefix(box[0], "> please explain how the") || box[1] != "  wrapping of a long prompt" || box[2] != "  works" {
		t.Errorf("the box is not wrapped at spaces:\n%s", strings.Join(box, "\n"))
	}
	if strings.Contains(strings.Join(box, "\n"), "…") {
		t.Errorf("the box still cuts the prompt off:\n%s", strings.Join(box, "\n"))
	}
}

func TestTheCursorFollowsTheTextOntoTheWrappedRow(t *testing.T) {
	view, _ := newView(t, 30, 12)
	typeInto(view, "please explain how the wrapping")

	_, x, y := drawWithCursor(t, view, 30, 12)

	// Row 0 of the box is screen row 9 (12 rows: 8 of conversation, the rule,
	// then 3 of box); "wrapping" is 8 runes on the second row, after "> " and
	// its own indent.
	if x != 2+len("wrapping") || y != 10 {
		t.Errorf("cursor at (%d, %d), want it after %q on the second row (%d, %d)", x, y, "wrapping", 2+len("wrapping"), 10)
	}
}

func TestTheCursorAtTheEndOfAFullRowStaysInsideThePane(t *testing.T) {
	// A row holds width-3 runes: prompt, space, and a column for exactly
	// this cursor. Without it the cursor sat on the frame and tcell hid it.
	view, _ := newView(t, 30, 12)
	typeInto(view, strings.Repeat("x", 27))

	_, x, y := drawWithCursor(t, view, 30, 12)

	if x != 29 || y != 9 {
		t.Errorf("cursor at (%d, %d), want (29, 9): the last column of the pane, first row of the box", x, y)
	}
}

func TestAWordLongerThanTheBoxBreaksAtTheEdgeRatherThanVanishing(t *testing.T) {
	view, _ := newView(t, 30, 12)
	typeInto(view, strings.Repeat("a", 40))

	rows, _, _ := drawWithCursor(t, view, 30, 12)
	box := inputRows(rows)

	if box[0] != "> "+strings.Repeat("a", 27) || box[1] != "  "+strings.Repeat("a", 13) {
		t.Errorf("a long word is not broken at the edge:\n%s", strings.Join(box, "\n"))
	}
}

func TestUpAndDownMoveByRowInsideAWrappedPrompt(t *testing.T) {
	// Three rows to the eye are three rows to the arrow keys. Up from the last
	// row lands on the middle one, not on the conversation.
	view, _ := newView(t, 30, 12)
	typeInto(view, "please explain how the wrapping of a long prompt works")

	if !view.HandleKey(tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)) {
		t.Fatal("Up was not taken by the box, though there is a row above the cursor")
	}
	_, x, y := drawWithCursor(t, view, 30, 12)
	if y != 10 || x != 2+len("works") {
		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"))
	}

	view.HandleKey(tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone))
	view.HandleKey(tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone))
	_, x, y = drawWithCursor(t, view, 30, 12)
	if y != 10 || x != 2+len("works") {
		t.Errorf("Up then Down did not come back: cursor at (%d, %d)", x, y)
	}

	// The column is kept, not the word: five in on the middle row is between
	// "wrapp" and "ing", which is where a key pressed now goes.
	typeInto(view, "!")
	if got := view.Input(); got != "please explain how the wrapp!ing of a long prompt works" {
		t.Errorf("typing after the moves went to the wrong place: %q", got)
	}
}

func TestUpFromTheFirstRowOfTheBoxIsLeftForTheConversation(t *testing.T) {
	view, _ := newView(t, 30, 12)
	typeInto(view, "short")

	if view.HandleKey(tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)) && view.Input() != "short" {
		t.Error("Up on a one-row prompt changed the input")
	}
}

func TestAPromptTallerThanTheBoxScrollsToKeepTheCursorInSight(t *testing.T) {
	// Three rows of box, five rows of prompt: the first two go up under the
	// rule, and the prompt marker goes with them.
	view, _ := newView(t, 30, 12)
	typeInto(view, "one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty")

	rows, _, y := drawWithCursor(t, view, 30, 12)
	box := inputRows(rows)

	if y != 11 {
		t.Errorf("the cursor is on screen row %d, want the last row of the box, 11", y)
	}
	if strings.HasPrefix(box[0], ">") {
		t.Errorf("the prompt marker is beside a row that is not the first:\n%s", strings.Join(box, "\n"))
	}
	if !strings.HasSuffix(box[2], "twenty") {
		t.Errorf("the last row does not end with what was typed last:\n%s", strings.Join(box, "\n"))
	}
}

func TestATypedTrailingSpaceAtTheEdgePutsTheCursorOnTheNextRow(t *testing.T) {
	// 27 runes fill a row; the space after them is the break, not drawn, and
	// the cursor is at the start of an empty row below rather than on the frame.
	view, _ := newView(t, 30, 12)
	typeInto(view, strings.Repeat("x", 27)+" ")

	_, x, y := drawWithCursor(t, view, 30, 12)
	if x != 2 || y != 10 {
		t.Errorf("cursor at (%d, %d), want (2, 10)", x, y)
	}
}