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
103 changes: 69 additions & 34 deletions gnovm/cmd/gno/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ import (
"github.com/gnolang/gno/tm2/pkg/commands"
)

const indentSize = 4

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"
Expand All @@ -28,6 +31,18 @@ const (
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 {
Expand Down Expand Up @@ -150,31 +165,17 @@ func handleEditor(line string) (string, bool) {
return "", false
} else if l == editorCommand {
fmt.Fprintln(os.Stdout, "// enter a single ';' to quit and commit")

return "", true
}

return line, false
}

func updateIndentLevel(line string, indentLevel int) int {
openCount, closeCount := 0, 0
increaseIndent := false

for i, char := range line {
switch char {
case '{', '(', '[':
openCount++

if i < len(line)-1 && line[i+1] == '\n' {
increaseIndent = true
}
case '}', ')', ']':
closeCount++
}
}

openCount, closeCount := countBrackets(line)
increaseIndent := shouldIncreaseIndent(line)
indentLevel += openCount - closeCount

if indentLevel < 0 {
indentLevel = 0
}
Expand All @@ -183,18 +184,57 @@ func updateIndentLevel(line string, indentLevel int) int {
indentLevel++
}

if strings.HasSuffix(line, ":") {
indentLevel++
return indentLevel
}

func countBrackets(line string) (int, int) {
openCount, closeCount := 0, 0
inString, inComment, inSingleLineComment := false, false, false
var stringChar rune

for i, char := range line {
if !inString && !inComment && !inSingleLineComment {
switch char {
case '{', '(', '[':
openCount++
case '}', ')', ']':
closeCount++
case '"', '\'':
inString = true
stringChar = char
case '/':
if i < len(line)-1 {
if line[i+1] == '/' {
inSingleLineComment = true
} else if line[i+1] == '*' {
inComment = true
}
}
}
} else if inString && char == stringChar {
inString = false
} else if inComment && i < len(line)-1 && char == '*' && line[i+1] == '/' {
inComment = false
}
}

return indentLevel
return openCount, closeCount
}

func shouldIncreaseIndent(line string) bool {
openIndex := strings.IndexAny(line, "{([")
if openIndex != -1 && openIndex < len(line)-1 && line[openIndex+1] == '\n' {
return true
}
return false
}

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

Expand Down Expand Up @@ -260,14 +300,9 @@ func clearScreen(executor CommandExecutor, osGetter OsGetter) {
}

func printHelp() {
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>

`)
fmt.Printf(
helpText, importExample, funcExample,
srcCommand, editorCommand, clearCommand,
resetCommand, exitCommand, printExample,
)
}
70 changes: 65 additions & 5 deletions gnovm/cmd/gno/repl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func TestReplApp(t *testing.T) {
}

func TestUpdateIndentLevel(t *testing.T) {
t.Parallel()
tests := []struct {
name string
line string
Expand Down Expand Up @@ -45,7 +46,25 @@ func TestUpdateIndentLevel(t *testing.T) {
name: "Test with colon",
line: "case 'a':",
indentLevel: 0,
want: 1,
want: 0,
},
{
name: "Test with colon and closed bracket",
line: "case 'a': }",
indentLevel: 1,
want: 0,
},
{
name: "Test with colon in string",
line: "\"case 'a':\"",
indentLevel: 0,
want: 0,
},
{
name: "Test with colon in string and string end with colon",
line: "case ':':",
indentLevel: 0,
want: 0,
},
{
name: "Test with multiple open brackets",
Expand Down Expand Up @@ -78,17 +97,55 @@ func TestUpdateIndentLevel(t *testing.T) {
want: 1,
},
{
name: "Test with colon and closed bracket",
line: "case 'a': }",
indentLevel: 1,
name: "Test with brackets in string",
line: "\"}}}}\"",
indentLevel: 0,
want: 0,
},
{
name: "Test with brackets in single line comment",
line: "// { [ (",
indentLevel: 0,
want: 0,
},
{
name: "Test with brackets in multi line comment",
line: "/* {{{{ */",
indentLevel: 0,
want: 0,
},
{
name: "Test with brackets in string and comment",
line: "ufmt.Println(\"{ [ ( ) ] } {{\") // { [ ( ) ] ",
indentLevel: 0,
want: 0,
},
{
name: "Test string and single line comment",
line: "CurlyToken = '{' // {",
indentLevel: 0,
want: 0,
},
{
name: "Test curly bracket in string",
line: "a := '{'",
indentLevel: 0,
want: 0,
},
{
name: "Test curly bracket in string 2",
line: "a := \"{hello\"",
indentLevel: 0,
want: 0,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := updateIndentLevel(tt.line, tt.indentLevel); got != tt.want {
t.Errorf("updateIndentLevel() = %v, want %v", got, tt.want)
t.Errorf("%s = %v, want %v", tt.name, got, tt.want)
}
})
}
Expand All @@ -112,6 +169,7 @@ func (m MockOSGetter) Get() string {
}

func TestClearScreen(t *testing.T) {
t.Parallel()
tests := []struct {
name string
osGetter OsGetter
Expand All @@ -122,7 +180,9 @@ func TestClearScreen(t *testing.T) {
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Set up the mock executor
executor := &MockCommandExecutor{}

Expand Down
5 changes: 5 additions & 0 deletions gnovm/pkg/repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ type Repl struct {
stdin io.Reader
}

// Read implements io.Reader.
func (r *Repl) Read(p []byte) (n int, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this necessary? Things seem to work okay without it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, i added that line to test the raw terminal mode. i forget to remove this.

panic("unimplemented")
}

// NewRepl creates a Repl struct. It is able to process input source code and eventually run it.
func NewRepl(opts ...ReplOption) *Repl {
t := template.Must(template.New("tmpl").Parse(fileTemplate))
Expand Down
4 changes: 4 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading