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