Skip to content

Commit

Permalink
feat(repl): improve support of multi-line statements (gnolang#1129)
Browse files Browse the repository at this point in the history
![demo](https://github.com/gnolang/gno/assets/5792239/308e61bc-bdf9-498b-9fa7-cd756835f774)

This is a followup of gnolang#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: gnolang#446 gnolang#950

<!-- please provide a detailed description of the changes made in this
pull request. -->

<details><summary>Contributors' checklist...</summary>

- [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).
</details>

---------

Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>
  • Loading branch information
mvertes and moul committed Nov 14, 2023
1 parent 3d065d7 commit ea46c90
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 54 deletions.
101 changes: 48 additions & 53 deletions gnovm/cmd/gno/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package main

import (
"bufio"
"bytes"
"context"
"errors"
"flag"
"fmt"
"go/scanner"
"os"
"strings"

Expand Down Expand Up @@ -88,91 +89,85 @@ 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 <Ctrl-D>
`)
}

return runRepl(cfg)
}

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()
case "/src":
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
}
2 changes: 1 addition & 1 deletion gnovm/pkg/repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit ea46c90

Please sign in to comment.