github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/cmd/gno/repl.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	"go/scanner"
    10  	"os"
    11  	"strings"
    12  
    13  	"github.com/gnolang/gno/gnovm/pkg/gnoenv"
    14  	"github.com/gnolang/gno/gnovm/pkg/repl"
    15  	"github.com/gnolang/gno/tm2/pkg/commands"
    16  )
    17  
    18  type replCfg struct {
    19  	rootDir        string
    20  	initialCommand string
    21  	skipUsage      bool
    22  }
    23  
    24  func newReplCmd() *commands.Command {
    25  	cfg := &replCfg{}
    26  
    27  	return commands.NewCommand(
    28  		commands.Metadata{
    29  			Name:       "repl",
    30  			ShortUsage: "repl [flags]",
    31  			ShortHelp:  "starts a GnoVM REPL",
    32  		},
    33  		cfg,
    34  		func(_ context.Context, args []string) error {
    35  			return execRepl(cfg, args)
    36  		},
    37  	)
    38  }
    39  
    40  func (c *replCfg) RegisterFlags(fs *flag.FlagSet) {
    41  	fs.StringVar(
    42  		&c.rootDir,
    43  		"root-dir",
    44  		"",
    45  		"clone location of github.com/gnolang/gno (gno tries to guess it)",
    46  	)
    47  
    48  	fs.StringVar(
    49  		&c.initialCommand,
    50  		"command",
    51  		"",
    52  		"initial command to run",
    53  	)
    54  
    55  	fs.BoolVar(
    56  		&c.skipUsage,
    57  		"skip-usage",
    58  		false,
    59  		"do not print usage",
    60  	)
    61  }
    62  
    63  func execRepl(cfg *replCfg, args []string) error {
    64  	if len(args) > 0 {
    65  		return flag.ErrHelp
    66  	}
    67  
    68  	if cfg.rootDir == "" {
    69  		cfg.rootDir = gnoenv.RootDir()
    70  	}
    71  
    72  	if !cfg.skipUsage {
    73  		fmt.Fprint(os.Stderr, `// Usage:
    74  //   gno> import "gno.land/p/demo/avl"     // import the p/demo/avl package
    75  //   gno> func a() string { return "a" }   // declare a new function named a
    76  //   gno> /src                             // print current generated source
    77  //   gno> /editor                          // enter in multi-line mode, end with ';'
    78  //   gno> /reset                           // remove all previously inserted code
    79  //   gno> println(a())                     // print the result of calling a()
    80  //   gno> /exit                            // alternative to <Ctrl-D>
    81  `)
    82  	}
    83  
    84  	return runRepl(cfg)
    85  }
    86  
    87  func runRepl(cfg *replCfg) error {
    88  	r := repl.NewRepl()
    89  
    90  	if cfg.initialCommand != "" {
    91  		handleInput(r, cfg.initialCommand)
    92  	}
    93  
    94  	fmt.Fprint(os.Stdout, "gno> ")
    95  
    96  	inEdit := false
    97  	prev := ""
    98  	liner := bufio.NewScanner(os.Stdin)
    99  
   100  	for liner.Scan() {
   101  		line := liner.Text()
   102  
   103  		if l := strings.TrimSpace(line); l == ";" {
   104  			line, inEdit = "", false
   105  		} else if l == "/editor" {
   106  			line, inEdit = "", true
   107  			fmt.Fprintln(os.Stdout, "// enter a single ';' to quit and commit")
   108  		}
   109  		if prev != "" {
   110  			line = prev + "\n" + line
   111  			prev = ""
   112  		}
   113  		if inEdit {
   114  			fmt.Fprint(os.Stdout, "...  ")
   115  			prev = line
   116  			continue
   117  		}
   118  
   119  		if err := handleInput(r, line); err != nil {
   120  			var goScanError scanner.ErrorList
   121  			if errors.As(err, &goScanError) {
   122  				// We assune that a Go scanner error indicates an incomplete Go statement.
   123  				// Append next line and retry.
   124  				prev = line
   125  			} else {
   126  				fmt.Fprintln(os.Stderr, err)
   127  			}
   128  		}
   129  
   130  		if prev == "" {
   131  			fmt.Fprint(os.Stdout, "gno> ")
   132  		} else {
   133  			fmt.Fprint(os.Stdout, "...  ")
   134  		}
   135  	}
   136  	return nil
   137  }
   138  
   139  // handleInput executes specific "/" commands, or evaluates input as Gno source code.
   140  func handleInput(r *repl.Repl, input string) error {
   141  	switch strings.TrimSpace(input) {
   142  	case "/reset":
   143  		r.Reset()
   144  	case "/src":
   145  		fmt.Fprintln(os.Stdout, r.Src())
   146  	case "/exit":
   147  		os.Exit(0)
   148  	case "":
   149  		// Avoid to increase the repl execution counter if no input.
   150  	default:
   151  		out, err := r.Process(input)
   152  		if err != nil {
   153  			return err
   154  		}
   155  		fmt.Fprintln(os.Stdout, out)
   156  	}
   157  	return nil
   158  }