-
Notifications
You must be signed in to change notification settings - Fork 6
/
completer.go
384 lines (317 loc) · 10 KB
/
completer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
package console
import (
"bytes"
"errors"
"fmt"
"os"
"regexp"
"strings"
"unicode"
"unicode/utf8"
"github.com/rsteube/carapace"
"github.com/rsteube/carapace/pkg/style"
completer "github.com/rsteube/carapace/pkg/x"
"github.com/rsteube/carapace/pkg/xdg"
"github.com/reeflective/readline"
)
func (c *Console) complete(line []rune, pos int) readline.Completions {
menu := c.activeMenu()
// Ensure the carapace library is called so that the function
// completer.Complete() variable is correctly initialized before use.
carapace.Gen(menu.Command)
// Split the line as shell words, only using
// what the right buffer (up to the cursor)
args, prefixComp, prefixLine := splitArgs(line, pos)
// Prepare arguments for the carapace completer
// (we currently need those two dummies for avoiding a panic).
args = append([]string{c.name, "_carapace"}, args...)
// Call the completer with our current command context.
completions, err := completer.Complete(menu.Command, args...)
// The completions are never nil: fill out our own object
// with everything it contains, regardless of errors.
raw := make([]readline.Completion, len(completions.Values))
for idx, val := range completions.Values.Decolor() {
raw[idx] = readline.Completion{
Value: unescapeValue(prefixComp, prefixLine, val.Value),
Display: val.Display,
Description: val.Description,
Style: val.Style,
Tag: val.Tag,
}
if !completions.Nospace.Matches(val.Value) {
raw[idx].Value = val.Value + " "
}
}
// Assign both completions and command/flags/args usage strings.
comps := readline.CompleteRaw(raw)
comps = comps.Usage(completions.Usage)
comps = c.justifyCommandComps(comps)
// If any errors arose from the completion call itself.
if err != nil {
comps = readline.CompleteMessage("failed to load config: " + err.Error())
}
// Completion status/errors
for _, msg := range completions.Messages.Get() {
comps = comps.Merge(readline.CompleteMessage(msg))
}
// Suffix matchers for the completions if any.
suffixes, err := completions.Nospace.MarshalJSON()
if len(suffixes) > 0 && err == nil {
comps = comps.NoSpace([]rune(string(suffixes))...)
}
// If we have a quote/escape sequence unaccounted
// for in our completions, add it to all of them.
comps = comps.Prefix(prefixComp)
comps.PREFIX = prefixLine
// Finally, reset our command tree for the next call.
completer.ClearStorage()
menu.resetPreRun()
menu.hideFilteredCommands(menu.Command)
return comps
}
func (c *Console) justifyCommandComps(comps readline.Completions) readline.Completions {
justified := []string{}
comps.EachValue(func(comp readline.Completion) readline.Completion {
if !strings.HasSuffix(comp.Tag, "commands") {
return comp
}
justified = append(justified, comp.Tag)
return comp
})
if len(justified) > 0 {
return comps.JustifyDescriptions(justified...)
}
return comps
}
func (c *Console) defaultStyleConfig() {
// If carapace config file is found, just return.
if dir, err := xdg.UserConfigDir(); err == nil {
_, err := os.Stat(fmt.Sprintf("%v/carapace/styles.json", dir))
if err == nil {
return
}
}
// Overwrite all default styles for color
for i := 1; i < 13; i++ {
styleStr := fmt.Sprintf("carapace.Highlight%d", i)
style.Set(styleStr, "bright-white")
}
// Overwrite all default styles for flags
style.Set("carapace.FlagArg", "bright-white")
style.Set("carapace.FlagMultiArg", "bright-white")
style.Set("carapace.FlagNoArg", "bright-white")
style.Set("carapace.FlagOptArg", "bright-white")
}
// splitArgs splits the line in valid words, prepares them in various ways before calling
// the completer with them, and also determines which parts of them should be used as
// prefixes, in the completions and/or in the line.
func splitArgs(line []rune, pos int) (args []string, prefixComp, prefixLine string) {
line = line[:pos]
// Remove all colors from the string
line = []rune(strip(string(line)))
// Split the line as shellwords, return them if all went fine.
args, remain, err := splitCompWords(string(line))
// We might have either no error and args, or no error and
// the cursor ready to complete a new word (last character
// in line is a space).
// In some of those cases we append a single dummy argument
// for the completer to understand we want a new word comp.
mustComplete, args, remain := mustComplete(line, args, remain, err)
if mustComplete {
return sanitizeArgs(args), "", remain
}
// But the completion candidates themselves might need slightly
// different prefixes, for an optimal completion experience.
arg, prefixComp, prefixLine := adjustQuotedPrefix(remain, err)
// The remainder is everything following the open charater.
// Pass it as is to the carapace completion engine.
args = append(args, arg)
return sanitizeArgs(args), prefixComp, prefixLine
}
func mustComplete(line []rune, args []string, remain string, err error) (bool, []string, string) {
dummyArg := ""
// Empty command line, complete the root command.
if len(args) == 0 || len(line) == 0 {
return true, append(args, dummyArg), remain
}
// If we have an error, we must handle it later.
if err != nil {
return false, args, remain
}
lastChar := line[len(line)-1]
// No remain and a trailing space means we want to complete
// for the next word, except when this last space was escaped.
if remain == "" && unicode.IsSpace(lastChar) {
if strings.HasSuffix(string(line), "\\ ") {
return true, args, args[len(args)-1]
}
return true, append(args, dummyArg), remain
}
// Else there is a character under the cursor, which means we are
// in the middle/at the end of a posentially completed word.
return true, args, remain
}
func adjustQuotedPrefix(remain string, err error) (arg, comp, line string) {
arg = remain
switch {
case errors.Is(err, errUnterminatedDoubleQuote):
comp = "\""
line = comp + arg
case errors.Is(err, errUnterminatedSingleQuote):
comp = "'"
line = comp + arg
case errors.Is(err, errUnterminatedEscape):
arg = strings.ReplaceAll(arg, "\\", "")
}
return arg, comp, line
}
// sanitizeArg unescapes a restrained set of characters.
func sanitizeArgs(args []string) (sanitized []string) {
for _, arg := range args {
arg = replacer.Replace(arg)
sanitized = append(sanitized, arg)
}
return sanitized
}
// when the completer has returned us some completions, we sometimes
// needed to post-process them a little before passing them to our shell.
func unescapeValue(prefixComp, prefixLine, val string) string {
quoted := strings.HasPrefix(prefixLine, "\"") ||
strings.HasPrefix(prefixLine, "'")
if quoted {
val = strings.ReplaceAll(val, "\\ ", " ")
}
return val
}
// split has been copied from go-shellquote and slightly modified so as to also
// return the remainder when the parsing failed because of an unterminated quote.
func splitCompWords(input string) (words []string, remainder string, err error) {
var buf bytes.Buffer
words = make([]string, 0)
for len(input) > 0 {
// skip any splitChars at the start
char, read := utf8.DecodeRuneInString(input)
if strings.ContainsRune(splitChars, char) {
input = input[read:]
continue
} else if char == escapeChar {
// Look ahead for escaped newline so we can skip over it
next := input[read:]
if len(next) == 0 {
remainder = string(escapeChar)
err = errUnterminatedEscape
return
}
c2, l2 := utf8.DecodeRuneInString(next)
if c2 == '\n' {
input = next[l2:]
continue
}
}
var word string
word, input, err = splitCompWord(input, &buf)
if err != nil {
return words, word + input, err
}
words = append(words, word)
}
return words, remainder, nil
}
// splitWord has been modified to return the remainder of the input (the part that has not been
// added to the buffer) even when an error is returned.
func splitCompWord(input string, buf *bytes.Buffer) (word string, remainder string, err error) {
buf.Reset()
raw:
{
cur := input
for len(cur) > 0 {
char, read := utf8.DecodeRuneInString(cur)
cur = cur[read:]
switch {
case char == singleChar:
buf.WriteString(input[0 : len(input)-len(cur)-read])
input = cur
goto single
case char == doubleChar:
buf.WriteString(input[0 : len(input)-len(cur)-read])
input = cur
goto double
case char == escapeChar:
buf.WriteString(input[0 : len(input)-len(cur)-read])
buf.WriteRune(char)
input = cur
goto escape
case strings.ContainsRune(splitChars, char):
buf.WriteString(input[0 : len(input)-len(cur)-read])
return buf.String(), cur, nil
}
}
if len(input) > 0 {
buf.WriteString(input)
input = ""
}
goto done
}
escape:
{
if len(input) == 0 {
input = buf.String() + input
return "", input, errUnterminatedEscape
}
c, l := utf8.DecodeRuneInString(input)
if c != '\n' {
buf.WriteString(input[:l])
}
input = input[l:]
}
goto raw
single:
{
i := strings.IndexRune(input, singleChar)
if i == -1 {
return "", input, errUnterminatedSingleQuote
}
buf.WriteString(input[0:i])
input = input[i+1:]
goto raw
}
double:
{
cur := input
for len(cur) > 0 {
c, read := utf8.DecodeRuneInString(cur)
cur = cur[read:]
switch c {
case doubleChar:
buf.WriteString(input[0 : len(input)-len(cur)-read])
input = cur
goto raw
case escapeChar:
// bash only supports certain escapes in double-quoted strings
char2, l2 := utf8.DecodeRuneInString(cur)
cur = cur[l2:]
if strings.ContainsRune(doubleEscapeChars, char2) {
buf.WriteString(input[0 : len(input)-len(cur)-read-l2])
if char2 != '\n' {
buf.WriteRune(char2)
}
input = cur
}
}
}
return "", input, errUnterminatedDoubleQuote
}
done:
return buf.String(), input, nil
}
const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
var re = regexp.MustCompile(ansi)
// strip removes all ANSI escaped color sequences in a string.
func strip(str string) string {
return re.ReplaceAllString(str, "")
}
var replacer = strings.NewReplacer(
"\n", ` `,
"\t", ` `,
"\\ ", " ", // User-escaped spaces in words.
)