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 }