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 }