From 09a7042b45f9758a4cb13aac7f68d1236b825eb1 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 15 Sep 2023 15:04:32 +0200 Subject: [PATCH 1/3] 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) { From 7f05c3a4b828416843e828348c3da84ba7da16dc Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Fri, 15 Sep 2023 17:27:27 +0200 Subject: [PATCH 2/3] typo in comment --- gnovm/cmd/gno/repl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnovm/cmd/gno/repl.go b/gnovm/cmd/gno/repl.go index 783d06a910d..c872084a079 100644 --- a/gnovm/cmd/gno/repl.go +++ b/gnovm/cmd/gno/repl.go @@ -120,7 +120,7 @@ func runRepl(cfg *replCfg) error { 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. + // We assume that a Go scanner error indicates an incomplete Go statement. // Append next line and retry. prevline = line } else { From f5cda85f82b07d4af4ac7de5ee7916e8d86a9e19 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 19 Oct 2023 19:24:50 -0400 Subject: [PATCH 3/3] reintroduce /editor command as per Antonio feedback and apply suggestions from Morgan --- gnovm/cmd/gno/repl.go | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/gnovm/cmd/gno/repl.go b/gnovm/cmd/gno/repl.go index c872084a079..0a9d4934ce3 100644 --- a/gnovm/cmd/gno/repl.go +++ b/gnovm/cmd/gno/repl.go @@ -89,6 +89,7 @@ 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 multi-line mode, end with ';' // gno> /reset // remove all previously inserted code // gno> println(a()) // print the result of calling a() // gno> /exit // alternative to @@ -107,31 +108,44 @@ func runRepl(cfg *replCfg) error { fmt.Fprint(os.Stdout, "gno> ") - prevline := "" + inEdit := false + prev := "" liner := bufio.NewScanner(os.Stdin) for liner.Scan() { line := liner.Text() - if prevline != "" { - line = prevline + "\n" + line - prevline = "" + + 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 } if err := handleInput(r, line); err != nil { var goScanError scanner.ErrorList if errors.As(err, &goScanError) { - // We assume that a Go scanner error indicates an incomplete Go statement. + // We assune that a Go scanner error indicates an incomplete Go statement. // Append next line and retry. - prevline = line + prev = line } else { fmt.Fprintln(os.Stderr, err) } } - if prevline == "" { + if prev == "" { fmt.Fprint(os.Stdout, "gno> ") } else { - fmt.Fprint(os.Stdout, "... ") + fmt.Fprint(os.Stdout, "... ") } } return nil