โ– 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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package editor

import "strings"

// In-document find (TASK-007): a case-insensitive substring search over the
// buffer. Matches are kept in document order; one is "active" (the cursor parks
// on it) and View highlights all of them, the active one more strongly. The app
// drives this from a find bar โ€” live SetFindQuery as the query is typed, then
// FindNext/FindPrev to cycle.

// match is a found range within a single logical line, in rune columns.
type match struct {
	row      int
	startCol int
	endCol   int
}

// SetFindQuery recomputes matches for q (case-insensitive) and activates the
// first match at or after the cursor, wrapping to the first otherwise. It moves
// the cursor onto that match and scrolls it into view. Returns the match count.
func (e *Editor) SetFindQuery(q string) int {
	e.findQuery = q
	e.find = nil
	e.findActive = -1
	if q == "" {
		return 0
	}
	qr := []rune(strings.ToLower(q))
	for row, line := range e.Lines {
		lr := []rune(strings.ToLower(line))
		for i := 0; i+len(qr) <= len(lr); {
			if runesEqual(lr[i:i+len(qr)], qr) {
				e.find = append(e.find, match{row: row, startCol: i, endCol: i + len(qr)})
				i += len(qr) // non-overlapping
			} else {
				i++
			}
		}
	}
	if len(e.find) == 0 {
		return 0
	}
	e.findActive = 0
	for idx, m := range e.find {
		if m.row > e.Cursor.Row || (m.row == e.Cursor.Row && m.startCol >= e.Cursor.Col) {
			e.findActive = idx
			break
		}
	}
	e.moveToActive()
	return len(e.find)
}

// FindNext activates the next match, wrapping around.
func (e *Editor) FindNext() {
	if len(e.find) == 0 {
		return
	}
	e.findActive = (e.findActive + 1) % len(e.find)
	e.moveToActive()
}

// FindPrev activates the previous match, wrapping around.
func (e *Editor) FindPrev() {
	if len(e.find) == 0 {
		return
	}
	e.findActive = (e.findActive - 1 + len(e.find)) % len(e.find)
	e.moveToActive()
}

// ClearFind drops all find state (called on Esc and when the buffer is replaced).
func (e *Editor) ClearFind() {
	e.find = nil
	e.findActive = -1
	e.findQuery = ""
}

// FindCount returns the number of current matches.
func (e *Editor) FindCount() int { return len(e.find) }

// ActiveMatch reports the active match's location, or ok=false if none.
func (e *Editor) ActiveMatch() (row, startCol, endCol int, ok bool) {
	if e.findActive < 0 || e.findActive >= len(e.find) {
		return 0, 0, 0, false
	}
	m := e.find[e.findActive]
	return m.row, m.startCol, m.endCol, true
}

// moveToActive parks the cursor at the active match start and scrolls to it.
func (e *Editor) moveToActive() {
	if e.findActive < 0 || e.findActive >= len(e.find) {
		return
	}
	m := e.find[e.findActive]
	e.Cursor = Position{Row: m.row, Col: m.startCol}
	e.setGoal()
	e.followCursor()
}

// rowMatch is a find match clipped to one visual row, in row-local rune columns.
type rowMatch struct {
	a, b   int
	active bool
}

// matchRangesForRow returns the find matches intersecting visual row vr.
func (e *Editor) matchRangesForRow(vr vrow) []rowMatch {
	if len(e.find) == 0 {
		return nil
	}
	rowStart, rowEnd := vr.start, vr.start+vr.runes
	var out []rowMatch
	for i, m := range e.find {
		if m.row != vr.logRow {
			continue
		}
		lo := max(m.startCol, rowStart)
		hi := min(m.endCol, rowEnd)
		if lo < hi {
			out = append(out, rowMatch{a: lo - rowStart, b: hi - rowStart, active: i == e.findActive})
		}
	}
	return out
}

func runesEqual(a, b []rune) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}