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} }