Filter matches as a final stage

This simplifies the prediction logic writing, the predictor
doesn't need to filter our according to line matching, instead
it returns everything and the filtering is done at the end.

This does not break current behavior.
This commit is contained in:
Eyal Posener 2017-11-04 10:21:41 +02:00
parent 88e59760ad
commit 7ee9623f2b
7 changed files with 56 additions and 83 deletions

View File

@ -1,7 +1,5 @@
package complete
import "github.com/posener/complete/match"
// Command represents a command line
// It holds the data that enables auto completion of command line
// Command can also be a sub command.
@ -25,9 +23,9 @@ type Command struct {
}
// Predict returns all possible predictions for args according to the command struct
func (c *Command) Predict(a Args) (predictions []string) {
predictions, _ = c.predict(a)
return
func (c *Command) Predict(a Args) []string {
options, _ := c.predict(a)
return options
}
// Commands is the type of Sub member, it maps a command name to a command struct
@ -36,9 +34,7 @@ type Commands map[string]Command
// Predict completion of sub command names names according to command line arguments
func (c Commands) Predict(a Args) (prediction []string) {
for sub := range c {
if match.Prefix(sub, a.Last) {
prediction = append(prediction, sub)
}
prediction = append(prediction, sub)
}
return
}
@ -56,10 +52,7 @@ func (f Flags) Predict(a Args) (prediction []string) {
if flagHyphenStart && !lastHyphenStart {
continue
}
if match.Prefix(flag, a.Last) {
prediction = append(prediction, flag)
}
prediction = append(prediction, flag)
}
return
}

View File

@ -8,10 +8,12 @@ package complete
import (
"flag"
"fmt"
"io"
"os"
"strings"
"github.com/posener/complete/cmd"
"github.com/posener/complete/match"
)
const (
@ -23,6 +25,7 @@ const (
type Complete struct {
Command Command
cmd.CLI
Out io.Writer
}
// New creates a new complete command.
@ -34,6 +37,7 @@ func New(name string, command Command) *Complete {
return &Complete{
Command: command,
CLI: cmd.CLI{Name: name},
Out: os.Stdout,
}
}
@ -59,13 +63,19 @@ func (c *Complete) Complete() bool {
return c.CLI.Run()
}
Log("Completing line: %s", line)
a := newArgs(line)
options := c.Command.Predict(a)
Log("Options: %s", options)
Log("Completion: %s", options)
output(options)
// filter only options that match the last argument
matches := []string{}
for _, option := range options {
if match.Prefix(option, a.Last) {
matches = append(matches, option)
}
}
Log("Matches: %s", matches)
c.output(matches)
return true
}
@ -77,10 +87,9 @@ func getLine() ([]string, bool) {
return strings.Split(line, " "), true
}
func output(options []string) {
Log("")
func (c *Complete) output(options []string) {
// stdout of program defines the complete options
for _, option := range options {
fmt.Println(option)
fmt.Fprintln(c.Out, option)
}
}

View File

@ -1,8 +1,10 @@
package complete
import (
"bytes"
"os"
"sort"
"strings"
"testing"
)
@ -34,6 +36,7 @@ func TestCompleter_Complete(t *testing.T) {
"-global1": PredictAnything,
},
}
cmp := New("cmd", c)
tests := []struct {
args string
@ -175,18 +178,13 @@ func TestCompleter_Complete(t *testing.T) {
for _, tt := range tests {
t.Run(tt.args, func(t *testing.T) {
tt.args = "cmd " + tt.args
os.Setenv(envComplete, tt.args)
line, _ := getLine()
got := c.Predict(newArgs(line))
got := runComplete(cmp, tt.args)
sort.Strings(tt.want)
sort.Strings(got)
if !equalSlices(got, tt.want) {
t.Errorf("failed '%s'\ngot = %s\nwant: %s", t.Name(), got, tt.want)
t.Errorf("failed '%s'\ngot: %s\nwant: %s", t.Name(), got, tt.want)
}
})
}
@ -222,6 +220,8 @@ func TestCompleter_Complete_SharedPrefix(t *testing.T) {
},
}
cmp := New("cmd", c)
tests := []struct {
args string
want []string
@ -258,12 +258,7 @@ func TestCompleter_Complete_SharedPrefix(t *testing.T) {
for _, tt := range tests {
t.Run(tt.args, func(t *testing.T) {
tt.args = "cmd " + tt.args
os.Setenv(envComplete, tt.args)
line, _ := getLine()
got := c.Predict(newArgs(line))
got := runComplete(cmp, tt.args)
sort.Strings(tt.want)
sort.Strings(got)
@ -275,6 +270,29 @@ 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)
b := bytes.NewBuffer(nil)
c.Out = b
c.Complete()
completions = parseOutput(b.String())
return
}
func parseOutput(output string) []string {
lines := strings.Split(output, "\n")
options := []string{}
for _, l := range lines {
if l != "" {
options = append(options, l)
}
}
return options
}
func equalSlices(a, b []string) bool {
if len(a) != len(b) {
return false

View File

@ -7,7 +7,6 @@ import (
"strings"
"github.com/posener/complete"
"github.com/posener/complete/match"
)
var (
@ -21,14 +20,8 @@ var (
// for test names use prefix of 'Test' or 'Example', and for benchmark
// test names use 'Benchmark'
func funcPredict(funcRegexp *regexp.Regexp) complete.Predictor {
return complete.PredictFunc(func(a complete.Args) (prediction []string) {
tests := funcNames(funcRegexp)
for _, t := range tests {
if match.Prefix(t, a.Last) {
prediction = append(prediction, t)
}
}
return
return complete.PredictFunc(func(a complete.Args) []string {
return funcNames(funcRegexp)
})
}

View File

@ -22,38 +22,11 @@ func TestPredictions(t *testing.T) {
predictor: predictTest,
want: []string{"TestPredictions", "Example"},
},
{
name: "predict tests not found",
predictor: predictTest,
last: "X",
},
{
name: "predict benchmark ok",
predictor: predictBenchmark,
want: []string{"BenchmarkFake"},
},
{
name: "predict benchmarks not found",
predictor: predictBenchmark,
last: "X",
},
{
name: "predict local ok",
predictor: complete.PredictFunc(predictPackages),
last: ".",
want: []string{"./"},
},
{
name: "predict system ok",
predictor: complete.PredictFunc(predictPackages),
last: "github.com/posener/complete/goc",
want: []string{"github.com/posener/complete/gocomplete/"},
},
{
name: "predict packages not found",
predictor: complete.PredictFunc(predictPackages),
last: "X",
},
}
for _, tt := range tests {

View File

@ -1,7 +1,5 @@
package complete
import "github.com/posener/complete/match"
// PredictSet expects specific set of terms, given in the options argument.
func PredictSet(options ...string) Predictor {
return predictSet(options)
@ -9,11 +7,6 @@ func PredictSet(options ...string) Predictor {
type predictSet []string
func (p predictSet) Predict(a Args) (prediction []string) {
for _, m := range p {
if match.Prefix(m, a.Last) {
prediction = append(prediction, m)
}
}
return
func (p predictSet) Predict(a Args) []string {
return p
}

View File

@ -21,12 +21,6 @@ func TestPredicate(t *testing.T) {
p: PredictSet("a", "b", "c"),
want: []string{"a", "b", "c"},
},
{
name: "set with does",
p: PredictSet("./..", "./x"),
argList: []string{"./.", "./.."},
want: []string{"./.."},
},
{
name: "set/empty",
p: PredictSet(),