From 8617bd7b96bbc8496af0d8059893adc614617ffc Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Thu, 19 Sep 2024 16:57:46 +1000 Subject: [PATCH] feat: completion for interactive mode fixes: #2708 --- frontend/cli/cmd_interactive.go | 87 +++++++++++++++++++++++++++++++-- frontend/cli/main.go | 5 +- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/frontend/cli/cmd_interactive.go b/frontend/cli/cmd_interactive.go index dac5845282..51dfed711f 100644 --- a/frontend/cli/cmd_interactive.go +++ b/frontend/cli/cmd_interactive.go @@ -9,19 +9,28 @@ 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 { +type FTLCompletion struct { + app *kong.Kong +} + +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 +47,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 +60,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 +74,72 @@ 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...)) } + +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)