github.com/elves/elvish@v0.15.0/pkg/edit/completion.go (about)

     1  package edit
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  	"sync"
     9  	"unicode/utf8"
    10  
    11  	"github.com/elves/elvish/pkg/cli"
    12  	"github.com/elves/elvish/pkg/cli/addons/completion"
    13  	"github.com/elves/elvish/pkg/edit/complete"
    14  	"github.com/elves/elvish/pkg/eval"
    15  	"github.com/elves/elvish/pkg/eval/vals"
    16  	"github.com/elves/elvish/pkg/fsutil"
    17  	"github.com/elves/elvish/pkg/parse"
    18  	"github.com/elves/elvish/pkg/strutil"
    19  	"github.com/xiaq/persistent/hash"
    20  )
    21  
    22  //elvdoc:var completion:arg-completer
    23  //
    24  // A map containing argument completers.
    25  
    26  //elvdoc:var completion:binding
    27  //
    28  // Keybinding for the completion mode.
    29  
    30  //elvdoc:var completion:matcher
    31  //
    32  // A map mapping from context names to matcher functions. See the
    33  // [Matcher](#matcher) section.
    34  
    35  //elvdoc:fn complete-filename
    36  //
    37  // ```elvish
    38  // edit:complete-filename $args...
    39  // ```
    40  //
    41  // Produces a list of filenames found in the directory of the last argument. All
    42  // other arguments are ignored. If the last argument does not contain a path
    43  // (either absolute or relative to the current directory), then the current
    44  // directory is used. Relevant files are output as `edit:complex-candidate`
    45  // objects.
    46  //
    47  // This function is the default handler for any commands without
    48  // explicit handlers in `$edit:completion:arg-completer`. See [Argument
    49  // Completer](#argument-completer).
    50  //
    51  // Example:
    52  //
    53  // ```elvish-transcript
    54  // ~> edit:complete-filename ''
    55  // ▶ (edit:complex-candidate Applications &code-suffix=/ &style='01;34')
    56  // ▶ (edit:complex-candidate Books &code-suffix=/ &style='01;34')
    57  // ▶ (edit:complex-candidate Desktop &code-suffix=/ &style='01;34')
    58  // ▶ (edit:complex-candidate Docsafe &code-suffix=/ &style='01;34')
    59  // ▶ (edit:complex-candidate Documents &code-suffix=/ &style='01;34')
    60  // ...
    61  // ~> edit:complete-filename .elvish/
    62  // ▶ (edit:complex-candidate .elvish/aliases &code-suffix=/ &style='01;34')
    63  // ▶ (edit:complex-candidate .elvish/db &code-suffix=' ' &style='')
    64  // ▶ (edit:complex-candidate .elvish/epm-installed &code-suffix=' ' &style='')
    65  // ▶ (edit:complex-candidate .elvish/lib &code-suffix=/ &style='01;34')
    66  // ▶ (edit:complex-candidate .elvish/rc.elv &code-suffix=' ' &style='')
    67  // ```
    68  
    69  //elvdoc:fn complex-candidate
    70  //
    71  // ```elvish
    72  // edit:complex-candidate $stem &display='' &code-suffix=''
    73  // ```
    74  //
    75  // Builds a complex candidate. This is mainly useful in [argument
    76  // completers](#argument-completer).
    77  //
    78  // The `&display` option controls how the candidate is shown in the UI. It can
    79  // be a string or a [styled](builtin.html#styled) text. If it is empty, `$stem`
    80  // is used.
    81  //
    82  // The `&code-suffix` option affects how the candidate is inserted into the code
    83  // when it is accepted. By default, a quoted version of `$stem` is inserted. If
    84  // `$code-suffix` is non-empty, it is added to that text, and the suffix is not
    85  // quoted.
    86  
    87  type complexCandidateOpts struct {
    88  	CodeSuffix string
    89  	Display    string
    90  }
    91  
    92  func (*complexCandidateOpts) SetDefaultOptions() {}
    93  
    94  func complexCandidate(fm *eval.Frame, opts complexCandidateOpts, stem string) complexItem {
    95  	display := opts.Display
    96  	if display == "" {
    97  		display = stem
    98  	}
    99  	return complexItem{
   100  		Stem:       stem,
   101  		CodeSuffix: opts.CodeSuffix,
   102  		Display:    display,
   103  	}
   104  }
   105  
   106  //elvdoc:fn match-prefix
   107  //
   108  // ```elvish
   109  // edit:match-prefix $seed $inputs?
   110  // ```
   111  //
   112  // For each input, outputs whether the input has $seed as a prefix. Uses the
   113  // result of `to-string` for non-string inputs.
   114  //
   115  // Roughly equivalent to the following Elvish function, but more efficient:
   116  //
   117  // ```elvish
   118  // use str
   119  // fn match-prefix [seed @input]{
   120  //   each [x]{ str:has-prefix (to-string $x) $seed } $@input
   121  // }
   122  // ```
   123  
   124  //elvdoc:fn match-subseq
   125  //
   126  // ```elvish
   127  // edit:match-subseq $seed $inputs?
   128  // ```
   129  //
   130  // For each input, outputs whether the input has $seed as a
   131  // [subsequence](https://en.wikipedia.org/wiki/Subsequence). Uses the result of
   132  // `to-string` for non-string inputs.
   133  
   134  //elvdoc:fn match-substr
   135  //
   136  // ```elvish
   137  // edit:match-substr $seed $inputs?
   138  // ```
   139  //
   140  // For each input, outputs whether the input has $seed as a substring. Uses the
   141  // result of `to-string` for non-string inputs.
   142  //
   143  // Roughly equivalent to the following Elvish function, but more efficient:
   144  //
   145  // ```elvish
   146  // use str
   147  // fn match-substr [seed @input]{
   148  //   each [x]{ str:has-contains (to-string $x) $seed } $@input
   149  // }
   150  // ```
   151  
   152  //elvdoc:fn completion:start
   153  //
   154  // Start the completion mode.
   155  
   156  //elvdoc:fn completion:smart-start
   157  //
   158  // Starts the completion mode. However, if all the candidates share a non-empty
   159  // prefix and that prefix starts with the seed, inserts the prefix instead.
   160  
   161  func completionStart(app cli.App, binding cli.Handler, cfg complete.Config, smart bool) {
   162  	buf := app.CodeArea().CopyState().Buffer
   163  	result, err := complete.Complete(
   164  		complete.CodeBuffer{Content: buf.Content, Dot: buf.Dot}, cfg)
   165  	if err != nil {
   166  		app.Notify(err.Error())
   167  		return
   168  	}
   169  	if smart {
   170  		prefix := ""
   171  		for i, item := range result.Items {
   172  			if i == 0 {
   173  				prefix = item.ToInsert
   174  				continue
   175  			}
   176  			prefix = commonPrefix(prefix, item.ToInsert)
   177  			if prefix == "" {
   178  				break
   179  			}
   180  		}
   181  		if prefix != "" {
   182  			insertedPrefix := false
   183  			app.CodeArea().MutateState(func(s *cli.CodeAreaState) {
   184  				rep := s.Buffer.Content[result.Replace.From:result.Replace.To]
   185  				if len(prefix) > len(rep) && strings.HasPrefix(prefix, rep) {
   186  					s.Pending = cli.PendingCode{
   187  						Content: prefix,
   188  						From:    result.Replace.From, To: result.Replace.To}
   189  					s.ApplyPending()
   190  					insertedPrefix = true
   191  				}
   192  			})
   193  			if insertedPrefix {
   194  				return
   195  			}
   196  		}
   197  	}
   198  	completion.Start(app, completion.Config{
   199  		Name: result.Name, Replace: result.Replace, Items: result.Items,
   200  		Binding: binding})
   201  }
   202  
   203  //elvdoc:fn completion:close
   204  //
   205  // Closes the completion mode UI.
   206  
   207  func initCompletion(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) {
   208  	bindingVar := newBindingVar(EmptyBindingMap)
   209  	binding := newMapBinding(ed, ev, bindingVar)
   210  	matcherMapVar := newMapVar(vals.EmptyMap)
   211  	argGeneratorMapVar := newMapVar(vals.EmptyMap)
   212  	cfg := func() complete.Config {
   213  		return complete.Config{
   214  			PureEvaler: pureEvaler{ev},
   215  			Filterer: adaptMatcherMap(
   216  				ed, ev, matcherMapVar.Get().(vals.Map)),
   217  			ArgGenerator: adaptArgGeneratorMap(
   218  				ev, argGeneratorMapVar.Get().(vals.Map)),
   219  		}
   220  	}
   221  	generateForSudo := func(args []string) ([]complete.RawItem, error) {
   222  		return complete.GenerateForSudo(cfg(), args)
   223  	}
   224  	nb.AddGoFns("<edit>", map[string]interface{}{
   225  		"complete-filename": wrapArgGenerator(complete.GenerateFileNames),
   226  		"complete-getopt":   completeGetopt,
   227  		"complete-sudo":     wrapArgGenerator(generateForSudo),
   228  		"complex-candidate": complexCandidate,
   229  		"match-prefix":      wrapMatcher(strings.HasPrefix),
   230  		"match-subseq":      wrapMatcher(strutil.HasSubseq),
   231  		"match-substr":      wrapMatcher(strings.Contains),
   232  	})
   233  	app := ed.app
   234  	nb.AddNs("completion",
   235  		eval.NsBuilder{
   236  			"arg-completer": argGeneratorMapVar,
   237  			"binding":       bindingVar,
   238  			"matcher":       matcherMapVar,
   239  		}.AddGoFns("<edit:completion>:", map[string]interface{}{
   240  			"accept":      func() { listingAccept(app) },
   241  			"smart-start": func() { completionStart(app, binding, cfg(), true) },
   242  			"start":       func() { completionStart(app, binding, cfg(), false) },
   243  			"close":       func() { completion.Close(app) },
   244  			"up":          func() { listingUp(app) },
   245  			"down":        func() { listingDown(app) },
   246  			"up-cycle":    func() { listingUpCycle(app) },
   247  			"down-cycle":  func() { listingDownCycle(app) },
   248  			"left":        func() { listingLeft(app) },
   249  			"right":       func() { listingRight(app) },
   250  		}).Ns())
   251  }
   252  
   253  // A wrapper type implementing Elvish value methods.
   254  type complexItem complete.ComplexItem
   255  
   256  func (c complexItem) Index(k interface{}) (interface{}, bool) {
   257  	switch k {
   258  	case "stem":
   259  		return c.Stem, true
   260  	case "code-suffix":
   261  		return c.CodeSuffix, true
   262  	case "display":
   263  		return c.Display, true
   264  	}
   265  	return nil, false
   266  }
   267  
   268  func (c complexItem) IterateKeys(f func(interface{}) bool) {
   269  	vals.Feed(f, "stem", "code-suffix", "display")
   270  }
   271  
   272  func (c complexItem) Kind() string { return "map" }
   273  
   274  func (c complexItem) Equal(a interface{}) bool {
   275  	rhs, ok := a.(complexItem)
   276  	return ok && c.Stem == rhs.Stem &&
   277  		c.CodeSuffix == rhs.CodeSuffix && c.Display == rhs.Display
   278  }
   279  
   280  func (c complexItem) Hash() uint32 {
   281  	h := hash.DJBInit
   282  	h = hash.DJBCombine(h, hash.String(c.Stem))
   283  	h = hash.DJBCombine(h, hash.String(c.CodeSuffix))
   284  	h = hash.DJBCombine(h, hash.String(c.Display))
   285  	return h
   286  }
   287  
   288  func (c complexItem) Repr(indent int) string {
   289  	// TODO(xiaq): Pretty-print when indent >= 0
   290  	return fmt.Sprintf("(edit:complex-candidate %s &code-suffix=%s &display=%s)",
   291  		parse.Quote(c.Stem), parse.Quote(c.CodeSuffix), parse.Quote(c.Display))
   292  }
   293  
   294  type wrappedArgGenerator func(*eval.Frame, ...string) error
   295  
   296  // Wraps an ArgGenerator into a function that can be then passed to
   297  // eval.NewGoFn.
   298  func wrapArgGenerator(gen complete.ArgGenerator) wrappedArgGenerator {
   299  	return func(fm *eval.Frame, args ...string) error {
   300  		rawItems, err := gen(args)
   301  		if err != nil {
   302  			return err
   303  		}
   304  		ch := fm.OutputChan()
   305  		for _, rawItem := range rawItems {
   306  			switch rawItem := rawItem.(type) {
   307  			case complete.ComplexItem:
   308  				ch <- complexItem(rawItem)
   309  			case complete.PlainItem:
   310  				ch <- string(rawItem)
   311  			default:
   312  				ch <- rawItem
   313  			}
   314  		}
   315  		return nil
   316  	}
   317  }
   318  
   319  func commonPrefix(s1, s2 string) string {
   320  	for i, r := range s1 {
   321  		if s2 == "" {
   322  			break
   323  		}
   324  		r2, n2 := utf8.DecodeRuneInString(s2)
   325  		if r2 != r {
   326  			return s1[:i]
   327  		}
   328  		s2 = s2[n2:]
   329  	}
   330  	return s1
   331  }
   332  
   333  // The type for a native Go matcher. This is not equivalent to the Elvish
   334  // counterpart, which streams input and output. This is because we can actually
   335  // afford calling a Go function for each item, so omitting the streaming
   336  // behavior makes the implementation simpler.
   337  //
   338  // Native Go matchers are wrapped into Elvish matchers, but never the other way
   339  // around.
   340  //
   341  // This type is satisfied by strings.Contains and strings.HasPrefix; they are
   342  // wrapped into match-substr and match-prefix respectively.
   343  type matcher func(text, seed string) bool
   344  
   345  type matcherOpts struct {
   346  	IgnoreCase bool
   347  	SmartCase  bool
   348  }
   349  
   350  func (*matcherOpts) SetDefaultOptions() {}
   351  
   352  type wrappedMatcher func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs)
   353  
   354  func wrapMatcher(m matcher) wrappedMatcher {
   355  	return func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) {
   356  		out := fm.OutputChan()
   357  		if opts.IgnoreCase || (opts.SmartCase && seed == strings.ToLower(seed)) {
   358  			if opts.IgnoreCase {
   359  				seed = strings.ToLower(seed)
   360  			}
   361  			inputs(func(v interface{}) {
   362  				out <- m(strings.ToLower(vals.ToString(v)), seed)
   363  			})
   364  		} else {
   365  			inputs(func(v interface{}) {
   366  				out <- m(vals.ToString(v), seed)
   367  			})
   368  		}
   369  	}
   370  }
   371  
   372  // Adapts $edit:completion:matcher into a Filterer.
   373  func adaptMatcherMap(nt notifier, ev *eval.Evaler, m vals.Map) complete.Filterer {
   374  	return func(ctxName, seed string, rawItems []complete.RawItem) []complete.RawItem {
   375  		matcher, ok := lookupFn(m, ctxName)
   376  		if !ok {
   377  			nt.notifyf(
   378  				"matcher for %s not a function, falling back to prefix matching", ctxName)
   379  		}
   380  		if matcher == nil {
   381  			return complete.FilterPrefix(ctxName, seed, rawItems)
   382  		}
   383  		input := make(chan interface{})
   384  		stopInputFeeder := make(chan struct{})
   385  		defer close(stopInputFeeder)
   386  		// Feed a string representing all raw candidates to the input channel.
   387  		go func() {
   388  			defer close(input)
   389  			for _, rawItem := range rawItems {
   390  				select {
   391  				case input <- rawItem.String():
   392  				case <-stopInputFeeder:
   393  					return
   394  				}
   395  			}
   396  		}()
   397  
   398  		// TODO: Supply the Chan component of port 2.
   399  		port1, collect, err := eval.CapturePort()
   400  		if err != nil {
   401  			nt.notifyf("cannot create pipe to run completion matcher: %v", err)
   402  			return nil
   403  		}
   404  
   405  		err = ev.Call(matcher,
   406  			eval.CallCfg{Args: []interface{}{seed}, From: "[editor matcher]"},
   407  			eval.EvalCfg{Ports: []*eval.Port{
   408  				// TODO: Supply the Chan component of port 2.
   409  				{Chan: input, File: eval.DevNull}, port1, {File: os.Stderr}}})
   410  		outputs := collect()
   411  
   412  		if err != nil {
   413  			nt.notifyError("matcher", err)
   414  			// Continue with whatever values have been output
   415  		}
   416  		if len(outputs) != len(rawItems) {
   417  			nt.notifyf(
   418  				"matcher has output %v values, not equal to %v inputs",
   419  				len(outputs), len(rawItems))
   420  		}
   421  		filtered := []complete.RawItem{}
   422  		for i := 0; i < len(rawItems) && i < len(outputs); i++ {
   423  			if vals.Bool(outputs[i]) {
   424  				filtered = append(filtered, rawItems[i])
   425  			}
   426  		}
   427  		return filtered
   428  	}
   429  }
   430  
   431  func adaptArgGeneratorMap(ev *eval.Evaler, m vals.Map) complete.ArgGenerator {
   432  	return func(args []string) ([]complete.RawItem, error) {
   433  		gen, ok := lookupFn(m, args[0])
   434  		if !ok {
   435  			return nil, fmt.Errorf("arg completer for %s not a function", args[0])
   436  		}
   437  		if gen == nil {
   438  			return complete.GenerateFileNames(args)
   439  		}
   440  		argValues := make([]interface{}, len(args))
   441  		for i, arg := range args {
   442  			argValues[i] = arg
   443  		}
   444  		var output []complete.RawItem
   445  		var outputMutex sync.Mutex
   446  		collect := func(item complete.RawItem) {
   447  			outputMutex.Lock()
   448  			defer outputMutex.Unlock()
   449  			output = append(output, item)
   450  		}
   451  		valueCb := func(ch <-chan interface{}) {
   452  			for v := range ch {
   453  				switch v := v.(type) {
   454  				case string:
   455  					collect(complete.PlainItem(v))
   456  				case complexItem:
   457  					collect(complete.ComplexItem(v))
   458  				default:
   459  					collect(complete.PlainItem(vals.ToString(v)))
   460  				}
   461  			}
   462  		}
   463  		bytesCb := func(r *os.File) {
   464  			buffered := bufio.NewReader(r)
   465  			for {
   466  				line, err := buffered.ReadString('\n')
   467  				if line != "" {
   468  					collect(complete.PlainItem(strutil.ChopLineEnding(line)))
   469  				}
   470  				if err != nil {
   471  					break
   472  				}
   473  			}
   474  		}
   475  		port1, done, err := eval.PipePort(valueCb, bytesCb)
   476  		if err != nil {
   477  			panic(err)
   478  		}
   479  		err = ev.Call(gen,
   480  			eval.CallCfg{Args: argValues, From: "[editor arg generator]"},
   481  			eval.EvalCfg{Ports: []*eval.Port{
   482  				// TODO: Supply the Chan component of port 2.
   483  				nil, port1, {File: os.Stderr}}})
   484  		done()
   485  
   486  		return output, err
   487  	}
   488  }
   489  
   490  func lookupFn(m vals.Map, ctxName string) (eval.Callable, bool) {
   491  	val, ok := m.Index(ctxName)
   492  	if !ok {
   493  		val, ok = m.Index("")
   494  	}
   495  	if !ok {
   496  		// No matcher, but not an error either
   497  		return nil, true
   498  	}
   499  	fn, ok := val.(eval.Callable)
   500  	if !ok {
   501  		return nil, false
   502  	}
   503  	return fn, true
   504  }
   505  
   506  type pureEvaler struct{ ev *eval.Evaler }
   507  
   508  func (pureEvaler) EachExternal(f func(string)) { fsutil.EachExternal(f) }
   509  
   510  func (pureEvaler) EachSpecial(f func(string)) {
   511  	for name := range eval.IsBuiltinSpecial {
   512  		f(name)
   513  	}
   514  }
   515  
   516  func (pe pureEvaler) EachNs(f func(string)) {
   517  	eachNsInTop(pe.ev.Builtin(), pe.ev.Global(), f)
   518  }
   519  
   520  func (pe pureEvaler) EachVariableInNs(ns string, f func(string)) {
   521  	eachVariableInTop(pe.ev.Builtin(), pe.ev.Global(), ns, f)
   522  }
   523  
   524  func (pe pureEvaler) PurelyEvalPrimary(pn *parse.Primary) interface{} {
   525  	return pe.ev.PurelyEvalPrimary(pn)
   526  }
   527  
   528  func (pe pureEvaler) PurelyEvalCompound(cn *parse.Compound) (string, bool) {
   529  	return pe.ev.PurelyEvalCompound(cn)
   530  }
   531  
   532  func (pe pureEvaler) PurelyEvalPartialCompound(cn *parse.Compound, upto int) (string, bool) {
   533  	return pe.ev.PurelyEvalPartialCompound(cn, upto)
   534  }