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

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