Add support for CMP_POINT

Fixes #72
This commit is contained in:
Eyal Posener 2018-10-19 20:10:37 +03:00
parent 0d98d7ee19
commit 5fdb1adfd7
4 changed files with 210 additions and 115 deletions

View File

@ -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 { func splitFields(line string) []string {
parts := strings.Fields(line) parts := strings.Fields(line)
// Add empty field if the last field was completed.
if len(line) > 0 && unicode.IsSpace(rune(line[len(line)-1])) { if len(line) > 0 && unicode.IsSpace(rune(line[len(line)-1])) {
parts = append(parts, "") parts = append(parts, "")
} }
// Treat the last field if it is of the form "a=b"
parts = splitLastEqual(parts) parts = splitLastEqual(parts)
return parts return parts
} }

View File

@ -10,14 +10,16 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strconv"
"github.com/posener/complete/cmd" "github.com/posener/complete/cmd"
"github.com/posener/complete/match" "github.com/posener/complete/match"
) )
const ( const (
envComplete = "COMP_LINE" envLine = "COMP_LINE"
envDebug = "COMP_DEBUG" envPoint = "COMP_POINT"
envDebug = "COMP_DEBUG"
) )
// Complete structs define completion for a command with CLI options // 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 // For installation: it assumes that flags were added and parsed before
// it was called. // it was called.
func (c *Complete) Complete() bool { func (c *Complete) Complete() bool {
line, ok := getLine() line, point, ok := getEnv()
if !ok { if !ok {
// make sure flags parsed, // make sure flags parsed,
// in case they were not added in the main program // in case they were not added in the main program
return c.CLI.Run() 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) Log("Completing last field: %s", a.Last)
options := c.Command.Predict(a) options := c.Command.Predict(a)
Log("Options: %s", options) Log("Options: %s", options)
@ -79,12 +84,19 @@ func (c *Complete) Complete() bool {
return true return true
} }
func getLine() (string, bool) { func getEnv() (line string, point int, ok bool) {
line := os.Getenv(envComplete) line = os.Getenv(envLine)
if line == "" { if line == "" {
return "", false return
} }
return line, true 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, point, true
} }
func (c *Complete) output(options []string) { func (c *Complete) output(options []string) {

View File

@ -2,14 +2,15 @@ package complete
import ( import (
"bytes" "bytes"
"fmt"
"os" "os"
"sort" "sort"
"strconv"
"strings" "strings"
"testing" "testing"
) )
func TestCompleter_Complete(t *testing.T) { func TestCompleter_Complete(t *testing.T) {
t.Parallel()
initTests() initTests()
c := Command{ c := Command{
@ -39,166 +40,229 @@ func TestCompleter_Complete(t *testing.T) {
cmp := New("cmd", c) cmp := New("cmd", c)
tests := []struct { tests := []struct {
args string line string
want []string point int // -1 indicates len(line)
want []string
}{ }{
{ {
args: "", line: "cmd ",
want: []string{"sub1", "sub2"}, point: -1,
want: []string{"sub1", "sub2"},
}, },
{ {
args: "-", line: "cmd -",
want: []string{"-h", "-global1", "-o"}, point: -1,
want: []string{"-h", "-global1", "-o"},
}, },
{ {
args: "-h ", line: "cmd -h ",
want: []string{"sub1", "sub2"}, point: -1,
want: []string{"sub1", "sub2"},
}, },
{ {
args: "-global1 ", // global1 is known follow flag line: "cmd -global1 ", // global1 is known follow flag
want: []string{}, point: -1,
want: []string{},
}, },
{ {
args: "sub", line: "cmd sub",
want: []string{"sub1", "sub2"}, point: -1,
want: []string{"sub1", "sub2"},
}, },
{ {
args: "sub1", line: "cmd sub1",
want: []string{"sub1"}, point: -1,
want: []string{"sub1"},
}, },
{ {
args: "sub2", line: "cmd sub2",
want: []string{"sub2"}, point: -1,
want: []string{"sub2"},
}, },
{ {
args: "sub1 ", line: "cmd sub1 ",
want: []string{}, point: -1,
want: []string{},
}, },
{ {
args: "sub1 -", line: "cmd sub1 -",
want: []string{"-flag1", "-flag2", "-h", "-global1"}, point: -1,
want: []string{"-flag1", "-flag2", "-h", "-global1"},
}, },
{ {
args: "sub2 ", line: "cmd sub2 ",
want: []string{"./", "dir/", "outer/", "readme.md"}, point: -1,
want: []string{"./", "dir/", "outer/", "readme.md"},
}, },
{ {
args: "sub2 ./", line: "cmd sub2 ./",
want: []string{"./", "./readme.md", "./dir/", "./outer/"}, point: -1,
want: []string{"./", "./readme.md", "./dir/", "./outer/"},
}, },
{ {
args: "sub2 re", line: "cmd sub2 re",
want: []string{"readme.md"}, point: -1,
want: []string{"readme.md"},
}, },
{ {
args: "sub2 ./re", line: "cmd sub2 ./re",
want: []string{"./readme.md"}, point: -1,
want: []string{"./readme.md"},
}, },
{ {
args: "sub2 -flag2 ", line: "cmd sub2 -flag2 ",
want: []string{"./", "dir/", "outer/", "readme.md"}, point: -1,
want: []string{"./", "dir/", "outer/", "readme.md"},
}, },
{ {
args: "sub1 -fl", line: "cmd sub1 -fl",
want: []string{"-flag1", "-flag2"}, point: -1,
want: []string{"-flag1", "-flag2"},
}, },
{ {
args: "sub1 -flag1", line: "cmd sub1 -flag1",
want: []string{"-flag1"}, point: -1,
want: []string{"-flag1"},
}, },
{ {
args: "sub1 -flag1 ", line: "cmd sub1 -flag1 ",
want: []string{}, // flag1 is unknown follow flag point: -1,
want: []string{}, // flag1 is unknown follow flag
}, },
{ {
args: "sub1 -flag2 -", line: "cmd sub1 -flag2 -",
want: []string{"-flag1", "-flag2", "-h", "-global1"}, point: -1,
want: []string{"-flag1", "-flag2", "-h", "-global1"},
}, },
{ {
args: "-no-such-flag", line: "cmd -no-such-flag",
want: []string{}, point: -1,
want: []string{},
}, },
{ {
args: "-no-such-flag ", line: "cmd -no-such-flag ",
want: []string{"sub1", "sub2"}, point: -1,
want: []string{"sub1", "sub2"},
}, },
{ {
args: "-no-such-flag -", line: "cmd -no-such-flag -",
want: []string{"-h", "-global1", "-o"}, point: -1,
want: []string{"-h", "-global1", "-o"},
}, },
{ {
args: "no-such-command", line: "cmd no-such-command",
want: []string{}, point: -1,
want: []string{},
}, },
{ {
args: "no-such-command ", line: "cmd no-such-command ",
want: []string{"sub1", "sub2"}, point: -1,
want: []string{"sub1", "sub2"},
}, },
{ {
args: "-o ", line: "cmd -o ",
want: []string{"a.txt", "b.txt", "c.txt", ".dot.txt", "./", "dir/", "outer/"}, point: -1,
want: []string{"a.txt", "b.txt", "c.txt", ".dot.txt", "./", "dir/", "outer/"},
}, },
{ {
args: "-o ./no-su", line: "cmd -o ./no-su",
want: []string{}, point: -1,
want: []string{},
}, },
{ {
args: "-o ./", line: "cmd -o ./",
want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, point: -1,
want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"},
}, },
{ {
args: "-o=./", line: "cmd -o=./",
want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, point: -1,
want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"},
}, },
{ {
args: "-o .", line: "cmd -o .",
want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"}, point: -1,
want: []string{"./a.txt", "./b.txt", "./c.txt", "./.dot.txt", "./", "./dir/", "./outer/"},
}, },
{ {
args: "-o ./b", line: "cmd -o ./b",
want: []string{"./b.txt"}, point: -1,
want: []string{"./b.txt"},
}, },
{ {
args: "-o=./b", line: "cmd -o=./b",
want: []string{"./b.txt"}, point: -1,
want: []string{"./b.txt"},
}, },
{ {
args: "-o ./read", line: "cmd -o ./read",
want: []string{}, point: -1,
want: []string{},
}, },
{ {
args: "-o=./read", line: "cmd -o=./read",
want: []string{}, point: -1,
want: []string{},
}, },
{ {
args: "-o ./readme.md", line: "cmd -o ./readme.md",
want: []string{}, point: -1,
want: []string{},
}, },
{ {
args: "-o ./readme.md ", line: "cmd -o ./readme.md ",
want: []string{"sub1", "sub2"}, point: -1,
want: []string{"sub1", "sub2"},
}, },
{ {
args: "-o=./readme.md ", line: "cmd -o=./readme.md ",
want: []string{"sub1", "sub2"}, point: -1,
want: []string{"sub1", "sub2"},
}, },
{ {
args: "-o sub2 -flag3 ", line: "cmd -o sub2 -flag3 ",
want: []string{"opt1", "opt2", "opt12"}, point: -1,
want: []string{"opt1", "opt2", "opt12"},
}, },
{ {
args: "-o sub2 -flag3 opt1", line: "cmd -o sub2 -flag3 opt1",
want: []string{"opt1", "opt12"}, point: -1,
want: []string{"opt1", "opt12"},
}, },
{ {
args: "-o sub2 -flag3 opt", line: "cmd -o sub2 -flag3 opt",
want: []string{"opt1", "opt2", "opt12"}, 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 { for _, tt := range tests {
t.Run(tt.args, func(t *testing.T) { t.Run(fmt.Sprintf("%s@%d", tt.line, tt.point), func(t *testing.T) {
got := runComplete(cmp, tt.args) got := runComplete(cmp, tt.line, tt.point)
sort.Strings(tt.want) sort.Strings(tt.want)
sort.Strings(got) sort.Strings(got)
@ -211,7 +275,6 @@ func TestCompleter_Complete(t *testing.T) {
} }
func TestCompleter_Complete_SharedPrefix(t *testing.T) { func TestCompleter_Complete_SharedPrefix(t *testing.T) {
t.Parallel()
initTests() initTests()
c := Command{ c := Command{
@ -243,42 +306,50 @@ func TestCompleter_Complete_SharedPrefix(t *testing.T) {
cmp := New("cmd", c) cmp := New("cmd", c)
tests := []struct { tests := []struct {
args string line string
want []string point int // -1 indicates len(line)
want []string
}{ }{
{ {
args: "", line: "cmd ",
want: []string{"status", "job"}, point: -1,
want: []string{"status", "job"},
}, },
{ {
args: "-", line: "cmd -",
want: []string{"-h", "-global1", "-o"}, point: -1,
want: []string{"-h", "-global1", "-o"},
}, },
{ {
args: "j", line: "cmd j",
want: []string{"job"}, point: -1,
want: []string{"job"},
}, },
{ {
args: "job ", line: "cmd job ",
want: []string{"status"}, point: -1,
want: []string{"status"},
}, },
{ {
args: "job -", line: "cmd job -",
want: []string{"-h", "-global1"}, point: -1,
want: []string{"-h", "-global1"},
}, },
{ {
args: "job status ", line: "cmd job status ",
want: []string{}, point: -1,
want: []string{},
}, },
{ {
args: "job status -", line: "cmd job status -",
want: []string{"-f4", "-h", "-global1"}, point: -1,
want: []string{"-f4", "-h", "-global1"},
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.args, func(t *testing.T) { t.Run(tt.line, func(t *testing.T) {
got := runComplete(cmp, tt.args) got := runComplete(cmp, tt.line, tt.point)
sort.Strings(tt.want) sort.Strings(tt.want)
sort.Strings(got) sort.Strings(got)
@ -293,8 +364,12 @@ func TestCompleter_Complete_SharedPrefix(t *testing.T) {
// runComplete runs the complete login for test purposes // runComplete runs the complete login for test purposes
// it gets the complete struct and command line arguments and returns // it gets the complete struct and command line arguments and returns
// the complete options // the complete options
func runComplete(c *Complete, args string) (completions []string) { func runComplete(c *Complete, line string, point int) (completions []string) {
os.Setenv(envComplete, "cmd "+args) if point == -1 {
point = len(line)
}
os.Setenv(envLine, line)
os.Setenv(envPoint, strconv.Itoa(point))
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
c.Out = b c.Out = b
c.Complete() c.Complete()

View File

@ -1,6 +1,5 @@
module gocomplete module gocomplete
require ( require github.com/posener/complete v1.1.2
github.com/hashicorp/go-multierror v1.0.0 // indirect
github.com/posener/complete v1.1.2 replace github.com/posener/complete v1.1.2 => ./..
)