github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/edit/complete/complete.go (about)

     1  // Package complete implements the code completion algorithm for Elvish.
     2  package complete
     3  
     4  import (
     5  	"errors"
     6  	"sort"
     7  
     8  	"github.com/markusbkk/elvish/pkg/cli/modes"
     9  	"github.com/markusbkk/elvish/pkg/diag"
    10  	"github.com/markusbkk/elvish/pkg/parse"
    11  )
    12  
    13  type item = modes.CompletionItem
    14  
    15  // An error returned by Complete if the config has not supplied a PureEvaler.
    16  var errNoPureEvaler = errors.New("no PureEvaler supplied")
    17  
    18  // An error returned by Complete as well as the completers if there is no
    19  // applicable completion.
    20  var errNoCompletion = errors.New("no completion")
    21  
    22  // Config stores the configuration required for code completion.
    23  type Config struct {
    24  	// An interface to access the runtime. Complete will return an error if this
    25  	// is nil.
    26  	PureEvaler PureEvaler
    27  	// A function for filtering raw candidates. If nil, no filtering is done.
    28  	Filterer Filterer
    29  	// Used to generate candidates for a command argument. Defaults to
    30  	// Filenames.
    31  	ArgGenerator ArgGenerator
    32  }
    33  
    34  // Filterer is the type of functions that filter raw candidates.
    35  type Filterer func(ctxName, seed string, rawItems []RawItem) []RawItem
    36  
    37  // ArgGenerator is the type of functions that generate raw candidates for a
    38  // command argument. It takes all the existing arguments, the last being the
    39  // argument to complete, and returns raw candidates or an error.
    40  type ArgGenerator func(args []string) ([]RawItem, error)
    41  
    42  // Result keeps the result of the completion algorithm.
    43  type Result struct {
    44  	Name    string
    45  	Replace diag.Ranging
    46  	Items   []modes.CompletionItem
    47  }
    48  
    49  // RawItem represents completion items before the quoting pass.
    50  type RawItem interface {
    51  	String() string
    52  	Cook(parse.PrimaryType) modes.CompletionItem
    53  }
    54  
    55  // PureEvaler encapsulates the functionality the completion algorithm needs from
    56  // the language runtime.
    57  type PureEvaler interface {
    58  	EachExternal(func(cmd string))
    59  	EachSpecial(func(special string))
    60  	EachNs(func(string))
    61  	EachVariableInNs(string, func(string))
    62  	PurelyEvalPrimary(pn *parse.Primary) interface{}
    63  	PurelyEvalCompound(*parse.Compound) (string, bool)
    64  	PurelyEvalPartialCompound(*parse.Compound, int) (string, bool)
    65  }
    66  
    67  // CodeBuffer is the same the type in github.com/markusbkk/elvish/pkg/el/codearea,
    68  // replicated here to avoid an unnecessary dependency.
    69  type CodeBuffer struct {
    70  	Content string
    71  	Dot     int
    72  }
    73  
    74  // Complete runs the code completion algorithm in the given context, and returns
    75  // the completion type, items and any error encountered.
    76  func Complete(code CodeBuffer, cfg Config) (*Result, error) {
    77  	if cfg.PureEvaler == nil {
    78  		return nil, errNoPureEvaler
    79  	}
    80  	if cfg.Filterer == nil {
    81  		cfg.Filterer = FilterPrefix
    82  	}
    83  	if cfg.ArgGenerator == nil {
    84  		cfg.ArgGenerator = GenerateFileNames
    85  	}
    86  
    87  	// Ignore the error; the function always returns a valid *ChunkNode.
    88  	tree, _ := parse.Parse(parse.Source{Name: "[interactive]", Code: code.Content}, parse.Config{})
    89  	path := findNodePath(tree.Root, code.Dot)
    90  	for _, completer := range completers {
    91  		ctx, rawItems, err := completer(path, cfg)
    92  		if err == errNoCompletion {
    93  			continue
    94  		}
    95  		rawItems = cfg.Filterer(ctx.name, ctx.seed, rawItems)
    96  		sort.Slice(rawItems, func(i, j int) bool {
    97  			return rawItems[i].String() < rawItems[j].String()
    98  		})
    99  		items := make([]modes.CompletionItem, len(rawItems))
   100  		for i, rawCand := range rawItems {
   101  			items[i] = rawCand.Cook(ctx.quote)
   102  		}
   103  		items = dedup(items)
   104  		return &Result{Name: ctx.name, Items: items, Replace: ctx.interval}, nil
   105  	}
   106  	return nil, errNoCompletion
   107  }
   108  
   109  func dedup(items []modes.CompletionItem) []modes.CompletionItem {
   110  	var result []modes.CompletionItem
   111  	for i, item := range items {
   112  		if i == 0 || item.ToInsert != items[i-1].ToInsert {
   113  			result = append(result, item)
   114  		}
   115  	}
   116  	return result
   117  }