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