github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/edit/complete/complete_test.go (about) 1 package complete 2 3 import ( 4 "fmt" 5 "os" 6 "runtime" 7 "testing" 8 9 "github.com/markusbkk/elvish/pkg/cli/lscolors" 10 "github.com/markusbkk/elvish/pkg/cli/modes" 11 "github.com/markusbkk/elvish/pkg/diag" 12 "github.com/markusbkk/elvish/pkg/eval" 13 "github.com/markusbkk/elvish/pkg/parse" 14 "github.com/markusbkk/elvish/pkg/testutil" 15 "github.com/markusbkk/elvish/pkg/tt" 16 "github.com/markusbkk/elvish/pkg/ui" 17 ) 18 19 var Args = tt.Args 20 21 // An implementation of PureEvaler useful in tests. 22 type testEvaler struct { 23 externals []string 24 specials []string 25 namespaces []string 26 variables map[string][]string 27 } 28 29 func feed(f func(string), ss []string) { 30 for _, s := range ss { 31 f(s) 32 } 33 } 34 35 func (ev testEvaler) EachExternal(f func(string)) { feed(f, ev.externals) } 36 func (ev testEvaler) EachSpecial(f func(string)) { feed(f, ev.specials) } 37 func (ev testEvaler) EachNs(f func(string)) { feed(f, ev.namespaces) } 38 39 func (ev testEvaler) EachVariableInNs(ns string, f func(string)) { 40 feed(f, ev.variables[ns]) 41 } 42 43 func (ev testEvaler) PurelyEvalPartialCompound(cn *parse.Compound, upto int) (string, bool) { 44 return (*eval.Evaler)(nil).PurelyEvalPartialCompound(cn, upto) 45 } 46 47 func (ev testEvaler) PurelyEvalCompound(cn *parse.Compound) (string, bool) { 48 return (*eval.Evaler)(nil).PurelyEvalCompound(cn) 49 } 50 51 func (ev testEvaler) PurelyEvalPrimary(pn *parse.Primary) interface{} { 52 return (*eval.Evaler)(nil).PurelyEvalPrimary(pn) 53 } 54 55 func TestComplete(t *testing.T) { 56 lscolors.SetTestLsColors(t) 57 testutil.InTempDir(t) 58 testutil.ApplyDir(testutil.Dir{ 59 "a.exe": testutil.File{Perm: 0755, Content: ""}, 60 "non-exe": "", 61 "d": testutil.Dir{ 62 "a.exe": testutil.File{Perm: 0755, Content: ""}, 63 }, 64 }) 65 66 var cfg Config 67 cfg = Config{ 68 Filterer: FilterPrefix, 69 PureEvaler: testEvaler{ 70 externals: []string{"ls", "make"}, 71 specials: []string{"if", "for"}, 72 variables: map[string][]string{ 73 "": {"foo", "bar", "fn~", "ns:"}, 74 "ns1:": {"lorem"}, 75 "ns2:": {"ipsum"}, 76 }, 77 namespaces: []string{"ns1:", "ns2:"}, 78 }, 79 ArgGenerator: func(args []string) ([]RawItem, error) { 80 if len(args) >= 2 && args[0] == "sudo" { 81 return GenerateForSudo(cfg, args) 82 } 83 return GenerateFileNames(args) 84 }, 85 } 86 87 argGeneratorDebugCfg := Config{ 88 PureEvaler: cfg.PureEvaler, 89 Filterer: func(ctxName, seed string, items []RawItem) []RawItem { 90 return items 91 }, 92 ArgGenerator: func(args []string) ([]RawItem, error) { 93 item := noQuoteItem(fmt.Sprintf("%#v", args)) 94 return []RawItem{item}, nil 95 }, 96 } 97 98 dupCfg := Config{ 99 PureEvaler: cfg.PureEvaler, 100 ArgGenerator: func([]string) ([]RawItem, error) { 101 return []RawItem{PlainItem("a"), PlainItem("b"), PlainItem("a")}, nil 102 }, 103 } 104 105 allFileNameItems := []modes.CompletionItem{ 106 fc("a.exe", " "), fc("d"+string(os.PathSeparator), ""), fc("non-exe", " "), 107 } 108 109 allCommandItems := []modes.CompletionItem{ 110 c("fn"), c("for"), c("if"), c("ls"), c("make"), c("ns:"), 111 } 112 113 tt.Test(t, tt.Fn("Complete", Complete), tt.Table{ 114 // No PureEvaler. 115 Args(cb(""), Config{}).Rets( 116 (*Result)(nil), 117 errNoPureEvaler), 118 // Candidates are deduplicated. 119 Args(cb("ls "), dupCfg).Rets( 120 &Result{ 121 Name: "argument", Replace: r(3, 3), 122 Items: []modes.CompletionItem{ 123 c("a"), c("b"), 124 }, 125 }, 126 nil), 127 // Complete arguments using GenerateFileNames. 128 Args(cb("ls "), cfg).Rets( 129 &Result{ 130 Name: "argument", Replace: r(3, 3), 131 Items: allFileNameItems}, 132 nil), 133 Args(cb("ls a"), cfg).Rets( 134 &Result{ 135 Name: "argument", Replace: r(3, 4), 136 Items: []modes.CompletionItem{fc("a.exe", " ")}}, 137 nil), 138 // GenerateForSudo completing external commands. 139 Args(cb("sudo "), cfg).Rets( 140 &Result{ 141 Name: "argument", Replace: r(5, 5), 142 Items: []modes.CompletionItem{c("ls"), c("make")}}, 143 nil), 144 // GenerateForSudo completing non-command arguments. 145 Args(cb("sudo ls "), cfg).Rets( 146 &Result{ 147 Name: "argument", Replace: r(8, 8), 148 Items: allFileNameItems}, 149 nil), 150 // Custom arg completer, new argument 151 Args(cb("ls a "), argGeneratorDebugCfg).Rets( 152 &Result{ 153 Name: "argument", Replace: r(5, 5), 154 Items: []modes.CompletionItem{c(`[]string{"ls", "a", ""}`)}}, 155 nil), 156 Args(cb("ls a b"), argGeneratorDebugCfg).Rets( 157 &Result{ 158 Name: "argument", Replace: r(5, 6), 159 Items: []modes.CompletionItem{c(`[]string{"ls", "a", "b"}`)}}, 160 nil), 161 162 // Complete for special command "set". 163 Args(cb("set "), cfg).Rets( 164 &Result{ 165 Name: "argument", Replace: r(4, 4), 166 Items: []modes.CompletionItem{ 167 c("bar"), c("fn~"), c("foo"), c("ns:"), 168 }, 169 }), 170 Args(cb("set @"), cfg).Rets( 171 &Result{ 172 Name: "argument", Replace: r(4, 5), 173 Items: []modes.CompletionItem{ 174 c("@bar"), c("@fn~"), c("@foo"), c("@ns:"), 175 }, 176 }), 177 Args(cb("set ns1:"), cfg).Rets( 178 &Result{ 179 Name: "argument", Replace: r(4, 8), 180 Items: []modes.CompletionItem{ 181 c("ns1:lorem"), 182 }, 183 }), 184 Args(cb("set a = "), cfg).Rets( 185 &Result{ 186 Name: "argument", Replace: r(8, 8), 187 Items: nil, 188 }), 189 // "tmp" has the same completer. 190 Args(cb("tmp "), cfg).Rets( 191 &Result{ 192 Name: "argument", Replace: r(4, 4), 193 Items: []modes.CompletionItem{ 194 c("bar"), c("fn~"), c("foo"), c("ns:"), 195 }, 196 }), 197 198 // Complete commands at an empty buffer, generating special forms, 199 // externals, functions, namespaces and variable assignments. 200 Args(cb(""), cfg).Rets( 201 &Result{Name: "command", Replace: r(0, 0), Items: allCommandItems}, 202 nil), 203 // Complete at an empty closure. 204 Args(cb("{ "), cfg).Rets( 205 &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, 206 nil), 207 // Complete after a newline. 208 Args(cb("a\n"), cfg).Rets( 209 &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, 210 nil), 211 // Complete after a semicolon. 212 Args(cb("a;"), cfg).Rets( 213 &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, 214 nil), 215 // Complete after a pipe. 216 Args(cb("a|"), cfg).Rets( 217 &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, 218 nil), 219 // Complete at the beginning of output capture. 220 Args(cb("a ("), cfg).Rets( 221 &Result{Name: "command", Replace: r(3, 3), Items: allCommandItems}, 222 nil), 223 // Complete at the beginning of exception capture. 224 Args(cb("a ?("), cfg).Rets( 225 &Result{Name: "command", Replace: r(4, 4), Items: allCommandItems}, 226 nil), 227 228 // Complete external commands with the e: prefix. 229 Args(cb("e:"), cfg).Rets( 230 &Result{ 231 Name: "command", Replace: r(0, 2), 232 Items: []modes.CompletionItem{c("e:ls"), c("e:make")}}, 233 nil), 234 235 // TODO(xiaq): Add tests for completing indices. 236 237 // Complete filenames for redirection. 238 Args(cb("p >"), cfg).Rets( 239 &Result{Name: "redir", Replace: r(3, 3), Items: allFileNameItems}, 240 nil), 241 Args(cb("p > a"), cfg).Rets( 242 &Result{ 243 Name: "redir", Replace: r(4, 5), 244 Items: []modes.CompletionItem{fc("a.exe", " ")}}, 245 nil), 246 247 // Completing variables. 248 Args(cb("p $"), cfg).Rets( 249 &Result{ 250 Name: "variable", Replace: r(3, 3), 251 Items: []modes.CompletionItem{ 252 c("bar"), c("fn~"), c("foo"), c("ns1:"), c("ns2:"), c("ns:")}}, 253 nil), 254 Args(cb("p $f"), cfg).Rets( 255 &Result{ 256 Name: "variable", Replace: r(3, 4), 257 Items: []modes.CompletionItem{c("fn~"), c("foo")}}, 258 nil), 259 // 0123456 260 Args(cb("p $ns1:"), cfg).Rets( 261 &Result{ 262 Name: "variable", Replace: r(7, 7), 263 Items: []modes.CompletionItem{c("lorem")}}, 264 nil), 265 }) 266 267 // Symlinks and executable bits are not available on Windows. 268 if goos := runtime.GOOS; goos != "windows" { 269 err := os.Symlink("d", "d2") 270 if err != nil { 271 panic(err) 272 } 273 allLocalCommandItems := []modes.CompletionItem{ 274 fc("./a.exe", " "), fc("./d/", ""), fc("./d2/", ""), 275 } 276 tt.Test(t, tt.Fn("Complete", Complete), tt.Table{ 277 // Filename completion treats symlink to directories as directories. 278 // 01234 279 Args(cb("p > d"), cfg).Rets( 280 &Result{ 281 Name: "redir", Replace: r(4, 5), 282 Items: []modes.CompletionItem{fc("d/", ""), fc("d2/", "")}}, 283 nil, 284 ), 285 286 // Complete local external commands. 287 // 288 // TODO(xiaq): Make this test applicable to Windows by using a 289 // different criteria for executable files on Window. 290 Args(cb("./"), cfg).Rets( 291 &Result{ 292 Name: "command", Replace: r(0, 2), 293 Items: allLocalCommandItems}, 294 nil), 295 // After sudo. 296 Args(cb("sudo ./"), cfg).Rets( 297 &Result{ 298 Name: "argument", Replace: r(5, 7), 299 Items: allLocalCommandItems}, 300 nil), 301 }) 302 } 303 } 304 305 func cb(s string) CodeBuffer { return CodeBuffer{s, len(s)} } 306 307 func c(s string) modes.CompletionItem { return modes.CompletionItem{ToShow: ui.T(s), ToInsert: s} } 308 309 func fc(s, suffix string) modes.CompletionItem { 310 return modes.CompletionItem{ 311 ToShow: ui.T(s, ui.StylingFromSGR(lscolors.GetColorist().GetStyle(s))), 312 ToInsert: parse.Quote(s) + suffix} 313 } 314 315 func r(i, j int) diag.Ranging { return diag.Ranging{From: i, To: j} }