From 2af4f01ed2e39f5b2d5c666365a4a28908d3db74 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 15 Sep 2023 14:48:23 +0200 Subject: [PATCH] feat(repl): improve support of multi-line statements This is a followup of #978. Instead of starting in multi-line mode prior to submit, the line is parsed, and new inputs are appended to it as long as the statment is not complete, as detected by the Go scanner. This is simpler and more general than previous attempt. The secondary prompt is "...", different from primary "gno>", similarly to many REPL programs (node, python, bash, ...). The "/editor" command is removed as not useful anymore. Note also that it is now possible to exit using Ctrl-D. --- gnovm/cmd/gno/repl.go | 87 +++++++++++++++++------------------------- gnovm/pkg/repl/repl.go | 2 +- 2 files changed, 35 insertions(+), 54 deletions(-) diff --git a/gnovm/cmd/gno/repl.go b/gnovm/cmd/gno/repl.go index 1acb96c3cb9..783d06a910d 100644 --- a/gnovm/cmd/gno/repl.go +++ b/gnovm/cmd/gno/repl.go @@ -2,10 +2,11 @@ package main import ( "bufio" - "bytes" "context" + "errors" "flag" "fmt" + "go/scanner" "os" "strings" @@ -88,10 +89,9 @@ func execRepl(cfg *replCfg, args []string) error { // gno> import "gno.land/p/demo/avl" // import the p/demo/avl package // gno> func a() string { return "a" } // declare a new function named a // gno> /src // print current generated source -// gno> /editor // enter in editor mode to add several lines // gno> /reset // remove all previously inserted code // gno> println(a()) // print the result of calling a() -// gno> /exit +// gno> /exit // alternative to `) } @@ -99,30 +99,46 @@ func execRepl(cfg *replCfg, args []string) error { } func runRepl(cfg *replCfg) error { - // init repl state r := repl.NewRepl() if cfg.initialCommand != "" { handleInput(r, cfg.initialCommand) } - var multiline bool - for { - fmt.Fprint(os.Stdout, "gno> ") + fmt.Fprint(os.Stdout, "gno> ") - input, err := getInput(multiline) - if err != nil { - return err + prevline := "" + liner := bufio.NewScanner(os.Stdin) + + for liner.Scan() { + line := liner.Text() + if prevline != "" { + line = prevline + "\n" + line + prevline = "" + } + + if err := handleInput(r, line); err != nil { + var goScanError scanner.ErrorList + if errors.As(err, &goScanError) { + // We assune that a Go scanner error indicates an incomplete Go statement. + // Append next line and retry. + prevline = line + } else { + fmt.Fprintln(os.Stderr, err) + } } - multiline = handleInput(r, input) + if prevline == "" { + fmt.Fprint(os.Stdout, "gno> ") + } else { + fmt.Fprint(os.Stdout, "... ") + } } + return nil } -// handleInput reads the input string and parses it depending if it -// is a specific command, or source code. It returns true if the following -// input is expected to be on more than one line. -func handleInput(r *repl.Repl, input string) bool { +// handleInput executes specific "/" commands, or evaluates input as Gno source code. +func handleInput(r *repl.Repl, input string) error { switch strings.TrimSpace(input) { case "/reset": r.Reset() @@ -130,49 +146,14 @@ func handleInput(r *repl.Repl, input string) bool { fmt.Fprintln(os.Stdout, r.Src()) case "/exit": os.Exit(0) - case "/editor": - fmt.Fprintln(os.Stdout, "// Entering editor mode (^D to finish)") - return true case "": - // avoid to increase the repl execution counter if sending empty content - fmt.Fprintln(os.Stdout, "") - return false + // Avoid to increase the repl execution counter if no input. default: out, err := r.Process(input) if err != nil { - fmt.Fprintln(os.Stderr, err) + return err } fmt.Fprintln(os.Stdout, out) } - - return false -} - -const ( - inputBreaker = "^D" - nl = "\n" -) - -func getInput(ml bool) (string, error) { - s := bufio.NewScanner(os.Stdin) - var mlOut bytes.Buffer - for s.Scan() { - line := s.Text() - if !ml { - return line, nil - } - - if line == inputBreaker { - break - } - - mlOut.WriteString(line) - mlOut.WriteString(nl) - } - - if err := s.Err(); err != nil { - return "", err - } - - return mlOut.String(), nil + return nil } diff --git a/gnovm/pkg/repl/repl.go b/gnovm/pkg/repl/repl.go index 0f60b948f39..c7786cf08b0 100644 --- a/gnovm/pkg/repl/repl.go +++ b/gnovm/pkg/repl/repl.go @@ -161,7 +161,7 @@ func (r *Repl) Process(input string) (out string, err error) { return r.handleExpression(exp) } - return "", fmt.Errorf("error parsing code:\n\t- as expression (error: %q)\n\t- as declarations (error: %q)", expErr.Error(), declErr.Error()) + return "", fmt.Errorf("error parsing code:\n\t- as expression: %w\n\t- as declarations: %w", expErr, declErr) } func (r *Repl) handleExpression(e *ast.File) (string, error) {