diff --git a/args.go b/args.go index 1ba4d69..17ab2c6 100644 --- a/args.go +++ b/args.go @@ -57,11 +57,20 @@ func newArgs(line string) Args { } } +// splitFields returns a list of fields from the given command line. +// If the last character is space, it appends an empty field in the end +// indicating that the field before it was completed. +// If the last field is of the form "a=b", it splits it to two fields: "a", "b", +// So it can be completed. func splitFields(line string) []string { parts := strings.Fields(line) + + // Add empty field if the last field was completed. if len(line) > 0 && unicode.IsSpace(rune(line[len(line)-1])) { parts = append(parts, "") } + + // Treat the last field if it is of the form "a=b" parts = splitLastEqual(parts) return parts } diff --git a/complete.go b/complete.go index 185d1e8..168657e 100644 --- a/complete.go +++ b/complete.go @@ -10,14 +10,16 @@ import ( "fmt" "io" "os" + "strconv" "github.com/posener/complete/cmd" "github.com/posener/complete/match" ) const ( - envComplete = "COMP_LINE" - envDebug = "COMP_DEBUG" + envLine = "COMP_LINE" + envPoint = "COMP_POINT" + envDebug = "COMP_DEBUG" ) // Complete structs define completion for a command with CLI options @@ -55,14 +57,17 @@ func (c *Complete) Run() bool { // For installation: it assumes that flags were added and parsed before // it was called. func (c *Complete) Complete() bool { - line, ok := getLine() + line, point, ok := getEnv() if !ok { // make sure flags parsed, // in case they were not added in the main program return c.CLI.Run() } - Log("Completing line: %s", line) - a := newArgs(line) + + completePhrase := line[:point] + + Log("Completing phrase: %s", completePhrase) + a := newArgs(completePhrase) Log("Completing last field: %s", a.Last) options := c.Command.Predict(a) Log("Options: %s", options) @@ -79,12 +84,19 @@ func (c *Complete) Complete() bool { return true } -func getLine() (string, bool) { - line := os.Getenv(envComplete) +func getEnv() (line string, point int, ok bool) { + line = os.Getenv(envLine) if line == "" { - return "", false + return + } + point, err := strconv.Atoi(os.Getenv(envPoint)) + if err != nil { + // If failed parsing point for some reason, set it to point + // on the end of the line. + Log("Failed parsing point %s: %v", os.Getenv(envPoint), err) + point = len(line) } - return line, true + return line, point, true } func (c *Complete) output(options []string) { diff --git a/complete_test.go b/complete_test.go index 1611ad4..45fa304 100644 --- a/complete_test.go +++ b/complete_test.go @@ -2,14 +2,15 @@ package complete import ( "bytes" + "fmt" "os" "sort" + "strconv" "strings" "testing" ) func TestCompleter_Complete(t *testing.T) { - t.Parallel() initTests() c := Command{ @@ -39,166 +40,229 @@ func TestCompleter_Complete(t *testing.T) { cmp := New("cmd", c) tests := []struct { - args string - want []string + line string + point int // -1 indicates len(line) + want []string }{ { - args: "", - want: []string{"sub1", "sub2"}, + line: "cmd ", + point: -1, + want: []string{"sub1", "sub2"}, }, { - args: "-", - want: []string{"-h", "-global1", "-o"}, + line: "cmd -", + point: -1, + want: []string{"-h", "-global1", "-o"}, }, { - args: "-h ", - want: []string{"sub1", "sub2"}, + line: "cmd -h ", + point: -1, + want: []string{"sub1", "sub2"}, }, { - args: "-global1 ", // global1 is known follow flag - want: []string{}, + line: "cmd -global1 ", // global1 is known follow flag + point: -1, + want: []string{}, }, { - args: "sub", - want: []string{"sub1", "sub2"}, + line: "cmd sub", + point: -1, + want: []string{"sub1", "sub2"}, }, { - args: "sub1", - want: []string{"sub1"}, + line: "cmd sub1", + point: -1, + want: []string{"sub1"}, }, { - args: "sub2", - want: []string{"sub2"}, + line: "cmd sub2", + point: -1, + want: []string{"sub2"}, }, { - args: "sub1 ", - want: []string{}, + line: "cmd sub1 ", + point: -1, + want: []string{}, }, { - args: "sub1 -", - want: []string{"-flag1", "-flag2", "-h", "-global1"}, + line: "cmd sub1 -", + point: -1, + want: []string{"-flag1", "-flag2", "-h", "-global1"}, }, { - args: "sub2 ", - want: []string{"./", "dir/", "outer/", "readme.md"}, + line: "cmd sub2 ", + point: -1, + want: []string{"./", "dir/", "outer/", "readme.md"}, }, { - args: "sub2 ./", - want: []string{"./", "./readme.md", "./dir/", "./outer/"}, + line: "cmd sub2 ./", + point: -1, + want: []string{"./", "./readme.md", "./dir/", "./outer/"}, }, { - args: "sub2 re", - want: []string{"readme.md"}, + line: "cmd sub2 re", + point: -1, + want: []string{"readme.md"}, }, { - args: "sub2 ./re", - want: []string{"./readme.md"}, + line: "cmd sub2 ./re", + point: -1, + want: []string{"./readme.md"}, }, { - args: "sub2 -flag2 ", - want: []string{"./", "dir/", "outer/", "readme.md"}, + line: "cmd sub2 -flag2 ", + point: -1, + want: []string{"./", "dir/", "outer/", "readme.md"}, }, { - args: "sub1 -fl", - want: []string{"-flag1", "-flag2"}, + line: "cmd sub1 -fl", + point: -1, + want: []string{"-flag1", "-flag2"}, }, { - args: "sub1 -flag1", - want: []string{"-flag1"}, + line: "cmd sub1 -flag1", + point: -1, + want: []string{"-flag1"}, }, { - args: "sub1 -flag1 ", - want: []string{}, // flag1 is unknown follow flag + line: "cmd sub1 -flag1 ", + point: -1, + want: []string{}, // flag1 is unknown follow flag }, { - args: "sub1 -flag2 -", - want: []string{"-flag1", "-flag2", "-h", "-global1"}, + line: "cmd sub1 -flag2 -", + point: -1, + want: []string{"-flag1", "-flag2", "-h", "-global1"}, }, { - args: "-no-such-flag", - want: []string{}, + line: "cmd -no-such-flag", + point: -1, + want: []string{}, }, { - args: "-no-such-flag ", - want: []string{"sub1", "sub2"}, + line: "cmd -no-such-flag ", + point: -1, + want: []string{"sub1", "sub2"}, }, { - args: "-no-such-flag -", - want: []string{"-h", "-global1", "-o"}, + line: "cmd -no-such-flag -", + point: -1, + want: []string{"-h", "-global1", "-o"}, }, { - args: "no-such-command", - want: []string{}, + line: "cmd no-such-command", + point: -1, + want: []string{}, }, { - args: "no-such-command ", - want: []string{"sub1", "sub2"}, + line: "cmd no-such-command ", + point: -1, + want: []string{"sub1", "sub2"}, }, { - args: "-o ", - want: []string{"a.txt", "b.txt", "c.txt", ".dot.txt", "./", "dir/", "outer/"}, + line: "cmd -o ", + point: -1, + want: []string{"a.txt", "b.txt", "c.txt", ".dot.txt", "./", "dir/", "outer/"}, }, { - args: "-o ./no-su", - want: []string{}, + line: "cmd -o ./no-su", + point: -1, + want: []string{}, }, { - args: "-o ./", - want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, + line: "cmd -o ./", + point: -1, + want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, }, { - args: "-o=./", - want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, + line: "cmd -o=./", + point: -1, + want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, }, { - args: "-o .", - want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, + line: "cmd -o .", + point: -1, + want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, }, { - args: "-o ./b", - want: []string{"./b.txt"}, + line: "cmd -o ./b", + point: -1, + want: []string{"./b.txt"}, }, { - args: "-o=./b", - want: []string{"./b.txt"}, + line: "cmd -o=./b", + point: -1, + want: []string{"./b.txt"}, }, { - args: "-o ./read", - want: []string{}, + line: "cmd -o ./read", + point: -1, + want: []string{}, }, { - args: "-o=./read", - want: []string{}, + line: "cmd -o=./read", + point: -1, + want: []string{}, }, { - args: "-o ./readme.md", - want: []string{}, + line: "cmd -o ./readme.md", + point: -1, + want: []string{}, }, { - args: "-o ./readme.md ", - want: []string{"sub1", "sub2"}, + line: "cmd -o ./readme.md ", + point: -1, + want: []string{"sub1", "sub2"}, }, { - args: "-o=./readme.md ", - want: []string{"sub1", "sub2"}, + line: "cmd -o=./readme.md ", + point: -1, + want: []string{"sub1", "sub2"}, }, { - args: "-o sub2 -flag3 ", - want: []string{"opt1", "opt2", "opt12"}, + line: "cmd -o sub2 -flag3 ", + point: -1, + want: []string{"opt1", "opt2", "opt12"}, }, { - args: "-o sub2 -flag3 opt1", - want: []string{"opt1", "opt12"}, + line: "cmd -o sub2 -flag3 opt1", + point: -1, + want: []string{"opt1", "opt12"}, }, { - args: "-o sub2 -flag3 opt", - want: []string{"opt1", "opt2", "opt12"}, + line: "cmd -o sub2 -flag3 opt", + point: -1, + want: []string{"opt1", "opt2", "opt12"}, + }, + { + line: "cmd -o ./b foo", + // ^ + point: 10, + want: []string{"./b.txt"}, + }, + { + line: "cmd -o=./b foo", + // ^ + point: 10, + want: []string{"./b.txt"}, + }, + { + line: "cmd -o sub2 -flag3 optfoo", + // ^ + point: 22, + want: []string{"opt1", "opt2", "opt12"}, + }, + { + line: "cmd -o ", + // ^ + point: 4, + want: []string{"sub1", "sub2"}, }, } for _, tt := range tests { - t.Run(tt.args, func(t *testing.T) { - got := runComplete(cmp, tt.args) + t.Run(fmt.Sprintf("%s@%d", tt.line, tt.point), func(t *testing.T) { + got := runComplete(cmp, tt.line, tt.point) sort.Strings(tt.want) sort.Strings(got) @@ -211,7 +275,6 @@ func TestCompleter_Complete(t *testing.T) { } func TestCompleter_Complete_SharedPrefix(t *testing.T) { - t.Parallel() initTests() c := Command{ @@ -243,42 +306,50 @@ func TestCompleter_Complete_SharedPrefix(t *testing.T) { cmp := New("cmd", c) tests := []struct { - args string - want []string + line string + point int // -1 indicates len(line) + want []string }{ { - args: "", - want: []string{"status", "job"}, + line: "cmd ", + point: -1, + want: []string{"status", "job"}, }, { - args: "-", - want: []string{"-h", "-global1", "-o"}, + line: "cmd -", + point: -1, + want: []string{"-h", "-global1", "-o"}, }, { - args: "j", - want: []string{"job"}, + line: "cmd j", + point: -1, + want: []string{"job"}, }, { - args: "job ", - want: []string{"status"}, + line: "cmd job ", + point: -1, + want: []string{"status"}, }, { - args: "job -", - want: []string{"-h", "-global1"}, + line: "cmd job -", + point: -1, + want: []string{"-h", "-global1"}, }, { - args: "job status ", - want: []string{}, + line: "cmd job status ", + point: -1, + want: []string{}, }, { - args: "job status -", - want: []string{"-f4", "-h", "-global1"}, + line: "cmd job status -", + point: -1, + want: []string{"-f4", "-h", "-global1"}, }, } for _, tt := range tests { - t.Run(tt.args, func(t *testing.T) { - got := runComplete(cmp, tt.args) + t.Run(tt.line, func(t *testing.T) { + got := runComplete(cmp, tt.line, tt.point) sort.Strings(tt.want) sort.Strings(got) @@ -293,8 +364,12 @@ func TestCompleter_Complete_SharedPrefix(t *testing.T) { // runComplete runs the complete login for test purposes // it gets the complete struct and command line arguments and returns // the complete options -func runComplete(c *Complete, args string) (completions []string) { - os.Setenv(envComplete, "cmd "+args) +func runComplete(c *Complete, line string, point int) (completions []string) { + if point == -1 { + point = len(line) + } + os.Setenv(envLine, line) + os.Setenv(envPoint, strconv.Itoa(point)) b := bytes.NewBuffer(nil) c.Out = b c.Complete() diff --git a/gocomplete/go.mod b/gocomplete/go.mod index d312331..9d98de6 100644 --- a/gocomplete/go.mod +++ b/gocomplete/go.mod @@ -1,6 +1,5 @@ module gocomplete -require ( - github.com/hashicorp/go-multierror v1.0.0 // indirect - github.com/posener/complete v1.1.2 -) +require github.com/posener/complete v1.1.2 + +replace github.com/posener/complete v1.1.2 => ./..