From 42c013248e52807bff2712a86f86993b7765c462 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Tue, 7 Nov 2023 09:07:01 +0100 Subject: [PATCH] feat(repl): improve support of multi-line statements (#1129) ![demo](https://github.com/gnolang/gno/assets/5792239/308e61bc-bdf9-498b-9fa7-cd756835f774) 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. Related issues: #446 #950
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/cmd/gno/repl.go | 101 ++++++++++++++++++++--------------------- gnovm/pkg/repl/repl.go | 2 +- 2 files changed, 49 insertions(+), 54 deletions(-) diff --git a/gnovm/cmd/gno/repl.go b/gnovm/cmd/gno/repl.go index 1acb96c3cb9..0a9d4934ce3 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,10 @@ 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> /editor // enter in multi-line mode, end with ';' // gno> /reset // remove all previously inserted code // gno> println(a()) // print the result of calling a() -// gno> /exit +// gno> /exit // alternative to `) } @@ -99,30 +100,59 @@ 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 + inEdit := false + prev := "" + liner := bufio.NewScanner(os.Stdin) + + for liner.Scan() { + line := liner.Text() + + if l := strings.TrimSpace(line); l == ";" { + line, inEdit = "", false + } else if l == "/editor" { + line, inEdit = "", true + fmt.Fprintln(os.Stdout, "// enter a single ';' to quit and commit") + } + if prev != "" { + line = prev + "\n" + line + prev = "" + } + if inEdit { + fmt.Fprint(os.Stdout, "... ") + prev = line + continue } - multiline = handleInput(r, input) + 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. + prev = line + } else { + fmt.Fprintln(os.Stderr, err) + } + } + + if prev == "" { + 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 +160,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) {