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
|
package editor
import "github.com/charmbracelet/lipgloss"
// segment is one visual row of a logical line: a contiguous rune slice plus the
// rune offset (start column) within the logical line where it begins.
type segment struct {
text string
start int
}
// wrapLine word-wraps a logical line into visual segments at the given width.
// It breaks just after the last space that fits; a word longer than width is
// hard-broken at width. The segments partition the line's runes contiguously
// (concatenating their text reproduces the line exactly), so the cursor maps to
// exactly one segment. A width <= 0 or a line that fits returns one segment; an
// empty line returns one empty segment.
func wrapLine(line string, width int) []segment {
r := []rune(line)
if width <= 0 || len(r) <= width {
return []segment{{text: line, start: 0}}
}
var segs []segment
start := 0
for start < len(r) {
if start+width >= len(r) {
segs = append(segs, segment{text: string(r[start:]), start: start})
break
}
end := start + width
brk := -1
for i := end - 1; i > start; i-- {
if r[i] == ' ' {
brk = i + 1 // keep the space as this segment's trailing rune
break
}
}
if brk <= start {
brk = end // long word: hard break
}
segs = append(segs, segment{text: string(r[start:brk]), start: start})
start = brk
}
if len(segs) == 0 {
segs = append(segs, segment{text: "", start: 0})
}
return segs
}
// sliceSpans returns the spans covering rune range [start, end) of a line whose
// spans concatenate to the full line. Boundary spans are split; styles are
// preserved.
func sliceSpans(spans []Span, start, end int) []Span {
var out []Span
pos := 0
for _, sp := range spans {
r := []rune(sp.Text)
spStart, spEnd := pos, pos+len(r)
pos = spEnd
lo := max(spStart, start)
hi := min(spEnd, end)
if lo < hi {
out = append(out, Span{Text: string(r[lo-spStart : hi-spStart]), Style: sp.Style})
}
}
return out
}
// vrow is a rendered visual row: which logical row it came from, the rune start
// offset and rune count within that line, and the styled spans for the segment.
type vrow struct {
logRow int
start int
runes int
spans []Span
}
// buildVisual wraps every logical line into visual rows, slicing each line's
// styled spans to the segment ranges. Every span is given the theme background
// so glyphs sit on the theme's paper rather than the terminal default.
func (e *Editor) buildVisual() []vrow {
all := ScanLines(e.Lines, e.theme)
var rows []vrow
for li := range e.Lines {
for _, s := range wrapLine(e.Lines[li], e.Width) {
n := len([]rune(s.text))
rows = append(rows, vrow{
logRow: li,
start: s.start,
runes: n,
spans: withBackground(sliceSpans(all[li], s.start, s.start+n), e.theme.Background),
})
}
}
return rows
}
// withBackground stamps the theme background onto every span's style so the
// rendered text has no terminal-default gaps between or under glyphs. Spans that
// already carry a background (e.g. ==highlight==) keep theirs.
func withBackground(spans []Span, bg lipgloss.Color) []Span {
for i := range spans {
if _, unset := spans[i].Style.GetBackground().(lipgloss.NoColor); unset {
spans[i].Style = spans[i].Style.Background(bg)
}
}
return spans
}
// cursorVIndex returns the index of the visual row holding the cursor. A cursor
// at the end of a line (or on an empty line) maps to that line's last segment.
func cursorVIndex(rows []vrow, cur Position) int {
last := -1
for i, vr := range rows {
if vr.logRow != cur.Row {
continue
}
last = i
if cur.Col >= vr.start && cur.Col < vr.start+vr.runes {
return i
}
}
return last
}
|