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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
package editor
import (
"strings"
"github.com/charmbracelet/lipgloss"
)
// selectionForRow returns the row-local rune range [a,b) of vr that is selected.
func (e *Editor) selectionForRow(vr vrow) (a, b int, ok bool) {
start, end, has := e.selRange()
if !has || vr.logRow < start.Row || vr.logRow > end.Row {
return 0, 0, false
}
rowStart, rowEnd := vr.start, vr.start+vr.runes
lo, hi := rowStart, rowEnd
if vr.logRow == start.Row && start.Col > lo {
lo = start.Col
}
if vr.logRow == end.Row && end.Col < hi {
hi = end.Col
}
if lo >= hi {
return 0, 0, false
}
return lo - rowStart, hi - rowStart, true
}
// overlaySelection restyles the runes in [a,b) of the row's spans with sel,
// keeping the rest as-is. The concatenated text is unchanged.
func overlaySelection(spans []Span, a, b int, sel lipgloss.Style) []Span {
total := 0
for _, sp := range spans {
total += len([]rune(sp.Text))
}
out := sliceSpans(spans, 0, a)
mid := sliceSpans(spans, a, b)
for i := range mid {
mid[i].Style = sel
mid[i].Wavy = false // selection highlight supersedes the misspell undercurl
}
out = append(out, mid...)
return append(out, sliceSpans(spans, b, total)...)
}
// before reports whether position a comes before b in the document.
func (a Position) before(b Position) bool {
if a.Row != b.Row {
return a.Row < b.Row
}
return a.Col < b.Col
}
// HasSelection reports whether a non-empty selection is active.
func (e *Editor) HasSelection() bool {
return e.anchor != nil && *e.anchor != e.Cursor
}
// ClearSelection drops any active selection.
func (e *Editor) ClearSelection() { e.anchor = nil }
// MouseAnchor moves the cursor to a visual click position and drops any active
// selection — the press that begins a click or a drag (TASK-027).
func (e *Editor) MouseAnchor(vi, col int) {
e.MoveToVisual(vi, col)
e.ClearSelection()
}
// MouseExtendTo extends a mouse selection to the drag point, anchoring at the
// press position on the first motion and holding that anchor across subsequent
// motions (TASK-027). Soft-wrap aware via MoveToVisual.
func (e *Editor) MouseExtendTo(vi, col int) {
e.startSelection()
e.MoveToVisual(vi, col)
}
// startSelection anchors a selection at the current cursor if none is active.
func (e *Editor) startSelection() {
if e.anchor == nil {
c := e.Cursor
e.anchor = &c
}
}
// selRange returns the ordered selection bounds and whether a selection exists.
func (e *Editor) selRange() (start, end Position, ok bool) {
if !e.HasSelection() {
return Position{}, Position{}, false
}
a, b := *e.anchor, e.Cursor
if b.before(a) {
a, b = b, a
}
return a, b, true
}
// SelectedText returns the selected text (with newlines for multi-line spans),
// or "" when there is no selection.
func (e *Editor) SelectedText() string {
start, end, ok := e.selRange()
if !ok {
return ""
}
if start.Row == end.Row {
r := []rune(e.Lines[start.Row])
return string(r[start.Col:end.Col])
}
var b strings.Builder
first := []rune(e.Lines[start.Row])
b.WriteString(string(first[start.Col:]))
for row := start.Row + 1; row < end.Row; row++ {
b.WriteByte('\n')
b.WriteString(e.Lines[row])
}
b.WriteByte('\n')
last := []rune(e.Lines[end.Row])
b.WriteString(string(last[:end.Col]))
return b.String()
}
// DeleteSelection removes the selected text, places the cursor at its start, and
// clears the selection. Returns true if anything was deleted.
func (e *Editor) DeleteSelection() bool {
start, end, ok := e.selRange()
if !ok {
return false
}
first := []rune(e.Lines[start.Row])
last := []rune(e.Lines[end.Row])
merged := string(first[:start.Col]) + string(last[end.Col:])
e.Lines[start.Row] = merged
if end.Row > start.Row {
e.Lines = append(e.Lines[:start.Row+1], e.Lines[end.Row+1:]...)
}
e.Cursor = start
e.anchor = nil
e.Dirty = true
e.invalidate()
e.setGoal()
e.followCursor()
return true
}
// InsertText inserts s at the cursor (replacing any selection), handling
// newlines — used by paste.
func (e *Editor) InsertText(s string) {
e.invalidate()
if e.HasSelection() {
e.DeleteSelection()
}
for _, r := range s {
if r == '\n' {
e.InsertNewline()
} else {
e.InsertRune(r)
}
}
}
|