▍ humdrum codex / glint v1.0.2
license AGPL-3.0
3.4 KB raw
  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
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.
func withBackground(spans []Span, bg lipgloss.Color) []Span {
	for i := range spans {
		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
}