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-runes.WidthAll(c)-runes.WidthAll(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() }