master sewn/kohai / ui / editor.go
  1package ui
  2
  3import (
  4	"strings"
  5
  6	"git.sr.ht/~rockorager/vaxis"
  7)
  8
  9type Completion struct {
 10	Async   any // not nil if this completion is asynchronously loading values
 11	AsyncID int
 12
 13	StartIdx  int
 14	EndIdx    int
 15	Text      []rune
 16	Display   []rune
 17	CursorIdx int // in runes
 18}
 19
 20type editorLine struct {
 21	runes    []rune
 22	clusters []int
 23}
 24
 25func newEditorLine() editorLine {
 26	return editorLine{
 27		runes:    []rune{},
 28		clusters: []int{0},
 29	}
 30}
 31
 32func (l *editorLine) copy() editorLine {
 33	return editorLine{
 34		runes:    append([]rune{}, l.runes...),
 35		clusters: append([]int{}, l.clusters...),
 36	}
 37}
 38
 39// Editor is the text field where the user writes messages and commands.
 40type Editor struct {
 41	ui *UI
 42
 43	// text contains the written runes. An empty slice means no text is written.
 44	text []editorLine
 45
 46	// history contains the original content of each previously sent message.
 47	// It gets out of sync with text when history is modified à la readline,
 48	// then overwrites text when a new message is added.
 49	history []editorLine
 50
 51	lineIdx int
 52
 53	// textWidth[i] contains the width of text.runes[:text.clusters[i]]. Therefore
 54	// len(textWidth) is always strictly greater than 0 and textWidth[0] is
 55	// always 0.
 56	textWidth []int
 57
 58	// cursorIdx is the index of clusters in text of the placement of the cursor.
 59	cursorIdx int
 60
 61	// offsetIdx is the number of clusters in text that are skipped when rendering.
 62	offsetIdx int
 63
 64	// width is the width of the screen.
 65	width int
 66
 67	autoCache    []Completion
 68	autoCacheIdx int
 69
 70	backsearch        bool
 71	backsearchPattern []rune // pre-lowercased
 72
 73	// oldest (lowest) index in text of lines that were changed.
 74	// used as an optimization to reduce copying when flushing lines.
 75	oldestTextChange int
 76}
 77
 78// NewEditor returns a new Editor.
 79// Call Resize() once before using it.
 80func NewEditor(ui *UI) Editor {
 81	return Editor{
 82		ui:        ui,
 83		text:      []editorLine{newEditorLine()},
 84		history:   []editorLine{},
 85		textWidth: []int{0},
 86	}
 87}
 88
 89func (e *Editor) Resize(width int) {
 90	if width < e.width {
 91		// Reset cursor to the same size, to recompute offsetIdx
 92		e.setCursor(e.textWidth[e.cursorIdx])
 93	}
 94	e.width = width
 95}
 96
 97// Content result must not be modified.
 98func (e *Editor) Content() []rune {
 99	return e.text[e.lineIdx].runes
100}
101
102func (e *Editor) Empty() bool {
103	return len(e.text[e.lineIdx].runes) == 0
104}
105
106// recompute must be called when runes is changed, to update
107// clusters, textWidth.
108func (e *Editor) recompute() {
109	c := make([]int, 0, len(e.text[e.lineIdx].runes)+1)
110	w := make([]int, 0, len(e.text[e.lineIdx].runes)+1)
111	nc := 0
112	nw := 0
113	for _, g := range vaxis.Characters(string(e.text[e.lineIdx].runes)) {
114		c = append(c, nc)
115		w = append(w, nw)
116		nc += len([]rune(g.Grapheme))
117		nw += stringWidth(e.ui.vx, g.Grapheme)
118	}
119	c = append(c, nc)
120	w = append(w, nw)
121	e.text[e.lineIdx].clusters = c
122	e.textWidth = w
123}
124
125// setCursor sets cursorIdx to the (grapheme cluster) offset
126// corresponding to the passed rune offset runeIdx, rounding up
127// to the next grapheme cluster as needed.
128func (e *Editor) setCursor(runeIdx int) {
129	for i, o := range e.text[e.lineIdx].clusters {
130		if o >= runeIdx {
131			e.cursorIdx = i
132			break
133		}
134	}
135	if e.width <= e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx] {
136		e.offsetIdx += 16
137		// If we went too far right, go back enough to show one cluster.
138		max := len(e.text[e.lineIdx].clusters) - 2
139		if max < e.offsetIdx {
140			e.offsetIdx = max
141		}
142	}
143}
144
145func (e *Editor) PutRune(r rune) {
146	e.autoCache = nil
147	lowerRune := runeToLower(r)
148	if e.backsearch && e.cursorIdx < len(e.text[e.lineIdx].clusters)-1 {
149		lowerNext := runeToLower(e.text[e.lineIdx].runes[e.text[e.lineIdx].clusters[e.cursorIdx]])
150		if lowerRune == lowerNext {
151			e.right()
152			e.backsearchPattern = append(e.backsearchPattern, lowerRune)
153			return
154		}
155	}
156	e.putRune(r)
157	if e.backsearch {
158		wasEmpty := len(e.backsearchPattern) == 0
159		e.backsearchPattern = append(e.backsearchPattern, lowerRune)
160		if wasEmpty {
161			e.backsearchUpdate(e.lineIdx - 1)
162		} else {
163			e.backsearchUpdate(e.lineIdx)
164		}
165	}
166}
167
168// putRune inserts a rune at the current cursor position,
169// then updates moves the cursor position after that rune.
170// (If inserting the rune merged a grapheme cluster, we
171// move the cursor after that cluster.)
172func (e *Editor) putRune(r rune) {
173	ci := e.text[e.lineIdx].clusters[e.cursorIdx]
174	e.text[e.lineIdx].runes = append(e.text[e.lineIdx].runes, ' ')
175	copy(e.text[e.lineIdx].runes[ci+1:], e.text[e.lineIdx].runes[ci:])
176	e.text[e.lineIdx].runes[ci] = r
177
178	e.recompute()
179	e.setCursor(ci + 1)
180
181	e.bumpOldestChange()
182}
183
184func (e *Editor) RemCluster() (ok bool) {
185	ok = 0 < e.cursorIdx
186	if !ok {
187		return
188	}
189	e.remClusterAt(e.cursorIdx - 1)
190	e.left()
191	e.autoCache = nil
192	if e.backsearch {
193		if e.Empty() || len(e.backsearchPattern) == 0 {
194			e.backsearchEnd()
195		} else {
196			e.backsearchPattern = e.backsearchPattern[:len(e.backsearchPattern)-1]
197			e.backsearchUpdate(e.lineIdx)
198		}
199	}
200	return
201}
202
203func (e *Editor) RemClusterForward() (ok bool) {
204	ok = e.cursorIdx < len(e.text[e.lineIdx].clusters)-1
205	if !ok {
206		return
207	}
208	e.remClusterAt(e.cursorIdx)
209	e.autoCache = nil
210	e.backsearchEnd()
211	return
212}
213
214func (e *Editor) RemBefore() (ok bool) {
215	ok = e.cursorIdx > 0
216	if !ok {
217		return
218	}
219	e.text[e.lineIdx].runes = e.text[e.lineIdx].runes[e.text[e.lineIdx].clusters[e.cursorIdx]:]
220	e.cursorIdx = 0
221	e.offsetIdx = 0
222
223	e.recompute()
224	e.bumpOldestChange()
225	e.autoCache = nil
226	e.backsearchEnd()
227	return
228}
229
230func (e *Editor) RemAfter() (ok bool) {
231	ok = e.cursorIdx < len(e.text[e.lineIdx].clusters)-1
232	if !ok {
233		return
234	}
235	e.text[e.lineIdx].runes = e.text[e.lineIdx].runes[:e.text[e.lineIdx].clusters[e.cursorIdx]]
236
237	e.recompute()
238	e.bumpOldestChange()
239	e.autoCache = nil
240	e.backsearchEnd()
241	return
242}
243
244func (e *Editor) remClusterAt(idx int) {
245	rs := e.text[e.lineIdx].clusters[idx]
246	re := e.text[e.lineIdx].clusters[idx+1]
247	copy(e.text[e.lineIdx].runes[rs:], e.text[e.lineIdx].runes[re:])
248	e.text[e.lineIdx].runes = e.text[e.lineIdx].runes[:len(e.text[e.lineIdx].runes)-(re-rs)]
249
250	e.recompute()
251	e.bumpOldestChange()
252}
253
254func (e *Editor) RemWord() (ok bool) {
255	ok = 0 < e.cursorIdx
256	if !ok {
257		return
258	}
259
260	line := e.text[e.lineIdx]
261
262	// To allow doing something like this (| is the cursor):
263	// Hello world|
264	// Hello |
265	// |
266	for e.cursorIdx > 0 && line.runes[line.clusters[e.cursorIdx-1]] == ' ' {
267		e.remClusterAt(e.cursorIdx - 1)
268		e.left()
269	}
270
271	for i := e.cursorIdx - 1; i >= 0; i -= 1 {
272		if line.runes[line.clusters[i]] == ' ' {
273			break
274		}
275		e.remClusterAt(i)
276		e.left()
277	}
278
279	e.autoCache = nil
280	e.backsearchEnd()
281	return
282}
283
284func (e *Editor) RemWordForward() (ok bool) {
285	ok = e.cursorIdx < len(e.text[e.lineIdx].clusters)-1
286	if !ok {
287		return
288	}
289
290	for e.cursorIdx < len(e.text[e.lineIdx].clusters)-1 && e.text[e.lineIdx].runes[e.text[e.lineIdx].clusters[e.cursorIdx]] == ' ' {
291		e.remClusterAt(e.cursorIdx)
292	}
293
294	for e.cursorIdx < len(e.text[e.lineIdx].clusters)-1 {
295		if e.text[e.lineIdx].runes[e.text[e.lineIdx].clusters[e.cursorIdx]] == ' ' {
296			break
297		}
298		e.remClusterAt(e.cursorIdx)
299	}
300
301	e.autoCache = nil
302	e.backsearchEnd()
303	return
304}
305
306func (e *Editor) Flush() string {
307	l := e.text[e.lineIdx]
308	content := string(l.runes)
309	if len(content) > 0 {
310		e.history = append(e.history, l.copy())
311	}
312	for i, line := range e.history[e.oldestTextChange:] {
313		i := i + e.oldestTextChange
314		e.text[i] = line.copy()
315	}
316	if len(content) > 0 {
317		e.text = append(e.text, newEditorLine())
318	} else {
319		e.text[len(e.text)-1] = newEditorLine()
320	}
321	e.lineIdx = len(e.text) - 1
322	e.textWidth = e.textWidth[:1]
323	e.cursorIdx = 0
324	e.offsetIdx = 0
325	e.autoCache = nil
326	e.backsearchEnd()
327	e.oldestTextChange = len(e.text) - 1
328	return content
329}
330
331func (e *Editor) Clear() bool {
332	if e.Empty() {
333		return false
334	}
335	e.text[e.lineIdx] = newEditorLine()
336	e.bumpOldestChange()
337	e.textWidth = e.textWidth[:1]
338	e.cursorIdx = 0
339	e.offsetIdx = 0
340	e.autoCache = nil
341	return true
342}
343
344func (e *Editor) Set(text string) {
345	r := []rune(text)
346	e.text[e.lineIdx].runes = r
347	e.recompute()
348	e.bumpOldestChange()
349	e.cursorIdx = len(e.text[e.lineIdx].clusters) - 1
350	e.offsetIdx = 0
351	for e.offsetIdx < len(e.textWidth)-1 && e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
352		e.offsetIdx++
353	}
354	e.autoCache = nil
355	e.backsearchEnd()
356}
357
358func (e *Editor) Enter() bool {
359	if e.autoCache != nil {
360		return e.AutoComplete()
361	}
362	return false
363}
364
365func (e *Editor) Right() {
366	e.right()
367	e.autoCache = nil
368	e.backsearchEnd()
369}
370
371func (e *Editor) right() {
372	if e.cursorIdx == len(e.text[e.lineIdx].clusters)-1 {
373		return
374	}
375	e.cursorIdx++
376	if e.width <= e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx] {
377		e.offsetIdx += 16
378		// If we went too far right, go back enough to show one cluster.
379		max := len(e.text[e.lineIdx].clusters) - 2
380		if max < e.offsetIdx {
381			e.offsetIdx = max
382		}
383	}
384}
385
386func (e *Editor) RightWord() {
387	line := e.text[e.lineIdx]
388
389	if e.cursorIdx == len(line.clusters)-1 {
390		return
391	}
392
393	for e.cursorIdx < len(line.clusters)-1 && line.runes[line.clusters[e.cursorIdx]] == ' ' {
394		e.Right()
395	}
396	for i := e.cursorIdx; i < len(line.clusters)-1 && line.runes[line.clusters[i]] != ' '; i++ {
397		e.Right()
398	}
399}
400
401func (e *Editor) Left() {
402	e.left()
403	e.backsearchEnd()
404}
405
406func (e *Editor) left() {
407	if e.cursorIdx == 0 {
408		return
409	}
410	e.cursorIdx--
411	if e.cursorIdx <= e.offsetIdx {
412		e.offsetIdx -= 16
413		if e.offsetIdx < 0 {
414			e.offsetIdx = 0
415		}
416	}
417}
418
419func (e *Editor) LeftWord() {
420	if e.cursorIdx == 0 {
421		return
422	}
423
424	line := e.text[e.lineIdx]
425
426	for e.cursorIdx > 0 && line.runes[line.clusters[e.cursorIdx-1]] == ' ' {
427		e.left()
428	}
429	for i := e.cursorIdx - 1; i >= 0 && line.runes[line.clusters[i]] != ' '; i-- {
430		e.left()
431	}
432
433	e.autoCache = nil
434	e.backsearchEnd()
435}
436
437func (e *Editor) Home() {
438	if e.cursorIdx == 0 {
439		return
440	}
441	e.cursorIdx = 0
442	e.offsetIdx = 0
443	e.autoCache = nil
444	e.backsearchEnd()
445}
446
447func (e *Editor) End() {
448	if e.cursorIdx == len(e.text[e.lineIdx].clusters)-1 {
449		return
450	}
451	e.cursorIdx = len(e.text[e.lineIdx].clusters) - 1
452	for e.offsetIdx < len(e.textWidth)-1 && e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
453		e.offsetIdx++
454	}
455	e.autoCache = nil
456	e.backsearchEnd()
457}
458
459func (e *Editor) Up() {
460	if e.autoCache != nil {
461		e.autoCacheIdx = (e.autoCacheIdx + 1) % len(e.autoCache)
462		return
463	}
464	if e.lineIdx == 0 {
465		return
466	}
467	e.lineIdx--
468	e.recompute()
469	e.cursorIdx = 0
470	e.offsetIdx = 0
471	e.autoCache = nil
472	e.backsearchEnd()
473	e.End()
474}
475
476func (e *Editor) Down() {
477	if e.autoCache != nil {
478		e.autoCacheIdx = (e.autoCacheIdx + len(e.autoCache) - 1) % len(e.autoCache)
479		return
480	}
481	if e.lineIdx == len(e.text)-1 {
482		e.Flush()
483		return
484	}
485	e.lineIdx++
486	e.recompute()
487	e.cursorIdx = 0
488	e.offsetIdx = 0
489	e.autoCache = nil
490	e.backsearchEnd()
491	e.End()
492}
493
494func (e *Editor) AutoComplete() (ok bool) {
495	if e.autoCache == nil {
496		e.autoCache = e.ui.config.AutoComplete(e.text[e.lineIdx].clusters[e.cursorIdx], e.text[e.lineIdx].runes)
497		if len(e.autoCache) == 0 {
498			e.autoCache = nil
499			return false
500		}
501		e.autoCacheIdx = 0
502		if len(e.autoCache) > 1 || e.autoCache[0].Async != nil {
503			return false
504		}
505	}
506	if e.autoCache[e.autoCacheIdx].Async != nil {
507		return false
508	}
509
510	e.text[e.lineIdx].runes = e.autoCache[e.autoCacheIdx].Text
511	e.recompute()
512	e.bumpOldestChange()
513	e.setCursor(e.autoCache[e.autoCacheIdx].CursorIdx)
514	if len(e.textWidth) <= e.offsetIdx {
515		e.offsetIdx = 0
516	}
517	for e.offsetIdx < len(e.textWidth)-1 && e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
518		e.offsetIdx++
519	}
520	e.autoCache = nil
521
522	e.backsearchEnd()
523	return true
524}
525
526func (e *Editor) AsyncCompletions(id int, cs []Completion) {
527	for i := 0; i < len(e.autoCache); i++ {
528		c := &e.autoCache[i]
529		if c.AsyncID != id {
530			continue
531		}
532		a := append([]Completion{}, e.autoCache[:i]...)
533		a = append(a, cs...)
534		a = append(a, e.autoCache[i+1:]...)
535		e.autoCache = a
536		break
537	}
538	if len(e.autoCache) == 0 {
539		e.autoCache = nil
540		e.autoCacheIdx = 0
541	} else if e.autoCacheIdx >= len(e.autoCache) {
542		e.autoCacheIdx = len(e.autoCache) - 1
543	}
544}
545
546func (e *Editor) BackSearch() {
547	if !e.backsearch {
548		e.backsearch = true
549		e.backsearchPattern = []rune(strings.ToLower(string(e.text[e.lineIdx].runes)))
550	}
551	e.backsearchUpdate(e.lineIdx - 1)
552}
553
554func (e *Editor) backsearchUpdate(start int) {
555	if len(e.backsearchPattern) == 0 {
556		return
557	}
558	pattern := string(e.backsearchPattern)
559	for i := start; i >= 0; i-- {
560		if match := strings.Index(strings.ToLower(string(e.text[i].runes)), pattern); match >= 0 {
561			e.lineIdx = i
562			e.recompute()
563			e.setCursor(runeOffset(string(e.text[i].runes), match) + len(e.backsearchPattern))
564			e.offsetIdx = 0
565			for e.offsetIdx < len(e.textWidth)-1 && e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
566				e.offsetIdx++
567			}
568			e.autoCache = nil
569			break
570		}
571	}
572}
573
574func (e *Editor) backsearchEnd() {
575	e.backsearch = false
576}
577
578// call this everytime e.text is modified
579func (e *Editor) bumpOldestChange() {
580	if e.oldestTextChange > e.lineIdx {
581		e.oldestTextChange = e.lineIdx
582	}
583}
584
585func (e *Editor) Draw(vx *Vaxis, x0, y int, hint string) {
586	var st vaxis.Style
587
588	x := x0
589	i := e.text[e.lineIdx].clusters[e.offsetIdx]
590	text := e.text[e.lineIdx].runes
591	showCursor := true
592
593	if len(text) == 0 && len(hint) > 0 && !e.backsearch {
594		i = 0
595		text = []rune(hint)
596		st.Foreground = e.ui.config.Colors.Status
597		showCursor = false
598	}
599
600	autoStart := -1
601	autoEnd := -1
602	autoX := x0
603	if e.autoCache != nil {
604		autoStart = e.autoCache[e.autoCacheIdx].StartIdx
605		autoEnd = e.autoCache[e.autoCacheIdx].EndIdx
606	}
607
608	ci := e.text[e.lineIdx].clusters[e.cursorIdx]
609	for i < len(text) {
610		r := text[i:]
611		s := st
612		if e.backsearch && i < ci && i >= ci-len(e.backsearchPattern) {
613			s.UnderlineStyle = vaxis.UnderlineSingle
614		}
615		if i >= autoStart && i < autoEnd {
616			s.UnderlineStyle = vaxis.UnderlineSingle
617		}
618		if i == autoStart {
619			autoX = x
620		}
621		switch r[0] {
622		case '\n':
623			s.Attribute |= vaxis.AttrBold
624			s.Foreground = ColorRed
625			r = []rune{'↲'}
626		case 0x02:
627			st.Attribute ^= vaxis.AttrBold
628		case 0x03, 0x04:
629			// TODO: Not possible to show the color without implementing
630			//       custom character to mark the end or add state
631		case 0x0F:
632			st.Attribute = vaxis.AttrNone
633		case 0x11:
634			st.Attribute ^= vaxis.AttrDim
635		case 0x16:
636			st.Attribute ^= vaxis.AttrReverse
637		case 0x1D:
638			st.Attribute ^= vaxis.AttrItalic
639		case 0x1E:
640			st.Attribute ^= vaxis.AttrStrikethrough
641		case 0x1F:
642			if st.UnderlineStyle == vaxis.UnderlineOff {
643				st.UnderlineStyle = vaxis.UnderlineSingle
644			} else {
645				st.UnderlineStyle = vaxis.UnderlineOff
646			}
647		}
648		dx, di := printCluster(vx, x, y, x0+e.width, r, s)
649		if di == 0 {
650			break
651		}
652		x += dx
653		i += di
654	}
655	if i == autoStart {
656		autoX = x
657	}
658
659	for x < x0+e.width {
660		setCell(vx, x, y, ' ', st)
661		x++
662	}
663
664	autoCount := y - 2
665	if autoCount < 0 {
666		autoCount = 0
667	} else if autoCount > len(e.autoCache) {
668		autoCount = len(e.autoCache)
669	} else if autoCount > 10 {
670		autoCount = 10
671	}
672	autoOff := 0
673	if len(e.autoCache) > autoCount {
674		autoOff = e.autoCacheIdx - autoCount/2
675		if autoOff < 0 {
676			autoOff = 0
677		} else if autoOff > len(e.autoCache)-autoCount {
678			autoOff = len(e.autoCache) - autoCount
679		}
680	}
681
682	for ci, completion := range e.autoCache[autoOff : autoOff+autoCount] {
683		display := completion.Display
684		if display == nil {
685			display = completion.Text[completion.StartIdx:]
686		}
687		var unselectable bool
688		if completion.Async != nil {
689			display = []rune("Loading...")
690			unselectable = true
691		} else if (ci == 0 && autoOff > 0) || (ci == autoCount-1 && autoOff+autoCount < len(e.autoCache)) {
692			display = []rune("...")
693			unselectable = true
694		}
695
696		x := autoX
697		y := y - ci - 1
698		i := 0
699		for i < len(display) {
700			s := vaxis.Style{
701				Attribute: vaxis.AttrReverse,
702			}
703			if ci+autoOff == e.autoCacheIdx {
704				s.Attribute |= vaxis.AttrBold
705			} else {
706				s.Attribute |= vaxis.AttrDim
707			}
708			if unselectable {
709				s.Attribute |= vaxis.AttrItalic
710			}
711			dx, di := printCluster(vx, x, y, x0+e.width, display[i:], s)
712			if di == 0 {
713				break
714			}
715			x += dx
716			i += di
717		}
718	}
719
720	if showCursor {
721		cursorX := x0 + e.textWidth[e.cursorIdx] - e.textWidth[e.offsetIdx]
722		vx.ShowCursor(cursorX, y, vaxis.CursorBeam)
723	} else {
724		vx.HideCursor()
725	}
726}
727
728// runeOffset returns the lowercase version of a rune
729// TODO: len(strings.ToLower(string(r))) == len(strings.ToUpper(string(r))) for all x?
730func runeToLower(r rune) rune {
731	return []rune(strings.ToLower(string(r)))[0]
732}
733
734// runeOffset returns the rune index of the rune starting at byte pos in string s
735func runeOffset(s string, pos int) int {
736	n := 0
737	for i := range s {
738		if i >= pos {
739			return n
740		}
741		n++
742	}
743	return n
744}