src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/complete/complete_test.go (about) 1 package complete 2 3 // Mocked builtin commands 4 5 import ( 6 "fmt" 7 "os" 8 "runtime" 9 "sort" 10 "strings" 11 "testing" 12 13 "src.elv.sh/pkg/cli/lscolors" 14 "src.elv.sh/pkg/cli/modes" 15 "src.elv.sh/pkg/diag" 16 "src.elv.sh/pkg/eval" 17 "src.elv.sh/pkg/eval/vars" 18 "src.elv.sh/pkg/parse" 19 "src.elv.sh/pkg/testutil" 20 "src.elv.sh/pkg/tt" 21 "src.elv.sh/pkg/ui" 22 ) 23 24 var Args = tt.Args 25 26 func TestComplete(t *testing.T) { 27 lscolors.SetTestLsColors(t) 28 testutil.InTempDir(t) 29 testutil.ApplyDir(testutil.Dir{ 30 "a.exe": testutil.File{Perm: 0755, Content: ""}, 31 "non-exe": "", 32 "d": testutil.Dir{ 33 "a.exe": testutil.File{Perm: 0755, Content: ""}, 34 }, 35 }) 36 testutil.Set(t, &eachExternal, func(f func(string)) { 37 f("external-cmd1") 38 f("external-cmd2") 39 }) 40 testutil.Set(t, &environ, func() []string { 41 return []string{"ENV1=", "ENV2="} 42 }) 43 44 ev := eval.NewEvaler() 45 err := ev.Eval(parse.SourceForTest(strings.Join([]string{ 46 "var local-var1 = $nil", 47 "var local-var2 = $nil", 48 "fn local-fn1 { }", 49 "fn local-fn2 { }", 50 "var local-ns1: = (ns [&lorem=$nil])", 51 "var local-ns2: = (ns [&ipsum=$nil])", 52 }, "\n")), eval.EvalCfg{}) 53 if err != nil { 54 t.Fatalf("evaler setup: %v", err) 55 } 56 ev.ReplaceBuiltin( 57 eval.BuildNs(). 58 AddVar("builtin-var1", vars.NewReadOnly(nil)). 59 AddVar("builtin-var2", vars.NewReadOnly(nil)). 60 AddGoFn("builtin-fn1", func() {}). 61 AddGoFn("builtin-fn2", func() {}). 62 Ns()) 63 64 var cfg Config 65 cfg = Config{ 66 Filterer: FilterPrefix, 67 ArgGenerator: func(args []string) ([]RawItem, error) { 68 if len(args) >= 2 && args[0] == "sudo" { 69 return GenerateForSudo(args, ev, cfg) 70 } 71 return GenerateFileNames(args) 72 }, 73 } 74 75 argGeneratorDebugCfg := Config{ 76 Filterer: func(ctxName, seed string, items []RawItem) []RawItem { 77 return items 78 }, 79 ArgGenerator: func(args []string) ([]RawItem, error) { 80 item := noQuoteItem(fmt.Sprintf("%#v", args)) 81 return []RawItem{item}, nil 82 }, 83 } 84 85 dupCfg := Config{ 86 ArgGenerator: func([]string) ([]RawItem, error) { 87 return []RawItem{PlainItem("a"), PlainItem("b"), PlainItem("a")}, nil 88 }, 89 } 90 91 allFileNameItems := []modes.CompletionItem{ 92 fci("a.exe", " "), fci("d"+string(os.PathSeparator), ""), fci("non-exe", " "), 93 } 94 95 allCommandItems := []modes.CompletionItem{ 96 ci("builtin-fn1"), ci("builtin-fn2"), 97 ci("external-cmd1"), ci("external-cmd2"), 98 ci("local-fn1"), ci("local-fn2"), 99 ci("local-ns1:"), ci("local-ns2:"), 100 } 101 // Add all special commands. 102 for name := range eval.IsBuiltinSpecial { 103 allCommandItems = append(allCommandItems, ci(name)) 104 } 105 sort.Slice(allCommandItems, func(i, j int) bool { 106 return allCommandItems[i].ToInsert < allCommandItems[j].ToInsert 107 }) 108 109 tt.Test(t, Complete, 110 // Candidates are deduplicated. 111 Args(cb("ls "), ev, dupCfg).Rets( 112 &Result{ 113 Name: "argument", Replace: r(3, 3), 114 Items: []modes.CompletionItem{ 115 ci("a"), ci("b"), 116 }, 117 }, 118 nil), 119 // Complete arguments using GenerateFileNames. 120 Args(cb("ls "), ev, cfg).Rets( 121 &Result{ 122 Name: "argument", Replace: r(3, 3), 123 Items: allFileNameItems}, 124 nil), 125 Args(cb("ls a"), ev, cfg).Rets( 126 &Result{ 127 Name: "argument", Replace: r(3, 4), 128 Items: []modes.CompletionItem{fci("a.exe", " ")}}, 129 nil), 130 // GenerateForSudo completing external commands. 131 Args(cb("sudo "), ev, cfg).Rets( 132 &Result{ 133 Name: "argument", Replace: r(5, 5), 134 Items: []modes.CompletionItem{ci("external-cmd1"), ci("external-cmd2")}}, 135 nil), 136 // GenerateForSudo completing non-command arguments. 137 Args(cb("sudo ls "), ev, cfg).Rets( 138 &Result{ 139 Name: "argument", Replace: r(8, 8), 140 Items: allFileNameItems}, 141 nil), 142 // Custom arg completer, new argument 143 Args(cb("ls a "), ev, argGeneratorDebugCfg).Rets( 144 &Result{ 145 Name: "argument", Replace: r(5, 5), 146 Items: []modes.CompletionItem{ci(`[]string{"ls", "a", ""}`)}}, 147 nil), 148 Args(cb("ls a b"), ev, argGeneratorDebugCfg).Rets( 149 &Result{ 150 Name: "argument", Replace: r(5, 6), 151 Items: []modes.CompletionItem{ci(`[]string{"ls", "a", "b"}`)}}, 152 nil), 153 154 // Complete for special command "set". 155 Args(cb("set "), ev, cfg).Rets( 156 &Result{ 157 Name: "argument", Replace: r(4, 4), 158 Items: []modes.CompletionItem{ 159 ci("builtin-fn1~"), ci("builtin-fn2~"), 160 ci("builtin-var1"), ci("builtin-var2"), 161 ci("local-fn1~"), ci("local-fn2~"), 162 ci("local-ns1:"), ci("local-ns2:"), 163 ci("local-var1"), ci("local-var2"), 164 }, 165 }), 166 Args(cb("set @"), ev, cfg).Rets( 167 &Result{ 168 Name: "argument", Replace: r(4, 5), 169 Items: []modes.CompletionItem{ 170 ci("@builtin-fn1~"), ci("@builtin-fn2~"), 171 ci("@builtin-var1"), ci("@builtin-var2"), 172 ci("@local-fn1~"), ci("@local-fn2~"), 173 ci("@local-ns1:"), ci("@local-ns2:"), 174 ci("@local-var1"), ci("@local-var2"), 175 }, 176 }), 177 Args(cb("set local-ns1:"), ev, cfg).Rets( 178 &Result{ 179 Name: "argument", Replace: r(4, 14), 180 Items: []modes.CompletionItem{ 181 ci("local-ns1:lorem"), 182 }, 183 }), 184 // Completing an argument after "=" use the default generator (in this 185 // case filenames). 186 Args(cb("set a = "), ev, cfg).Rets( 187 &Result{ 188 Name: "argument", Replace: r(8, 8), 189 Items: allFileNameItems, 190 }), 191 // But completing the "=" itself offers no candidates. 192 Args(cb("set a ="), ev, cfg).Rets( 193 &Result{ 194 Name: "argument", Replace: r(6, 7), 195 Items: nil, 196 }), 197 // "tmp" has the same completer. 198 Args(cb("tmp "), ev, cfg).Rets( 199 &Result{ 200 Name: "argument", Replace: r(4, 4), 201 Items: []modes.CompletionItem{ 202 ci("builtin-fn1~"), ci("builtin-fn2~"), 203 ci("builtin-var1"), ci("builtin-var2"), 204 ci("local-fn1~"), ci("local-fn2~"), 205 ci("local-ns1:"), ci("local-ns2:"), 206 ci("local-var1"), ci("local-var2"), 207 }, 208 }), 209 // "del" has a similar completer. 210 Args(cb("del "), ev, cfg).Rets( 211 &Result{ 212 Name: "argument", Replace: r(4, 4), 213 Items: []modes.CompletionItem{ 214 ci("local-fn1~"), ci("local-fn2~"), 215 ci("local-ns1:"), ci("local-ns2:"), 216 ci("local-var1"), ci("local-var2"), 217 }, 218 }), 219 220 // Complete commands at an empty buffer, generating special forms, 221 // externals, functions, namespaces and variable assignments. 222 Args(cb(""), ev, cfg).Rets( 223 &Result{Name: "command", Replace: r(0, 0), Items: allCommandItems}, 224 nil), 225 // Complete at an empty closure. 226 Args(cb("{ "), ev, cfg).Rets( 227 &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, 228 nil), 229 // Complete after a newline. 230 Args(cb("a\n"), ev, cfg).Rets( 231 &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, 232 nil), 233 // Complete after a semicolon. 234 Args(cb("a;"), ev, cfg).Rets( 235 &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, 236 nil), 237 // Complete after a pipe. 238 Args(cb("a|"), ev, cfg).Rets( 239 &Result{Name: "command", Replace: r(2, 2), Items: allCommandItems}, 240 nil), 241 // Complete at the beginning of output capture. 242 Args(cb("a ("), ev, cfg).Rets( 243 &Result{Name: "command", Replace: r(3, 3), Items: allCommandItems}, 244 nil), 245 // Complete at the beginning of exception capture. 246 Args(cb("a ?("), ev, cfg).Rets( 247 &Result{Name: "command", Replace: r(4, 4), Items: allCommandItems}, 248 nil), 249 // Complete external commands with the e: prefix. 250 Args(cb("e:"), ev, cfg).Rets( 251 &Result{ 252 Name: "command", Replace: r(0, 2), 253 Items: []modes.CompletionItem{ 254 ci("e:external-cmd1"), ci("e:external-cmd2"), 255 }}, 256 nil), 257 // Commands newly defined by fn are supported too. 258 Args(cb("fn new-fn { }; new-"), ev, cfg).Rets( 259 &Result{ 260 Name: "command", Replace: r(15, 19), 261 Items: []modes.CompletionItem{ci("new-fn")}}, 262 nil), 263 264 // TODO(xiaq): Add tests for completing indices. 265 266 // Complete filenames for redirection. 267 Args(cb("p >"), ev, cfg).Rets( 268 &Result{Name: "redir", Replace: r(3, 3), Items: allFileNameItems}, 269 nil), 270 Args(cb("p > a"), ev, cfg).Rets( 271 &Result{ 272 Name: "redir", Replace: r(4, 5), 273 Items: []modes.CompletionItem{fci("a.exe", " ")}}, 274 nil), 275 276 // Completing variables. 277 278 // All variables. 279 Args(cb("p $"), ev, cfg).Rets( 280 &Result{ 281 Name: "variable", Replace: r(3, 3), 282 Items: []modes.CompletionItem{ 283 ci("E:"), 284 ci("builtin-fn1~"), ci("builtin-fn2~"), 285 ci("builtin-var1"), ci("builtin-var2"), 286 ci("e:"), 287 ci("local-fn1~"), ci("local-fn2~"), 288 ci("local-ns1:"), ci("local-ns2:"), 289 ci("local-var1"), ci("local-var2"), 290 }}, 291 nil), 292 // Variables with a prefix. 293 Args(cb("p $local-"), ev, cfg).Rets( 294 &Result{ 295 Name: "variable", Replace: r(3, 9), 296 Items: []modes.CompletionItem{ 297 ci("local-fn1~"), ci("local-fn2~"), 298 ci("local-ns1:"), ci("local-ns2:"), 299 ci("local-var1"), ci("local-var2"), 300 }}, 301 nil), 302 // Variables newly defined in the code, in the current scope. 303 Args(cb("var new-var; p $new-"), ev, cfg).Rets( 304 &Result{ 305 Name: "variable", Replace: r(16, 20), 306 Items: []modes.CompletionItem{ci("new-var")}}, 307 nil), 308 // Sigils in "var" are not part of the variable name. 309 Args(cb("var @new-var = a b; p $new-"), ev, cfg).Rets( 310 &Result{ 311 Name: "variable", Replace: r(23, 27), 312 Items: []modes.CompletionItem{ci("new-var")}}, 313 nil), 314 // Function parameters are recognized as newly defined variables too. 315 Args(cb("{ |new-var| p $new-"), ev, cfg).Rets( 316 &Result{ 317 Name: "variable", Replace: r(15, 19), 318 Items: []modes.CompletionItem{ci("new-var")}}, 319 nil), 320 // Variables newly defined in the code, in an outer scope. 321 Args(cb("var new-var; { p $new-"), ev, cfg).Rets( 322 &Result{ 323 Name: "variable", Replace: r(18, 22), 324 Items: []modes.CompletionItem{ci("new-var")}}, 325 nil), 326 // Variables newly defined in the code, but in a scope not visible from 327 // the point of completion, are not included. 328 Args(cb("{ var new-var } p $new-"), ev, cfg).Rets( 329 &Result{ 330 Name: "variable", Replace: r(19, 23), 331 Items: nil, 332 }, 333 nil), 334 335 // Variables defined by fn are supported too. 336 Args(cb("fn new-fn { }; p $new-"), ev, cfg).Rets( 337 &Result{ 338 Name: "variable", Replace: r(18, 22), 339 Items: []modes.CompletionItem{ci("new-fn~")}}, 340 nil), 341 342 // Variables in a namespace. 343 // 01234567890123 344 Args(cb("p $local-ns1:"), ev, cfg).Rets( 345 &Result{ 346 Name: "variable", Replace: r(13, 13), 347 Items: []modes.CompletionItem{ci("lorem")}}, 348 nil), 349 // Variables in the special e: namespace. 350 // 012345 351 Args(cb("p $e:"), ev, cfg).Rets( 352 &Result{ 353 Name: "variable", Replace: r(5, 5), 354 Items: []modes.CompletionItem{ 355 ci("external-cmd1~"), ci("external-cmd2~"), 356 }}, 357 nil), 358 // Variable in the special E: namespace. 359 // 012345 360 Args(cb("p $E:"), ev, cfg).Rets( 361 &Result{ 362 Name: "variable", Replace: r(5, 5), 363 Items: []modes.CompletionItem{ 364 ci("ENV1"), ci("ENV2"), 365 }}, 366 nil), 367 // Variables in a nonexistent namespace. 368 // 01234567 369 Args(cb("p $bad:"), ev, cfg).Rets( 370 &Result{Name: "variable", Replace: r(7, 7)}, 371 nil), 372 // Variables in a nested nonexistent namespace. 373 // 0123456789012345678901 374 Args(cb("p $local-ns1:bad:bad:"), ev, cfg).Rets( 375 &Result{Name: "variable", Replace: r(21, 21)}, 376 nil), 377 378 // No completion in supported context. 379 Args(cb("nop ["), ev, cfg).Rets((*Result)(nil), errNoCompletion), 380 // No completion after parse error. 381 Args(cb("nop `"), ev, cfg).Rets((*Result)(nil), errNoCompletion), 382 ) 383 384 // Completions of filename involving symlinks and local commands. 385 386 if runtime.GOOS == "windows" { 387 // Symlinks require admin permissions on Windows, so we won't test them 388 389 // Completing local commands after forward slash 390 tt.Test(t, Complete, 391 // Complete local external commands. 392 Args(cb("./"), ev, cfg).Rets( 393 &Result{ 394 Name: "command", Replace: r(0, 2), 395 Items: []modes.CompletionItem{ 396 fci("./a.exe", " "), fci(`./d\`, "")}, 397 }, 398 nil), 399 ) 400 401 // Completing local commands after backslash 402 tt.Test(t, Complete, 403 // Complete local external commands. 404 Args(cb(`.\`), ev, cfg).Rets( 405 &Result{ 406 Name: "command", Replace: r(0, 2), 407 Items: []modes.CompletionItem{ 408 fci(`.\a.exe`, " "), fci(`.\d\`, "")}, 409 }, 410 nil), 411 ) 412 } else { 413 err := os.Symlink("d", "d2") 414 if err != nil { 415 panic(err) 416 } 417 allLocalCommandItems := []modes.CompletionItem{ 418 fci("./a.exe", " "), fci("./d/", ""), fci("./d2/", ""), 419 } 420 tt.Test(t, Complete, 421 // Filename completion treats symlink to directories as directories. 422 // 01234 423 Args(cb("p > d"), ev, cfg).Rets( 424 &Result{ 425 Name: "redir", Replace: r(4, 5), 426 Items: []modes.CompletionItem{fci("d/", ""), fci("d2/", "")}}, 427 nil, 428 ), 429 430 // Complete local external commands. 431 Args(cb("./"), ev, cfg).Rets( 432 &Result{ 433 Name: "command", Replace: r(0, 2), 434 Items: allLocalCommandItems}, 435 nil), 436 // After sudo. 437 Args(cb("sudo ./"), ev, cfg).Rets( 438 &Result{ 439 Name: "argument", Replace: r(5, 7), 440 Items: allLocalCommandItems}, 441 nil), 442 ) 443 } 444 } 445 446 func cb(s string) CodeBuffer { return CodeBuffer{s, len(s)} } 447 448 func ci(s string) modes.CompletionItem { return modes.CompletionItem{ToShow: ui.T(s), ToInsert: s} } 449 450 func fci(s, suffix string) modes.CompletionItem { 451 return modes.CompletionItem{ 452 ToShow: ui.T(s, ui.StylingFromSGR(lscolors.GetColorist().GetStyle(s))), 453 ToInsert: parse.Quote(s) + suffix} 454 } 455 456 func r(i, j int) diag.Ranging { return diag.Ranging{From: i, To: j} }