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}