Skip to content

Commit b556a46

Browse files
authored
Merge pull request #19 from Syuparn/enable-cursor
enable to use cursor
2 parents f83fcfb + 566211d commit b556a46

File tree

6 files changed

+232
-14
lines changed

6 files changed

+232
-14
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ Hello, world!
4141
tmpl:4> ^C
4242
```
4343

44+
In REPL mode, you can use histories and function autocompletes. See [peterh/liner](https://github.com/peterh/liner) for details.
45+
4446
# functions
4547

4648
Functions in [Sprig](http://masterminds.github.io/sprig/) are available.

completer.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package main
2+
3+
import (
4+
"strings"
5+
6+
"github.com/peterh/liner"
7+
)
8+
9+
func newWordCompleter() liner.WordCompleter {
10+
return func(line string, pos int) (head string, completions []string, tail string) {
11+
head, partial, tail := splitLine(line, pos)
12+
13+
if partial == "" {
14+
// match to nothing
15+
return
16+
}
17+
18+
// function auto-completes
19+
for name := range funcMap() {
20+
if strings.HasPrefix(name, partial) {
21+
completions = append(completions, name)
22+
}
23+
}
24+
return
25+
}
26+
}
27+
28+
func splitLine(line string, pos int) (head, partial, tail string) {
29+
startPos := partialStartPos(line, pos, "{}()<>=| ")
30+
endPos := partialEndPos(line, pos, "{}()<>=| ")
31+
32+
head = subStr(line, 0, startPos)
33+
tail = subStr(line, endPos, len(line))
34+
partial = subStr(line, startPos, endPos)
35+
return
36+
}
37+
38+
func partialStartPos(line string, pos int, delims string) int {
39+
delimPos := strings.LastIndexAny(subStr(line, 0, pos), delims)
40+
if delimPos < 0 {
41+
return 0
42+
}
43+
return delimPos + 1
44+
}
45+
46+
func partialEndPos(line string, pos int, delims string) int {
47+
relativeDelimPos := strings.IndexAny(subStr(line, pos, len(line)), delims)
48+
if relativeDelimPos < 0 {
49+
return len(line)
50+
}
51+
return pos + relativeDelimPos
52+
}
53+
54+
func subStr(str string, start, end int) string {
55+
if start >= len(str) {
56+
return ""
57+
}
58+
if end <= 0 {
59+
return ""
60+
}
61+
62+
if end > len(str) {
63+
end = len(str)
64+
}
65+
if start < 0 {
66+
start = 0
67+
}
68+
69+
return str[start:end]
70+
}

completer_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestNewCompleter(t *testing.T) {
10+
c := newWordCompleter()
11+
12+
type completion struct {
13+
head string
14+
completions []string
15+
tail string
16+
}
17+
18+
tests := []struct {
19+
title string
20+
line string
21+
pos int
22+
expected completion
23+
}{
24+
{
25+
"empty",
26+
"",
27+
0,
28+
completion{
29+
head: "",
30+
completions: []string{},
31+
tail: "",
32+
},
33+
},
34+
{
35+
"only partials",
36+
"low",
37+
3,
38+
completion{
39+
head: "",
40+
completions: []string{"lower"},
41+
tail: "",
42+
},
43+
},
44+
{
45+
"cursor is not at the end",
46+
"low",
47+
1,
48+
completion{
49+
head: "",
50+
completions: []string{"lower"},
51+
tail: "",
52+
},
53+
},
54+
{
55+
"two tokens",
56+
"if low",
57+
6,
58+
completion{
59+
head: "if ",
60+
completions: []string{"lower"},
61+
tail: "",
62+
},
63+
},
64+
{
65+
"two tokens and cursor in the middle",
66+
"if low",
67+
3,
68+
completion{
69+
head: "if ",
70+
completions: []string{"lower"},
71+
tail: "",
72+
},
73+
},
74+
{
75+
"cursor points non-function",
76+
"if low",
77+
0,
78+
completion{
79+
head: "",
80+
completions: []string{},
81+
tail: " low",
82+
},
83+
},
84+
{
85+
"many tokens (cursor at the end)",
86+
"1 | add int6",
87+
12,
88+
completion{
89+
head: "1 | add ",
90+
completions: []string{"int64"},
91+
tail: "",
92+
},
93+
},
94+
{
95+
"many tokens (cursor in the middle)",
96+
"1 | ad int64",
97+
4,
98+
completion{
99+
head: "1 | ",
100+
completions: []string{"add", "add1f", "add1", "addf", "adler32sum"},
101+
tail: " int64",
102+
},
103+
},
104+
}
105+
106+
for _, tt := range tests {
107+
t.Run(tt.title, func(t *testing.T) {
108+
head, completions, tail := c(tt.line, tt.pos)
109+
110+
assert.Equal(t, tt.expected.head, head, "wrong head")
111+
assert.ElementsMatch(t, tt.expected.completions, completions, "wrong completions")
112+
assert.Equal(t, tt.expected.tail, tail, "wrong tail")
113+
})
114+
}
115+
}
116+
117+
func allFuncNames() []string {
118+
names := []string{}
119+
for name := range funcMap() {
120+
names = append(names, name)
121+
}
122+
return names
123+
}

go.mod

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,23 @@ require (
77
github.com/google/uuid v1.2.0 // indirect
88
github.com/huandu/xstrings v1.3.2 // indirect
99
github.com/mitchellh/copystructure v1.1.1 // indirect
10-
github.com/stretchr/testify v1.7.0 // indirect
10+
github.com/stretchr/testify v1.7.0
1111
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
1212
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
1313
)
1414

15+
require github.com/peterh/liner v1.2.2
16+
1517
require (
1618
github.com/Masterminds/goutils v1.1.1 // indirect
1719
github.com/Masterminds/semver/v3 v3.1.1 // indirect
20+
github.com/davecgh/go-spew v1.1.1 // indirect
1821
github.com/imdario/mergo v0.3.11 // indirect
22+
github.com/mattn/go-runewidth v0.0.3 // indirect
1923
github.com/mitchellh/reflectwalk v1.0.1 // indirect
24+
github.com/pmezard/go-difflib v1.0.0 // indirect
2025
github.com/shopspring/decimal v1.2.0 // indirect
2126
github.com/spf13/cast v1.3.1 // indirect
27+
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
28+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
2229
)

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw
1515
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
1616
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
1717
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
18+
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
19+
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
1820
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
1921
github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4=
2022
github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4=
2123
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
2224
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
2325
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
26+
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
27+
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
2428
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2529
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2630
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
@@ -40,10 +44,13 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
4044
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
4145
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4246
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
47+
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
48+
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4349
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
4450
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
4551
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
4652
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
53+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
4754
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4855
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
4956
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=

main.go

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"text/template"
1212

1313
"golang.org/x/xerrors"
14+
15+
"github.com/peterh/liner"
1416
)
1517

1618
var (
@@ -85,28 +87,35 @@ func runPipeMode(tmplGen *template.Template, tmplStr string) error {
8587
}
8688

8789
func runREPLMode(tmplGen *template.Template) error {
88-
scanner := bufio.NewScanner(os.Stdin)
90+
line := liner.NewLiner()
91+
defer line.Close()
92+
93+
line.SetCtrlCAborts(true)
94+
line.SetWordCompleter(newWordCompleter())
95+
8996
tmplStr := ""
9097
lineNum := 1
9198

9299
for {
93-
// show prompt
94-
fmt.Printf("tmpl:%d> ", lineNum)
95-
96-
ok := scanner.Scan()
97-
if !ok {
98-
// end of repl
99-
// HACK: prepend \n to break line even if repl is stopped by SIGINT
100-
fmt.Println("\nBye.")
101-
return nil
100+
inputStr, err := line.Prompt(fmt.Sprintf("tmpl:%d> ", lineNum))
101+
if err != nil {
102+
if err == liner.ErrPromptAborted {
103+
// end of repl
104+
// HACK: prepend \n to break line even if repl is stopped by SIGINT
105+
fmt.Println("\nBye.")
106+
return nil
107+
}
108+
fmt.Fprintf(os.Stderr, "input error:\n%v\n", err)
109+
continue
102110
}
103111

104-
line := scanner.Text() + "\n"
112+
line.AppendHistory(inputStr)
113+
newLine := inputStr + "\n"
105114

106115
// NOTE: whole history is necessary to refer previous variable statement
107116
// HACK: insert "\034"(, which is not printable) to detect
108117
// output generated by the latest input line
109-
tmpl, err := tmplGen.Parse(tmplStr + "\034" + line)
118+
tmpl, err := tmplGen.Parse(tmplStr + "\034" + newLine)
110119
if err != nil {
111120
fmt.Fprintf(os.Stderr, "template error:\n%v\n", err)
112121
continue
@@ -129,7 +138,7 @@ func runREPLMode(tmplGen *template.Template) error {
129138
diffStr := splitted[1]
130139
fmt.Println(diffStr)
131140

132-
tmplStr = tmplStr + line
141+
tmplStr = tmplStr + newLine
133142
lineNum++
134143
}
135144
}

0 commit comments

Comments
 (0)