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

     1  package complete
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/markusbkk/elvish/pkg/cli/lscolors"
    10  	"github.com/markusbkk/elvish/pkg/eval"
    11  	"github.com/markusbkk/elvish/pkg/eval/vals"
    12  	"github.com/markusbkk/elvish/pkg/fsutil"
    13  	"github.com/markusbkk/elvish/pkg/parse"
    14  	"github.com/markusbkk/elvish/pkg/ui"
    15  )
    16  
    17  var pathSeparator = string(filepath.Separator)
    18  
    19  // GenerateFileNames returns filename candidates that are suitable for completing
    20  // the last argument. It can be used in Config.ArgGenerator.
    21  func GenerateFileNames(args []string) ([]RawItem, error) {
    22  	return generateFileNames(args[len(args)-1], false)
    23  }
    24  
    25  // GenerateForSudo generates candidates for sudo.
    26  func GenerateForSudo(cfg Config, args []string) ([]RawItem, error) {
    27  	switch {
    28  	case len(args) < 2:
    29  		return nil, errNoCompletion
    30  	case len(args) == 2:
    31  		// Complete external commands.
    32  		return generateExternalCommands(args[1], cfg.PureEvaler)
    33  	default:
    34  		return cfg.ArgGenerator(args[1:])
    35  	}
    36  }
    37  
    38  // Internal generators, used from completers.
    39  
    40  func generateArgs(args []string, cfg Config) ([]RawItem, error) {
    41  	switch args[0] {
    42  	case "set", "tmp":
    43  		for _, arg := range args[1:] {
    44  			if arg == "=" {
    45  				return nil, nil
    46  			}
    47  		}
    48  		seed := args[len(args)-1]
    49  		sigil, qname := eval.SplitSigil(seed)
    50  		ns, _ := eval.SplitIncompleteQNameNs(qname)
    51  		var items []RawItem
    52  		cfg.PureEvaler.EachVariableInNs(ns, func(varname string) {
    53  			items = append(items, noQuoteItem(sigil+parse.QuoteVariableName(ns+varname)))
    54  		})
    55  		return items, nil
    56  	}
    57  
    58  	items, err := cfg.ArgGenerator(args)
    59  	return items, err
    60  }
    61  
    62  func generateExternalCommands(seed string, ev PureEvaler) ([]RawItem, error) {
    63  	if fsutil.DontSearch(seed) {
    64  		// Completing a local external command name.
    65  		return generateFileNames(seed, true)
    66  	}
    67  	var items []RawItem
    68  	ev.EachExternal(func(s string) { items = append(items, PlainItem(s)) })
    69  	return items, nil
    70  }
    71  
    72  func generateCommands(seed string, ev PureEvaler) ([]RawItem, error) {
    73  	if fsutil.DontSearch(seed) {
    74  		// Completing a local external command name.
    75  		return generateFileNames(seed, true)
    76  	}
    77  
    78  	var cands []RawItem
    79  	addPlainItem := func(s string) { cands = append(cands, PlainItem(s)) }
    80  
    81  	if strings.HasPrefix(seed, "e:") {
    82  		// Generate all external commands with the e: prefix, and be done.
    83  		ev.EachExternal(func(command string) {
    84  			addPlainItem("e:" + command)
    85  		})
    86  		return cands, nil
    87  	}
    88  
    89  	// Generate all special forms.
    90  	ev.EachSpecial(addPlainItem)
    91  	// Generate all external commands (without the e: prefix).
    92  	ev.EachExternal(addPlainItem)
    93  
    94  	sigil, qname := eval.SplitSigil(seed)
    95  	ns, _ := eval.SplitIncompleteQNameNs(qname)
    96  	if sigil == "" {
    97  		// Generate functions, namespaces, and variable assignments.
    98  		ev.EachVariableInNs(ns, func(varname string) {
    99  			switch {
   100  			case strings.HasSuffix(varname, eval.FnSuffix):
   101  				addPlainItem(
   102  					ns + varname[:len(varname)-len(eval.FnSuffix)])
   103  			case strings.HasSuffix(varname, eval.NsSuffix):
   104  				addPlainItem(ns + varname)
   105  			}
   106  		})
   107  	}
   108  
   109  	return cands, nil
   110  }
   111  
   112  func generateFileNames(seed string, onlyExecutable bool) ([]RawItem, error) {
   113  	var items []RawItem
   114  
   115  	dir, fileprefix := filepath.Split(seed)
   116  	dirToRead := dir
   117  	if dirToRead == "" {
   118  		dirToRead = "."
   119  	}
   120  
   121  	files, err := os.ReadDir(dirToRead)
   122  	if err != nil {
   123  		return nil, fmt.Errorf("cannot list directory %s: %v", dirToRead, err)
   124  	}
   125  
   126  	lsColor := lscolors.GetColorist()
   127  
   128  	// Make candidates out of elements that match the file component.
   129  	for _, file := range files {
   130  		name := file.Name()
   131  		info, err := file.Info()
   132  		if err != nil {
   133  			continue
   134  		}
   135  		// Show dot files iff file part of pattern starts with dot, and vice
   136  		// versa.
   137  		if dotfile(fileprefix) != dotfile(name) {
   138  			continue
   139  		}
   140  		// Only accept searchable directories and executable files if
   141  		// executableOnly is true.
   142  		if onlyExecutable && (info.Mode()&0111) == 0 {
   143  			continue
   144  		}
   145  
   146  		// Full filename for source and getStyle.
   147  		full := dir + name
   148  
   149  		// Will be set to an empty space for non-directories
   150  		suffix := " "
   151  
   152  		if info.IsDir() {
   153  			full += pathSeparator
   154  			suffix = ""
   155  		} else if info.Mode()&os.ModeSymlink != 0 {
   156  			stat, err := os.Stat(full)
   157  			if err == nil && stat.IsDir() {
   158  				// Symlink to directory.
   159  				full += pathSeparator
   160  				suffix = ""
   161  			}
   162  		}
   163  
   164  		items = append(items, ComplexItem{
   165  			Stem:       full,
   166  			CodeSuffix: suffix,
   167  			Display:    ui.T(full, ui.StylingFromSGR(lsColor.GetStyle(full))),
   168  		})
   169  	}
   170  
   171  	return items, nil
   172  }
   173  
   174  func generateIndices(v interface{}) []RawItem {
   175  	var items []RawItem
   176  	vals.IterateKeys(v, func(k interface{}) bool {
   177  		if kstring, ok := k.(string); ok {
   178  			items = append(items, PlainItem(kstring))
   179  		}
   180  		return true
   181  	})
   182  	return items
   183  }
   184  
   185  func dotfile(fname string) bool {
   186  	return strings.HasPrefix(fname, ".")
   187  }