turbo-editors/turbo-corepublic Fork 0
v0.9.0
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.

view_select.go · 130 lines · 3.5 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 17h ago1package acp
2
3import "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.
14const noSelection = -1
15
16// Caret returns which laid-out line the transcript's cursor is on.
17func (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.
21func (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.
29func (v *View) SelectNothing() { v.anchor = noSelection }
30
31// moveCaret puts the cursor on a line, extending the selection or dropping it.
32func (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.
51func (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()
77func (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.
100func 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.
118func (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}