~taiite/public-inbox

senpai: Introduce backsearch message support with ctrl+R v1 APPLIED

delthas: 1
 Introduce backsearch message support with ctrl+R

 3 files changed, 125 insertions(+), 8 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~taiite/public-inbox/patches/23768/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH senpai] Introduce backsearch message support with ctrl+R Export this patch

Fixes: #47
---
 app.go       |   2 +
 ui/editor.go | 127 +++++++++++++++++++++++++++++++++++++++++++++++----
 ui/ui.go     |   4 ++
 3 files changed, 125 insertions(+), 8 deletions(-)

diff --git a/app.go b/app.go
index 01f9bc4..4b832b4 100644
--- a/app.go
+++ b/app.go
@@ -395,6 +395,8 @@ func (app *App) handleKeyEvent(ev *tcell.EventKey) {
			app.typing()
			app.updatePrompt()
		}
	case tcell.KeyCtrlR:
		app.win.InputBackSearch()
	case tcell.KeyTab:
		ok := app.win.InputAutoComplete(1)
		if ok {
diff --git a/ui/editor.go b/ui/editor.go
index bf21f85..9a7c808 100644
--- a/ui/editor.go
+++ b/ui/editor.go
@@ -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
}

func NewEditor(width int, autoComplete func(cursorIdx int, text []rune) []Completion) Editor {
@@ -51,6 +57,7 @@ func (e *Editor) Resize(width int) {
		e.cursorIdx = 0
		e.offsetIdx = 0
		e.autoCache = nil
		e.backsearchEnd()
	}
	e.width = width
}
@@ -68,9 +75,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) {
@@ -92,8 +121,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
}

@@ -104,6 +141,7 @@ func (e *Editor) RemRuneForward() (ok bool) {
	}
	e.remRuneAt(e.cursorIdx)
	e.autoCache = nil
	e.backsearchEnd()
	return
}

@@ -134,7 +172,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 {
@@ -142,10 +180,11 @@ func (e *Editor) RemWord() (ok bool) {
			break
		}
		e.remRuneAt(i)
		e.Left()
		e.left()
	}

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

@@ -161,12 +200,14 @@ func (e *Editor) Flush() (content string) {
	e.cursorIdx = 0
	e.offsetIdx = 0
	e.autoCache = nil
	e.backsearchEnd()
	return
}

func (e *Editor) Right() {
	e.right()
	e.autoCache = nil
	e.backsearchEnd()
}

func (e *Editor) right() {
@@ -199,6 +240,11 @@ func (e *Editor) RightWord() {
}

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

func (e *Editor) left() {
	if e.cursorIdx == 0 {
		return
	}
@@ -219,13 +265,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() {
@@ -235,6 +282,7 @@ func (e *Editor) Home() {
	e.cursorIdx = 0
	e.offsetIdx = 0
	e.autoCache = nil
	e.backsearchEnd()
}

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

func (e *Editor) Up() {
@@ -257,6 +306,7 @@ func (e *Editor) Up() {
	e.cursorIdx = 0
	e.offsetIdx = 0
	e.autoCache = nil
	e.backsearchEnd()
	e.End()
}

@@ -273,6 +323,7 @@ func (e *Editor) Down() {
	e.cursorIdx = 0
	e.offsetIdx = 0
	e.autoCache = nil
	e.backsearchEnd()
	e.End()
}

@@ -298,9 +349,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
@@ -318,7 +407,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++
	}
@@ -331,3 +424,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
}
diff --git a/ui/ui.go b/ui/ui.go
index be43272..9fb814a 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -227,6 +227,10 @@ func (ui *UI) InputEnter() (content string) {
	return ui.e.Flush()
}

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

func (ui *UI) Resize() {
	w, h := ui.screen.Size()
	ui.e.Resize(w - 9 - ui.config.ChanColWidth - ui.config.NickColWidth)
-- 
2.30.0
Pushed, thanks!