elvish/pkg/edit/builtins_test.go

550 lines
15 KiB
Go

package edit
import (
"io"
"strings"
"testing"
"src.elv.sh/pkg/cli/modes"
"src.elv.sh/pkg/cli/term"
"src.elv.sh/pkg/cli/tk"
"src.elv.sh/pkg/eval"
. "src.elv.sh/pkg/eval/evaltest"
"src.elv.sh/pkg/tt"
"src.elv.sh/pkg/ui"
)
func TestBindingTable(t *testing.T) {
f := setup(t)
evals(f.Evaler, `var called = $false`)
evals(f.Evaler, `var m = (edit:binding-table [&a={ set called = $true }])`)
_, ok := getGlobal(f.Evaler, "m").(bindingsMap)
if !ok {
t.Errorf("edit:binding-table did not create BindingMap variable")
}
}
func TestCloseMode(t *testing.T) {
f := setup(t)
f.Editor.app.PushAddon(tk.Empty{})
evals(f.Evaler, `edit:close-mode`)
if addons := f.Editor.app.CopyState().Addons; len(addons) > 0 {
t.Errorf("got addons %v, want nil or empty slice", addons)
}
}
func TestInsertRaw(t *testing.T) {
f := setup(t)
f.TTYCtrl.Inject(term.K('V', ui.Ctrl))
wantBuf := f.MakeBuffer(
"~> ", term.DotHere, "\n",
" RAW ", Styles,
"*****",
)
f.TTYCtrl.TestBuffer(t, wantBuf)
// Since we do not use real terminals in the test, we cannot have a
// realistic test case against actual raw inputs. However, we can still
// check that the builtin command does call the SetRawInput method with 1.
if raw := f.TTYCtrl.RawInput(); raw != 1 {
t.Errorf("RawInput() -> %d, want 1", raw)
}
// Raw mode does not respond to non-key events.
f.TTYCtrl.Inject(term.MouseEvent{})
f.TTYCtrl.TestBuffer(t, wantBuf)
// Raw mode is dismissed after a single key event.
f.TTYCtrl.Inject(term.K('+'))
f.TestTTY(t,
"~> +", Styles,
" v", term.DotHere,
)
}
func TestEndOfHistory(t *testing.T) {
f := setup(t)
evals(f.Evaler, `edit:end-of-history`)
f.TestTTYNotes(t, "End of history")
}
func TestKey(t *testing.T) {
f := setup(t)
evals(f.Evaler, `var k = (edit:key a)`)
wantK := ui.K('a')
if k, _ := f.Evaler.Global().Index("k"); k != wantK {
t.Errorf("$k is %v, want %v", k, wantK)
}
}
func TestRedraw(t *testing.T) {
f := setup(t)
evals(f.Evaler,
`set edit:current-command = echo`,
`edit:redraw`)
f.TestTTY(t,
"~> echo", Styles,
" vvvv", term.DotHere)
evals(f.Evaler, `edit:redraw &full=$true`)
// TODO(xiaq): Test that this is actually a full redraw.
f.TestTTY(t,
"~> echo", Styles,
" vvvv", term.DotHere)
}
func TestClear(t *testing.T) {
f := setup(t)
evals(f.Evaler, `set edit:current-command = echo`, `edit:clear`)
f.TestTTY(t,
"~> echo", Styles,
" vvvv", term.DotHere)
if cleared := f.TTYCtrl.ScreenCleared(); cleared != 1 {
t.Errorf("screen cleared %v times, want 1", cleared)
}
}
func TestNotify(t *testing.T) {
f := setup(t)
evals(f.Evaler, "edit:notify string")
f.TestTTYNotes(t, "string")
evals(f.Evaler, "edit:notify (styled styled red)")
f.TestTTYNotes(t,
"styled", Styles,
"!!!!!!")
evals(f.Evaler, "var err = ?(edit:notify [])")
if _, hasErr := getGlobal(f.Evaler, "err").(error); !hasErr {
t.Errorf("calling edit:notify with [] did not result in error")
// TODO: Test the exact error
}
}
func TestReturnCode(t *testing.T) {
f := setup(t)
codeArea(f.Editor.app).MutateState(func(s *tk.CodeAreaState) {
s.Buffer.Content = "test code"
})
evals(f.Evaler, `edit:return-line`)
code, err := f.Wait()
if code != "test code" {
t.Errorf("got code %q, want %q", code, "test code")
}
if err != nil {
t.Errorf("got err %v, want nil", err)
}
}
func TestReturnEOF(t *testing.T) {
f := setup(t)
evals(f.Evaler, `edit:return-eof`)
if _, err := f.Wait(); err != io.EOF {
t.Errorf("got err %v, want %v", err, io.EOF)
}
}
func TestSmartEnter_InsertsNewlineWhenIncomplete(t *testing.T) {
f := setup(t)
f.SetCodeBuffer(tk.CodeBuffer{Content: "put [", Dot: 5})
evals(f.Evaler, `edit:smart-enter`)
wantBuf := tk.CodeBuffer{Content: "put [\n", Dot: 6}
if buf := codeArea(f.Editor.app).CopyState().Buffer; buf != wantBuf {
t.Errorf("got code buffer %v, want %v", buf, wantBuf)
}
}
func TestSmartEnter_AcceptsCodeWhenWholeBufferIsComplete(t *testing.T) {
f := setup(t)
f.SetCodeBuffer(tk.CodeBuffer{Content: "put []", Dot: 5})
evals(f.Evaler, `edit:smart-enter`)
wantCode := "put []"
if code, _ := f.Wait(); code != wantCode {
t.Errorf("got return code %q, want %q", code, wantCode)
}
}
// TODO: Test that smart-enter applies autofix.
func TestWordify(t *testing.T) {
TestWithSetup(t, setupWordify,
That("wordify 'ls str [list]'").Puts("ls", "str", "[list]"),
That("wordify foo >&-").Throws(eval.ErrPortDoesNotSupportValueOutput),
)
}
func setupWordify(ev *eval.Evaler) {
ev.ExtendBuiltin(eval.BuildNs().AddGoFn("wordify", wordify))
}
var bufferBuiltinsTests = []struct {
name string
bufBefore tk.CodeBuffer
bufAfter tk.CodeBuffer
}{
{
"move-dot-left",
tk.CodeBuffer{Content: "ab", Dot: 1},
tk.CodeBuffer{Content: "ab", Dot: 0},
},
{
"move-dot-right",
tk.CodeBuffer{Content: "ab", Dot: 1},
tk.CodeBuffer{Content: "ab", Dot: 2},
},
{
"kill-rune-left",
tk.CodeBuffer{Content: "ab", Dot: 1},
tk.CodeBuffer{Content: "b", Dot: 0},
},
{
"kill-rune-right",
tk.CodeBuffer{Content: "ab", Dot: 1},
tk.CodeBuffer{Content: "a", Dot: 1},
},
{
"transpose-rune with empty buffer",
tk.CodeBuffer{Content: "", Dot: 0},
tk.CodeBuffer{Content: "", Dot: 0},
},
{
"transpose-rune with dot at beginning",
tk.CodeBuffer{Content: "abc", Dot: 0},
tk.CodeBuffer{Content: "bac", Dot: 2},
},
{
"transpose-rune with dot in middle",
tk.CodeBuffer{Content: "abc", Dot: 1},
tk.CodeBuffer{Content: "bac", Dot: 2},
},
{
"transpose-rune with dot at end",
tk.CodeBuffer{Content: "abc", Dot: 3},
tk.CodeBuffer{Content: "acb", Dot: 3},
},
{
"transpose-rune with one character and dot at end",
tk.CodeBuffer{Content: "a", Dot: 1},
tk.CodeBuffer{Content: "a", Dot: 1},
},
{
"transpose-rune with one character and dot at beginning",
tk.CodeBuffer{Content: "a", Dot: 0},
tk.CodeBuffer{Content: "a", Dot: 0},
},
{
"transpose-word with dot at beginning",
tk.CodeBuffer{Content: "ab bc cd", Dot: 0},
tk.CodeBuffer{Content: "bc ab cd", Dot: 6},
},
{
"transpose-word with dot in between words",
tk.CodeBuffer{Content: "ab bc cd", Dot: 6},
tk.CodeBuffer{Content: "ab cd bc", Dot: 9},
},
{
"transpose-word with dot at end",
tk.CodeBuffer{Content: "ab bc cd", Dot: 9},
tk.CodeBuffer{Content: "ab cd bc", Dot: 9},
},
{
"transpose-word with dot in the middle of a word",
tk.CodeBuffer{Content: "ab bc cd", Dot: 5},
tk.CodeBuffer{Content: "bc ab cd", Dot: 6},
},
{
"transpose-word with one word",
tk.CodeBuffer{Content: " ab ", Dot: 4},
tk.CodeBuffer{Content: " ab ", Dot: 4},
},
{
"transpose-word with no words",
tk.CodeBuffer{Content: " \t\n ", Dot: 4},
tk.CodeBuffer{Content: " \t\n ", Dot: 4},
},
{
"transpose-word with complex input",
tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4},
tk.CodeBuffer{Content: "~/downloads; cd", Dot: 15},
},
{
"transpose-small-word",
tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4},
tk.CodeBuffer{Content: "~/ cddownloads;", Dot: 5},
},
{
"transpose-alnum-word",
tk.CodeBuffer{Content: "cd ~/downloads;", Dot: 4},
tk.CodeBuffer{Content: "downloads ~/cd;", Dot: 14},
},
}
func TestBufferBuiltins(t *testing.T) {
f := setup(t)
app := f.Editor.app
for _, test := range bufferBuiltinsTests {
t.Run(test.name, func(t *testing.T) {
codeArea(app).MutateState(func(s *tk.CodeAreaState) {
s.Buffer = test.bufBefore
})
cmd := strings.Split(test.name, " ")[0]
evals(f.Evaler, "edit:"+cmd)
if buf := codeArea(app).CopyState().Buffer; buf != test.bufAfter {
t.Errorf("got buf %v, want %v", buf, test.bufAfter)
}
})
}
}
// Builtins that expect the focused widget to be code areas. This
// includes some builtins defined in files other than builtins.go.
var focusedWidgetNotCodeAreaTests = []string{
"edit:insert-raw",
"edit:smart-enter",
"edit:move-dot-right", // other buffer builtins not tested
"edit:completion:start",
"edit:history:start",
}
func TestBuiltins_FocusedWidgetNotCodeArea(t *testing.T) {
for _, code := range focusedWidgetNotCodeAreaTests {
t.Run(code, func(t *testing.T) {
f := setup(t)
f.Editor.app.PushAddon(tk.Label{})
evals(f.Evaler, code)
f.TestTTYNotes(t,
"error: "+modes.ErrFocusedWidgetNotCodeArea.Error(), Styles,
"!!!!!!")
})
}
}
// Tests for pure movers.
func TestMoveDotLeftRight(t *testing.T) {
tt.Test(t, tt.Fn("moveDotLeft", moveDotLeft), tt.Table{
Args("foo", 0).Rets(0),
Args("bar", 3).Rets(2),
Args("精灵", 0).Rets(0),
Args("精灵", 3).Rets(0),
Args("精灵", 6).Rets(3),
})
tt.Test(t, tt.Fn("moveDotRight", moveDotRight), tt.Table{
Args("foo", 0).Rets(1),
Args("bar", 3).Rets(3),
Args("精灵", 0).Rets(3),
Args("精灵", 3).Rets(6),
Args("精灵", 6).Rets(6),
})
}
func TestMoveDotSOLEOL(t *testing.T) {
buffer := "abc\ndef"
// Index:
// 012 34567
tt.Test(t, tt.Fn("moveDotSOL", moveDotSOL), tt.Table{
Args(buffer, 0).Rets(0),
Args(buffer, 1).Rets(0),
Args(buffer, 2).Rets(0),
Args(buffer, 3).Rets(0),
Args(buffer, 4).Rets(4),
Args(buffer, 5).Rets(4),
Args(buffer, 6).Rets(4),
Args(buffer, 7).Rets(4),
})
tt.Test(t, tt.Fn("moveDotEOL", moveDotEOL), tt.Table{
Args(buffer, 0).Rets(3),
Args(buffer, 1).Rets(3),
Args(buffer, 2).Rets(3),
Args(buffer, 3).Rets(3),
Args(buffer, 4).Rets(7),
Args(buffer, 5).Rets(7),
Args(buffer, 6).Rets(7),
Args(buffer, 7).Rets(7),
})
}
func TestMoveDotUpDown(t *testing.T) {
buffer := "abc\n精灵语\ndef"
// Index:
// 012 34 7 0 34567
// + 10 * 0 1
tt.Test(t, tt.Fn("moveDotUp", moveDotUp), tt.Table{
Args(buffer, 0).Rets(0), // a -> a
Args(buffer, 1).Rets(1), // b -> b
Args(buffer, 2).Rets(2), // c -> c
Args(buffer, 3).Rets(3), // EOL1 -> EOL1
Args(buffer, 4).Rets(0), // 精 -> a
Args(buffer, 7).Rets(2), // 灵 -> c
Args(buffer, 10).Rets(3), // 语 -> EOL1
Args(buffer, 13).Rets(3), // EOL2 -> EOL1
Args(buffer, 14).Rets(4), // d -> 精
Args(buffer, 15).Rets(4), // e -> 精 (jump left half width)
Args(buffer, 16).Rets(7), // f -> 灵
Args(buffer, 17).Rets(7), // EOL3 -> 灵 (jump left half width)
})
tt.Test(t, tt.Fn("moveDotDown", moveDotDown), tt.Table{
Args(buffer, 0).Rets(4), // a -> 精
Args(buffer, 1).Rets(4), // b -> 精 (jump left half width)
Args(buffer, 2).Rets(7), // c -> 灵
Args(buffer, 3).Rets(7), // EOL1 -> 灵 (jump left half width)
Args(buffer, 4).Rets(14), // 精 -> d
Args(buffer, 7).Rets(16), // 灵 -> f
Args(buffer, 10).Rets(17), // 语 -> EOL3
Args(buffer, 13).Rets(17), // EOL2 -> EOL3
Args(buffer, 14).Rets(14), // d -> d
Args(buffer, 15).Rets(15), // e -> e
Args(buffer, 16).Rets(16), // f -> f
Args(buffer, 17).Rets(17), // EOL3 -> EOL3
})
}
// Word movement tests.
// The string below is carefully chosen to test all word, small-word, and
// alnum-word move/kill functions, because it contains features to set the
// different movement behaviors apart.
//
// The string is annotated with carets (^) to indicate the beginning of words,
// and periods (.) to indicate trailing runes of words. Indices are also
// annotated.
//
// cd ~/downloads; rm -rf 2018aug07-pics/*;
// ^. ^........... ^. ^.. ^................ (word)
// ^. ^.^........^ ^. ^^. ^........^^...^.. (small-word)
// ^. ^........ ^. ^. ^........ ^... (alnum-word)
// 01234567890123456789012345678901234567890
// 0 1 2 3 4
//
// word boundaries: 0 3 16 19 23
// small-word boundaries: 0 3 5 14 16 19 20 23 32 33 37
// alnum-word boundaries: 0 5 16 20 23 33
var wordMoveTestBuffer = "cd ~/downloads; rm -rf 2018aug07-pics/*;"
var (
// word boundaries: 0 3 16 19 23
moveDotLeftWordTests = tt.Table{
Args(wordMoveTestBuffer, 0).Rets(0),
Args(wordMoveTestBuffer, 1).Rets(0),
Args(wordMoveTestBuffer, 2).Rets(0),
Args(wordMoveTestBuffer, 3).Rets(0),
Args(wordMoveTestBuffer, 4).Rets(3),
Args(wordMoveTestBuffer, 16).Rets(3),
Args(wordMoveTestBuffer, 19).Rets(16),
Args(wordMoveTestBuffer, 23).Rets(19),
Args(wordMoveTestBuffer, 40).Rets(23),
}
moveDotRightWordTests = tt.Table{
Args(wordMoveTestBuffer, 0).Rets(3),
Args(wordMoveTestBuffer, 1).Rets(3),
Args(wordMoveTestBuffer, 2).Rets(3),
Args(wordMoveTestBuffer, 3).Rets(16),
Args(wordMoveTestBuffer, 16).Rets(19),
Args(wordMoveTestBuffer, 19).Rets(23),
Args(wordMoveTestBuffer, 23).Rets(40),
}
// small-word boundaries: 0 3 5 14 16 19 20 23 32 33 37
moveDotLeftSmallWordTests = tt.Table{
Args(wordMoveTestBuffer, 0).Rets(0),
Args(wordMoveTestBuffer, 1).Rets(0),
Args(wordMoveTestBuffer, 2).Rets(0),
Args(wordMoveTestBuffer, 3).Rets(0),
Args(wordMoveTestBuffer, 4).Rets(3),
Args(wordMoveTestBuffer, 5).Rets(3),
Args(wordMoveTestBuffer, 14).Rets(5),
Args(wordMoveTestBuffer, 16).Rets(14),
Args(wordMoveTestBuffer, 19).Rets(16),
Args(wordMoveTestBuffer, 20).Rets(19),
Args(wordMoveTestBuffer, 23).Rets(20),
Args(wordMoveTestBuffer, 32).Rets(23),
Args(wordMoveTestBuffer, 33).Rets(32),
Args(wordMoveTestBuffer, 37).Rets(33),
Args(wordMoveTestBuffer, 40).Rets(37),
}
moveDotRightSmallWordTests = tt.Table{
Args(wordMoveTestBuffer, 0).Rets(3),
Args(wordMoveTestBuffer, 1).Rets(3),
Args(wordMoveTestBuffer, 2).Rets(3),
Args(wordMoveTestBuffer, 3).Rets(5),
Args(wordMoveTestBuffer, 5).Rets(14),
Args(wordMoveTestBuffer, 14).Rets(16),
Args(wordMoveTestBuffer, 16).Rets(19),
Args(wordMoveTestBuffer, 19).Rets(20),
Args(wordMoveTestBuffer, 20).Rets(23),
Args(wordMoveTestBuffer, 23).Rets(32),
Args(wordMoveTestBuffer, 32).Rets(33),
Args(wordMoveTestBuffer, 33).Rets(37),
Args(wordMoveTestBuffer, 37).Rets(40),
}
// alnum-word boundaries: 0 5 16 20 23 33
moveDotLeftAlnumWordTests = tt.Table{
Args(wordMoveTestBuffer, 0).Rets(0),
Args(wordMoveTestBuffer, 1).Rets(0),
Args(wordMoveTestBuffer, 2).Rets(0),
Args(wordMoveTestBuffer, 3).Rets(0),
Args(wordMoveTestBuffer, 4).Rets(0),
Args(wordMoveTestBuffer, 5).Rets(0),
Args(wordMoveTestBuffer, 6).Rets(5),
Args(wordMoveTestBuffer, 16).Rets(5),
Args(wordMoveTestBuffer, 20).Rets(16),
Args(wordMoveTestBuffer, 23).Rets(20),
Args(wordMoveTestBuffer, 33).Rets(23),
Args(wordMoveTestBuffer, 40).Rets(33),
}
moveDotRightAlnumWordTests = tt.Table{
Args(wordMoveTestBuffer, 0).Rets(5),
Args(wordMoveTestBuffer, 1).Rets(5),
Args(wordMoveTestBuffer, 2).Rets(5),
Args(wordMoveTestBuffer, 3).Rets(5),
Args(wordMoveTestBuffer, 4).Rets(5),
Args(wordMoveTestBuffer, 5).Rets(16),
Args(wordMoveTestBuffer, 16).Rets(20),
Args(wordMoveTestBuffer, 20).Rets(23),
Args(wordMoveTestBuffer, 23).Rets(33),
Args(wordMoveTestBuffer, 33).Rets(40),
}
)
func TestMoveDotWord(t *testing.T) {
tt.Test(t, tt.Fn("moveDotLeftWord", moveDotLeftWord), moveDotLeftWordTests)
tt.Test(t, tt.Fn("moveDotRightWord", moveDotRightWord), moveDotRightWordTests)
}
func TestMoveDotSmallWord(t *testing.T) {
tt.Test(t,
tt.Fn("moveDotLeftSmallWord", moveDotLeftSmallWord),
moveDotLeftSmallWordTests,
)
tt.Test(t,
tt.Fn("moveDotRightSmallWord", moveDotRightSmallWord),
moveDotRightSmallWordTests,
)
}
func TestMoveDotAlnumWord(t *testing.T) {
tt.Test(t,
tt.Fn("moveDotLeftAlnumWord", moveDotLeftAlnumWord),
moveDotLeftAlnumWordTests,
)
tt.Test(t,
tt.Fn("moveDotRightAlnumWord", moveDotRightAlnumWord),
moveDotRightAlnumWordTests,
)
}