forked from u-root/u-root
-
Notifications
You must be signed in to change notification settings - Fork 0
/
arg_completers.go
237 lines (210 loc) · 7.65 KB
/
arg_completers.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
package completion
import (
"bufio"
"errors"
"io"
"os"
"strings"
"github.com/u-root/u-root/cmds/elvish/eval"
"github.com/u-root/u-root/cmds/elvish/hash"
"github.com/u-root/u-root/cmds/elvish/hashmap"
)
// For an overview of completion, see the comment in completers.go.
//
// When completing arguments of commands (as opposed to variable names, map
// indicies, etc.), the list of candidates often depends on the command in
// question; e.g. "ls <Tab>" and "apt <Tab>" should yield different results
// because the commands "ls" and "apt" accept different arguments. To reflec
// this, Elvish has a map of "argument completers", with the key being the name
// of the command, and the value being the argument completer itself, accessible
// to script as $edit:arg-completer. The one entry with an empty string as the
// key is the fallback completer, and is used when an argument completer for the
// current command has not been defined.
//
// When completing an argument, Elvish first finds out the name of the command
// (e.g. "ls" or "apt") can tries to evaluate its arguments. It then calls the
// suitable completer with the name of the command and the arguments. The
// arguments are in evaluated forms: e.g. if an argument is 'foo' (with quotes),
// the argument is its value foo, not a literal 'foo'. The last argument is what
// needs to be completed; if the user is starting a new argument, e.g. by typing
// "ls a " (without quotes), the last argument passed to the argument completer
// will be an empty string.
//
// The argument completer should then return a list of what can replace the last
// argument. The results are of type rawCandidate, which basically means that
// argument completers do not need to worry about quoting of candidates; the raw
// candidates will be "cooked" into actual candidates that appear in the
// interface, which includes quoting.
//
// There are a few finer points in this process:
//
// 1. If some of the arguments cannot be evaluated statically (for instance,
// consider this: echo (uname)), it will be an empty string. There needs
// probably be a better way to distinguish empty arguments and unknown
// arguments, but normally there is not much argument completers can do for
// unknown arguments.
//
// 2. The argument completer normally **should not** perform filtering. For
// instance, if the user has typed "ls x", the argument completer for "ls"
// should return **all** files, not just those whose names start with x. This
// is to make it possible for user to specify a different matching algorithm
// than the default prefix matching.
//
// However, argument completers **should** look at the argument to decide
// which **type** of candidates to generate. For instance, if the user has
// typed "ls --x", the argument completer should generate all long options
// for "ls", but not only those starting with "x".
var (
// errCompleterMustBeFn is thrown if the user has put a non-function entry
// in $edit:completer, and that entry needs to be used for completion.
// TODO(xiaq): Detect the type violation when the user modifies
// $edit:completer.
errCompleterMustBeFn = errors.New("completer must be fn")
// errNoMatchingCompleter is thrown if there is no completer matching the
// current command.
errNoMatchingCompleter = errors.New("no matching completer")
// errCompleterArgMustBeString is thrown when a builtin argument completer
// is called with non-string arguments.
errCompleterArgMustBeString = errors.New("arguments to arg completers must be string")
// errTooFewArguments is thrown when a builtin argument completer is called
// with too few arguments.
errTooFewArguments = errors.New("too few arguments")
)
var (
argCompletersData = map[string]*argCompleterEntry{
"": {"complete-filename", complFilename},
"sudo": {"complete-sudo", complSudo},
}
)
type argCompleterEntry struct {
name string
impl func([]string, *eval.Evaler, hashmap.Map, chan<- rawCandidate) error
}
// completeArg calls the correct argument completers according to the command
// name. It is used by complArg and can also be useful when further dispatching
// based on command name is needed -- e.g. in the argument completer for "sudo".
func completeArg(words []string, ev *eval.Evaler, ac hashmap.Map, rawCands chan<- rawCandidate) error {
logger.Printf("completing argument: %q", words)
var v interface{}
index := words[0]
v, ok := ac.Index(index)
if !ok {
v, ok = ac.Index("")
if !ok {
return errNoMatchingCompleter
}
}
fn, ok := v.(eval.Callable)
if !ok {
return errCompleterMustBeFn
}
return callArgCompleter(fn, ev, words, rawCands)
}
type builtinArgCompleter struct {
name string
impl func([]string, *eval.Evaler, hashmap.Map, chan<- rawCandidate) error
argCompleter hashmap.Map
}
var _ eval.Callable = &builtinArgCompleter{}
func (bac *builtinArgCompleter) Kind() string {
return "fn"
}
// Equal compares by identity.
func (bac *builtinArgCompleter) Equal(a interface{}) bool {
return bac == a
}
func (bac *builtinArgCompleter) Hash() uint32 {
return hash.Hash(bac)
}
func (bac *builtinArgCompleter) Repr(int) string {
return "$edit:" + bac.name + eval.FnSuffix
}
func (bac *builtinArgCompleter) Call(ec *eval.Frame, args []interface{}, opts map[string]interface{}) error {
eval.TakeNoOpt(opts)
words := make([]string, len(args))
for i, arg := range args {
s, ok := arg.(string)
if !ok {
throw(errCompleterArgMustBeString)
}
words[i] = s
}
rawCands := make(chan rawCandidate)
var err error
go func() {
defer close(rawCands)
err = bac.impl(words, ec.Evaler, bac.argCompleter, rawCands)
}()
output := ec.OutputChan()
for rc := range rawCands {
output <- rc
}
return err
}
func complFilename(words []string, ev *eval.Evaler, ac hashmap.Map, rawCands chan<- rawCandidate) error {
if len(words) < 1 {
return errTooFewArguments
}
return complFilenameInner(words[len(words)-1], false, rawCands)
}
func complSudo(words []string, ev *eval.Evaler, ac hashmap.Map, rawCands chan<- rawCandidate) error {
if len(words) < 2 {
return errTooFewArguments
}
if len(words) == 2 {
return complFormHeadInner(words[1], ev, rawCands)
}
return completeArg(words[1:], ev, ac, rawCands)
}
// callArgCompleter calls a Fn, assuming that it is an arg completer. It calls
// the Fn with specified arguments and closed input, and converts its output to
// candidate objects.
func callArgCompleter(fn eval.Callable, ev *eval.Evaler, words []string, rawCands chan<- rawCandidate) error {
// Quick path for builtin arg completers.
if builtin, ok := fn.(*builtinArgCompleter); ok {
return builtin.impl(words, ev, builtin.argCompleter, rawCands)
}
args := make([]interface{}, len(words))
for i, word := range words {
args[i] = word
}
ports := []*eval.Port{
eval.DevNullClosedChan,
{}, // Will be replaced when capturing output
{File: os.Stderr},
}
valuesCb := func(ch <-chan interface{}) {
for v := range ch {
switch v := v.(type) {
case rawCandidate:
rawCands <- v
case string:
rawCands <- plainCandidate(v)
default:
logger.Printf("completer must output string or candidate")
}
}
}
bytesCb := func(r *os.File) {
buffered := bufio.NewReader(r)
for {
line, err := buffered.ReadString('\n')
if line != "" {
rawCands <- plainCandidate(strings.TrimSuffix(line, "\n"))
}
if err != nil {
if err != io.EOF {
logger.Println("error on reading:", err)
}
break
}
}
}
// XXX There is no source to pass to NewTopEvalCtx.
ec := eval.NewTopFrame(ev, eval.NewInternalSource("[editor completer]"), ports)
err := ec.CallWithOutputCallback(fn, args, eval.NoOpts, valuesCb, bytesCb)
if err != nil {
err = errors.New("completer error: " + err.Error())
}
return err
}