github.com/mem/u-root@v2.0.1-0.20181004165302-9b18b4636a33+incompatible/cmds/elvish/edit/completion/arg_completers.go (about)

     1  package completion
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"io"
     7  	"os"
     8  	"strings"
     9  	"unsafe"
    10  
    11  	"github.com/u-root/u-root/cmds/elvish/eval"
    12  	"github.com/xiaq/persistent/hash"
    13  	"github.com/xiaq/persistent/hashmap"
    14  )
    15  
    16  // For an overview of completion, see the comment in completers.go.
    17  //
    18  // When completing arguments of commands (as opposed to variable names, map
    19  // indicies, etc.), the list of candidates often depends on the command in
    20  // question; e.g. "ls <Tab>" and "apt <Tab>" should yield different results
    21  // because the commands "ls" and "apt" accept different arguments. To reflec
    22  // this, Elvish has a map of "argument completers", with the key being the name
    23  // of the command, and the value being the argument completer itself, accessible
    24  // to script as $edit:arg-completer. The one entry with an empty string as the
    25  // key is the fallback completer, and is used when an argument completer for the
    26  // current command has not been defined.
    27  //
    28  // When completing an argument, Elvish first finds out the name of the command
    29  // (e.g. "ls" or "apt") can tries to evaluate its arguments. It then calls the
    30  // suitable completer with the name of the command and the arguments. The
    31  // arguments are in evaluated forms: e.g. if an argument is 'foo' (with quotes),
    32  // the argument is its value foo, not a literal 'foo'. The last argument is what
    33  // needs to be completed; if the user is starting a new argument, e.g. by typing
    34  // "ls a " (without quotes), the last argument passed to the argument completer
    35  // will be an empty string.
    36  //
    37  // The argument completer should then return a list of what can replace the last
    38  // argument. The results are of type rawCandidate, which basically means that
    39  // argument completers do not need to worry about quoting of candidates; the raw
    40  // candidates will be "cooked" into actual candidates that appear in the
    41  // interface, which includes quoting.
    42  //
    43  // There are a few finer points in this process:
    44  //
    45  // 1. If some of the arguments cannot be evaluated statically (for instance,
    46  //    consider this: echo (uname)), it will be an empty string. There needs
    47  //    probably be a better way to distinguish empty arguments and unknown
    48  //    arguments, but normally there is not much argument completers can do for
    49  //    unknown arguments.
    50  //
    51  // 2. The argument completer normally **should not** perform filtering. For
    52  //    instance, if the user has typed "ls x", the argument completer for "ls"
    53  //    should return **all** files, not just those whose names start with x. This
    54  //    is to make it possible for user to specify a different matching algorithm
    55  //    than the default prefix matching.
    56  //
    57  //    However, argument completers **should** look at the argument to decide
    58  //    which **type** of candidates to generate. For instance, if the user has
    59  //    typed "ls --x", the argument completer should generate all long options
    60  //    for "ls", but not only those starting with "x".
    61  
    62  var (
    63  	// errCompleterMustBeFn is thrown if the user has put a non-function entry
    64  	// in $edit:completer, and that entry needs to be used for completion.
    65  	// TODO(xiaq): Detect the type violation when the user modifies
    66  	// $edit:completer.
    67  	errCompleterMustBeFn = errors.New("completer must be fn")
    68  	// errNoMatchingCompleter is thrown if there is no completer matching the
    69  	// current command.
    70  	errNoMatchingCompleter = errors.New("no matching completer")
    71  	// errCompleterArgMustBeString is thrown when a builtin argument completer
    72  	// is called with non-string arguments.
    73  	errCompleterArgMustBeString = errors.New("arguments to arg completers must be string")
    74  	// errTooFewArguments is thrown when a builtin argument completer is called
    75  	// with too few arguments.
    76  	errTooFewArguments = errors.New("too few arguments")
    77  )
    78  
    79  var (
    80  	argCompletersData = map[string]*argCompleterEntry{
    81  		"":     {"complete-filename", complFilename},
    82  		"sudo": {"complete-sudo", complSudo},
    83  	}
    84  )
    85  
    86  type argCompleterEntry struct {
    87  	name string
    88  	impl func([]string, *eval.Evaler, hashmap.Map, chan<- rawCandidate) error
    89  }
    90  
    91  // completeArg calls the correct argument completers according to the command
    92  // name. It is used by complArg and can also be useful when further dispatching
    93  // based on command name is needed -- e.g. in the argument completer for "sudo".
    94  func completeArg(words []string, ev *eval.Evaler, ac hashmap.Map, rawCands chan<- rawCandidate) error {
    95  	logger.Printf("completing argument: %q", words)
    96  	var v interface{}
    97  	index := words[0]
    98  	v, ok := ac.Index(index)
    99  	if !ok {
   100  		v, ok = ac.Index("")
   101  		if !ok {
   102  			return errNoMatchingCompleter
   103  		}
   104  	}
   105  	fn, ok := v.(eval.Callable)
   106  	if !ok {
   107  		return errCompleterMustBeFn
   108  	}
   109  	return callArgCompleter(fn, ev, words, rawCands)
   110  }
   111  
   112  type builtinArgCompleter struct {
   113  	name string
   114  	impl func([]string, *eval.Evaler, hashmap.Map, chan<- rawCandidate) error
   115  
   116  	argCompleter hashmap.Map
   117  }
   118  
   119  var _ eval.Callable = &builtinArgCompleter{}
   120  
   121  func (bac *builtinArgCompleter) Kind() string {
   122  	return "fn"
   123  }
   124  
   125  // Equal compares by identity.
   126  func (bac *builtinArgCompleter) Equal(a interface{}) bool {
   127  	return bac == a
   128  }
   129  
   130  func (bac *builtinArgCompleter) Hash() uint32 {
   131  	return hash.Pointer(unsafe.Pointer(bac))
   132  }
   133  
   134  func (bac *builtinArgCompleter) Repr(int) string {
   135  	return "$edit:" + bac.name + eval.FnSuffix
   136  }
   137  
   138  func (bac *builtinArgCompleter) Call(ec *eval.Frame, args []interface{}, opts map[string]interface{}) error {
   139  	eval.TakeNoOpt(opts)
   140  	words := make([]string, len(args))
   141  	for i, arg := range args {
   142  		s, ok := arg.(string)
   143  		if !ok {
   144  			throw(errCompleterArgMustBeString)
   145  		}
   146  		words[i] = s
   147  	}
   148  
   149  	rawCands := make(chan rawCandidate)
   150  	var err error
   151  	go func() {
   152  		defer close(rawCands)
   153  		err = bac.impl(words, ec.Evaler, bac.argCompleter, rawCands)
   154  	}()
   155  
   156  	output := ec.OutputChan()
   157  	for rc := range rawCands {
   158  		output <- rc
   159  	}
   160  	return err
   161  }
   162  
   163  func complFilename(words []string, ev *eval.Evaler, ac hashmap.Map, rawCands chan<- rawCandidate) error {
   164  	if len(words) < 1 {
   165  		return errTooFewArguments
   166  	}
   167  	return complFilenameInner(words[len(words)-1], false, rawCands)
   168  }
   169  
   170  func complSudo(words []string, ev *eval.Evaler, ac hashmap.Map, rawCands chan<- rawCandidate) error {
   171  	if len(words) < 2 {
   172  		return errTooFewArguments
   173  	}
   174  	if len(words) == 2 {
   175  		return complFormHeadInner(words[1], ev, rawCands)
   176  	}
   177  	return completeArg(words[1:], ev, ac, rawCands)
   178  }
   179  
   180  // callArgCompleter calls a Fn, assuming that it is an arg completer. It calls
   181  // the Fn with specified arguments and closed input, and converts its output to
   182  // candidate objects.
   183  func callArgCompleter(fn eval.Callable, ev *eval.Evaler, words []string, rawCands chan<- rawCandidate) error {
   184  
   185  	// Quick path for builtin arg completers.
   186  	if builtin, ok := fn.(*builtinArgCompleter); ok {
   187  		return builtin.impl(words, ev, builtin.argCompleter, rawCands)
   188  	}
   189  
   190  	args := make([]interface{}, len(words))
   191  	for i, word := range words {
   192  		args[i] = word
   193  	}
   194  
   195  	ports := []*eval.Port{
   196  		eval.DevNullClosedChan,
   197  		{}, // Will be replaced when capturing output
   198  		{File: os.Stderr},
   199  	}
   200  
   201  	valuesCb := func(ch <-chan interface{}) {
   202  		for v := range ch {
   203  			switch v := v.(type) {
   204  			case rawCandidate:
   205  				rawCands <- v
   206  			case string:
   207  				rawCands <- plainCandidate(v)
   208  			default:
   209  				logger.Printf("completer must output string or candidate")
   210  			}
   211  		}
   212  	}
   213  
   214  	bytesCb := func(r *os.File) {
   215  		buffered := bufio.NewReader(r)
   216  		for {
   217  			line, err := buffered.ReadString('\n')
   218  			if line != "" {
   219  				rawCands <- plainCandidate(strings.TrimSuffix(line, "\n"))
   220  			}
   221  			if err != nil {
   222  				if err != io.EOF {
   223  					logger.Println("error on reading:", err)
   224  				}
   225  				break
   226  			}
   227  		}
   228  	}
   229  
   230  	// XXX There is no source to pass to NewTopEvalCtx.
   231  	ec := eval.NewTopFrame(ev, eval.NewInternalSource("[editor completer]"), ports)
   232  	err := ec.CallWithOutputCallback(fn, args, eval.NoOpts, valuesCb, bytesCb)
   233  	if err != nil {
   234  		err = errors.New("completer error: " + err.Error())
   235  	}
   236  
   237  	return err
   238  }