gfishline/completer.go

211 lines
4.6 KiB
Go

package gfishline
import (
"fmt"
"slices"
"strings"
"gitea.zaclys.com/bvaudour/gob/format"
"gitea.zaclys.com/bvaudour/gob/shell/console"
"gitea.zaclys.com/bvaudour/gob/shell/console/atom"
)
func coordToIndex(x, y, c, n int) int {
l, r := n/c, n%c
switch {
case r == 0:
return x*l + y
case x < r:
return x*(l+1) + y
case y >= l:
return -1
default:
return r*(l+1) + (x-r)*l + y
}
}
func indexToCoord(i, c, n int) (x, y int) {
l, r := n/c, n%c
switch {
case r == 0:
x, y = i/l, i%l
case i < r*(l+1):
x, y = i/(l+1), i%(l+1)
default:
j := i - r*l
x, y = r+j/l, j%l
}
return
}
type CompleteFunc func(words []string, wordIndex int, isIndexSpace bool) (choices []string)
func NoCompletion(words []string, wordIndex int, isIndexSpace bool) (choices []string) {
return
}
func DefaultCompletion(commands, options []string) CompleteFunc {
return func(words []string, wordIndex int, isIndexSpace bool) (choices []string) {
if isIndexSpace {
if wordIndex <= 0 {
return commands
}
return options
}
word, sl := words[wordIndex], options
if wordIndex == 0 {
sl = commands
}
for _, choice := range sl {
if strings.HasPrefix(choice, word) {
choices = append(choices, choice)
}
}
return
}
}
type completer struct {
*splitter
wordIndex int
wordPos int
isInit bool
needAdd bool
choices console.Cycler
}
func newCompleter(buffer string, cursor int, complete CompleteFunc) *completer {
c := completer{
splitter: newSplitter(buffer, cursor),
isInit: true,
}
wordIndex, wordCursor, isIndexSpace := c.compute()
if isIndexSpace && wordCursor == 0 && wordIndex > 0 {
isIndexSpace = false
c.cursor--
}
choices := complete(c.words, wordIndex, isIndexSpace)
slices.Sort(choices)
if !isIndexSpace {
current := c.words[wordIndex]
if slices.Contains(choices, current) {
choices = append([]string{current}, slices.DeleteFunc(choices, func(e string) bool { return e == current })...)
}
}
choices = slices.Compact(choices)
c.choices = atom.NewCycler(true, choices...)
c.needAdd, c.wordIndex, c.wordPos = isIndexSpace, wordIndex, wordCursor
return &c
}
func (c *completer) update(next string) {
c.setWord(c.wordIndex, next)
if c.isInit {
c.cursor += 2
if _, ok := c.space[c.wordIndex+1]; !ok {
c.space[c.wordIndex+1] = " "
}
c.isInit = false
}
}
func (c *completer) add(next string) {
c.addWord(c.wordIndex, next, c.wordPos)
c.isInit, c.needAdd = false, false
}
func (c *completer) set() (ok bool) {
var next string
if next, ok = console.FocusedElement(c.choices).Get(); !ok {
return
}
if c.needAdd {
c.add(next)
} else {
c.update(next)
}
return
}
func (c *completer) prev() bool { return c.choices.Prev() && c.set() }
func (c *completer) next() bool { return c.choices.Next() && c.set() }
func (c *completer) move(index int) bool { return c.choices.SetCursor(index) && c.set() }
func (c *completer) formatChoices(currentWord string, terminalWidth, terminalHeight int) string {
var buf strings.Builder
l, cursor := c.choices.Len(), c.choices.Cursor()
if l == 0 {
return ""
}
var maxLen int
choices := make([]string, l)
for i := 0; i < l; i++ {
word, _ := c.choices.Index(i).Get()
maxLen, choices[i] = max(atom.VisibleWidth(word)+1, maxLen), word
}
maxCol := min(max(terminalWidth/maxLen, 1), 5)
lines := l / maxCol
if l%maxCol > 0 {
lines++
}
lineMin, lineMax := 0, lines-1
var isPartial bool
if isPartial = terminalHeight < lines; isPartial {
_, cursorLine := indexToCoord(cursor, maxCol, l)
if cursorLine < terminalHeight {
lineMax = min(terminalHeight, lines) - 1
} else {
lineMin, lineMax = cursorLine+1-min(terminalHeight, lines), cursorLine
}
}
colWith := terminalWidth / maxCol
if colWith > maxLen {
colWith = (colWith + maxLen) >> 1
}
for y := lineMin; y <= lineMax; y++ {
if y > 0 {
buf.WriteRune('\n')
}
for x := 0; x < maxCol; x++ {
i := coordToIndex(x, y, maxCol, l)
if i >= 0 {
word := choices[i]
realSize, displaySize := len([]rune(word)), atom.VisibleWidth(word)
size := maxLen + realSize - displaySize
word = strings.TrimPrefix(format.Left(word, size), currentWord)
if i == cursor {
buf.WriteString(format.Apply(currentWord, "bg_l_white", "black", "bold", "underline"))
buf.WriteString(format.Apply(word, "bg_l_white", "black"))
} else {
buf.WriteString(format.Apply(currentWord, "bold", "underline"))
buf.WriteString(word)
}
}
}
}
if isPartial {
position := fmt.Sprintf("lignes %d à %d (%d)", lineMin+1, lineMax+1, lines)
buf.WriteRune('\n')
buf.WriteString(format.Apply(position, "bg_l_cyan", "black"))
}
return buf.String()
}