| 🛟 Updated. 28d5985 k33g 17h ago | 1 | package acp |
| 2 | |
| 3 | import "strings" |
| 4 | |
| 5 | // The transcript's own cursor and selection. |
| 6 | // |
| 7 | // A conversation is read as well as written, and what is read is usually |
| 8 | // wanted somewhere else: a command to run, a block of code to paste into the |
| 9 | // file beside it. Selecting is by whole lines rather than by character — |
| 10 | // nothing in a conversation is edited, so a half line is never what somebody |
| 11 | // means, and whole lines keep a copied code block's indentation intact. |
| 12 | |
| 13 | // noSelection is the anchor's value when nothing is selected. |
| 14 | const noSelection = -1 |
| 15 | |
| 16 | // Caret returns which laid-out line the transcript's cursor is on. |
| 17 | func (v *View) Caret() int { return v.caret } |
| 18 | |
| 19 | // Selection returns the first and last laid-out line selected, and whether |
| 20 | // there is a selection at all. |
| 21 | func (v *View) Selection() (from, to int, ok bool) { |
| 22 | if v.anchor == noSelection { |
| 23 | return 0, 0, false |
| 24 | } |
| 25 | return min(v.anchor, v.caret), max(v.anchor, v.caret), true |
| 26 | } |
| 27 | |
| 28 | // SelectNothing drops the selection, leaving the cursor where it is. |
| 29 | func (v *View) SelectNothing() { v.anchor = noSelection } |
| 30 | |
| 31 | // moveCaret puts the cursor on a line, extending the selection or dropping it. |
| 32 | func (v *View) moveCaret(to int, extend bool) { |
| 33 | switch { |
| 34 | case !extend: |
| 35 | v.anchor = noSelection |
| 36 | case v.anchor == noSelection: |
| 37 | v.anchor = v.caret |
| 38 | } |
| 39 | |
| 40 | v.caret = max(to, 0) |
| 41 | if v.laidOut > 0 { |
| 42 | v.caret = min(v.caret, v.laidOut-1) |
| 43 | } |
| 44 | v.showCaret() |
| 45 | } |
| 46 | |
| 47 | // showCaret scrolls just far enough to bring the cursor into view. |
| 48 | // |
| 49 | // Only far enough: jumping the cursor to the middle of the window loses the |
| 50 | // line somebody was reading, which is the whole reason they moved it. |
| 51 | func (v *View) showCaret() { |
| 52 | height := v.transcriptHeight() |
| 53 | if height < 1 { |
| 54 | return |
| 55 | } |
| 56 | |
| 57 | switch { |
| 58 | case v.caret < v.scroll: |
| 59 | v.scrollTo(v.caret) |
| 60 | case v.caret >= v.scroll+height: |
| 61 | v.scrollTo(v.caret - height + 1) |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | // Copy returns the text to put on the clipboard, and whether there is any. |
| 66 | // |
| 67 | // With a selection, that is the selected lines. With none, it is the **region |
| 68 | // under the cursor**: one fenced code block, one passage of prose, one tool |
| 69 | // call's output. That second case is the one that matters in practice — the |
| 70 | // thing somebody wants is almost always a code block, and selecting it by hand |
| 71 | // first is work the editor can do for them. |
| 72 | // |
| 73 | // The indent every body line is drawn with is taken off, so pasted code is |
| 74 | // flush against the margin rather than two spaces in. |
| 75 | // |
| 76 | // text, ok := view.Copy() |
| 77 | func (v *View) Copy() (string, bool) { |
| 78 | lines := v.lines() |
| 79 | if len(lines) == 0 { |
| 80 | return "", false |
| 81 | } |
| 82 | |
| 83 | from, to, selected := v.Selection() |
| 84 | if !selected { |
| 85 | from, to = regionAround(lines, min(v.caret, len(lines)-1)) |
| 86 | } |
| 87 | from, to = max(from, 0), min(to, len(lines)-1) |
| 88 | |
| 89 | var out []string |
| 90 | for _, line := range lines[from : to+1] { |
| 91 | out = append(out, strings.TrimPrefix(line.Text, pad)) |
| 92 | } |
| 93 | |
| 94 | text := strings.TrimRight(strings.Join(out, "\n"), "\n ") |
| 95 | return text, strings.TrimSpace(text) != "" |
| 96 | } |
| 97 | |
| 98 | // regionAround returns the first and last line of the copyable region a line |
| 99 | // belongs to. |
| 100 | func regionAround(lines []Line, at int) (from, to int) { |
| 101 | if at < 0 || at >= len(lines) { |
| 102 | return 0, -1 |
| 103 | } |
| 104 | |
| 105 | region := lines[at].Region |
| 106 | from, to = at, at |
| 107 | for from > 0 && lines[from-1].Region == region { |
| 108 | from-- |
| 109 | } |
| 110 | for to < len(lines)-1 && lines[to+1].Region == region { |
| 111 | to++ |
| 112 | } |
| 113 | return from, to |
| 114 | } |
| 115 | |
| 116 | // copySelection hands the text to whoever wired up OnCopy, and says what |
| 117 | // happened on the status bar. |
| 118 | func (v *View) copySelection() bool { |
| 119 | if v.OnCopy == nil { |
| 120 | return false |
| 121 | } |
| 122 | |
| 123 | text, ok := v.Copy() |
| 124 | if !ok { |
| 125 | return false |
| 126 | } |
| 127 | v.OnCopy(text) |
| 128 | v.SelectNothing() |
| 129 | return true |
| 130 | } |