1package readline 2 3import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "io" 8) 9 10type AutoCompleter interface { 11 // Readline will pass the whole line and current offset to it 12 // Completer need to pass all the candidates, and how long they shared the same characters in line 13 // Example: 14 // [go, git, git-shell, grep] 15 // Do("g", 1) => ["o", "it", "it-shell", "rep"], 1 16 // Do("gi", 2) => ["t", "t-shell"], 2 17 // Do("git", 3) => ["", "-shell"], 3 18 Do(line []rune, pos int) (newLine [][]rune, length int) 19} 20 21type TabCompleter struct{} 22 23func (t *TabCompleter) Do([]rune, int) ([][]rune, int) { 24 return [][]rune{[]rune("\t")}, 0 25} 26 27type opCompleter struct { 28 w io.Writer 29 op *Operation 30 width int 31 32 inCompleteMode bool 33 inSelectMode bool 34 candidate [][]rune 35 candidateSource []rune 36 candidateOff int 37 candidateChoise int 38 candidateColNum int 39} 40 41func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter { 42 return &opCompleter{ 43 w: w, 44 op: op, 45 width: width, 46 } 47} 48 49func (o *opCompleter) doSelect() { 50 if len(o.candidate) == 1 { 51 o.op.buf.WriteRunes(o.candidate[0]) 52 o.ExitCompleteMode(false) 53 return 54 } 55 o.nextCandidate(1) 56 o.CompleteRefresh() 57} 58 59func (o *opCompleter) nextCandidate(i int) { 60 o.candidateChoise += i 61 o.candidateChoise = o.candidateChoise % len(o.candidate) 62 if o.candidateChoise < 0 { 63 o.candidateChoise = len(o.candidate) + o.candidateChoise 64 } 65} 66 67func (o *opCompleter) OnComplete() bool { 68 if o.width == 0 { 69 return false 70 } 71 if o.IsInCompleteSelectMode() { 72 o.doSelect() 73 return true 74 } 75 76 buf := o.op.buf 77 rs := buf.Runes() 78 79 if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) { 80 o.EnterCompleteSelectMode() 81 o.doSelect() 82 return true 83 } 84 85 o.ExitCompleteSelectMode() 86 o.candidateSource = rs 87 newLines, offset := o.op.cfg.AutoComplete.Do(rs, buf.idx) 88 if len(newLines) == 0 { 89 o.ExitCompleteMode(false) 90 return true 91 } 92 93 // only Aggregate candidates in non-complete mode 94 if !o.IsInCompleteMode() { 95 if len(newLines) == 1 { 96 buf.WriteRunes(newLines[0]) 97 o.ExitCompleteMode(false) 98 return true 99 } 100 101 same, size := runes.Aggregate(newLines) 102 if size > 0 { 103 buf.WriteRunes(same) 104 o.ExitCompleteMode(false) 105 return true 106 } 107 } 108 109 o.EnterCompleteMode(offset, newLines) 110 return true 111} 112 113func (o *opCompleter) IsInCompleteSelectMode() bool { 114 return o.inSelectMode 115} 116 117func (o *opCompleter) IsInCompleteMode() bool { 118 return o.inCompleteMode 119} 120 121func (o *opCompleter) HandleCompleteSelect(r rune) bool { 122 next := true 123 switch r { 124 case CharEnter, CharCtrlJ: 125 next = false 126 o.op.buf.WriteRunes(o.op.candidate[o.op.candidateChoise]) 127 o.ExitCompleteMode(false) 128 case CharLineStart: 129 num := o.candidateChoise % o.candidateColNum 130 o.nextCandidate(-num) 131 case CharLineEnd: 132 num := o.candidateColNum - o.candidateChoise%o.candidateColNum - 1 133 o.candidateChoise += num 134 if o.candidateChoise >= len(o.candidate) { 135 o.candidateChoise = len(o.candidate) - 1 136 } 137 case CharBackspace: 138 o.ExitCompleteSelectMode() 139 next = false 140 case CharTab, CharForward: 141 o.doSelect() 142 case CharBell, CharInterrupt: 143 o.ExitCompleteMode(true) 144 next = false 145 case CharNext: 146 tmpChoise := o.candidateChoise + o.candidateColNum 147 if tmpChoise >= o.getMatrixSize() { 148 tmpChoise -= o.getMatrixSize() 149 } else if tmpChoise >= len(o.candidate) { 150 tmpChoise += o.candidateColNum 151 tmpChoise -= o.getMatrixSize() 152 } 153 o.candidateChoise = tmpChoise 154 case CharBackward: 155 o.nextCandidate(-1) 156 case CharPrev: 157 tmpChoise := o.candidateChoise - o.candidateColNum 158 if tmpChoise < 0 { 159 tmpChoise += o.getMatrixSize() 160 if tmpChoise >= len(o.candidate) { 161 tmpChoise -= o.candidateColNum 162 } 163 } 164 o.candidateChoise = tmpChoise 165 default: 166 next = false 167 o.ExitCompleteSelectMode() 168 } 169 if next { 170 o.CompleteRefresh() 171 return true 172 } 173 return false 174} 175 176func (o *opCompleter) getMatrixSize() int { 177 line := len(o.candidate) / o.candidateColNum 178 if len(o.candidate)%o.candidateColNum != 0 { 179 line++ 180 } 181 return line * o.candidateColNum 182} 183 184func (o *opCompleter) OnWidthChange(newWidth int) { 185 o.width = newWidth 186} 187 188func (o *opCompleter) CompleteRefresh() { 189 if !o.inCompleteMode { 190 return 191 } 192 lineCnt := o.op.buf.CursorLineCount() 193 colWidth := 0 194 for _, c := range o.candidate { 195 w := runes.WidthAll(c) 196 if w > colWidth { 197 colWidth = w 198 } 199 } 200 colWidth += o.candidateOff + 1 201 same := o.op.buf.RuneSlice(-o.candidateOff) 202 203 // -1 to avoid reach the end of line 204 width := o.width - 1 205 colNum := width / colWidth 206 if colNum != 0 { 207 colWidth += (width - (colWidth * colNum)) / colNum 208 } 209 210 o.candidateColNum = colNum 211 buf := bufio.NewWriter(o.w) 212 buf.Write(bytes.Repeat([]byte("\n"), lineCnt)) 213 214 colIdx := 0 215 lines := 1 216 buf.WriteString("\033[J") 217 for idx, c := range o.candidate { 218 inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode() 219 if inSelect { 220 buf.WriteString("\033[30;47m") 221 } 222 buf.WriteString(string(same)) 223 buf.WriteString(string(c)) 224 buf.Write(bytes.Repeat([]byte(" "), colWidth-runes.WidthAll(c)-runes.WidthAll(same))) 225 226 if inSelect { 227 buf.WriteString("\033[0m") 228 } 229 230 colIdx++ 231 if colIdx == colNum { 232 buf.WriteString("\n") 233 lines++ 234 colIdx = 0 235 } 236 } 237 238 // move back 239 fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines) 240 fmt.Fprintf(buf, "\033[%dC", o.op.buf.idx+o.op.buf.PromptLen()) 241 buf.Flush() 242} 243 244func (o *opCompleter) aggCandidate(candidate [][]rune) int { 245 offset := 0 246 for i := 0; i < len(candidate[0]); i++ { 247 for j := 0; j < len(candidate)-1; j++ { 248 if i > len(candidate[j]) { 249 goto aggregate 250 } 251 if candidate[j][i] != candidate[j+1][i] { 252 goto aggregate 253 } 254 } 255 offset = i 256 } 257aggregate: 258 return offset 259} 260 261func (o *opCompleter) EnterCompleteSelectMode() { 262 o.inSelectMode = true 263 o.candidateChoise = -1 264 o.CompleteRefresh() 265} 266 267func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) { 268 o.inCompleteMode = true 269 o.candidate = candidate 270 o.candidateOff = offset 271 o.CompleteRefresh() 272} 273 274func (o *opCompleter) ExitCompleteSelectMode() { 275 o.inSelectMode = false 276 o.candidate = nil 277 o.candidateChoise = -1 278 o.candidateOff = -1 279 o.candidateSource = nil 280} 281 282func (o *opCompleter) ExitCompleteMode(revent bool) { 283 o.inCompleteMode = false 284 o.ExitCompleteSelectMode() 285} 286