package acp import "strings" // The transcript's own cursor and selection. // // A conversation is read as well as written, and what is read is usually // wanted somewhere else: a command to run, a block of code to paste into the // file beside it. Selecting is by whole lines rather than by character — // nothing in a conversation is edited, so a half line is never what somebody // means, and whole lines keep a copied code block's indentation intact. // noSelection is the anchor's value when nothing is selected. const noSelection = -1 // Caret returns which laid-out line the transcript's cursor is on. func (v *View) Caret() int { return v.caret } // Selection returns the first and last laid-out line selected, and whether // there is a selection at all. func (v *View) Selection() (from, to int, ok bool) { if v.anchor == noSelection { return 0, 0, false } return min(v.anchor, v.caret), max(v.anchor, v.caret), true } // SelectNothing drops the selection, leaving the cursor where it is. func (v *View) SelectNothing() { v.anchor = noSelection } // moveCaret puts the cursor on a line, extending the selection or dropping it. func (v *View) moveCaret(to int, extend bool) { switch { case !extend: v.anchor = noSelection case v.anchor == noSelection: v.anchor = v.caret } v.caret = max(to, 0) if v.laidOut > 0 { v.caret = min(v.caret, v.laidOut-1) } v.showCaret() } // showCaret scrolls just far enough to bring the cursor into view. // // Only far enough: jumping the cursor to the middle of the window loses the // line somebody was reading, which is the whole reason they moved it. func (v *View) showCaret() { height := v.transcriptHeight() if height < 1 { return } switch { case v.caret < v.scroll: v.scrollTo(v.caret) case v.caret >= v.scroll+height: v.scrollTo(v.caret - height + 1) } } // Copy returns the text to put on the clipboard, and whether there is any. // // With a selection, that is the selected lines. With none, it is the **region // under the cursor**: one fenced code block, one passage of prose, one tool // call's output. That second case is the one that matters in practice — the // thing somebody wants is almost always a code block, and selecting it by hand // first is work the editor can do for them. // // The indent every body line is drawn with is taken off, so pasted code is // flush against the margin rather than two spaces in. // // text, ok := view.Copy() func (v *View) Copy() (string, bool) { lines := v.lines() if len(lines) == 0 { return "", false } from, to, selected := v.Selection() if !selected { from, to = regionAround(lines, min(v.caret, len(lines)-1)) } from, to = max(from, 0), min(to, len(lines)-1) var out []string for _, line := range lines[from : to+1] { out = append(out, strings.TrimPrefix(line.Text, pad)) } text := strings.TrimRight(strings.Join(out, "\n"), "\n ") return text, strings.TrimSpace(text) != "" } // regionAround returns the first and last line of the copyable region a line // belongs to. func regionAround(lines []Line, at int) (from, to int) { if at < 0 || at >= len(lines) { return 0, -1 } region := lines[at].Region from, to = at, at for from > 0 && lines[from-1].Region == region { from-- } for to < len(lines)-1 && lines[to+1].Region == region { to++ } return from, to } // copySelection hands the text to whoever wired up OnCopy, and says what // happened on the status bar. func (v *View) copySelection() bool { if v.OnCopy == nil { return false } text, ok := v.Copy() if !ok { return false } v.OnCopy(text) v.SelectNothing() return true }