src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/completion.go (about)

     1  package edit
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"os"
     7  	"reflect"
     8  	"strings"
     9  	"sync"
    10  	"unicode/utf8"
    11  
    12  	"src.elv.sh/pkg/cli/modes"
    13  	"src.elv.sh/pkg/cli/tk"
    14  	"src.elv.sh/pkg/edit/complete"
    15  	"src.elv.sh/pkg/eval"
    16  	"src.elv.sh/pkg/eval/errs"
    17  	"src.elv.sh/pkg/eval/vals"
    18  	"src.elv.sh/pkg/eval/vars"
    19  	"src.elv.sh/pkg/parse"
    20  	"src.elv.sh/pkg/persistent/hash"
    21  	"src.elv.sh/pkg/strutil"
    22  	"src.elv.sh/pkg/ui"
    23  )
    24  
    25  type complexCandidateOpts struct {
    26  	CodeSuffix string
    27  	Display    any
    28  }
    29  
    30  func (*complexCandidateOpts) SetDefaultOptions() {}
    31  
    32  func complexCandidate(fm *eval.Frame, opts complexCandidateOpts, stem string) (complexItem, error) {
    33  	var display ui.Text
    34  	switch displayOpt := opts.Display.(type) {
    35  	case nil:
    36  		// Leave display = nil
    37  	case string:
    38  		display = ui.T(displayOpt)
    39  	case ui.Text:
    40  		display = displayOpt
    41  	default:
    42  		return complexItem{}, errs.BadValue{What: "&display",
    43  			Valid: "string or styled", Actual: vals.ReprPlain(displayOpt)}
    44  	}
    45  	return complexItem{
    46  		Stem:       stem,
    47  		CodeSuffix: opts.CodeSuffix,
    48  		Display:    display,
    49  	}, nil
    50  }
    51  
    52  func completionStart(ed *Editor, bindings tk.Bindings, ev *eval.Evaler, cfg complete.Config, smart bool) {
    53  	codeArea, ok := focusedCodeArea(ed.app)
    54  	if !ok {
    55  		return
    56  	}
    57  	if smart {
    58  		ed.applyAutofix()
    59  	}
    60  	buf := codeArea.CopyState().Buffer
    61  	result, err := complete.Complete(
    62  		complete.CodeBuffer{Content: buf.Content, Dot: buf.Dot}, ev, cfg)
    63  	if err != nil {
    64  		ed.app.Notify(modes.ErrorText(err))
    65  		return
    66  	}
    67  	if smart {
    68  		prefix := ""
    69  		for i, item := range result.Items {
    70  			if i == 0 {
    71  				prefix = item.ToInsert
    72  				continue
    73  			}
    74  			prefix = commonPrefix(prefix, item.ToInsert)
    75  			if prefix == "" {
    76  				break
    77  			}
    78  		}
    79  		if prefix != "" {
    80  			insertedPrefix := false
    81  			codeArea.MutateState(func(s *tk.CodeAreaState) {
    82  				rep := s.Buffer.Content[result.Replace.From:result.Replace.To]
    83  				if len(prefix) > len(rep) && strings.HasPrefix(prefix, rep) {
    84  					s.Pending = tk.PendingCode{
    85  						Content: prefix,
    86  						From:    result.Replace.From, To: result.Replace.To}
    87  					s.ApplyPending()
    88  					insertedPrefix = true
    89  				}
    90  			})
    91  			if insertedPrefix {
    92  				return
    93  			}
    94  		}
    95  	}
    96  	w, err := modes.NewCompletion(ed.app, modes.CompletionSpec{
    97  		Name: result.Name, Replace: result.Replace, Items: result.Items,
    98  		Filter: filterSpec, Bindings: bindings,
    99  	})
   100  	if w != nil {
   101  		ed.app.PushAddon(w)
   102  	}
   103  	if err != nil {
   104  		ed.app.Notify(modes.ErrorText(err))
   105  	}
   106  }
   107  
   108  func initCompletion(ed *Editor, ev *eval.Evaler, nb eval.NsBuilder) {
   109  	bindingVar := newBindingVar(emptyBindingsMap)
   110  	bindings := newMapBindings(ed, ev, bindingVar)
   111  	matcherMapVar := newMapVar(vals.EmptyMap)
   112  	argGeneratorMapVar := newMapVar(vals.EmptyMap)
   113  	cfg := func() complete.Config {
   114  		return complete.Config{
   115  			Filterer: adaptMatcherMap(
   116  				ed, ev, matcherMapVar.Get().(vals.Map)),
   117  			ArgGenerator: adaptArgGeneratorMap(
   118  				ev, argGeneratorMapVar.Get().(vals.Map)),
   119  		}
   120  	}
   121  	generateForSudo := func(args []string) ([]complete.RawItem, error) {
   122  		return complete.GenerateForSudo(args, ev, cfg())
   123  	}
   124  	nb.AddGoFns(map[string]any{
   125  		"complete-filename": wrapArgGenerator(complete.GenerateFileNames),
   126  		"complete-getopt":   completeGetopt,
   127  		"complete-sudo":     wrapArgGenerator(generateForSudo),
   128  		"complex-candidate": complexCandidate,
   129  		"match-prefix":      wrapMatcher(strings.HasPrefix),
   130  		"match-subseq":      wrapMatcher(strutil.HasSubseq),
   131  		"match-substr":      wrapMatcher(strings.Contains),
   132  	})
   133  	app := ed.app
   134  	nb.AddNs("completion",
   135  		eval.BuildNsNamed("edit:completion").
   136  			AddVars(map[string]vars.Var{
   137  				"arg-completer": argGeneratorMapVar,
   138  				"binding":       bindingVar,
   139  				"matcher":       matcherMapVar,
   140  			}).
   141  			AddGoFns(map[string]any{
   142  				"accept":      func() { listingAccept(app) },
   143  				"smart-start": func() { completionStart(ed, bindings, ev, cfg(), true) },
   144  				"start":       func() { completionStart(ed, bindings, ev, cfg(), false) },
   145  				"up":          func() { listingUp(app) },
   146  				"down":        func() { listingDown(app) },
   147  				"up-cycle":    func() { listingUpCycle(app) },
   148  				"down-cycle":  func() { listingDownCycle(app) },
   149  				"left":        func() { listingLeft(app) },
   150  				"right":       func() { listingRight(app) },
   151  			}))
   152  }
   153  
   154  // A wrapper type implementing Elvish value methods.
   155  type complexItem complete.ComplexItem
   156  
   157  func (c complexItem) Index(k any) (any, bool) {
   158  	switch k {
   159  	case "stem":
   160  		return c.Stem, true
   161  	case "code-suffix":
   162  		return c.CodeSuffix, true
   163  	case "display":
   164  		return c.Display, true
   165  	}
   166  	return nil, false
   167  }
   168  
   169  func (c complexItem) IterateKeys(f func(any) bool) {
   170  	vals.Feed(f, "stem", "code-suffix", "display")
   171  }
   172  
   173  func (c complexItem) Kind() string { return "map" }
   174  
   175  func (c complexItem) Equal(a any) bool {
   176  	rhs, ok := a.(complexItem)
   177  	return ok && c.Stem == rhs.Stem &&
   178  		c.CodeSuffix == rhs.CodeSuffix && reflect.DeepEqual(c.Display, rhs.Display)
   179  }
   180  
   181  func (c complexItem) Hash() uint32 {
   182  	h := hash.DJBInit
   183  	h = hash.DJBCombine(h, hash.String(c.Stem))
   184  	h = hash.DJBCombine(h, hash.String(c.CodeSuffix))
   185  	// TODO: Add c.Display
   186  	return h
   187  }
   188  
   189  func (c complexItem) Repr(indent int) string {
   190  	// TODO(xiaq): Pretty-print when indent >= 0
   191  	return fmt.Sprintf("(edit:complex-candidate %s &code-suffix=%s &display=%s)",
   192  		parse.Quote(c.Stem), parse.Quote(c.CodeSuffix), vals.Repr(c.Display, indent+1))
   193  }
   194  
   195  type wrappedArgGenerator func(*eval.Frame, ...string) error
   196  
   197  // Wraps an ArgGenerator into a function that can be then passed to
   198  // eval.NewGoFn.
   199  func wrapArgGenerator(gen complete.ArgGenerator) wrappedArgGenerator {
   200  	return func(fm *eval.Frame, args ...string) error {
   201  		rawItems, err := gen(args)
   202  		if err != nil {
   203  			return err
   204  		}
   205  		out := fm.ValueOutput()
   206  		for _, rawItem := range rawItems {
   207  			var v any
   208  			switch rawItem := rawItem.(type) {
   209  			case complete.ComplexItem:
   210  				v = complexItem(rawItem)
   211  			case complete.PlainItem:
   212  				v = string(rawItem)
   213  			default:
   214  				v = rawItem
   215  			}
   216  			err := out.Put(v)
   217  			if err != nil {
   218  				return err
   219  			}
   220  		}
   221  		return nil
   222  	}
   223  }
   224  
   225  func commonPrefix(s1, s2 string) string {
   226  	for i, r := range s1 {
   227  		if s2 == "" {
   228  			break
   229  		}
   230  		r2, n2 := utf8.DecodeRuneInString(s2)
   231  		if r2 != r {
   232  			return s1[:i]
   233  		}
   234  		s2 = s2[n2:]
   235  	}
   236  	return s1
   237  }
   238  
   239  // The type for a native Go matcher. This is not equivalent to the Elvish
   240  // counterpart, which streams input and output. This is because we can actually
   241  // afford calling a Go function for each item, so omitting the streaming
   242  // behavior makes the implementation simpler.
   243  //
   244  // Native Go matchers are wrapped into Elvish matchers, but never the other way
   245  // around.
   246  //
   247  // This type is satisfied by strings.Contains and strings.HasPrefix; they are
   248  // wrapped into match-substr and match-prefix respectively.
   249  type matcher func(text, seed string) bool
   250  
   251  type matcherOpts struct {
   252  	IgnoreCase bool
   253  	SmartCase  bool
   254  }
   255  
   256  func (*matcherOpts) SetDefaultOptions() {}
   257  
   258  type wrappedMatcher func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) error
   259  
   260  func wrapMatcher(m matcher) wrappedMatcher {
   261  	return func(fm *eval.Frame, opts matcherOpts, seed string, inputs eval.Inputs) error {
   262  		out := fm.ValueOutput()
   263  		var errOut error
   264  		if opts.IgnoreCase || (opts.SmartCase && seed == strings.ToLower(seed)) {
   265  			if opts.IgnoreCase {
   266  				seed = strings.ToLower(seed)
   267  			}
   268  			inputs(func(v any) {
   269  				if errOut != nil {
   270  					return
   271  				}
   272  				errOut = out.Put(m(strings.ToLower(vals.ToString(v)), seed))
   273  			})
   274  		} else {
   275  			inputs(func(v any) {
   276  				if errOut != nil {
   277  					return
   278  				}
   279  				errOut = out.Put(m(vals.ToString(v), seed))
   280  			})
   281  		}
   282  		return errOut
   283  	}
   284  }
   285  
   286  // Adapts $edit:completion:matcher into a Filterer.
   287  func adaptMatcherMap(nt notifier, ev *eval.Evaler, m vals.Map) complete.Filterer {
   288  	return func(ctxName, seed string, rawItems []complete.RawItem) []complete.RawItem {
   289  		matcher, ok := lookupFn(m, ctxName)
   290  		if !ok {
   291  			nt.notifyf(
   292  				"matcher for %s not a function, falling back to prefix matching", ctxName)
   293  		}
   294  		if matcher == nil {
   295  			return complete.FilterPrefix(ctxName, seed, rawItems)
   296  		}
   297  		input := make(chan any)
   298  		stopInputFeeder := make(chan struct{})
   299  		defer close(stopInputFeeder)
   300  		// Feed a string representing all raw candidates to the input channel.
   301  		go func() {
   302  			defer close(input)
   303  			for _, rawItem := range rawItems {
   304  				select {
   305  				case input <- rawItem.String():
   306  				case <-stopInputFeeder:
   307  					return
   308  				}
   309  			}
   310  		}()
   311  
   312  		// TODO: Supply the Chan component of port 2.
   313  		port1, collect, err := eval.ValueCapturePort()
   314  		if err != nil {
   315  			nt.notifyf("cannot create pipe to run completion matcher: %v", err)
   316  			return nil
   317  		}
   318  
   319  		err = ev.Call(matcher,
   320  			eval.CallCfg{Args: []any{seed}, From: "[editor matcher]"},
   321  			eval.EvalCfg{Ports: []*eval.Port{
   322  				// TODO: Supply the Chan component of port 2.
   323  				{Chan: input, File: eval.DevNull}, port1, {File: os.Stderr}}})
   324  		outputs := collect()
   325  
   326  		if err != nil {
   327  			nt.notifyError("matcher", err)
   328  			// Continue with whatever values have been output
   329  		}
   330  		if len(outputs) != len(rawItems) {
   331  			nt.notifyf(
   332  				"matcher has output %v values, not equal to %v inputs",
   333  				len(outputs), len(rawItems))
   334  		}
   335  		filtered := []complete.RawItem{}
   336  		for i := 0; i < len(rawItems) && i < len(outputs); i++ {
   337  			if vals.Bool(outputs[i]) {
   338  				filtered = append(filtered, rawItems[i])
   339  			}
   340  		}
   341  		return filtered
   342  	}
   343  }
   344  
   345  func adaptArgGeneratorMap(ev *eval.Evaler, m vals.Map) complete.ArgGenerator {
   346  	return func(args []string) ([]complete.RawItem, error) {
   347  		gen, ok := lookupFn(m, args[0])
   348  		if !ok {
   349  			return nil, fmt.Errorf("arg completer for %s not a function", args[0])
   350  		}
   351  		if gen == nil {
   352  			return complete.GenerateFileNames(args)
   353  		}
   354  		argValues := make([]any, len(args))
   355  		for i, arg := range args {
   356  			argValues[i] = arg
   357  		}
   358  		var output []complete.RawItem
   359  		var outputMutex sync.Mutex
   360  		collect := func(item complete.RawItem) {
   361  			outputMutex.Lock()
   362  			defer outputMutex.Unlock()
   363  			output = append(output, item)
   364  		}
   365  		valueCb := func(ch <-chan any) {
   366  			for v := range ch {
   367  				switch v := v.(type) {
   368  				case string:
   369  					collect(complete.PlainItem(v))
   370  				case complexItem:
   371  					collect(complete.ComplexItem(v))
   372  				default:
   373  					collect(complete.PlainItem(vals.ToString(v)))
   374  				}
   375  			}
   376  		}
   377  		bytesCb := func(r *os.File) {
   378  			buffered := bufio.NewReader(r)
   379  			for {
   380  				line, err := buffered.ReadString('\n')
   381  				if line != "" {
   382  					collect(complete.PlainItem(strutil.ChopLineEnding(line)))
   383  				}
   384  				if err != nil {
   385  					break
   386  				}
   387  			}
   388  		}
   389  		port1, done, err := eval.PipePort(valueCb, bytesCb)
   390  		if err != nil {
   391  			panic(err)
   392  		}
   393  		err = ev.Call(gen,
   394  			eval.CallCfg{Args: argValues, From: "[editor arg generator]"},
   395  			eval.EvalCfg{Ports: []*eval.Port{
   396  				// TODO: Supply the Chan component of port 2.
   397  				nil, port1, {File: os.Stderr}}})
   398  		done()
   399  
   400  		return output, err
   401  	}
   402  }
   403  
   404  func lookupFn(m vals.Map, ctxName string) (eval.Callable, bool) {
   405  	val, ok := m.Index(ctxName)
   406  	if !ok {
   407  		val, ok = m.Index("")
   408  	}
   409  	if !ok {
   410  		// No matcher, but not an error either
   411  		return nil, true
   412  	}
   413  	fn, ok := val.(eval.Callable)
   414  	if !ok {
   415  		return nil, false
   416  	}
   417  	return fn, true
   418  }