src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/builtins_test.go (about)

     1  package edit
     2  
     3  import (
     4  	"io"
     5  	"strings"
     6  	"testing"
     7  
     8  	"src.elv.sh/pkg/cli/modes"
     9  	"src.elv.sh/pkg/cli/term"
    10  	"src.elv.sh/pkg/cli/tk"
    11  	"src.elv.sh/pkg/tt"
    12  	"src.elv.sh/pkg/ui"
    13  )
    14  
    15  func TestBindingTable(t *testing.T) {
    16  	f := setup(t)
    17  
    18  	evals(f.Evaler, `var called = $false`)
    19  	evals(f.Evaler, `var m = (edit:binding-table [&a={ set called = $true }])`)
    20  	_, ok := getGlobal(f.Evaler, "m").(bindingsMap)
    21  	if !ok {
    22  		t.Errorf("edit:binding-table did not create BindingMap variable")
    23  	}
    24  }
    25  
    26  func TestCloseMode(t *testing.T) {
    27  	f := setup(t)
    28  
    29  	f.Editor.app.PushAddon(tk.Empty{})
    30  	evals(f.Evaler, `edit:close-mode`)
    31  
    32  	if addons := f.Editor.app.CopyState().Addons; len(addons) > 0 {
    33  		t.Errorf("got addons %v, want nil or empty slice", addons)
    34  	}
    35  }
    36  
    37  func TestInsertRaw(t *testing.T) {
    38  	f := setup(t)
    39  
    40  	f.TTYCtrl.Inject(term.K('V', ui.Ctrl))
    41  	wantBuf := f.MakeBuffer(
    42  		"~> ", term.DotHere, "\n",
    43  		" RAW ", Styles,
    44  		"*****",
    45  	)
    46  	f.TTYCtrl.TestBuffer(t, wantBuf)
    47  	// Since we do not use real terminals in the test, we cannot have a
    48  	// realistic test case against actual raw inputs. However, we can still
    49  	// check that the builtin command does call the SetRawInput method with 1.
    50  	if raw := f.TTYCtrl.RawInput(); raw != 1 {
    51  		t.Errorf("RawInput() -> %d, want 1", raw)
    52  	}
    53  
    54  	// Raw mode does not respond to non-key events.
    55  	f.TTYCtrl.Inject(term.MouseEvent{})
    56  	f.TTYCtrl.TestBuffer(t, wantBuf)
    57  
    58  	// Raw mode is dismissed after a single key event.
    59  	f.TTYCtrl.Inject(term.K('+'))
    60  	f.TestTTY(t,
    61  		"~> +", Styles,
    62  		"   v", term.DotHere,
    63  	)
    64  }
    65  
    66  func TestEndOfHistory(t *testing.T) {
    67  	f := setup(t)
    68  
    69  	evals(f.Evaler, `edit:end-of-history`)
    70  	f.TestTTYNotes(t, "End of history")
    71  }
    72  
    73  func TestKey(t *testing.T) {
    74  	f := setup(t)
    75  
    76  	evals(f.Evaler, `var k = (edit:key a)`)
    77  	wantK := ui.K('a')
    78  	if k, _ := f.Evaler.Global().Index("k"); k != wantK {
    79  		t.Errorf("$k is %v, want %v", k, wantK)
    80  	}
    81  }
    82  
    83  func TestRedraw(t *testing.T) {
    84  	f := setup(t)
    85  
    86  	evals(f.Evaler,
    87  		`set edit:current-command = echo`,
    88  		`edit:redraw`)
    89  	f.TestTTY(t,
    90  		"~> echo", Styles,
    91  		"   vvvv", term.DotHere)
    92  	evals(f.Evaler, `edit:redraw &full=$true`)
    93  	// TODO(xiaq): Test that this is actually a full redraw.
    94  	f.TestTTY(t,
    95  		"~> echo", Styles,
    96  		"   vvvv", term.DotHere)
    97  }
    98  
    99  func TestClear(t *testing.T) {
   100  	f := setup(t)
   101  
   102  	evals(f.Evaler, `set edit:current-command = echo`, `edit:clear`)
   103  	f.TestTTY(t,
   104  		"~> echo", Styles,
   105  		"   vvvv", term.DotHere)
   106  	if cleared := f.TTYCtrl.ScreenCleared(); cleared != 1 {
   107  		t.Errorf("screen cleared %v times, want 1", cleared)
   108  	}
   109  }
   110  
   111  func TestNotify(t *testing.T) {
   112  	f := setup(t)
   113  	evals(f.Evaler, "edit:notify string")
   114  	f.TestTTYNotes(t, "string")
   115  
   116  	evals(f.Evaler, "edit:notify (styled styled red)")
   117  	f.TestTTYNotes(t,
   118  		"styled", Styles,
   119  		"!!!!!!")
   120  
   121  	evals(f.Evaler, "var err = ?(edit:notify [])")
   122  	if _, hasErr := getGlobal(f.Evaler, "err").(error); !hasErr {
   123  		t.Errorf("calling edit:notify with [] did not result in error")
   124  		// TODO: Test the exact error
   125  	}
   126  }
   127  
   128  func TestReturnCode(t *testing.T) {
   129  	f := setup(t)
   130  
   131  	codeArea(f.Editor.app).MutateState(func(s *tk.CodeAreaState) {
   132  		s.Buffer.Content = "test code"
   133  	})
   134  	evals(f.Evaler, `edit:return-line`)
   135  	code, err := f.Wait()
   136  	if code != "test code" {
   137  		t.Errorf("got code %q, want %q", code, "test code")
   138  	}
   139  	if err != nil {
   140  		t.Errorf("got err %v, want nil", err)
   141  	}
   142  }
   143  
   144  func TestReturnEOF(t *testing.T) {
   145  	f := setup(t)
   146  
   147  	evals(f.Evaler, `edit:return-eof`)
   148  	if _, err := f.Wait(); err != io.EOF {
   149  		t.Errorf("got err %v, want %v", err, io.EOF)
   150  	}
   151  }
   152  
   153  func TestSmartEnter_InsertsNewlineWhenIncomplete(t *testing.T) {
   154  	f := setup(t)
   155  
   156  	f.SetCodeBuffer(tk.CodeBuffer{Content: "put [", Dot: 5})
   157  	evals(f.Evaler, `edit:smart-enter`)
   158  	wantBuf := tk.CodeBuffer{Content: "put [\n", Dot: 6}
   159  	if buf := codeArea(f.Editor.app).CopyState().Buffer; buf != wantBuf {
   160  		t.Errorf("got code buffer %v, want %v", buf, wantBuf)
   161  	}
   162  }
   163  
   164  func TestSmartEnter_AcceptsCodeWhenWholeBufferIsComplete(t *testing.T) {
   165  	f := setup(t)
   166  
   167  	f.SetCodeBuffer(tk.CodeBuffer{Content: "put []", Dot: 5})
   168  	evals(f.Evaler, `edit:smart-enter`)
   169  	wantCode := "put []"
   170  	if code, _ := f.Wait(); code != wantCode {
   171  		t.Errorf("got return code %q, want %q", code, wantCode)
   172  	}
   173  }
   174  
   175  // TODO: Test that smart-enter applies autofix.
   176  
   177  var bufferBuiltinsTests = []struct {
   178  	name      string
   179  	bufBefore tk.CodeBuffer
   180  	bufAfter  tk.CodeBuffer
   181  }{
   182  	{
   183  		"move-dot-left",
   184  		tk.CodeBuffer{Content: "ab", Dot: 1},
   185  		tk.CodeBuffer{Content: "ab", Dot: 0},
   186  	},
   187  	{
   188  		"move-dot-right",
   189  		tk.CodeBuffer{Content: "ab", Dot: 1},
   190  		tk.CodeBuffer{Content: "ab", Dot: 2},
   191  	},
   192  	{
   193  		"kill-rune-left",
   194  		tk.CodeBuffer{Content: "ab", Dot: 1},
   195  		tk.CodeBuffer{Content: "b", Dot: 0},
   196  	},
   197  	{
   198  		"kill-rune-right",
   199  		tk.CodeBuffer{Content: "ab", Dot: 1},
   200  		tk.CodeBuffer{Content: "a", Dot: 1},
   201  	},
   202  	{
   203  		"transpose-rune with empty buffer",
   204  		tk.CodeBuffer{Content: "", Dot: 0},
   205  		tk.CodeBuffer{Content: "", Dot: 0},
   206  	},
   207  	{
   208  		"transpose-rune with dot at beginning",
   209  		tk.CodeBuffer{Content: "abc", Dot: 0},
   210  		tk.CodeBuffer{Content: "bac", Dot: 2},
   211  	},
   212  	{
   213  		"transpose-rune with dot in middle",
   214  		tk.CodeBuffer{Content: "abc", Dot: 1},
   215  		tk.CodeBuffer{Content: "bac", Dot: 2},
   216  	},
   217  	{
   218  		"transpose-rune with dot at end",
   219  		tk.CodeBuffer{Content: "abc", Dot: 3},
   220  		tk.CodeBuffer{Content: "acb", Dot: 3},
   221  	},
   222  	{
   223  		"transpose-rune with one character and dot at end",
   224  		tk.CodeBuffer{Content: "a", Dot: 1},
   225  		tk.CodeBuffer{Content: "a", Dot: 1},
   226  	},
   227  	{
   228  		"transpose-rune with one character and dot at beginning",
   229  		tk.CodeBuffer{Content: "a", Dot: 0},
   230  		tk.CodeBuffer{Content: "a", Dot: 0},
   231  	},
   232  	{
   233  		"transpose-word with dot at beginning",
   234  		tk.CodeBuffer{Content: "ab  bc cd", Dot: 0},
   235  		tk.CodeBuffer{Content: "bc  ab cd", Dot: 6},
   236  	},
   237  	{
   238  		"transpose-word with dot in between words",
   239  		tk.CodeBuffer{Content: "ab  bc cd", Dot: 6},
   240  		tk.CodeBuffer{Content: "ab  cd bc", Dot: 9},
   241  	},
   242  	{
   243  		"transpose-word with dot at end",
   244  		tk.CodeBuffer{Content: "ab  bc cd", Dot: 9},
   245  		tk.CodeBuffer{Content: "ab  cd bc", Dot: 9},
   246  	},
   247  	{
   248  		"transpose-word with dot in the middle of a word",
   249  		tk.CodeBuffer{Content: "ab  bc cd", Dot: 5},
   250  		tk.CodeBuffer{Content: "bc  ab cd", Dot: 6},
   251  	},
   252  	{
   253  		"transpose-word with one word",
   254  		tk.CodeBuffer{Content: " ab  ", Dot: 4},
   255  		tk.CodeBuffer{Content: " ab  ", Dot: 4},
   256  	},
   257  	{
   258  		"transpose-word with no words",
   259  		tk.CodeBuffer{Content: " \t\n  ", Dot: 4},
   260  		tk.CodeBuffer{Content: " \t\n  ", Dot: 4},
   261  	},
   262  	{
   263  		"transpose-word with complex input",
   264  		tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4},
   265  		tk.CodeBuffer{Content: "~/downloads; cd", Dot: 15},
   266  	},
   267  	{
   268  		"transpose-small-word",
   269  		tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4},
   270  		tk.CodeBuffer{Content: "~/ cddownloads;", Dot: 5},
   271  	},
   272  	{
   273  		"transpose-alnum-word",
   274  		tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4},
   275  		tk.CodeBuffer{Content: "downloads ~/cd;", Dot: 14},
   276  	},
   277  }
   278  
   279  func TestBufferBuiltins(t *testing.T) {
   280  	f := setup(t)
   281  	app := f.Editor.app
   282  
   283  	for _, test := range bufferBuiltinsTests {
   284  		t.Run(test.name, func(t *testing.T) {
   285  			codeArea(app).MutateState(func(s *tk.CodeAreaState) {
   286  				s.Buffer = test.bufBefore
   287  			})
   288  			cmd := strings.Split(test.name, " ")[0]
   289  			evals(f.Evaler, "edit:"+cmd)
   290  			if buf := codeArea(app).CopyState().Buffer; buf != test.bufAfter {
   291  				t.Errorf("got buf %v, want %v", buf, test.bufAfter)
   292  			}
   293  		})
   294  	}
   295  }
   296  
   297  // Builtins that expect the focused widget to be code areas. This
   298  // includes some builtins defined in files other than builtins.go.
   299  var focusedWidgetNotCodeAreaTests = []string{
   300  	"edit:insert-raw",
   301  	"edit:smart-enter",
   302  	"edit:move-dot-right", // other buffer builtins not tested
   303  	"edit:completion:start",
   304  	"edit:history:start",
   305  }
   306  
   307  func TestBuiltins_FocusedWidgetNotCodeArea(t *testing.T) {
   308  	for _, code := range focusedWidgetNotCodeAreaTests {
   309  		t.Run(code, func(t *testing.T) {
   310  			f := setup(t)
   311  			f.Editor.app.PushAddon(tk.Label{})
   312  
   313  			evals(f.Evaler, code)
   314  			f.TestTTYNotes(t,
   315  				"error: "+modes.ErrFocusedWidgetNotCodeArea.Error(), Styles,
   316  				"!!!!!!")
   317  		})
   318  	}
   319  }
   320  
   321  // Tests for pure movers.
   322  
   323  func TestMoveDotLeftRight(t *testing.T) {
   324  	tt.Test(t, moveDotLeft,
   325  		Args("foo", 0).Rets(0),
   326  		Args("bar", 3).Rets(2),
   327  		Args("精灵", 0).Rets(0),
   328  		Args("精灵", 3).Rets(0),
   329  		Args("精灵", 6).Rets(3),
   330  	)
   331  
   332  	tt.Test(t, moveDotRight,
   333  		Args("foo", 0).Rets(1),
   334  		Args("bar", 3).Rets(3),
   335  		Args("精灵", 0).Rets(3),
   336  		Args("精灵", 3).Rets(6),
   337  		Args("精灵", 6).Rets(6),
   338  	)
   339  }
   340  
   341  func TestMoveDotSOLEOL(t *testing.T) {
   342  	buffer := "abc\ndef"
   343  	// Index:
   344  	//         012 34567
   345  	tt.Test(t, moveDotSOL,
   346  		Args(buffer, 0).Rets(0),
   347  		Args(buffer, 1).Rets(0),
   348  		Args(buffer, 2).Rets(0),
   349  		Args(buffer, 3).Rets(0),
   350  		Args(buffer, 4).Rets(4),
   351  		Args(buffer, 5).Rets(4),
   352  		Args(buffer, 6).Rets(4),
   353  		Args(buffer, 7).Rets(4),
   354  	)
   355  	tt.Test(t, moveDotEOL,
   356  		Args(buffer, 0).Rets(3),
   357  		Args(buffer, 1).Rets(3),
   358  		Args(buffer, 2).Rets(3),
   359  		Args(buffer, 3).Rets(3),
   360  		Args(buffer, 4).Rets(7),
   361  		Args(buffer, 5).Rets(7),
   362  		Args(buffer, 6).Rets(7),
   363  		Args(buffer, 7).Rets(7),
   364  	)
   365  }
   366  
   367  func TestMoveDotUpDown(t *testing.T) {
   368  	buffer := "abc\n精灵语\ndef"
   369  	// Index:
   370  	//         012 34 7 0  34567
   371  	// + 10 *  0        1
   372  
   373  	tt.Test(t, moveDotUp,
   374  		Args(buffer, 0).Rets(0),  // a -> a
   375  		Args(buffer, 1).Rets(1),  // b -> b
   376  		Args(buffer, 2).Rets(2),  // c -> c
   377  		Args(buffer, 3).Rets(3),  // EOL1 -> EOL1
   378  		Args(buffer, 4).Rets(0),  // 精 -> a
   379  		Args(buffer, 7).Rets(2),  // 灵 -> c
   380  		Args(buffer, 10).Rets(3), // 语 -> EOL1
   381  		Args(buffer, 13).Rets(3), // EOL2 -> EOL1
   382  		Args(buffer, 14).Rets(4), // d -> 精
   383  		Args(buffer, 15).Rets(4), // e -> 精 (jump left half width)
   384  		Args(buffer, 16).Rets(7), // f -> 灵
   385  		Args(buffer, 17).Rets(7), // EOL3 -> 灵 (jump left half width)
   386  	)
   387  
   388  	tt.Test(t, moveDotDown,
   389  		Args(buffer, 0).Rets(4),   // a -> 精
   390  		Args(buffer, 1).Rets(4),   // b -> 精 (jump left half width)
   391  		Args(buffer, 2).Rets(7),   // c -> 灵
   392  		Args(buffer, 3).Rets(7),   // EOL1 -> 灵 (jump left half width)
   393  		Args(buffer, 4).Rets(14),  // 精 -> d
   394  		Args(buffer, 7).Rets(16),  // 灵 -> f
   395  		Args(buffer, 10).Rets(17), // 语 -> EOL3
   396  		Args(buffer, 13).Rets(17), // EOL2 -> EOL3
   397  		Args(buffer, 14).Rets(14), // d -> d
   398  		Args(buffer, 15).Rets(15), // e -> e
   399  		Args(buffer, 16).Rets(16), // f -> f
   400  		Args(buffer, 17).Rets(17), // EOL3 -> EOL3
   401  	)
   402  }
   403  
   404  // Word movement tests.
   405  
   406  // The string below is carefully chosen to test all word, small-word, and
   407  // alnum-word move/kill functions, because it contains features to set the
   408  // different movement behaviors apart.
   409  //
   410  // The string is annotated with carets (^) to indicate the beginning of words,
   411  // and periods (.) to indicate trailing runes of words. Indices are also
   412  // annotated.
   413  //
   414  //	cd ~/downloads; rm -rf 2018aug07-pics/*;
   415  //	^. ^........... ^. ^.. ^................  (word)
   416  //	^. ^.^........^ ^. ^^. ^........^^...^..  (small-word)
   417  //	^.   ^........  ^.  ^. ^........ ^...     (alnum-word)
   418  //	01234567890123456789012345678901234567890
   419  //	0         1         2         3         4
   420  //
   421  //	word boundaries:         0 3      16 19    23
   422  //	small-word boundaries:   0 3 5 14 16 19 20 23 32 33 37
   423  //	alnum-word boundaries:   0   5    16    20 23    33
   424  var wordMoveTestBuffer = "cd ~/downloads; rm -rf 2018aug07-pics/*;"
   425  
   426  var (
   427  	// word boundaries: 0 3 16 19 23
   428  	moveDotLeftWordTests = []*tt.Case{
   429  		Args(wordMoveTestBuffer, 0).Rets(0),
   430  		Args(wordMoveTestBuffer, 1).Rets(0),
   431  		Args(wordMoveTestBuffer, 2).Rets(0),
   432  		Args(wordMoveTestBuffer, 3).Rets(0),
   433  		Args(wordMoveTestBuffer, 4).Rets(3),
   434  		Args(wordMoveTestBuffer, 16).Rets(3),
   435  		Args(wordMoveTestBuffer, 19).Rets(16),
   436  		Args(wordMoveTestBuffer, 23).Rets(19),
   437  		Args(wordMoveTestBuffer, 40).Rets(23),
   438  	}
   439  	moveDotRightWordTests = []*tt.Case{
   440  		Args(wordMoveTestBuffer, 0).Rets(3),
   441  		Args(wordMoveTestBuffer, 1).Rets(3),
   442  		Args(wordMoveTestBuffer, 2).Rets(3),
   443  		Args(wordMoveTestBuffer, 3).Rets(16),
   444  		Args(wordMoveTestBuffer, 16).Rets(19),
   445  		Args(wordMoveTestBuffer, 19).Rets(23),
   446  		Args(wordMoveTestBuffer, 23).Rets(40),
   447  	}
   448  
   449  	// small-word boundaries: 0 3 5 14 16 19 20 23 32 33 37
   450  	moveDotLeftSmallWordTests = []*tt.Case{
   451  		Args(wordMoveTestBuffer, 0).Rets(0),
   452  		Args(wordMoveTestBuffer, 1).Rets(0),
   453  		Args(wordMoveTestBuffer, 2).Rets(0),
   454  		Args(wordMoveTestBuffer, 3).Rets(0),
   455  		Args(wordMoveTestBuffer, 4).Rets(3),
   456  		Args(wordMoveTestBuffer, 5).Rets(3),
   457  		Args(wordMoveTestBuffer, 14).Rets(5),
   458  		Args(wordMoveTestBuffer, 16).Rets(14),
   459  		Args(wordMoveTestBuffer, 19).Rets(16),
   460  		Args(wordMoveTestBuffer, 20).Rets(19),
   461  		Args(wordMoveTestBuffer, 23).Rets(20),
   462  		Args(wordMoveTestBuffer, 32).Rets(23),
   463  		Args(wordMoveTestBuffer, 33).Rets(32),
   464  		Args(wordMoveTestBuffer, 37).Rets(33),
   465  		Args(wordMoveTestBuffer, 40).Rets(37),
   466  	}
   467  	moveDotRightSmallWordTests = []*tt.Case{
   468  		Args(wordMoveTestBuffer, 0).Rets(3),
   469  		Args(wordMoveTestBuffer, 1).Rets(3),
   470  		Args(wordMoveTestBuffer, 2).Rets(3),
   471  		Args(wordMoveTestBuffer, 3).Rets(5),
   472  		Args(wordMoveTestBuffer, 5).Rets(14),
   473  		Args(wordMoveTestBuffer, 14).Rets(16),
   474  		Args(wordMoveTestBuffer, 16).Rets(19),
   475  		Args(wordMoveTestBuffer, 19).Rets(20),
   476  		Args(wordMoveTestBuffer, 20).Rets(23),
   477  		Args(wordMoveTestBuffer, 23).Rets(32),
   478  		Args(wordMoveTestBuffer, 32).Rets(33),
   479  		Args(wordMoveTestBuffer, 33).Rets(37),
   480  		Args(wordMoveTestBuffer, 37).Rets(40),
   481  	}
   482  
   483  	// alnum-word boundaries: 0 5 16 20 23 33
   484  	moveDotLeftAlnumWordTests = []*tt.Case{
   485  		Args(wordMoveTestBuffer, 0).Rets(0),
   486  		Args(wordMoveTestBuffer, 1).Rets(0),
   487  		Args(wordMoveTestBuffer, 2).Rets(0),
   488  		Args(wordMoveTestBuffer, 3).Rets(0),
   489  		Args(wordMoveTestBuffer, 4).Rets(0),
   490  		Args(wordMoveTestBuffer, 5).Rets(0),
   491  		Args(wordMoveTestBuffer, 6).Rets(5),
   492  		Args(wordMoveTestBuffer, 16).Rets(5),
   493  		Args(wordMoveTestBuffer, 20).Rets(16),
   494  		Args(wordMoveTestBuffer, 23).Rets(20),
   495  		Args(wordMoveTestBuffer, 33).Rets(23),
   496  		Args(wordMoveTestBuffer, 40).Rets(33),
   497  	}
   498  	moveDotRightAlnumWordTests = []*tt.Case{
   499  		Args(wordMoveTestBuffer, 0).Rets(5),
   500  		Args(wordMoveTestBuffer, 1).Rets(5),
   501  		Args(wordMoveTestBuffer, 2).Rets(5),
   502  		Args(wordMoveTestBuffer, 3).Rets(5),
   503  		Args(wordMoveTestBuffer, 4).Rets(5),
   504  		Args(wordMoveTestBuffer, 5).Rets(16),
   505  		Args(wordMoveTestBuffer, 16).Rets(20),
   506  		Args(wordMoveTestBuffer, 20).Rets(23),
   507  		Args(wordMoveTestBuffer, 23).Rets(33),
   508  		Args(wordMoveTestBuffer, 33).Rets(40),
   509  	}
   510  )
   511  
   512  func TestMoveDotWord(t *testing.T) {
   513  	tt.Test(t, moveDotLeftWord, moveDotLeftWordTests...)
   514  	tt.Test(t, moveDotRightWord, moveDotRightWordTests...)
   515  }
   516  
   517  func TestMoveDotSmallWord(t *testing.T) {
   518  	tt.Test(t, moveDotLeftSmallWord, moveDotLeftSmallWordTests...)
   519  	tt.Test(t, moveDotRightSmallWord, moveDotRightSmallWordTests...)
   520  }
   521  
   522  func TestMoveDotAlnumWord(t *testing.T) {
   523  	tt.Test(t, moveDotLeftAlnumWord, moveDotLeftAlnumWordTests...)
   524  	tt.Test(t, moveDotRightAlnumWord, moveDotRightAlnumWordTests...)
   525  }