add auto complete

This commit is contained in:
Cheney 2015-09-25 22:56:00 +08:00
parent 28ad744d6b
commit 04f86e9c53
8 changed files with 443 additions and 15 deletions

View File

@ -7,7 +7,7 @@ const (
CharDelete = 4
CharLineEnd = 5
CharForward = 6
CharCannel = 7
CharCancel = 7
CharCtrlH = 8
CharTab = 9
CharCtrlJ = 10

249
complete.go Normal file
View File

@ -0,0 +1,249 @@
package readline
import (
"bytes"
"fmt"
"io"
)
type opCompleter struct {
w io.Writer
op *Operation
ac AutoCompleter
inCompleteMode bool
inSelectMode bool
candicate [][]rune
candicateSource []rune
candicateOff int
candicateChoise int
candicateColNum int
}
func newOpCompleter(w io.Writer, op *Operation) *opCompleter {
return &opCompleter{
w: w,
op: op,
ac: op.cfg.AutoComplete,
}
}
func (o *opCompleter) doSelect() {
if len(o.candicate) == 1 {
o.op.buf.WriteRunes(o.candicate[0])
o.ExitCompleteMode(false)
return
}
o.nextCandicate(1)
o.CompleteRefresh()
}
func (o *opCompleter) nextCandicate(i int) {
o.candicateChoise += i
o.candicateChoise = o.candicateChoise % len(o.candicate)
if o.candicateChoise < 0 {
o.candicateChoise = len(o.candicate) + o.candicateChoise
}
}
func (o *opCompleter) OnComplete() {
if o.IsInCompleteSelectMode() {
o.doSelect()
return
}
buf := o.op.buf
rs := buf.Runes()
if o.IsInCompleteMode() && EqualRunes(rs, o.candicateSource) {
o.EnterCompleteSelectMode()
o.doSelect()
return
}
o.ExitCompleteSelectMode()
o.candicateSource = rs
newLines, offset := o.ac.Do(rs, buf.idx)
if len(newLines) == 0 {
o.ExitCompleteMode(false)
return
}
// only Aggregate candicates in non-complete mode
if !o.IsInCompleteMode() {
if len(newLines) == 1 {
buf.WriteRunes(newLines[0])
o.ExitCompleteMode(false)
return
}
same, size := AggRunes(newLines)
if size > 0 {
buf.WriteRunes(same)
o.ExitCompleteMode(false)
return
}
}
o.EnterCompleteMode(offset, newLines)
}
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.candicate[o.op.candicateChoise])
o.ExitCompleteMode(false)
case CharLineStart:
num := o.candicateChoise % o.candicateColNum
o.nextCandicate(-num)
case CharLineEnd:
num := o.candicateColNum - o.candicateChoise%o.candicateColNum - 1
o.candicateChoise += num
if o.candicateChoise >= len(o.candicate) {
o.candicateChoise = len(o.candicate) - 1
}
case CharBackspace:
o.ExitCompleteSelectMode()
next = false
case CharTab, CharForward:
o.doSelect()
case CharCancel, CharInterrupt:
o.ExitCompleteMode(true)
next = false
case CharNext:
tmpChoise := o.candicateChoise + o.candicateColNum
if tmpChoise >= o.getMatrixSize() {
tmpChoise -= o.getMatrixSize()
} else if tmpChoise >= len(o.candicate) {
tmpChoise += o.candicateColNum
tmpChoise -= o.getMatrixSize()
}
o.candicateChoise = tmpChoise
case CharBackward:
o.nextCandicate(-1)
case CharPrev:
tmpChoise := o.candicateChoise - o.candicateColNum
if tmpChoise < 0 {
tmpChoise += o.getMatrixSize()
if tmpChoise >= len(o.candicate) {
tmpChoise -= o.candicateColNum
}
}
o.candicateChoise = tmpChoise
default:
next = false
}
if next {
o.CompleteRefresh()
return true
}
return false
}
func (o *opCompleter) getMatrixSize() int {
line := len(o.candicate) / o.candicateColNum
if len(o.candicate)%o.candicateColNum != 0 {
line++
}
return line * o.candicateColNum
}
func (o *opCompleter) CompleteRefresh() {
if !o.inCompleteMode {
return
}
lineCnt := o.op.buf.CursorLineCount()
colWidth := 0
for _, c := range o.candicate {
w := RunesWidth(c)
if w > colWidth {
colWidth = w
}
}
colNum := getWidth() / (colWidth + o.candicateOff + 2)
o.candicateColNum = colNum
buf := bytes.NewBuffer(nil)
buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
same := o.op.buf.RuneSlice(-o.candicateOff)
colIdx := 0
lines := 1
buf.WriteString("\033[J")
for idx, c := range o.candicate {
inSelect := idx == o.candicateChoise && 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)))
if inSelect {
buf.WriteString("\033[0m")
}
buf.WriteString(" ")
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())
o.w.Write(buf.Bytes())
}
func (o *opCompleter) aggCandicate(candicate [][]rune) int {
offset := 0
for i := 0; i < len(candicate[0]); i++ {
for j := 0; j < len(candicate)-1; j++ {
if i > len(candicate[j]) {
goto aggregate
}
if candicate[j][i] != candicate[j+1][i] {
goto aggregate
}
}
offset = i
}
aggregate:
return offset
}
func (o *opCompleter) EnterCompleteSelectMode() {
o.inSelectMode = true
o.candicateChoise = -1
o.CompleteRefresh()
}
func (o *opCompleter) EnterCompleteMode(offset int, candicate [][]rune) {
o.inCompleteMode = true
o.candicate = candicate
o.candicateOff = offset
o.CompleteRefresh()
}
func (o *opCompleter) ExitCompleteSelectMode() {
o.inSelectMode = false
o.candicate = nil
o.candicateChoise = -1
o.candicateOff = -1
o.candicateSource = nil
}
func (o *opCompleter) ExitCompleteMode(revent bool) {
o.inCompleteMode = false
o.ExitCompleteSelectMode()
}

View File

@ -1,9 +1,11 @@
package main
import (
"fmt"
"io"
"log"
"strconv"
"strings"
"time"
"github.com/chzyer/readline"
@ -16,10 +18,30 @@ bye: quit
`[1:])
}
type Completer struct {
}
func (c *Completer) Do(line []rune, pos int) (newLine [][]rune, off int) {
list := [][]rune{
[]rune("sayhello"), []rune("help"), []rune("bye"),
}
for i := 0; i <= 100; i++ {
list = append(list, []rune(fmt.Sprintf("com%d", i)))
}
line = line[:pos]
for _, r := range list {
if strings.HasPrefix(string(r), string(line)) {
newLine = append(newLine, r[len(line):])
}
}
return newLine, len(line)
}
func main() {
l, err := readline.NewEx(&readline.Config{
Prompt: "\033[31m»\033[0m ",
HistoryFile: "/tmp/readline.tmp",
Prompt: "\033[31m»\033[0m ",
HistoryFile: "/tmp/readline.tmp",
AutoComplete: new(Completer),
})
if err != nil {
panic(err)

View File

@ -13,6 +13,7 @@ type Operation struct {
*opHistory
*opSearch
*opCompleter
}
type wrapWriter struct {
@ -31,6 +32,9 @@ func (w *wrapWriter) Write(b []byte) (int, error) {
if w.r.IsSearchMode() {
w.r.SearchRefresh(-1)
}
if w.r.IsInCompleteMode() {
w.r.CompleteRefresh()
}
return n, err
}
@ -43,6 +47,7 @@ func NewOperation(t *Terminal, cfg *Config) *Operation {
opHistory: newOpHistory(cfg.HistoryFile),
}
op.opSearch = newOpSearch(op.buf.w, op.buf, op.opHistory)
op.opCompleter = newOpCompleter(op.buf.w, op)
go op.ioloop()
return op
}
@ -50,17 +55,41 @@ func NewOperation(t *Terminal, cfg *Config) *Operation {
func (o *Operation) ioloop() {
for {
keepInSearchMode := false
keepInCompleteMode := false
r := o.t.ReadRune()
if o.IsInCompleteSelectMode() {
keepInCompleteMode = o.HandleCompleteSelect(r)
if keepInCompleteMode {
continue
}
o.buf.Refresh()
switch r {
case CharInterrupt, CharEnter, CharCtrlJ:
o.t.KickRead()
fallthrough
case CharCancel:
continue
}
}
switch r {
case CharCannel:
case CharCancel:
if o.IsSearchMode() {
o.ExitSearchMode(true)
o.buf.Refresh()
}
if o.IsInCompleteMode() {
o.ExitCompleteMode(true)
o.buf.Refresh()
}
case CharTab:
if o.cfg.AutoComplete == nil {
if o.opCompleter == nil {
break
}
o.OnComplete()
keepInCompleteMode = true
case CharBckSearch:
o.SearchMode(S_DIR_BCK)
keepInSearchMode = true
@ -69,6 +98,7 @@ func (o *Operation) ioloop() {
keepInSearchMode = true
case CharKill:
o.buf.Kill()
keepInCompleteMode = true
case MetaNext:
o.buf.MoveToNextWord()
case CharTranspose:
@ -87,8 +117,12 @@ func (o *Operation) ioloop() {
if o.IsSearchMode() {
o.SearchBackspace()
keepInSearchMode = true
} else {
o.buf.Backspace()
break
}
o.buf.Backspace()
if o.IsInCompleteMode() {
o.OnComplete()
}
case MetaBackspace, CharCtrlW:
o.buf.BackEscapeWord()
@ -122,6 +156,12 @@ func (o *Operation) ioloop() {
o.ExitSearchMode(true)
break
}
if o.IsInCompleteMode() {
o.t.KickRead()
o.ExitCompleteMode(true)
o.buf.Refresh()
break
}
o.buf.MoveToLineEnd()
o.buf.Refresh()
o.buf.WriteString("^C\n")
@ -130,13 +170,25 @@ func (o *Operation) ioloop() {
if o.IsSearchMode() {
o.SearchChar(r)
keepInSearchMode = true
} else {
o.buf.WriteRune(r)
break
}
o.buf.WriteRune(r)
if o.IsInCompleteMode() {
o.OnComplete()
keepInCompleteMode = true
}
}
if !keepInSearchMode && o.IsSearchMode() {
o.ExitSearchMode(false)
o.buf.Refresh()
} else if o.IsInCompleteMode() {
if !keepInCompleteMode {
o.ExitCompleteMode(false)
o.buf.Refresh()
} else {
o.buf.Refresh()
o.CompleteRefresh()
}
}
if !o.IsSearchMode() {
o.UpdateHistory(o.buf.Runes(), false)

View File

@ -8,7 +8,11 @@ type Instance struct {
}
type AutoCompleter interface {
Do(line []rune, pos int) (newLine []rune, newPos int, ok bool)
Do(line []rune, pos int) (newLine [][]rune, offset int)
}
type AutoCompleteHinter interface {
Hint(line []rune, pos int) ([]rune, bool)
}
type Config struct {

View File

@ -28,8 +28,21 @@ func (r *RuneBuffer) PromptLen() int {
return RunesWidth(RunesColorFilter(r.prompt))
}
func (r *RuneBuffer) RuneSlice(i int) []rune {
if i > 0 {
rs := make([]rune, i)
copy(rs, r.buf[r.idx:r.idx+i])
return rs
}
rs := make([]rune, -i)
copy(rs, r.buf[r.idx+i:r.idx])
return rs
}
func (r *RuneBuffer) Runes() []rune {
return r.buf
newr := make([]rune, len(r.buf))
copy(newr, r.buf)
return newr
}
func (r *RuneBuffer) Pos() int {
@ -67,7 +80,7 @@ func (r *RuneBuffer) WriteRune(s rune) {
func (r *RuneBuffer) WriteRunes(s []rune) {
tail := append(s, r.buf[r.idx:]...)
r.buf = append(r.buf[:r.idx], tail...)
r.idx++
r.idx += len(s)
r.Refresh()
}

View File

@ -228,3 +228,45 @@ func RunesWidth(r []rune) (length int) {
}
return
}
func AggRunes(candicate [][]rune) (same []rune, size int) {
for i := 0; i < len(candicate[0]); i++ {
for j := 0; j < len(candicate)-1; j++ {
if i >= len(candicate[j]) || i >= len(candicate[j+1]) {
goto aggregate
}
if candicate[j][i] != candicate[j+1][i] {
goto aggregate
}
}
size = i + 1
}
aggregate:
if size > 0 {
same = CopyRunes(candicate[0][:size])
for i := 0; i < len(candicate); i++ {
n := CopyRunes(candicate[i])
copy(n, n[size:])
candicate[i] = n[:len(n)-size]
}
}
return
}
func CopyRunes(r []rune) []rune {
n := make([]rune, len(r))
copy(n, r)
return n
}
func EqualRunes(r, r2 []rune) bool {
if len(r) != len(r2) {
return false
}
for idx := range r {
if r[idx] != r2[idx] {
return false
}
}
return true
}

View File

@ -1,14 +1,17 @@
package readline
import "testing"
import (
"reflect"
"testing"
)
type Twidth struct {
type twidth struct {
r []rune
length int
}
func TestRuneWidth(t *testing.T) {
runes := []Twidth{
runes := []twidth{
{[]rune("☭"), 1},
{[]rune("a"), 1},
{[]rune("你"), 2},
@ -20,3 +23,46 @@ func TestRuneWidth(t *testing.T) {
}
}
}
type tagg struct {
r [][]rune
e [][]rune
length int
}
func TestAggRunes(t *testing.T) {
runes := []tagg{
{
[][]rune{[]rune("ab"), []rune("a"), []rune("abc")},
[][]rune{[]rune("b"), []rune(""), []rune("bc")},
1,
},
{
[][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")},
[][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")},
1,
},
{
[][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")},
[][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")},
0,
},
{
[][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")},
[][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")},
2,
},
}
for _, r := range runes {
same, off := AggRunes(r.r)
if off != r.length {
t.Fatal("result not expect", off)
}
if len(same) != off {
t.Fatal("result not expect", same)
}
if !reflect.DeepEqual(r.r, r.e) {
t.Fatal("result not expect")
}
}
}