current commit
260602.1 · Allow bare put action in transform output
package fzf

import (
	"fmt"
	"os"
	"testing"

	"github.com/junegunn/fzf/src/tui"
)

func TestDelimiterRegex(t *testing.T) {
	// Valid regex, but a single character -> string
	delim := delimiterRegexp(".")
	if delim.regex != nil || *delim.str != "." {
		t.Error(delim)
	}
	delim = delimiterRegexp("|")
	if delim.regex != nil || *delim.str != "|" {
		t.Error(delim)
	}
	// Broken regex -> string
	delim = delimiterRegexp("[0-9")
	if delim.regex != nil || *delim.str != "[0-9" {
		t.Error(delim)
	}
	// Valid regex
	delim = delimiterRegexp("[0-9]")
	if delim.regex.String() != "[0-9]" || delim.str != nil {
		t.Error(delim)
	}
	// Tab character
	delim = delimiterRegexp("\t")
	if delim.regex != nil || *delim.str != "\t" {
		t.Error(delim)
	}
	// Tab expression
	delim = delimiterRegexp("\\t")
	if delim.regex != nil || *delim.str != "\t" {
		t.Error(delim)
	}
	// Tabs -> regex
	delim = delimiterRegexp("\t+")
	if delim.regex == nil || delim.str != nil {
		t.Error(delim)
	}
}

func TestDelimiterRegexString(t *testing.T) {
	delim := delimiterRegexp("*")
	tokens := Tokenize("-*--*---**---", delim)
	if delim.regex != nil ||
		tokens[0].text.ToString() != "-*" ||
		tokens[1].text.ToString() != "--*" ||
		tokens[2].text.ToString() != "---*" ||
		tokens[3].text.ToString() != "*" ||
		tokens[4].text.ToString() != "---" {
		t.Errorf("%s %v %d", delim, tokens, len(tokens))
	}
}

func TestDelimiterRegexRegex(t *testing.T) {
	delim := delimiterRegexp("--\\*")
	tokens := Tokenize("-*--*---**---", delim)
	if delim.str != nil ||
		tokens[0].text.ToString() != "-*--*" ||
		tokens[1].text.ToString() != "---*" ||
		tokens[2].text.ToString() != "*---" {
		t.Errorf("%s %d", tokens, len(tokens))
	}
}

func TestDelimiterRegexRegexCaret(t *testing.T) {
	delim := delimiterRegexp(`(^\s*|\s+)`)
	tokens := Tokenize("foo  bar baz", delim)
	if delim.str != nil ||
		len(tokens) != 4 ||
		tokens[0].text.ToString() != "" ||
		tokens[1].text.ToString() != "foo  " ||
		tokens[2].text.ToString() != "bar " ||
		tokens[3].text.ToString() != "baz" {
		t.Errorf("%s %d", tokens, len(tokens))
	}
}

func TestSplitNth(t *testing.T) {
	{
		ranges, _ := splitNth("..")
		if len(ranges) != 1 ||
			ranges[0].begin != rangeEllipsis ||
			ranges[0].end != rangeEllipsis {
			t.Errorf("%v", ranges)
		}
	}
	{
		ranges, _ := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
		if len(ranges) != 10 ||
			ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
			ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
			ranges[2].begin != 2 || ranges[2].end != 3 ||
			ranges[3].begin != 4 || ranges[3].end != rangeEllipsis ||
			ranges[4].begin != -3 || ranges[4].end != -2 ||
			ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis ||
			ranges[6].begin != 2 || ranges[6].end != 2 ||
			ranges[7].begin != -2 || ranges[7].end != -2 ||
			ranges[8].begin != 2 || ranges[8].end != -2 ||
			ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis {
			t.Errorf("%v", ranges)
		}
	}
}

func TestIrrelevantNth(t *testing.T) {
	index := 0
	{
		opts := defaultOptions()
		words := []string{"--nth", "..", "-x"}
		parseOptions(&index, opts, words)
		postProcessOptions(opts)
		if len(opts.Nth) != 0 {
			t.Errorf("nth should be empty: %v", opts.Nth)
		}
	}
	for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
		{
			opts := defaultOptions()
			parseOptions(&index, opts, words)
			postProcessOptions(opts)
			if len(opts.Nth) != 0 {
				t.Errorf("nth should be empty: %v", opts.Nth)
			}
		}
		{
			opts := defaultOptions()
			words = append(words, "-x")
			parseOptions(&index, opts, words)
			postProcessOptions(opts)
			if len(opts.Nth) != 2 {
				t.Errorf("nth should not be empty: %v", opts.Nth)
			}
		}
	}
}

func TestParseKeys(t *testing.T) {
	pairs, _, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
	checkEvent := func(e tui.Event, s string) {
		if pairs[e] != s {
			t.Errorf("%s != %s", pairs[e], s)
		}
	}
	check := func(et tui.EventType, s string) {
		checkEvent(et.AsEvent(), s)
	}
	if len(pairs) != 12 {
		t.Error(12)
	}
	check(tui.CtrlZ, "ctrl-z")
	check(tui.F2, "f2")
	check(tui.CtrlG, "ctrl-G")
	checkEvent(tui.AltKey('z'), "alt-z")
	checkEvent(tui.Key('@'), "@")
	checkEvent(tui.AltKey('a'), "Alt-a")
	checkEvent(tui.Key('!'), "!")
	checkEvent(tui.Key('J'), "J")
	checkEvent(tui.Key('g'), "g")
	checkEvent(tui.CtrlAltKey('a'), "ctrl-alt-a")
	checkEvent(tui.CtrlAltKey('m'), "ALT-enter")
	checkEvent(tui.AltKey(' '), "alt-SPACE")

	// Synonyms
	pairs, _, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
	if len(pairs) != 9 {
		t.Error(9)
	}
	check(tui.Enter, "Return")
	checkEvent(tui.Key(' '), "space")
	check(tui.Tab, "tab")
	check(tui.ShiftTab, "btab")
	check(tui.Esc, "esc")
	check(tui.Up, "up")
	check(tui.Down, "down")
	check(tui.Left, "left")
	check(tui.Right, "right")

	pairs, _, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
	if len(pairs) != 11 {
		t.Error(11)
	}
	check(tui.Tab, "Ctrl-I")
	check(tui.PageUp, "page-up")
	check(tui.PageDown, "Page-Down")
	check(tui.Home, "Home")
	check(tui.End, "End")
	check(tui.AltBackspace, "Alt-BSpace")
	check(tui.ShiftLeft, "shift-left")
	check(tui.ShiftRight, "shift-right")
	check(tui.ShiftTab, "shift-tab")
	check(tui.Enter, "Enter")
	check(tui.Backspace, "bspace")
}

func TestParseKeysWithComma(t *testing.T) {
	checkN := func(a int, b int) {
		if a != b {
			t.Errorf("%d != %d", a, b)
		}
	}
	check := func(pairs map[tui.Event]string, e tui.Event, s string) {
		if pairs[e] != s {
			t.Errorf("%s != %s", pairs[e], s)
		}
	}

	pairs, _, _ := parseKeyChords(",", "")
	checkN(len(pairs), 1)
	check(pairs, tui.Key(','), ",")

	pairs, _, _ = parseKeyChords(",,a,b", "")
	checkN(len(pairs), 3)
	check(pairs, tui.Key('a'), "a")
	check(pairs, tui.Key('b'), "b")
	check(pairs, tui.Key(','), ",")

	pairs, _, _ = parseKeyChords("a,b,,", "")
	checkN(len(pairs), 3)
	check(pairs, tui.Key('a'), "a")
	check(pairs, tui.Key('b'), "b")
	check(pairs, tui.Key(','), ",")

	pairs, _, _ = parseKeyChords("a,,,b", "")
	checkN(len(pairs), 3)
	check(pairs, tui.Key('a'), "a")
	check(pairs, tui.Key('b'), "b")
	check(pairs, tui.Key(','), ",")

	pairs, _, _ = parseKeyChords("a,,,b,c", "")
	checkN(len(pairs), 4)
	check(pairs, tui.Key('a'), "a")
	check(pairs, tui.Key('b'), "b")
	check(pairs, tui.Key('c'), "c")
	check(pairs, tui.Key(','), ",")

	pairs, _, _ = parseKeyChords(",,,", "")
	checkN(len(pairs), 1)
	check(pairs, tui.Key(','), ",")

	pairs, _, _ = parseKeyChords(",ALT-,,", "")
	checkN(len(pairs), 1)
	check(pairs, tui.AltKey(','), "ALT-,")
}

func TestBind(t *testing.T) {
	keymap := defaultKeymap()
	check := func(event tui.Event, arg1 string, types ...actionType) {
		if len(keymap[event]) != len(types) {
			t.Errorf("invalid number of actions for %v (%d != %d)",
				event, len(types), len(keymap[event]))
			return
		}
		for idx, action := range keymap[event] {
			if types[idx] != action.t {
				t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
			}
		}
		if len(arg1) > 0 && keymap[event][0].a != arg1 {
			t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[event][0].a)
		}
	}
	check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
	parseKeymap(keymap,
		"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
			"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
			"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
			"x:Execute(foo+bar),X:execute/bar+baz/"+
			",f1:+first,f1:+top"+
			",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
	check(tui.CtrlA.AsEvent(), "", actKillLine)
	check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
	check(tui.Key('c'), "", actPageUp)
	check(tui.Key(','), "", actAbort)
	check(tui.Key(':'), "", actAccept)
	check(tui.AltKey('z'), "", actPageDown)
	check(tui.F1.AsEvent(), "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actFirst, actFirst)
	check(tui.F2.AsEvent(), "echo {}, {}, {}", actExecute)
	check(tui.F3.AsEvent(), "echo '({})'", actExecute)
	check(tui.F4.AsEvent(), "less {}", actExecute)
	check(tui.Key('x'), "foo+bar", actExecute)
	check(tui.Key('X'), "bar+baz", actExecute)
	check(tui.AltKey('a'), "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
	check(tui.AltKey('b'), "echo (,),[,],/,:,@,%,{}", actExecute)
	check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)

	for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
		parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
		check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
	}

	parseKeymap(keymap, "f1:abort")
	check(tui.F1.AsEvent(), "", actAbort)
}

func TestParseEveryEvent(t *testing.T) {
	pairs, _, err := parseKeyChords("every(2),every(0.5)", "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(pairs) != 2 {
		t.Errorf("expected 2 distinct every events, got %d", len(pairs))
	}
	if pairs[(tui.Event{Type: tui.Every, Char: 2000})] != "every(2)" {
		t.Errorf("every(2) not registered")
	}
	if pairs[(tui.Event{Type: tui.Every, Char: 500})] != "every(0.5)" {
		t.Errorf("every(0.5) not registered")
	}

	// Floor at 0.01s -> 10ms
	pairs, _, err = parseKeyChords("every(0.001)", "")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if pairs[(tui.Event{Type: tui.Every, Char: 10})] != "every(0.001)" {
		t.Errorf("every(0.001) should floor to 10ms")
	}

	// Reject zero, negatives, and overflow (>= 2^31 ms = ~24.85 days)
	for _, bad := range []string{"every(0)", "every(-1)", "every(abc)", "every()", "every(2147484)"} {
		if _, _, err := parseKeyChords(bad, ""); err == nil {
			t.Errorf("%s should be rejected", bad)
		}
	}

}

func TestColorSpec(t *testing.T) {
	var base *tui.ColorTheme
	theme := tui.Dark256
	base, dark, _ := parseTheme(theme, "dark")
	if *dark != *base {
		t.Errorf("incorrect base theme returned")
	}
	if *dark != *theme {
		t.Errorf("colors should be equivalent")
	}
	if dark == theme {
		t.Errorf("point should not be equivalent")
	}

	base, light, _ := parseTheme(theme, "dark,light")
	if *light != *base {
		t.Errorf("incorrect base theme returned")
	}
	if *light == *theme {
		t.Errorf("should not be equivalent")
	}
	if *light != *tui.Light256 {
		t.Errorf("colors should be equivalent")
	}
	if light == theme {
		t.Errorf("point should not be equivalent")
	}

	_, customized, _ := parseTheme(theme, "fg:231,bg:232")
	if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
		t.Errorf("color not customized")
	}
	if *tui.Dark256 == *customized {
		t.Errorf("colors should not be equivalent")
	}
	customized.Fg = tui.Dark256.Fg
	customized.Bg = tui.Dark256.Bg
	if *tui.Dark256 != *customized {
		t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
	}

	_, customized, _ = parseTheme(theme, "fg:231,dark   bg:232")
	if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
		t.Errorf("color not customized")
	}
}

func TestDefaultCtrlNP(t *testing.T) {
	index := 0
	check := func(words []string, et tui.EventType, expected actionType) {
		e := et.AsEvent()
		opts := defaultOptions()
		parseOptions(&index, opts, words)
		postProcessOptions(opts)
		if opts.Keymap[e][0].t != expected {
			t.Error()
		}
	}
	check([]string{}, tui.CtrlN, actDownMatch)
	check([]string{}, tui.CtrlP, actUpMatch)

	check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
	check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)

	f, _ := os.CreateTemp("", "fzf-history")
	f.Close()
	hist := "--history=" + f.Name()
	check([]string{hist}, tui.CtrlN, actNextHistory)
	check([]string{hist}, tui.CtrlP, actPrevHistory)

	check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
	check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPrevHistory)

	check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory)
	check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
}

func optsFor(words ...string) *Options {
	index := 0
	opts := defaultOptions()
	parseOptions(&index, opts, words)
	postProcessOptions(opts)
	return opts
}

func TestToggle(t *testing.T) {
	opts := optsFor()
	if opts.ToggleSort {
		t.Error()
	}

	opts = optsFor("--bind=a:toggle-sort")
	if !opts.ToggleSort {
		t.Error()
	}

	opts = optsFor("--bind=a:toggle-sort", "--bind=a:up")
	if opts.ToggleSort {
		t.Error()
	}
}

func TestPreviewOpts(t *testing.T) {
	opts := optsFor()
	if !(opts.Preview.command == "" &&
		opts.Preview.hidden == false &&
		opts.Preview.wrap == false &&
		opts.Preview.position == posRight &&
		opts.Preview.size.percent == true &&
		opts.Preview.size.size == 50) {
		t.Error()
	}
	opts = optsFor("--preview", "cat {}", "--preview-window=left:15,hidden,wrap:+{1}-/2")
	if !(opts.Preview.command == "cat {}" &&
		opts.Preview.hidden == true &&
		opts.Preview.wrap == true &&
		opts.Preview.position == posLeft &&
		opts.Preview.scroll == "+{1}-/2" &&
		opts.Preview.size.percent == false &&
		opts.Preview.size.size == 15) {
		t.Error(opts.Preview)
	}
	opts = optsFor("--preview-window=up,15,wrap,hidden,+{1}+3-1-2/2", "--preview-window=down", "--preview-window=cycle")
	if !(opts.Preview.command == "" &&
		opts.Preview.hidden == true &&
		opts.Preview.wrap == true &&
		opts.Preview.cycle == true &&
		opts.Preview.position == posDown &&
		opts.Preview.scroll == "+{1}+3-1-2/2" &&
		opts.Preview.size.percent == false &&
		opts.Preview.size.size == 15) {
		t.Error(opts.Preview.size.size)
	}
	opts = optsFor("--preview-window=up:15:wrap:hidden")
	if !(opts.Preview.command == "" &&
		opts.Preview.hidden == true &&
		opts.Preview.wrap == true &&
		opts.Preview.position == posUp &&
		opts.Preview.size.percent == false &&
		opts.Preview.size.size == 15) {
		t.Error(opts.Preview)
	}
	opts = optsFor("--preview=foo", "--preview-window=up", "--preview-window=default:70%")
	if !(opts.Preview.command == "foo" &&
		opts.Preview.position == posRight &&
		opts.Preview.size.percent == true &&
		opts.Preview.size.size == 70) {
		t.Error(opts.Preview)
	}

	// wrap-word tests
	opts = optsFor("--preview-window=wrap-word")
	if !(opts.Preview.wrap == true && opts.Preview.wrapWord == true) {
		t.Errorf("wrap-word: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
	}
	opts = optsFor("--preview-window=wrap-word,nowrap")
	if !(opts.Preview.wrap == false && opts.Preview.wrapWord == false) {
		t.Errorf("wrap-word,nowrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
	}
	opts = optsFor("--preview-window=wrap-word,wrap")
	if !(opts.Preview.wrap == true && opts.Preview.wrapWord == false) {
		t.Errorf("wrap-word,wrap: wrap=%v, wrapWord=%v", opts.Preview.wrap, opts.Preview.wrapWord)
	}
}

func TestPreviewWrapSign(t *testing.T) {
	// Default: no preview wrap sign override
	opts := optsFor()
	if opts.PreviewWrapSign != nil {
		t.Errorf("expected nil PreviewWrapSign, got %v", *opts.PreviewWrapSign)
	}

	// --preview-wrap-sign sets PreviewWrapSign
	opts = optsFor("--preview-wrap-sign", ">> ")
	if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
		t.Errorf("expected '>> ', got %v", opts.PreviewWrapSign)
	}

	// --preview-wrap-sign is independent of --wrap-sign
	opts = optsFor("--wrap-sign", "| ", "--preview-wrap-sign", ">> ")
	if opts.WrapSign == nil || *opts.WrapSign != "| " {
		t.Errorf("expected WrapSign '| ', got %v", opts.WrapSign)
	}
	if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != ">> " {
		t.Errorf("expected PreviewWrapSign '>> ', got %v", opts.PreviewWrapSign)
	}

	// --preview-wrap-sign without --wrap-sign
	opts = optsFor("--preview-wrap-sign", "→ ")
	if opts.WrapSign != nil {
		t.Errorf("expected nil WrapSign, got %v", *opts.WrapSign)
	}
	if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "→ " {
		t.Errorf("expected PreviewWrapSign '→ ', got %v", opts.PreviewWrapSign)
	}

	// Last --preview-wrap-sign wins
	opts = optsFor("--preview-wrap-sign", "A ", "--preview-wrap-sign", "B ")
	if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "B " {
		t.Errorf("expected PreviewWrapSign 'B ', got %v", opts.PreviewWrapSign)
	}

	// Empty string is allowed
	opts = optsFor("--preview-wrap-sign", "")
	if opts.PreviewWrapSign == nil || *opts.PreviewWrapSign != "" {
		t.Errorf("expected empty PreviewWrapSign, got %v", opts.PreviewWrapSign)
	}
}

func TestAdditiveExpect(t *testing.T) {
	opts := optsFor("--expect=a", "--expect", "b", "--expect=c")
	if len(opts.Expect) != 3 {
		t.Error(opts.Expect)
	}
}

func TestValidateSign(t *testing.T) {
	testCases := []struct {
		inputSign string
		isValid   bool
	}{
		{"> ", true},
		{"아", true},
		{"😀", true},
		{">>>", false},
	}

	for _, testCase := range testCases {
		err := validateSign(testCase.inputSign, "", 2)
		if testCase.isValid && err != nil {
			t.Errorf("Input sign `%s` caused error", testCase.inputSign)
		}

		if !testCase.isValid && err == nil {
			t.Errorf("Input sign `%s` did not cause error", testCase.inputSign)
		}
	}
}

func TestParseSingleActionList(t *testing.T) {
	actions, _ := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", false)
	if len(actions) != 4 {
		t.Errorf("Invalid number of actions parsed:%d", len(actions))
	}
	if actions[0].t != actExecute || actions[0].a != "foo+bar,baz" {
		t.Errorf("Invalid action parsed: %v", actions[0])
	}
	if actions[1].t != actUp || actions[2].t != actUp {
		t.Errorf("Invalid action parsed: %v / %v", actions[1], actions[2])
	}
	if actions[3].t != actReload || actions[3].a != "down+down" {
		t.Errorf("Invalid action parsed: %v", actions[3])
	}
}

func TestParseSingleActionListError(t *testing.T) {
	_, err := parseSingleActionList("change-query(foobar)baz", false)
	if err == nil {
		t.Errorf("Failed to detect error")
	}
}

func TestMaskActionContents(t *testing.T) {
	original := ":execute((f)(o)(o)(b)(a)(r))+change-query@qu@ry@+up,x:reload:hello:world"
	expected := ":execute                    +change-query       +up,x:reload            "
	masked := maskActionContents(original)
	if masked != expected {
		t.Errorf("Not masked: %s", masked)
	}
}