From 2b0d47ad3fd2a7e59b765e0093c271f6f77ed625 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Thu, 19 Sep 2024 19:08:32 +1000 Subject: [PATCH] feat: completion for interactive mode (#2729) fixes: #2708 --- frontend/cli/cmd_interactive.go | 87 +++++++++++++++++++++++++++++++-- frontend/cli/main.go | 5 +- go.mod | 2 +- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/frontend/cli/cmd_interactive.go b/frontend/cli/cmd_interactive.go index dac5845282..df5debf97a 100644 --- a/frontend/cli/cmd_interactive.go +++ b/frontend/cli/cmd_interactive.go @@ -9,19 +9,24 @@ import ( "github.com/alecthomas/kong" "github.com/chzyer/readline" + kongcompletion "github.com/jotaen/kong-completion" "github.com/kballard/go-shellquote" + "github.com/posener/complete" "github.com/TBD54566975/ftl/internal/projectconfig" ) +var _ readline.AutoCompleter = &FTLCompletion{} + type interactiveCmd struct { } -func (i *interactiveCmd) Run(ctx context.Context, k *kong.Kong, projectConfig projectconfig.Config) error { +func (i *interactiveCmd) Run(ctx context.Context, k *kong.Kong, projectConfig projectconfig.Config, app *kong.Kong) error { l, err := readline.NewEx(&readline.Config{ Prompt: "\033[32m>\033[0m ", InterruptPrompt: "^C", EOFPrompt: "exit", + AutoComplete: &FTLCompletion{app: app}, }) if err != nil { return fmt.Errorf("init readline: %w", err) @@ -38,6 +43,9 @@ func (i *interactiveCmd) Run(ctx context.Context, k *kong.Kong, projectConfig pr break } line = strings.TrimSpace(line) + if line == "" { + continue + } args, err := shellquote.Split(line) if err != nil { errorf("%s", err) @@ -48,11 +56,11 @@ func (i *interactiveCmd) Run(ctx context.Context, k *kong.Kong, projectConfig pr errorf("%s", err) continue } - subctx := bindContext(ctx, kctx, projectConfig) + subctx := bindContext(ctx, kctx, projectConfig, app) err = kctx.Run(subctx) if err != nil { - errorf("%s", err) + errorf("error: %s", err) continue } } @@ -62,3 +70,76 @@ func (i *interactiveCmd) Run(ctx context.Context, k *kong.Kong, projectConfig pr func errorf(format string, args ...any) { fmt.Printf("\033[31m%s\033[0m\n", fmt.Sprintf(format, args...)) } + +type FTLCompletion struct { + app *kong.Kong +} + +func (f *FTLCompletion) Do(line []rune, pos int) ([][]rune, int) { + parser := f.app + if parser == nil { + return nil, 0 + } + all := []string{} + completed := []string{} + last := "" + lastCompleted := "" + lastSpace := false + // We don't care about anything past pos + // this completer can't handle completing in the middle of things + if pos < len(line) { + line = line[:pos] + } + current := 0 + for i, arg := range line { + if i == pos { + break + } + if arg == ' ' { + lastWord := string(line[current:i]) + all = append(all, lastWord) + completed = append(completed, lastWord) + current = i + 1 + lastSpace = true + } else { + lastSpace = false + } + } + if pos > 0 { + if lastSpace { + lastCompleted = all[len(all)-1] + } else { + if current < len(line) { + last = string(line[current:]) + all = append(all, last) + } + if len(all) > 0 { + lastCompleted = all[len(all)-1] + } + } + } + + args := complete.Args{ + Completed: completed, + All: all, + Last: last, + LastCompleted: lastCompleted, + } + + command, err := kongcompletion.Command(parser) + if err != nil { + // TODO handle error + println(err.Error()) + } + result := command.Predict(args) + runes := [][]rune{} + for _, s := range result { + if !strings.HasPrefix(s, last) || s == "interactive" { + continue + } + s = s[len(last):] + str := []rune(s) + runes = append(runes, str) + } + return runes, pos +} diff --git a/frontend/cli/main.go b/frontend/cli/main.go index 98beec3839..9fdc878dfa 100644 --- a/frontend/cli/main.go +++ b/frontend/cli/main.go @@ -132,14 +132,15 @@ func main() { if err != nil && !errors.Is(err, os.ErrNotExist) { kctx.FatalIfErrorf(err) } - ctx = bindContext(ctx, kctx, config) + ctx = bindContext(ctx, kctx, config, app) err = kctx.Run(ctx) kctx.FatalIfErrorf(err) } -func bindContext(ctx context.Context, kctx *kong.Context, projectConfig projectconfig.Config) context.Context { +func bindContext(ctx context.Context, kctx *kong.Context, projectConfig projectconfig.Config, app *kong.Kong) context.Context { kctx.Bind(projectConfig) + kctx.Bind(app) controllerServiceClient := rpc.Dial(ftlv1connect.NewControllerServiceClient, cli.Endpoint.String(), log.Error) ctx = rpc.ContextWithClient(ctx, controllerServiceClient) diff --git a/go.mod b/go.mod index 40e2ee6fc6..57baf76b3c 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/mattn/go-isatty v0.0.20 github.com/multiformats/go-base36 v0.2.0 github.com/otiai10/copy v1.14.0 + github.com/posener/complete v1.2.3 github.com/radovskyb/watcher v1.0.7 github.com/rs/cors v1.11.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 @@ -127,7 +128,6 @@ require ( github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.6 // indirect - github.com/posener/complete v1.2.3 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect