| 📦 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 11h ago | 1 | // 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 | |
| 4 | package 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. |
| 13 | type 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. |
| 21 | func (v *View) inputWidth() int { |
| 22 | return max(v.Bounds().W-3, 1) |
| 23 | } |
| 24 | |
| 25 | // inputRows lays every typed line out at a width. |
| 26 | func (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. |
| 45 | func 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. |
| 62 | func 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. |
| 75 | func (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. |
| 94 | func (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 | } |