diff --git a/char.go b/char.go index 4d42e1f..3d65824 100644 --- a/char.go +++ b/char.go @@ -7,7 +7,7 @@ const ( CharDelete = 4 CharLineEnd = 5 CharForward = 6 - CharCannel = 7 + CharCancel = 7 CharCtrlH = 8 CharTab = 9 CharCtrlJ = 10 diff --git a/complete.go b/complete.go new file mode 100644 index 0000000..4655946 --- /dev/null +++ b/complete.go @@ -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() +} diff --git a/example/main.go b/example/main.go index bd380ef..c9199e6 100644 --- a/example/main.go +++ b/example/main.go @@ -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) diff --git a/operation.go b/operation.go index 27e013e..398f7ac 100644 --- a/operation.go +++ b/operation.go @@ -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) diff --git a/readline.go b/readline.go index 3bd5603..6da9267 100644 --- a/readline.go +++ b/readline.go @@ -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 { diff --git a/runebuf.go b/runebuf.go index d19abc8..bae2862 100644 --- a/runebuf.go +++ b/runebuf.go @@ -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() } diff --git a/utils.go b/utils.go index 7e38b92..2870d59 100644 --- a/utils.go +++ b/utils.go @@ -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 +} diff --git a/utils_test.go b/utils_test.go index 078bb17..de74175 100644 --- a/utils_test.go +++ b/utils_test.go @@ -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") + } + } +}