~taiite/senpai

edb9412b834d246c8b8dbf78e52e6c0129bb66ef — delthas 11 months ago e9a2fc4
Introduce backsearch message support with ctrl+R

Fixes: #47
3 files changed, 125 insertions(+), 8 deletions(-)

M app.go
M ui/editor.go
M ui/ui.go
M app.go => app.go +2 -0
@@ 401,6 401,8 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
		if ok {
			app.typing()
		}
	case tcell.KeyCtrlR:
		app.win.InputBackSearch()
	case tcell.KeyTab:
		ok := app.win.InputAutoComplete(1)
		if ok {

M ui/editor.go => ui/editor.go +119 -8
@@ 1,6 1,8 @@
package ui

import (
	"strings"

	"github.com/gdamore/tcell/v2"
)



@@ 35,6 37,10 @@ type Editor struct {
	autoComplete func(cursorIdx int, text []rune) []Completion
	autoCache    []Completion
	autoCacheIdx int

	backsearch        bool
	backsearchPattern []rune // pre-lowercased
	backsearchIdx     int
}

// NewEditor returns a new Editor.


@@ 52,6 58,7 @@ func (e *Editor) Resize(width int) {
		e.cursorIdx = 0
		e.offsetIdx = 0
		e.autoCache = nil
		e.backsearchEnd()
	}
	e.width = width
}


@@ 69,9 76,31 @@ func (e *Editor) TextLen() int {
}

func (e *Editor) PutRune(r rune) {
	e.putRune(r)
	e.Right()
	e.autoCache = nil
	lowerRune := runeToLower(r)
	if e.backsearch && e.cursorIdx < e.TextLen() {
		lowerNext := runeToLower(e.text[e.lineIdx][e.cursorIdx])
		if lowerRune == lowerNext {
			e.right()
			e.backsearchPattern = append(e.backsearchPattern, lowerRune)
			return
		}
	}
	e.putRune(r)
	e.right()
	if e.backsearch {
		wasEmpty := len(e.backsearchPattern) == 0
		e.backsearchPattern = append(e.backsearchPattern, lowerRune)
		if wasEmpty {
			clearLine := e.lineIdx == len(e.text)-1
			e.backsearchUpdate(e.lineIdx - 1)
			if clearLine && e.lineIdx < len(e.text)-1 {
				e.text = e.text[:len(e.text)-1]
			}
		} else {
			e.backsearchUpdate(e.lineIdx)
		}
	}
}

func (e *Editor) putRune(r rune) {


@@ 93,8 122,16 @@ func (e *Editor) RemRune() (ok bool) {
		return
	}
	e.remRuneAt(e.cursorIdx - 1)
	e.left()
	e.autoCache = nil
	e.Left()
	if e.backsearch {
		if e.TextLen() == 0 {
			e.backsearchEnd()
		} else {
			e.backsearchPattern = e.backsearchPattern[:len(e.backsearchPattern)-1]
			e.backsearchUpdate(e.lineIdx)
		}
	}
	return
}



@@ 105,6 142,7 @@ func (e *Editor) RemRuneForward() (ok bool) {
	}
	e.remRuneAt(e.cursorIdx)
	e.autoCache = nil
	e.backsearchEnd()
	return
}



@@ 135,7 173,7 @@ func (e *Editor) RemWord() (ok bool) {
	// |
	for e.cursorIdx > 0 && line[e.cursorIdx-1] == ' ' {
		e.remRuneAt(e.cursorIdx - 1)
		e.Left()
		e.left()
	}

	for i := e.cursorIdx - 1; i >= 0; i -= 1 {


@@ 143,10 181,11 @@ func (e *Editor) RemWord() (ok bool) {
			break
		}
		e.remRuneAt(i)
		e.Left()
		e.left()
	}

	e.autoCache = nil
	e.backsearchEnd()
	return
}



@@ 162,6 201,7 @@ func (e *Editor) Flush() (content string) {
	e.cursorIdx = 0
	e.offsetIdx = 0
	e.autoCache = nil
	e.backsearchEnd()
	return
}



@@ 180,6 220,7 @@ func (e *Editor) Clear() bool {
func (e *Editor) Right() {
	e.right()
	e.autoCache = nil
	e.backsearchEnd()
}

func (e *Editor) right() {


@@ 212,6 253,11 @@ func (e *Editor) RightWord() {
}

func (e *Editor) Left() {
	e.left()
	e.backsearchEnd()
}

func (e *Editor) left() {
	if e.cursorIdx == 0 {
		return
	}


@@ 232,13 278,14 @@ func (e *Editor) LeftWord() {
	line := e.text[e.lineIdx]

	for e.cursorIdx > 0 && line[e.cursorIdx-1] == ' ' {
		e.Left()
		e.left()
	}
	for i := e.cursorIdx - 1; i >= 0 && line[i] != ' '; i -= 1 {
		e.Left()
		e.left()
	}

	e.autoCache = nil
	e.backsearchEnd()
}

func (e *Editor) Home() {


@@ 248,6 295,7 @@ func (e *Editor) Home() {
	e.cursorIdx = 0
	e.offsetIdx = 0
	e.autoCache = nil
	e.backsearchEnd()
}

func (e *Editor) End() {


@@ 259,6 307,7 @@ func (e *Editor) End() {
		e.offsetIdx++
	}
	e.autoCache = nil
	e.backsearchEnd()
}

func (e *Editor) Up() {


@@ 270,6 319,7 @@ func (e *Editor) Up() {
	e.cursorIdx = 0
	e.offsetIdx = 0
	e.autoCache = nil
	e.backsearchEnd()
	e.End()
}



@@ 286,6 336,7 @@ func (e *Editor) Down() {
	e.cursorIdx = 0
	e.offsetIdx = 0
	e.autoCache = nil
	e.backsearchEnd()
	e.End()
}



@@ 311,9 362,47 @@ func (e *Editor) AutoComplete(offset int) (ok bool) {
		e.offsetIdx++
	}

	e.backsearchEnd()
	return true
}

func (e *Editor) BackSearch() {
	clearLine := false
	if !e.backsearch {
		e.backsearch = true
		e.backsearchPattern = []rune(strings.ToLower(string(e.text[e.lineIdx])))
		clearLine = e.lineIdx == len(e.text)-1
	}
	e.backsearchUpdate(e.lineIdx - 1)
	if clearLine && e.lineIdx < len(e.text)-1 {
		e.text = e.text[:len(e.text)-1]
	}
}

func (e *Editor) backsearchUpdate(start int) {
	if len(e.backsearchPattern) == 0 {
		return
	}
	pattern := string(e.backsearchPattern)
	for i := start; i >= 0; i-- {
		if match := strings.Index(strings.ToLower(string(e.text[i])), pattern); match >= 0 {
			e.lineIdx = i
			e.computeTextWidth()
			e.cursorIdx = runeOffset(string(e.text[i]), match) + len(e.backsearchPattern)
			e.offsetIdx = 0
			for e.width < e.textWidth[e.cursorIdx]-e.textWidth[e.offsetIdx]+16 {
				e.offsetIdx++
			}
			e.autoCache = nil
			break
		}
	}
}

func (e *Editor) backsearchEnd() {
	e.backsearch = false
}

func (e *Editor) computeTextWidth() {
	e.textWidth = e.textWidth[:1]
	rw := 0


@@ 331,7 420,11 @@ func (e *Editor) Draw(screen tcell.Screen, x0, y int) {

	for i < len(e.text[e.lineIdx]) && x < x0+e.width {
		r := e.text[e.lineIdx][i]
		screen.SetContent(x, y, r, nil, st)
		s := st
		if e.backsearch && i < e.cursorIdx && i >= e.cursorIdx-len(e.backsearchPattern) {
			s = s.Underline(true)
		}
		screen.SetContent(x, y, r, nil, s)
		x += runeWidth(r)
		i++
	}


@@ 344,3 437,21 @@ func (e *Editor) Draw(screen tcell.Screen, x0, y int) {
	cursorX := x0 + e.textWidth[e.cursorIdx] - e.textWidth[e.offsetIdx]
	screen.ShowCursor(cursorX, y)
}

// runeOffset returns the lowercase version of a rune
// TODO: len(strings.ToLower(string(r))) == len(strings.ToUpper(string(r))) for all x?
func runeToLower(r rune) rune {
	return []rune(strings.ToLower(string(r)))[0]
}

// runeOffset returns the rune index of the rune starting at byte i in string s
func runeOffset(s string, pos int) int {
	n := 0
	for i, _ := range s {
		if i >= pos {
			return n
		}
		n++
	}
	return n
}

M ui/ui.go => ui/ui.go +4 -0
@@ 265,6 265,10 @@ func (ui *UI) InputClear() bool {
	return ui.e.Clear()
}

func (ui *UI) InputBackSearch() {
	ui.e.BackSearch()
}

func (ui *UI) Resize() {
	w, h := ui.screen.Size()
	innerWidth := w - 9 - ui.config.ChanColWidth - ui.config.NickColWidth - ui.config.MemberColWidth