cliedit: Implement completion:smart-start.

This commit is contained in:
Qi Xiao 2019-11-18 23:22:43 +00:00
parent 572c09d81f
commit dc67d670b8
3 changed files with 103 additions and 21 deletions

View File

@ -6,10 +6,12 @@ import (
"os"
"strings"
"sync"
"unicode/utf8"
"github.com/elves/elvish/cli"
"github.com/elves/elvish/cli/addons/completion"
"github.com/elves/elvish/cli/el"
"github.com/elves/elvish/cli/el/codearea"
"github.com/elves/elvish/cliedit/complete"
"github.com/elves/elvish/eval"
"github.com/elves/elvish/eval/vals"
@ -135,7 +137,12 @@ func complexCandidate(opts complexCandidateOpts, stem string) complexItem {
//
// Start the completion mode.
func completionStart(app cli.App, binding el.Handler, cfg complete.Config) {
//elvdoc:fn completion:smart-start
//
// Starts the completion mode. However, if all the candidates share a non-empty
// prefix and that prefix starts with the seed, inserts the prefix instead.
func completionStart(app cli.App, binding el.Handler, cfg complete.Config, smart bool) {
buf := app.CodeArea().CopyState().Buffer
result, err := complete.Complete(
complete.CodeBuffer{Content: buf.Content, Dot: buf.Dot}, cfg)
@ -143,6 +150,35 @@ func completionStart(app cli.App, binding el.Handler, cfg complete.Config) {
app.Notify(err.Error())
return
}
if smart {
prefix := ""
for i, item := range result.Items {
if i == 0 {
prefix = item.ToInsert
continue
}
prefix = commonPrefix(prefix, item.ToInsert)
if prefix == "" {
break
}
}
if prefix != "" {
insertedPrefix := false
app.CodeArea().MutateState(func(s *codearea.State) {
rep := s.Buffer.Content[result.Replace.From:result.Replace.To]
if len(prefix) > len(rep) && strings.HasPrefix(prefix, rep) {
s.Pending = codearea.Pending{
Content: prefix,
From: result.Replace.From, To: result.Replace.To}
s.ApplyPending()
insertedPrefix = true
}
})
if insertedPrefix {
return
}
}
}
completion.Start(app, completion.Config{
Name: result.Name, Replace: result.Replace, Items: result.Items,
Binding: binding})
@ -157,7 +193,7 @@ func initCompletion(app cli.App, ev *eval.Evaler, ns eval.Ns) {
binding := newMapBinding(app, ev, bindingVar)
matcherMapVar := newMapVar(vals.EmptyMap)
argGeneratorMapVar := newMapVar(vals.EmptyMap)
getCfg := func() complete.Config {
cfg := func() complete.Config {
return complete.Config{
PureEvaler: pureEvaler{ev},
Filterer: adaptMatcherMap(
@ -167,7 +203,7 @@ func initCompletion(app cli.App, ev *eval.Evaler, ns eval.Ns) {
}
}
generateForSudo := func(args []string) ([]complete.RawItem, error) {
return complete.GenerateForSudo(getCfg(), args)
return complete.GenerateForSudo(cfg(), args)
}
ns.AddGoFns("<edit>", map[string]interface{}{
"complete-filename": wrapArgGenerator(complete.GenerateFileNames),
@ -184,15 +220,16 @@ func initCompletion(app cli.App, ev *eval.Evaler, ns eval.Ns) {
"binding": bindingVar,
"matcher": matcherMapVar,
}.AddGoFns("<edit:completion>", map[string]interface{}{
"accept": func() { listingAccept(app) },
"start": func() { completionStart(app, binding, getCfg()) },
"close": func() { completion.Close(app) },
"up": func() { listingUp(app) },
"down": func() { listingDown(app) },
"up-cycle": func() { listingUpCycle(app) },
"down-cycle": func() { listingDownCycle(app) },
"left": func() { listingLeft(app) },
"right": func() { listingRight(app) },
"accept": func() { listingAccept(app) },
"smart-start": func() { completionStart(app, binding, cfg(), true) },
"start": func() { completionStart(app, binding, cfg(), false) },
"close": func() { completion.Close(app) },
"up": func() { listingUp(app) },
"down": func() { listingDown(app) },
"up-cycle": func() { listingUpCycle(app) },
"down-cycle": func() { listingDownCycle(app) },
"left": func() { listingLeft(app) },
"right": func() { listingRight(app) },
}))
}
@ -262,6 +299,20 @@ func wrapArgGenerator(gen complete.ArgGenerator) wrappedArgGenerator {
}
}
func commonPrefix(s1, s2 string) string {
for i, r := range s1 {
if s2 == "" {
break
}
r2, n2 := utf8.DecodeRuneInString(s2)
if r2 != r {
return s1[:i]
}
s2 = s2[n2:]
}
return s1
}
// The type for a native Go matcher. This is not equivalent to the Elvish
// counterpart, which streams input and output. This is because we can actually
// afford calling a Go function for each item, so omitting the streaming

View File

@ -29,6 +29,37 @@ func TestCompletionAddon(t *testing.T) {
f.TTYCtrl.TestBuffer(t, wantBuf)
}
func TestCompletionAddon_CompletesLongestCommonPrefix(t *testing.T) {
f := setup()
defer f.Cleanup()
util.ApplyDir(util.Dir{"foo1": "", "foo2": "", "foo": "", "fox": ""})
feedInput(f.TTYCtrl, "echo \t")
wantBuf := bb().
WriteMarkedLines(
"~> echo fo", styles,
" gggg").
SetDotHere().
Buffer()
f.TTYCtrl.TestBuffer(t, wantBuf)
feedInput(f.TTYCtrl, "\t")
wantBuf = bb().
WriteMarkedLines(
"~> echo foo ", styles,
" gggg ----",
"COMPLETING argument ", styles,
"mmmmmmmmmmmmmmmmmmm ").
SetDotHere().
Newline().
WriteMarkedLines(
"foo foo1 foo2 fox", styles,
"### ",
).
Buffer()
f.TTYCtrl.TestBuffer(t, wantBuf)
}
func TestCompleteFilename(t *testing.T) {
f := setup()
defer f.Cleanup()
@ -83,21 +114,21 @@ func TestCompletionArgCompleter_ArgsAndValueOutput(t *testing.T) {
`fn foo { }`,
`edit:completion:arg-completer[foo] = [@args]{
foo-args = $args
put val1
edit:complex-candidate val2 &display-suffix=_
put 1val
edit:complex-candidate 2val &display-suffix=_
}`)
feedInput(f.TTYCtrl, "foo foo1 foo2 \t")
wantBuf := bb().
WriteMarkedLines(
"~> foo foo1 foo2 val1", styles,
"~> foo foo1 foo2 1val", styles,
" ggg ----",
"COMPLETING argument ", styles,
"mmmmmmmmmmmmmmmmmmm ").
SetDotHere().
Newline().
WriteMarkedLines(
"val1 val2_", styles,
"1val 2val_", styles,
"#### ",
).
Buffer()
@ -113,21 +144,21 @@ func TestCompletionArgCompleter_BytesOutput(t *testing.T) {
evals(f.Evaler,
`fn foo { }`,
`edit:completion:arg-completer[foo] = [@args]{
echo val1
echo val2
echo 1val
echo 2val
}`)
feedInput(f.TTYCtrl, "foo foo1 foo2 \t")
wantBuf := bb().
WriteMarkedLines(
"~> foo foo1 foo2 val1", styles,
"~> foo foo1 foo2 1val", styles,
" ggg ----",
"COMPLETING argument ", styles,
"mmmmmmmmmmmmmmmmmmm ").
SetDotHere().
Newline().
WriteMarkedLines(
"val1 val2", styles,
"1val 2val", styles,
"#### ",
).
Buffer()

View File

@ -28,7 +28,7 @@ insert:binding = (binding-table [
&Ctrl-R= $histlist:start~
&Ctrl-L= $location:start~
&Ctrl-N= $navigation:start~
&Tab= $completion:start~
&Tab= $completion:smart-start~
&Up= $history:start~
&Enter= $smart-enter~