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.go · 111 lines · 3.8 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 9h ago1// Laying the box you type in out as rows: a long line wraps at spaces rather
2// than running off the edge and taking the cursor with it.
3
4package acp
5
6// inputRow is one row of the box as drawn: which typed line it shows, and
7// which of that line's runes, as a half-open range.
8//
9// The typed lines stay the model — Alt-Enter makes one, Backspace at a line
10// start joins two — and wrapping is only how they are shown. A row is never
11// stored: it is recomputed from the lines and the width at every frame and
12// every key, so a resize cannot leave the two out of step.
13type inputRow struct {
14 line int
15 start, end int
16}
17
18// inputWidth is how many runes a row of the box holds: the prompt, a space,
19// and one column kept free so the cursor sitting just past a full row is
20// still inside the pane rather than on the frame.
21func (v *View) inputWidth() int {
22 return max(v.Bounds().W-3, 1)
23}
24
25// inputRows lays every typed line out at a width.
26func (v *View) inputRows(width int) []inputRow {
27 var rows []inputRow
28 for line, text := range v.input {
29 rows = append(rows, wrapInputLine(line, []rune(text), width)...)
30 }
31 return rows
32}
33
34// wrapInputLine breaks one typed line into rows no wider than width, at the
35// last space that fits and mid-word when a word is longer than a row.
36//
37// The space a row breaks at stays at the end of that row rather than opening
38// the next, so the runes of the line are exactly the rows laid end to end and
39// a cursor position is one row's and no other's. A row may therefore hold
40// width+1 runes, the last a space, which is not drawn.
41//
42// A line that ends in such a space — you have just typed it — is followed by
43// an empty row, which falls out of the loop: what is left after the break is
44// nothing, and nothing is a row. That is where the cursor then sits.
45func wrapInputLine(line int, runes []rune, width int) []inputRow {
46 var rows []inputRow
47 start := 0
48 for {
49 remaining := runes[start:]
50 if len(remaining) <= width {
51 return append(rows, inputRow{line: line, start: start, end: len(runes)})
52 }
53 at := start + inputBreak(remaining, width)
54 rows = append(rows, inputRow{line: line, start: start, end: at})
55 start = at
56 }
57}
58
59// inputBreak returns how many runes the first row of a run longer than width
60// takes: through the last space in the first width+1 runes, or width when
61// there is none.
62func inputBreak(runes []rune, width int) int {
63 for at := width; at >= 0; at-- {
64 if runes[at] == ' ' {
65 return at + 1
66 }
67 }
68 return width
69}
70
71// inputCursor returns which row the cursor is on and how far along it.
72//
73// A position at the very end of a row that continues belongs to the next
74// row's start, which is what makes typing across a wrap look like typing.
75func (v *View) inputCursor(rows []inputRow) (row, column int) {
76 for i, r := range rows {
77 if r.line != v.cursor {
78 continue
79 }
80 last := i == len(rows)-1 || rows[i+1].line != r.line
81 if v.column < r.end || (last && v.column <= r.end) {
82 return i, v.column - r.start
83 }
84 }
85 return max(len(rows)-1, 0), 0
86}
87
88// moveInputRow moves the cursor to the row above or below the one it is on,
89// keeping its column where the row allows, and reports whether there was one.
90//
91// Rows, not lines: a paragraph that wraps three times is three rows to the
92// eye, and Up from its last row should land on its middle, not on the
93// paragraph before.
94func (v *View) moveInputRow(by int) bool {
95 rows := v.inputRows(v.inputWidth())
96 row, column := v.inputCursor(rows)
97 target := row + by
98 if target < 0 || target >= len(rows) {
99 return false
100 }
101
102 r := rows[target]
103 widest := r.end - r.start
104 if last := target == len(rows)-1 || rows[target+1].line != r.line; !last {
105 widest-- // the row's last position is the next row's first
106 }
107 v.cursor = r.line
108 v.column = r.start + min(column, max(widest, 0))
109 v.leaveToken()
110 return true
111}