mirror of
https://github.com/strongdm/comply
synced 2024-08-30 10:53:34 +00:00
286 lines
6.1 KiB
Go
286 lines
6.1 KiB
Go
|
package readline
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
)
|
||
|
|
||
|
type AutoCompleter interface {
|
||
|
// Readline will pass the whole line and current offset to it
|
||
|
// Completer need to pass all the candidates, and how long they shared the same characters in line
|
||
|
// Example:
|
||
|
// [go, git, git-shell, grep]
|
||
|
// Do("g", 1) => ["o", "it", "it-shell", "rep"], 1
|
||
|
// Do("gi", 2) => ["t", "t-shell"], 2
|
||
|
// Do("git", 3) => ["", "-shell"], 3
|
||
|
Do(line []rune, pos int) (newLine [][]rune, length int)
|
||
|
}
|
||
|
|
||
|
type TabCompleter struct{}
|
||
|
|
||
|
func (t *TabCompleter) Do([]rune, int) ([][]rune, int) {
|
||
|
return [][]rune{[]rune("\t")}, 0
|
||
|
}
|
||
|
|
||
|
type opCompleter struct {
|
||
|
w io.Writer
|
||
|
op *Operation
|
||
|
width int
|
||
|
|
||
|
inCompleteMode bool
|
||
|
inSelectMode bool
|
||
|
candidate [][]rune
|
||
|
candidateSource []rune
|
||
|
candidateOff int
|
||
|
candidateChoise int
|
||
|
candidateColNum int
|
||
|
}
|
||
|
|
||
|
func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter {
|
||
|
return &opCompleter{
|
||
|
w: w,
|
||
|
op: op,
|
||
|
width: width,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) doSelect() {
|
||
|
if len(o.candidate) == 1 {
|
||
|
o.op.buf.WriteRunes(o.candidate[0])
|
||
|
o.ExitCompleteMode(false)
|
||
|
return
|
||
|
}
|
||
|
o.nextCandidate(1)
|
||
|
o.CompleteRefresh()
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) nextCandidate(i int) {
|
||
|
o.candidateChoise += i
|
||
|
o.candidateChoise = o.candidateChoise % len(o.candidate)
|
||
|
if o.candidateChoise < 0 {
|
||
|
o.candidateChoise = len(o.candidate) + o.candidateChoise
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) OnComplete() bool {
|
||
|
if o.width == 0 {
|
||
|
return false
|
||
|
}
|
||
|
if o.IsInCompleteSelectMode() {
|
||
|
o.doSelect()
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
buf := o.op.buf
|
||
|
rs := buf.Runes()
|
||
|
|
||
|
if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) {
|
||
|
o.EnterCompleteSelectMode()
|
||
|
o.doSelect()
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
o.ExitCompleteSelectMode()
|
||
|
o.candidateSource = rs
|
||
|
newLines, offset := o.op.cfg.AutoComplete.Do(rs, buf.idx)
|
||
|
if len(newLines) == 0 {
|
||
|
o.ExitCompleteMode(false)
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// only Aggregate candidates in non-complete mode
|
||
|
if !o.IsInCompleteMode() {
|
||
|
if len(newLines) == 1 {
|
||
|
buf.WriteRunes(newLines[0])
|
||
|
o.ExitCompleteMode(false)
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
same, size := runes.Aggregate(newLines)
|
||
|
if size > 0 {
|
||
|
buf.WriteRunes(same)
|
||
|
o.ExitCompleteMode(false)
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
o.EnterCompleteMode(offset, newLines)
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) IsInCompleteSelectMode() bool {
|
||
|
return o.inSelectMode
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) IsInCompleteMode() bool {
|
||
|
return o.inCompleteMode
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) HandleCompleteSelect(r rune) bool {
|
||
|
next := true
|
||
|
switch r {
|
||
|
case CharEnter, CharCtrlJ:
|
||
|
next = false
|
||
|
o.op.buf.WriteRunes(o.op.candidate[o.op.candidateChoise])
|
||
|
o.ExitCompleteMode(false)
|
||
|
case CharLineStart:
|
||
|
num := o.candidateChoise % o.candidateColNum
|
||
|
o.nextCandidate(-num)
|
||
|
case CharLineEnd:
|
||
|
num := o.candidateColNum - o.candidateChoise%o.candidateColNum - 1
|
||
|
o.candidateChoise += num
|
||
|
if o.candidateChoise >= len(o.candidate) {
|
||
|
o.candidateChoise = len(o.candidate) - 1
|
||
|
}
|
||
|
case CharBackspace:
|
||
|
o.ExitCompleteSelectMode()
|
||
|
next = false
|
||
|
case CharTab, CharForward:
|
||
|
o.doSelect()
|
||
|
case CharBell, CharInterrupt:
|
||
|
o.ExitCompleteMode(true)
|
||
|
next = false
|
||
|
case CharNext:
|
||
|
tmpChoise := o.candidateChoise + o.candidateColNum
|
||
|
if tmpChoise >= o.getMatrixSize() {
|
||
|
tmpChoise -= o.getMatrixSize()
|
||
|
} else if tmpChoise >= len(o.candidate) {
|
||
|
tmpChoise += o.candidateColNum
|
||
|
tmpChoise -= o.getMatrixSize()
|
||
|
}
|
||
|
o.candidateChoise = tmpChoise
|
||
|
case CharBackward:
|
||
|
o.nextCandidate(-1)
|
||
|
case CharPrev:
|
||
|
tmpChoise := o.candidateChoise - o.candidateColNum
|
||
|
if tmpChoise < 0 {
|
||
|
tmpChoise += o.getMatrixSize()
|
||
|
if tmpChoise >= len(o.candidate) {
|
||
|
tmpChoise -= o.candidateColNum
|
||
|
}
|
||
|
}
|
||
|
o.candidateChoise = tmpChoise
|
||
|
default:
|
||
|
next = false
|
||
|
o.ExitCompleteSelectMode()
|
||
|
}
|
||
|
if next {
|
||
|
o.CompleteRefresh()
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) getMatrixSize() int {
|
||
|
line := len(o.candidate) / o.candidateColNum
|
||
|
if len(o.candidate)%o.candidateColNum != 0 {
|
||
|
line++
|
||
|
}
|
||
|
return line * o.candidateColNum
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) OnWidthChange(newWidth int) {
|
||
|
o.width = newWidth
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) CompleteRefresh() {
|
||
|
if !o.inCompleteMode {
|
||
|
return
|
||
|
}
|
||
|
lineCnt := o.op.buf.CursorLineCount()
|
||
|
colWidth := 0
|
||
|
for _, c := range o.candidate {
|
||
|
w := runes.WidthAll(c)
|
||
|
if w > colWidth {
|
||
|
colWidth = w
|
||
|
}
|
||
|
}
|
||
|
colWidth += o.candidateOff + 1
|
||
|
same := o.op.buf.RuneSlice(-o.candidateOff)
|
||
|
|
||
|
// -1 to avoid reach the end of line
|
||
|
width := o.width - 1
|
||
|
colNum := width / colWidth
|
||
|
if colNum != 0 {
|
||
|
colWidth += (width - (colWidth * colNum)) / colNum
|
||
|
}
|
||
|
|
||
|
o.candidateColNum = colNum
|
||
|
buf := bufio.NewWriter(o.w)
|
||
|
buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
|
||
|
|
||
|
colIdx := 0
|
||
|
lines := 1
|
||
|
buf.WriteString("\033[J")
|
||
|
for idx, c := range o.candidate {
|
||
|
inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode()
|
||
|
if inSelect {
|
||
|
buf.WriteString("\033[30;47m")
|
||
|
}
|
||
|
buf.WriteString(string(same))
|
||
|
buf.WriteString(string(c))
|
||
|
buf.Write(bytes.Repeat([]byte(" "), colWidth-len(c)-len(same)))
|
||
|
|
||
|
if inSelect {
|
||
|
buf.WriteString("\033[0m")
|
||
|
}
|
||
|
|
||
|
colIdx++
|
||
|
if colIdx == colNum {
|
||
|
buf.WriteString("\n")
|
||
|
lines++
|
||
|
colIdx = 0
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// move back
|
||
|
fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines)
|
||
|
fmt.Fprintf(buf, "\033[%dC", o.op.buf.idx+o.op.buf.PromptLen())
|
||
|
buf.Flush()
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) aggCandidate(candidate [][]rune) int {
|
||
|
offset := 0
|
||
|
for i := 0; i < len(candidate[0]); i++ {
|
||
|
for j := 0; j < len(candidate)-1; j++ {
|
||
|
if i > len(candidate[j]) {
|
||
|
goto aggregate
|
||
|
}
|
||
|
if candidate[j][i] != candidate[j+1][i] {
|
||
|
goto aggregate
|
||
|
}
|
||
|
}
|
||
|
offset = i
|
||
|
}
|
||
|
aggregate:
|
||
|
return offset
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) EnterCompleteSelectMode() {
|
||
|
o.inSelectMode = true
|
||
|
o.candidateChoise = -1
|
||
|
o.CompleteRefresh()
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) {
|
||
|
o.inCompleteMode = true
|
||
|
o.candidate = candidate
|
||
|
o.candidateOff = offset
|
||
|
o.CompleteRefresh()
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) ExitCompleteSelectMode() {
|
||
|
o.inSelectMode = false
|
||
|
o.candidate = nil
|
||
|
o.candidateChoise = -1
|
||
|
o.candidateOff = -1
|
||
|
o.candidateSource = nil
|
||
|
}
|
||
|
|
||
|
func (o *opCompleter) ExitCompleteMode(revent bool) {
|
||
|
o.inCompleteMode = false
|
||
|
o.ExitCompleteSelectMode()
|
||
|
}
|