Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gnovm): Add Basic Code Indentation in REPL #1596

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
228 changes: 199 additions & 29 deletions gnovm/cmd/gno/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,43 @@
"fmt"
"go/scanner"
"os"
"os/exec"
"runtime"
"strings"

"github.com/gnolang/gno/gnovm/pkg/gnoenv"
"github.com/gnolang/gno/gnovm/pkg/repl"
"github.com/gnolang/gno/tm2/pkg/commands"
)

const (
indentSize = 4

importExample = "import \"gno.land/p/demo/avl\""
funcExample = "func a() string { return \"a\" }"
printExample = "println(a())"
srcCommand = "/src"
editorCommand = "/editor"
resetCommand = "/reset"
exitCommand = "/exit"
clearCommand = "/clear"
helpCommand = "/help"
gnoREPL = "gno> "
inEditMode = "... "

helpText = `// Usage:
// gno> %-35s // import the p/demo/avl package
// gno> %-35s // declare a new function named a
// gno> %-35s // print current generated source
// gno> %-35s // enter in multi-line mode, end with ';'
// gno> %-35s // clear the terminal screen
// gno> %-35s // remove all previously inserted code
// gno> %-35s // print the result of calling a()
// gno> %-35s // alternative to <Ctrl-D>

`
)

type replCfg struct {
rootDir string
initialCommand string
Expand Down Expand Up @@ -70,15 +100,7 @@
}

if !cfg.skipUsage {
fmt.Fprint(os.Stderr, `// Usage:
// 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 <Ctrl-D>
`)
printHelp()

Check warning on line 103 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L103

Added line #L103 was not covered by tests
}

return runRepl(cfg)
Expand All @@ -91,68 +113,216 @@
handleInput(r, cfg.initialCommand)
}

fmt.Fprint(os.Stdout, "gno> ")
fmt.Fprint(os.Stdout, gnoREPL)

Check warning on line 116 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L116

Added line #L116 was not covered by tests

var (
inEdit bool
prev string
indentLevel int
)

Check warning on line 122 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L118-L122

Added lines #L118 - L122 were not covered by tests

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")
}
trimmedLine := strings.TrimSpace(line)

Check warning on line 129 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L129

Added line #L129 was not covered by tests

indentLevel = updateIndentLevel(trimmedLine, indentLevel)
line, inEdit = handleEditor(line)

Check warning on line 132 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L131-L132

Added lines #L131 - L132 were not covered by tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change breaks the editor mode. I'd recommend restoring it since it is only called from this one location and moving it to its own function doesn't have too much benefit. The issue can by entering editor mode with /editor and then doing some variable assignments. Notice that editor mode is exited after doing a single variable assignment when it should remain in editor mode until the semicolon is encountered.


if prev != "" {
line = prev + "\n" + line
prev = ""
}

if inEdit {
fmt.Fprint(os.Stdout, "... ")
fmt.Fprint(os.Stdout, inEditMode)

Check warning on line 140 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L140

Added line #L140 was not covered by tests
prev = line

continue
}

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.
prev = line
} else {
fmt.Fprintln(os.Stderr, err)
}
}

if prev == "" {
fmt.Fprint(os.Stdout, "gno> ")
} else {
fmt.Fprint(os.Stdout, "... ")
}
printPrompt(indentLevel, prev)

Check warning on line 157 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L157

Added line #L157 was not covered by tests
}

return nil
}

func handleEditor(line string) (string, bool) {
if l := strings.TrimSpace(line); l == ";" {
return "", false
} else if l == editorCommand {
fmt.Fprintln(os.Stdout, "// enter a single ';' to quit and commit")
return "", true

Check warning on line 168 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L163-L168

Added lines #L163 - L168 were not covered by tests
}

return line, false

Check warning on line 171 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L171

Added line #L171 was not covered by tests
}

func updateIndentLevel(line string, indentLevel int) int {
openCount, closeCount := countBrackets(line)
indentLevel += openCount - closeCount

if indentLevel < 0 {
indentLevel = 0

Check warning on line 179 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L179

Added line #L179 was not covered by tests
}

return indentLevel
}

// replState represents the current state of the REPL.
type replState int

const (
defaultState replState = iota
stringState
singleLineCommentState
multiLineCommentState
backtickState
)

func countBrackets(line string) (int, int) {
var (
openCount, closeCount int
stringDelim rune
state = defaultState
)

for i, char := range line {
switch state {
case defaultState:
switch char {
case '{', '(', '[':
openCount++
case '}', ')', ']':
closeCount++
case '"', '\'':
notJoon marked this conversation as resolved.
Show resolved Hide resolved
state = stringState
stringDelim = char
case '`':
state = backtickState
case '/':
if i < len(line)-1 {
nextChar := line[i+1]
if nextChar == '/' {
state = singleLineCommentState
} else if nextChar == '*' {
state = multiLineCommentState
}
}
}
case stringState:
if char == stringDelim {
state = defaultState
}
case singleLineCommentState:
if char == '\n' {
state = defaultState

Check warning on line 232 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L232

Added line #L232 was not covered by tests
}
case multiLineCommentState:
if i < len(line)-1 && char == '*' && line[i+1] == '/' {
state = defaultState
}
case backtickState:
if char == '`' {
state = defaultState
}
}

if state == singleLineCommentState && char == '\n' {
state = defaultState

Check warning on line 245 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L245

Added line #L245 was not covered by tests
}
}

return openCount, closeCount
}

func printPrompt(indentLevel int, prev string) {
indent := strings.Repeat(" ", indentLevel*indentSize)
if prev == "" {
fmt.Fprintf(os.Stdout, "%s%s", gnoREPL, indent)
} else {
fmt.Fprintf(os.Stdout, "%s%s", inEditMode, indent)

Check warning on line 257 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L252-L257

Added lines #L252 - L257 were not covered by tests
}
}

// 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":
input = strings.TrimSpace(input)
switch input {
case resetCommand:

Check warning on line 265 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L263-L265

Added lines #L263 - L265 were not covered by tests
r.Reset()
case "/src":
case srcCommand:

Check warning on line 267 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L267

Added line #L267 was not covered by tests
fmt.Fprintln(os.Stdout, r.Src())
case "/exit":
case clearCommand:
clearScreen(&RealCommandExecutor{}, RealOSGetter{})
case exitCommand:

Check warning on line 271 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L269-L271

Added lines #L269 - L271 were not covered by tests
os.Exit(0)
case helpCommand:
printHelp()

Check warning on line 274 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L273-L274

Added lines #L273 - L274 were not covered by tests
case "":
// Avoid to increase the repl execution counter if no input.
default:
out, err := r.Process(input)
if err != nil {
return err
}

fmt.Fprintln(os.Stdout, out)
}

return nil
}

type CommandExecutor interface {
Execute(cmd *exec.Cmd) error
}

type RealCommandExecutor struct{}

func (e *RealCommandExecutor) Execute(cmd *exec.Cmd) error {
cmd.Stdout = os.Stdout
return cmd.Run()

Check warning on line 297 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L295-L297

Added lines #L295 - L297 were not covered by tests
}

type OsGetter interface {
Get() string
}

type RealOSGetter struct{}

func (r RealOSGetter) Get() string {
return runtime.GOOS

Check warning on line 307 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L306-L307

Added lines #L306 - L307 were not covered by tests
}

func clearScreen(executor CommandExecutor, osGetter OsGetter) {
var cmd *exec.Cmd

if osGetter.Get() == "windows" {
cmd = exec.Command("cmd", "/c", "cls")
} else {
cmd = exec.Command("clear")
}

executor.Execute(cmd)
}

func printHelp() {
fmt.Printf(
helpText, importExample, funcExample,
srcCommand, editorCommand, clearCommand,
resetCommand, printExample, exitCommand,
)

Check warning on line 327 in gnovm/cmd/gno/repl.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/repl.go#L322-L327

Added lines #L322 - L327 were not covered by tests
}
Loading
Loading