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.

🛟 Updated. 28d5985 · on v0.9.0 · k33g · 14h ago
view_select.go · 130 lines · 3.5 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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
}