github.com/elves/elvish@v0.15.0/pkg/cli/codearea_test.go (about)

     1  package cli
     2  
     3  import (
     4  	"errors"
     5  	"reflect"
     6  	"testing"
     7  
     8  	"github.com/elves/elvish/pkg/cli/term"
     9  	"github.com/elves/elvish/pkg/tt"
    10  	"github.com/elves/elvish/pkg/ui"
    11  )
    12  
    13  var bb = term.NewBufferBuilder
    14  
    15  func p(t ui.Text) func() ui.Text { return func() ui.Text { return t } }
    16  
    17  var codeAreaRenderTests = []RenderTest{
    18  	{
    19  		Name: "prompt only",
    20  		Given: NewCodeArea(CodeAreaSpec{
    21  			Prompt: p(ui.T("~>", ui.Bold))}),
    22  		Width: 10, Height: 24,
    23  		Want: bb(10).WriteStringSGR("~>", "1").SetDotHere(),
    24  	},
    25  	{
    26  		Name: "rprompt only",
    27  		Given: NewCodeArea(CodeAreaSpec{
    28  			RPrompt: p(ui.T("RP", ui.Inverse))}),
    29  		Width: 10, Height: 24,
    30  		Want: bb(10).SetDotHere().WriteSpaces(8).WriteStringSGR("RP", "7"),
    31  	},
    32  	{
    33  		Name: "code only with dot at beginning",
    34  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
    35  			Buffer: CodeBuffer{Content: "code", Dot: 0}}}),
    36  		Width: 10, Height: 24,
    37  		Want: bb(10).SetDotHere().Write("code"),
    38  	},
    39  	{
    40  		Name: "code only with dot at middle",
    41  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
    42  			Buffer: CodeBuffer{Content: "code", Dot: 2}}}),
    43  		Width: 10, Height: 24,
    44  		Want: bb(10).Write("co").SetDotHere().Write("de"),
    45  	},
    46  	{
    47  		Name: "code only with dot at end",
    48  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
    49  			Buffer: CodeBuffer{Content: "code", Dot: 4}}}),
    50  		Width: 10, Height: 24,
    51  		Want: bb(10).Write("code").SetDotHere(),
    52  	},
    53  	{
    54  		Name: "prompt, code and rprompt",
    55  		Given: NewCodeArea(CodeAreaSpec{
    56  			Prompt:  p(ui.T("~>")),
    57  			RPrompt: p(ui.T("RP")),
    58  			State:   CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}),
    59  		Width: 10, Height: 24,
    60  		Want: bb(10).Write("~>code").SetDotHere().Write("  RP"),
    61  	},
    62  
    63  	{
    64  		Name: "prompt explicitly hidden ",
    65  		Given: NewCodeArea(CodeAreaSpec{
    66  			Prompt:  p(ui.T("~>")),
    67  			RPrompt: p(ui.T("RP")),
    68  			State:   CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}, HideRPrompt: true}}),
    69  		Width: 10, Height: 24,
    70  		Want: bb(10).Write("~>code").SetDotHere(),
    71  	},
    72  	{
    73  		Name: "rprompt too long",
    74  		Given: NewCodeArea(CodeAreaSpec{
    75  			Prompt:  p(ui.T("~>")),
    76  			RPrompt: p(ui.T("1234")),
    77  			State:   CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}),
    78  		Width: 10, Height: 24,
    79  		Want: bb(10).Write("~>code").SetDotHere(),
    80  	},
    81  	{
    82  		Name: "highlighted code",
    83  		Given: NewCodeArea(CodeAreaSpec{
    84  			Highlighter: func(code string) (ui.Text, []error) {
    85  				return ui.T(code, ui.Bold), nil
    86  			},
    87  			State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}),
    88  		Width: 10, Height: 24,
    89  		Want: bb(10).WriteStringSGR("code", "1").SetDotHere(),
    90  	},
    91  	{
    92  		Name: "static errors in code",
    93  		Given: NewCodeArea(CodeAreaSpec{
    94  			Prompt: p(ui.T("> ")),
    95  			Highlighter: func(code string) (ui.Text, []error) {
    96  				err := errors.New("static error")
    97  				return ui.T(code), []error{err}
    98  			},
    99  			State: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}}),
   100  		Width: 10, Height: 24,
   101  		Want: bb(10).Write("> code").SetDotHere().
   102  			Newline().Write("static error"),
   103  	},
   104  	{
   105  		Name: "pending code inserting at the dot",
   106  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   107  			Buffer:  CodeBuffer{Content: "code", Dot: 4},
   108  			Pending: PendingCode{From: 4, To: 4, Content: "x"},
   109  		}}),
   110  		Width: 10, Height: 24,
   111  		Want: bb(10).Write("code").WriteStringSGR("x", "4").SetDotHere(),
   112  	},
   113  	{
   114  		Name: "pending code replacing at the dot",
   115  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   116  			Buffer:  CodeBuffer{Content: "code", Dot: 2},
   117  			Pending: PendingCode{From: 2, To: 4, Content: "x"},
   118  		}}),
   119  		Width: 10, Height: 24,
   120  		Want: bb(10).Write("co").WriteStringSGR("x", "4").SetDotHere(),
   121  	},
   122  	{
   123  		Name: "pending code to the left of the dot",
   124  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   125  			Buffer:  CodeBuffer{Content: "code", Dot: 4},
   126  			Pending: PendingCode{From: 1, To: 3, Content: "x"},
   127  		}}),
   128  		Width: 10, Height: 24,
   129  		Want: bb(10).Write("c").WriteStringSGR("x", "4").Write("e").SetDotHere(),
   130  	},
   131  	{
   132  		Name: "pending code to the right of the cursor",
   133  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   134  			Buffer:  CodeBuffer{Content: "code", Dot: 1},
   135  			Pending: PendingCode{From: 2, To: 3, Content: "x"},
   136  		}}),
   137  		Width: 10, Height: 24,
   138  		Want: bb(10).Write("c").SetDotHere().Write("o").
   139  			WriteStringSGR("x", "4").Write("e"),
   140  	},
   141  	{
   142  		Name: "ignore invalid pending code 1",
   143  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   144  			Buffer:  CodeBuffer{Content: "code", Dot: 4},
   145  			Pending: PendingCode{From: 2, To: 1, Content: "x"},
   146  		}}),
   147  		Width: 10, Height: 24,
   148  		Want: bb(10).Write("code").SetDotHere(),
   149  	},
   150  	{
   151  		Name: "ignore invalid pending code 2",
   152  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   153  			Buffer:  CodeBuffer{Content: "code", Dot: 4},
   154  			Pending: PendingCode{From: 5, To: 6, Content: "x"},
   155  		}}),
   156  		Width: 10, Height: 24,
   157  		Want: bb(10).Write("code").SetDotHere(),
   158  	},
   159  	{
   160  		Name: "prioritize lines before the cursor with small height",
   161  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   162  			Buffer: CodeBuffer{Content: "a\nb\nc\nd", Dot: 3},
   163  		}}),
   164  		Width: 10, Height: 2,
   165  		Want: bb(10).Write("a").Newline().Write("b").SetDotHere(),
   166  	},
   167  	{
   168  		Name: "show only the cursor line when height is 1",
   169  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   170  			Buffer: CodeBuffer{Content: "a\nb\nc\nd", Dot: 3},
   171  		}}),
   172  		Width: 10, Height: 1,
   173  		Want: bb(10).Write("b").SetDotHere(),
   174  	},
   175  	{
   176  		Name: "show lines after the cursor when all lines before the cursor are shown",
   177  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   178  			Buffer: CodeBuffer{Content: "a\nb\nc\nd", Dot: 3},
   179  		}}),
   180  		Width: 10, Height: 3,
   181  		Want: bb(10).Write("a").Newline().Write("b").SetDotHere().
   182  			Newline().Write("c"),
   183  	},
   184  }
   185  
   186  func TestCodeArea_Render(t *testing.T) {
   187  	TestRender(t, codeAreaRenderTests)
   188  }
   189  
   190  var codeAreaHandleTests = []HandleTest{
   191  	{
   192  		Name:         "simple inserts",
   193  		Given:        NewCodeArea(CodeAreaSpec{}),
   194  		Events:       []term.Event{term.K('c'), term.K('o'), term.K('d'), term.K('e')},
   195  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}},
   196  	},
   197  	{
   198  		Name:         "unicode inserts",
   199  		Given:        NewCodeArea(CodeAreaSpec{}),
   200  		Events:       []term.Event{term.K('你'), term.K('好')},
   201  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "你好", Dot: 6}},
   202  	},
   203  	{
   204  		Name:         "unterminated paste",
   205  		Given:        NewCodeArea(CodeAreaSpec{}),
   206  		Events:       []term.Event{term.PasteSetting(true), term.K('"'), term.K('x')},
   207  		WantNewState: CodeAreaState{},
   208  	},
   209  	{
   210  		Name:  "literal paste",
   211  		Given: NewCodeArea(CodeAreaSpec{}),
   212  		Events: []term.Event{
   213  			term.PasteSetting(true),
   214  			term.K('"'), term.K('x'),
   215  			term.PasteSetting(false)},
   216  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "\"x", Dot: 2}},
   217  	},
   218  	{
   219  		Name:  "literal paste swallowing functional keys",
   220  		Given: NewCodeArea(CodeAreaSpec{}),
   221  		Events: []term.Event{
   222  			term.PasteSetting(true),
   223  			term.K('a'), term.K(ui.F1), term.K('b'),
   224  			term.PasteSetting(false)},
   225  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "ab", Dot: 2}},
   226  	},
   227  	{
   228  		Name:  "quoted paste",
   229  		Given: NewCodeArea(CodeAreaSpec{QuotePaste: func() bool { return true }}),
   230  		Events: []term.Event{
   231  			term.PasteSetting(true),
   232  			term.K('"'), term.K('x'),
   233  			term.PasteSetting(false)},
   234  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "'\"x'", Dot: 4}},
   235  	},
   236  	{
   237  		Name:  "backspace at end of code",
   238  		Given: NewCodeArea(CodeAreaSpec{}),
   239  		Events: []term.Event{
   240  			term.K('c'), term.K('o'), term.K('d'), term.K('e'),
   241  			term.K(ui.Backspace)},
   242  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "cod", Dot: 3}},
   243  	},
   244  	{
   245  		Name: "backspace at middle of buffer",
   246  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   247  			Buffer: CodeBuffer{Content: "code", Dot: 2}}}),
   248  		Events:       []term.Event{term.K(ui.Backspace)},
   249  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "cde", Dot: 1}},
   250  	},
   251  	{
   252  		Name: "backspace at beginning of buffer",
   253  		Given: NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   254  			Buffer: CodeBuffer{Content: "code", Dot: 0}}}),
   255  		Events:       []term.Event{term.K(ui.Backspace)},
   256  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 0}},
   257  	},
   258  	{
   259  		Name:  "backspace deleting unicode character",
   260  		Given: NewCodeArea(CodeAreaSpec{}),
   261  		Events: []term.Event{
   262  			term.K('你'), term.K('好'), term.K(ui.Backspace)},
   263  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "你", Dot: 3}},
   264  	},
   265  	// Regression test for https://b.elv.sh/1178
   266  	{
   267  		Name:  "Ctrl-H being equivalent to backspace",
   268  		Given: NewCodeArea(CodeAreaSpec{}),
   269  		Events: []term.Event{
   270  			term.K('c'), term.K('o'), term.K('d'), term.K('e'),
   271  			term.K('H', ui.Ctrl)},
   272  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "cod", Dot: 3}},
   273  	},
   274  	{
   275  		Name: "abbreviation expansion",
   276  		Given: NewCodeArea(CodeAreaSpec{
   277  			Abbreviations: func(f func(abbr, full string)) {
   278  				f("dn", "/dev/null")
   279  			},
   280  		}),
   281  		Events:       []term.Event{term.K('d'), term.K('n')},
   282  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "/dev/null", Dot: 9}},
   283  	},
   284  	{
   285  		Name: "abbreviation expansion 2",
   286  		Given: NewCodeArea(CodeAreaSpec{
   287  			Abbreviations: func(f func(abbr, full string)) {
   288  				f("||", " | less")
   289  			},
   290  		}),
   291  		Events:       []term.Event{term.K('x'), term.K('|'), term.K('|')},
   292  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "x | less", Dot: 8}},
   293  	},
   294  	{
   295  		Name: "abbreviation expansion after other content",
   296  		Given: NewCodeArea(CodeAreaSpec{
   297  			Abbreviations: func(f func(abbr, full string)) {
   298  				f("||", " | less")
   299  			},
   300  		}),
   301  		Events:       []term.Event{term.K('{'), term.K('e'), term.K('c'), term.K('h'), term.K('o'), term.K(' '), term.K('x'), term.K('}'), term.K('|'), term.K('|')},
   302  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "{echo x} | less", Dot: 15}},
   303  	},
   304  	{
   305  		Name: "abbreviation expansion preferring longest",
   306  		Given: NewCodeArea(CodeAreaSpec{
   307  			Abbreviations: func(f func(abbr, full string)) {
   308  				f("n", "none")
   309  				f("dn", "/dev/null")
   310  			},
   311  		}),
   312  		Events:       []term.Event{term.K('d'), term.K('n')},
   313  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "/dev/null", Dot: 9}},
   314  	},
   315  	{
   316  		Name: "abbreviation expansion interrupted by function key",
   317  		Given: NewCodeArea(CodeAreaSpec{
   318  			Abbreviations: func(f func(abbr, full string)) {
   319  				f("dn", "/dev/null")
   320  			},
   321  		}),
   322  		Events:       []term.Event{term.K('d'), term.K(ui.F1), term.K('n')},
   323  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "dn", Dot: 2}},
   324  	},
   325  	{
   326  		Name: "small word abbreviation expansion space trigger",
   327  		Given: NewCodeArea(CodeAreaSpec{
   328  			SmallWordAbbreviations: func(f func(abbr, full string)) {
   329  				f("eh", "echo hello")
   330  			},
   331  		}),
   332  		Events:       []term.Event{term.K('e'), term.K('h'), term.K(' ')},
   333  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "echo hello ", Dot: 11}},
   334  	},
   335  	{
   336  		Name: "small word abbreviation expansion non-space trigger",
   337  		Given: NewCodeArea(CodeAreaSpec{
   338  			SmallWordAbbreviations: func(f func(abbr, full string)) {
   339  				f("h", "hello")
   340  			},
   341  		}),
   342  		Events:       []term.Event{term.K('x'), term.K('['), term.K('h'), term.K(']')},
   343  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "x[hello]", Dot: 8}},
   344  	},
   345  	{
   346  		Name: "small word abbreviation expansion preceding char invalid",
   347  		Given: NewCodeArea(CodeAreaSpec{
   348  			SmallWordAbbreviations: func(f func(abbr, full string)) {
   349  				f("h", "hello")
   350  			},
   351  		}),
   352  		Events:       []term.Event{term.K('g'), term.K('h'), term.K(' ')},
   353  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "gh ", Dot: 3}},
   354  	},
   355  	{
   356  		Name: "small word abbreviation expansion after backspace preceding char invalid",
   357  		Given: NewCodeArea(CodeAreaSpec{
   358  			SmallWordAbbreviations: func(f func(abbr, full string)) {
   359  				f("h", "hello")
   360  			},
   361  		}),
   362  		Events: []term.Event{term.K('g'), term.K(' '), term.K(ui.Backspace),
   363  			term.K('h'), term.K(' ')},
   364  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "gh ", Dot: 3}},
   365  	},
   366  	{
   367  		Name: "overlay handler",
   368  		Given: codeAreaWithOverlay(CodeAreaSpec{}, func(w *codeArea) Handler {
   369  			return MapHandler{
   370  				term.K('a'): func() { w.State.Buffer.InsertAtDot("b") },
   371  			}
   372  		}),
   373  		Events:       []term.Event{term.K('a')},
   374  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "b", Dot: 1}},
   375  	},
   376  	{
   377  		// Regression test for #890.
   378  		Name: "overlay handler does not apply when pasting",
   379  		Given: codeAreaWithOverlay(CodeAreaSpec{}, func(w *codeArea) Handler {
   380  			return MapHandler{term.K('\n'): func() {}}
   381  		}),
   382  		Events: []term.Event{
   383  			term.PasteSetting(true), term.K('\n'), term.PasteSetting(false)},
   384  		WantNewState: CodeAreaState{Buffer: CodeBuffer{Content: "\n", Dot: 1}},
   385  	},
   386  }
   387  
   388  func TestCodeArea_Handle(t *testing.T) {
   389  	TestHandle(t, codeAreaHandleTests)
   390  }
   391  
   392  // A utility for building a CodeArea with an OverlayHandler as a single
   393  // expression.
   394  func codeAreaWithOverlay(spec CodeAreaSpec, f func(*codeArea) Handler) CodeArea {
   395  	w := NewCodeArea(spec)
   396  	ww := w.(*codeArea)
   397  	ww.OverlayHandler = f(ww)
   398  	return w
   399  }
   400  
   401  var codeAreaUnhandledEvents = []term.Event{
   402  	// Mouse events are unhandled
   403  	term.MouseEvent{},
   404  	// Function keys are unhandled (except Backspace)
   405  	term.K(ui.F1),
   406  	term.K('X', ui.Ctrl),
   407  }
   408  
   409  func TestCodeArea_Handle_UnhandledEvents(t *testing.T) {
   410  	w := NewCodeArea(CodeAreaSpec{})
   411  	for _, event := range codeAreaUnhandledEvents {
   412  		handled := w.Handle(event)
   413  		if handled {
   414  			t.Errorf("event %v got handled", event)
   415  		}
   416  	}
   417  }
   418  
   419  func TestCodeArea_Handle_AbbreviationExpansionInterruptedByExternalMutation(t *testing.T) {
   420  	w := NewCodeArea(CodeAreaSpec{
   421  		Abbreviations: func(f func(abbr, full string)) {
   422  			f("dn", "/dev/null")
   423  		},
   424  	})
   425  	w.Handle(term.K('d'))
   426  	w.MutateState(func(s *CodeAreaState) { s.Buffer.InsertAtDot("d") })
   427  	w.Handle(term.K('n'))
   428  	wantState := CodeAreaState{Buffer: CodeBuffer{Content: "ddn", Dot: 3}}
   429  	if state := w.CopyState(); !reflect.DeepEqual(state, wantState) {
   430  		t.Errorf("got state %v, want %v", state, wantState)
   431  	}
   432  }
   433  
   434  func TestCodeArea_Handle_EnterEmitsSubmit(t *testing.T) {
   435  	submitted := false
   436  	w := NewCodeArea(CodeAreaSpec{
   437  		OnSubmit: func() { submitted = true },
   438  		State:    CodeAreaState{Buffer: CodeBuffer{Content: "code", Dot: 4}}})
   439  	w.Handle(term.K('\n'))
   440  	if submitted != true {
   441  		t.Errorf("OnSubmit not triggered")
   442  	}
   443  }
   444  
   445  func TestCodeArea_Handle_DefaultNoopSubmit(t *testing.T) {
   446  	w := NewCodeArea(CodeAreaSpec{State: CodeAreaState{
   447  		Buffer: CodeBuffer{Content: "code", Dot: 4}}})
   448  	w.Handle(term.K('\n'))
   449  	// No panic, we are good
   450  }
   451  
   452  func TestCodeArea_State(t *testing.T) {
   453  	w := NewCodeArea(CodeAreaSpec{})
   454  	w.MutateState(func(s *CodeAreaState) { s.Buffer.Content = "code" })
   455  	if w.CopyState().Buffer.Content != "code" {
   456  		t.Errorf("state not mutated")
   457  	}
   458  }
   459  
   460  func TestCodeAreaState_ApplyPending(t *testing.T) {
   461  	applyPending := func(s CodeAreaState) CodeAreaState {
   462  		s.ApplyPending()
   463  		return s
   464  	}
   465  	tt.Test(t, tt.Fn("applyPending", applyPending), tt.Table{
   466  		tt.Args(CodeAreaState{Buffer: CodeBuffer{}, Pending: PendingCode{0, 0, "ls"}}).
   467  			Rets(CodeAreaState{Buffer: CodeBuffer{Content: "ls", Dot: 2}, Pending: PendingCode{}}),
   468  		tt.Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}, Pending: PendingCode{0, 0, "ls"}}).
   469  			Rets(CodeAreaState{Buffer: CodeBuffer{Content: "lsx", Dot: 3}, Pending: PendingCode{}}),
   470  		// No-op when Pending is empty.
   471  		tt.Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}}).
   472  			Rets(CodeAreaState{Buffer: CodeBuffer{Content: "x", Dot: 1}}),
   473  		// HideRPrompt is kept intact.
   474  		tt.Args(CodeAreaState{Buffer: CodeBuffer{"x", 1}, HideRPrompt: true}).
   475  			Rets(CodeAreaState{Buffer: CodeBuffer{Content: "x", Dot: 1}, HideRPrompt: true}),
   476  	})
   477  }