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