Skip to content
This repository was archived by the owner on Apr 19, 2024. It is now read-only.

Commit 3ec04a9

Browse files
(fix): arrow key behavior on input prompt (#361)
* first pass at input using RuneReader.ReadLine with suggestions * fix newline from readline * better callback name * (fix): skip tests with editor * (feat): allow prompt callback on rune input and initial input * (fix): fallback to ReadLine when not using "auto complete" * (feat): tests input navagation * (fix): suggestions can be dismissed * (fix): editor blocking testing Co-authored-by: Alec Aivazis <alec@aivazis.com>
1 parent c5bc9bf commit 3ec04a9

File tree

6 files changed

+174
-98
lines changed

6 files changed

+174
-98
lines changed

_tasks.hcl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
task "install-deps" {
22
description = "Install all of package dependencies"
33
pipeline = [
4-
"go get {{.files}}",
4+
"go get -v {{.files}}",
55
]
66
}
77

88
task "tests" {
99
description = "Run the test suite"
10-
command = "go test {{.files}}"
10+
command = "go test -v {{.files}}"
1111
environment = {
1212
GOFLAGS = "-mod=vendor"
1313
}

editor_test.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,6 @@ func TestEditorRender(t *testing.T) {
102102
}
103103

104104
func TestEditorPrompt(t *testing.T) {
105-
if os.Getenv("SKIP_EDITOR_PROMPT_TESTS") != "" {
106-
t.Skip("editor prompt tests skipped by dev")
107-
}
108-
109105
if _, err := exec.LookPath("vi"); err != nil {
110106
t.Skip("vi not found in PATH")
111107
}
@@ -122,7 +118,7 @@ func TestEditorPrompt(t *testing.T) {
122118
c.SendLine("")
123119
go c.ExpectEOF()
124120
time.Sleep(time.Millisecond)
125-
c.Send("iAdd editor prompt tests\x1b")
121+
c.Send("ccAdd editor prompt tests\x1b")
126122
c.SendLine(":wq!")
127123
},
128124
"Add editor prompt tests\n",
@@ -155,7 +151,7 @@ func TestEditorPrompt(t *testing.T) {
155151
c.SendLine("")
156152
go c.ExpectEOF()
157153
time.Sleep(time.Millisecond)
158-
c.Send("iAdd editor prompt tests\x1b")
154+
c.Send("ccAdd editor prompt tests\x1b")
159155
c.SendLine(":wq!")
160156
},
161157
"Add editor prompt tests\n",
@@ -196,7 +192,7 @@ func TestEditorPrompt(t *testing.T) {
196192
c.SendLine("")
197193
go c.ExpectEOF()
198194
time.Sleep(time.Millisecond)
199-
c.Send("iAdd editor prompt tests\x1b")
195+
c.Send("ccAdd editor prompt tests\x1b")
200196
c.SendLine(":wq!")
201197
},
202198
"Add editor prompt tests\n",
@@ -230,7 +226,7 @@ func TestEditorPrompt(t *testing.T) {
230226
c.SendLine("")
231227
go c.ExpectEOF()
232228
time.Sleep(time.Millisecond)
233-
c.Send("iAdd editor prompt tests\x1b")
229+
c.Send("ccAdd editor prompt tests\x1b")
234230
c.SendLine(":wq!")
235231
},
236232
"Add editor prompt tests\n",

input.go

Lines changed: 87 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package survey
22

33
import (
4+
"errors"
5+
46
"github.com/AlecAivazis/survey/v2/core"
57
"github.com/AlecAivazis/survey/v2/terminal"
68
)
@@ -19,8 +21,8 @@ type Input struct {
1921
Default string
2022
Help string
2123
Suggest func(toComplete string) []string
22-
typedAnswer string
2324
answer string
25+
typedAnswer string
2426
options []core.OptionAnswer
2527
selectedIndex int
2628
showingHelp bool
@@ -58,86 +60,90 @@ var InputQuestionTemplate = `
5860
{{- if and .Suggest }}{{color "cyan"}}{{ print .Config.SuggestInput }} for suggestions{{end -}}
5961
]{{color "reset"}} {{end}}
6062
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
61-
{{- .Answer -}}
6263
{{- end}}`
6364

64-
func (i *Input) OnChange(key rune, config *PromptConfig) (bool, error) {
65-
if key == terminal.KeyEnter || key == '\n' {
66-
if i.answer != config.HelpInput || i.Help == "" {
67-
// we're done
68-
return true, nil
69-
} else {
70-
i.answer = ""
71-
i.showingHelp = true
72-
}
73-
} else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
74-
i.answer = ""
75-
} else if key == terminal.KeyEscape && i.Suggest != nil {
76-
if len(i.options) > 0 {
65+
func (i *Input) onRune(config *PromptConfig) terminal.OnRuneFn {
66+
return terminal.OnRuneFn(func(key rune, line []rune) ([]rune, bool, error) {
67+
if i.options != nil && (key == terminal.KeyEnter || key == '\n') {
68+
return []rune(i.answer), true, nil
69+
} else if i.options != nil && key == terminal.KeyEscape {
7770
i.answer = i.typedAnswer
78-
}
79-
i.options = nil
80-
} else if key == terminal.KeyArrowUp && len(i.options) > 0 {
81-
if i.selectedIndex == 0 {
82-
i.selectedIndex = len(i.options) - 1
83-
} else {
84-
i.selectedIndex--
85-
}
86-
i.answer = i.options[i.selectedIndex].Value
87-
} else if (key == terminal.KeyArrowDown || key == terminal.KeyTab) && len(i.options) > 0 {
88-
if i.selectedIndex == len(i.options)-1 {
71+
i.options = nil
72+
} else if key == terminal.KeyArrowUp && len(i.options) > 0 {
73+
if i.selectedIndex == 0 {
74+
i.selectedIndex = len(i.options) - 1
75+
} else {
76+
i.selectedIndex--
77+
}
78+
i.answer = i.options[i.selectedIndex].Value
79+
} else if (key == terminal.KeyArrowDown || key == terminal.KeyTab) && len(i.options) > 0 {
80+
if i.selectedIndex == len(i.options)-1 {
81+
i.selectedIndex = 0
82+
} else {
83+
i.selectedIndex++
84+
}
85+
i.answer = i.options[i.selectedIndex].Value
86+
} else if key == terminal.KeyTab && i.Suggest != nil {
87+
i.answer = string(line)
88+
i.typedAnswer = i.answer
89+
options := i.Suggest(i.answer)
8990
i.selectedIndex = 0
90-
} else {
91-
i.selectedIndex++
92-
}
93-
i.answer = i.options[i.selectedIndex].Value
94-
} else if key == terminal.KeyTab && i.Suggest != nil {
95-
options := i.Suggest(i.answer)
96-
i.selectedIndex = 0
97-
i.typedAnswer = i.answer
98-
if len(options) > 0 {
91+
if len(options) == 0 {
92+
return line, false, nil
93+
}
94+
9995
i.answer = options[0]
10096
if len(options) == 1 {
97+
i.typedAnswer = i.answer
10198
i.options = nil
10299
} else {
103100
i.options = core.OptionAnswerList(options)
104101
}
102+
} else {
103+
if i.options == nil {
104+
return line, false, nil
105+
}
106+
107+
if key >= terminal.KeySpace {
108+
i.answer += string(key)
109+
}
110+
i.typedAnswer = i.answer
111+
112+
i.options = nil
105113
}
106-
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
107-
if i.answer != "" {
108-
runeAnswer := []rune(i.answer)
109-
i.answer = string(runeAnswer[0 : len(runeAnswer)-1])
110-
}
111-
} else if key >= terminal.KeySpace {
112-
i.answer += string(key)
113-
i.typedAnswer = i.answer
114-
i.options = nil
115-
}
116114

117-
pageSize := config.PageSize
118-
opts, idx := paginate(pageSize, i.options, i.selectedIndex)
119-
err := i.Render(
120-
InputQuestionTemplate,
121-
InputTemplateData{
122-
Input: *i,
123-
Answer: i.answer,
124-
ShowHelp: i.showingHelp,
125-
SelectedIndex: idx,
126-
PageEntries: opts,
127-
Config: config,
128-
},
129-
)
115+
pageSize := config.PageSize
116+
opts, idx := paginate(pageSize, i.options, i.selectedIndex)
117+
err := i.Render(
118+
InputQuestionTemplate,
119+
InputTemplateData{
120+
Input: *i,
121+
Answer: i.answer,
122+
ShowHelp: i.showingHelp,
123+
SelectedIndex: idx,
124+
PageEntries: opts,
125+
Config: config,
126+
},
127+
)
128+
129+
if err == nil {
130+
err = readLineAgain
131+
}
130132

131-
return err != nil, err
133+
return []rune(i.typedAnswer), true, err
134+
})
132135
}
133136

137+
var readLineAgain = errors.New("read line again")
138+
134139
func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
135140
// render the template
136141
err := i.Render(
137142
InputQuestionTemplate,
138143
InputTemplateData{
139-
Input: *i,
140-
Config: config,
144+
Input: *i,
145+
Config: config,
146+
ShowHelp: i.showingHelp,
141147
},
142148
)
143149
if err != nil {
@@ -155,27 +161,34 @@ func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
155161
defer cursor.Show() // show the cursor when we're done
156162
}
157163

158-
// start waiting for input
164+
var line []rune
165+
159166
for {
160-
r, _, err := rr.ReadRune()
161-
if err != nil {
162-
return "", err
167+
if i.options != nil {
168+
line = []rune{}
163169
}
164-
if r == terminal.KeyInterrupt {
165-
return "", terminal.InterruptErr
166-
}
167-
if r == terminal.KeyEndTransmission {
168-
break
170+
171+
line, err = rr.ReadLineWithDefault(0, line, i.onRune(config))
172+
if err == readLineAgain {
173+
continue
169174
}
170175

171-
b, err := i.OnChange(r, config)
172176
if err != nil {
173177
return "", err
174178
}
175179

176-
if b {
177-
break
178-
}
180+
break
181+
}
182+
183+
i.answer = string(line)
184+
// readline print an empty line, go up before we render the follow up
185+
cursor.Up(1)
186+
187+
// if we ran into the help string
188+
if i.answer == config.HelpInput && i.Help != "" {
189+
// show the help and prompt again
190+
i.showingHelp = true
191+
return i.Prompt(config)
179192
}
180193

181194
// if the line is empty

input_test.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,6 @@ func TestInputRender(t *testing.T) {
102102
defaultIcons().Question.Text, defaultPromptConfig().Icons.SelectFocus.Text,
103103
),
104104
},
105-
{
106-
"Test Input question output with suggestion complemented",
107-
Input{Message: "What is your favorite month:", Suggest: suggestFn},
108-
InputTemplateData{Answer: "February and"},
109-
fmt.Sprintf(
110-
"%s What is your favorite month: [%s for suggestions] February and",
111-
defaultIcons().Question.Text, defaultPromptConfig().SuggestInput,
112-
),
113-
},
114105
}
115106

116107
for _, test := range tests {
@@ -377,6 +368,54 @@ func TestInputPrompt(t *testing.T) {
377368
},
378369
"special answer",
379370
},
371+
{
372+
"Test Input prompt must allow moving cursor using right and left arrows",
373+
&Input{Message: "Filename to save:"},
374+
func(c *expect.Console) {
375+
c.ExpectString("Filename to save:")
376+
c.Send("essay.txt")
377+
c.Send(string(terminal.KeyArrowLeft))
378+
c.Send(string(terminal.KeyArrowLeft))
379+
c.Send(string(terminal.KeyArrowLeft))
380+
c.Send(string(terminal.KeyArrowLeft))
381+
c.Send("_final")
382+
c.Send(string(terminal.KeyArrowRight))
383+
c.Send(string(terminal.KeyArrowRight))
384+
c.Send(string(terminal.KeyArrowRight))
385+
c.Send(string(terminal.KeyArrowRight))
386+
c.Send(string(terminal.KeyBackspace))
387+
c.Send(string(terminal.KeyBackspace))
388+
c.Send(string(terminal.KeyBackspace))
389+
c.Send("md")
390+
c.Send(string(terminal.KeyArrowLeft))
391+
c.Send(string(terminal.KeyArrowLeft))
392+
c.Send(string(terminal.KeyArrowLeft))
393+
c.SendLine("2")
394+
c.ExpectEOF()
395+
},
396+
"essay_final2.md",
397+
},
398+
{
399+
"Test Input prompt must allow moving cursor using right and left arrows, even after suggestions",
400+
&Input{Message: "Filename to save:", Suggest: func(string) []string { return []string{".txt", ".csv", ".go"} }},
401+
func(c *expect.Console) {
402+
c.ExpectString("Filename to save:")
403+
c.Send(string(terminal.KeyTab))
404+
c.ExpectString(".txt")
405+
c.ExpectString(".csv")
406+
c.ExpectString(".go")
407+
c.Send(string(terminal.KeyTab))
408+
c.Send(string(terminal.KeyArrowLeft))
409+
c.Send(string(terminal.KeyArrowLeft))
410+
c.Send(string(terminal.KeyArrowLeft))
411+
c.Send(string(terminal.KeyArrowLeft))
412+
c.Send(string(terminal.KeyArrowLeft))
413+
c.Send("newtable")
414+
c.SendLine("")
415+
c.ExpectEOF()
416+
},
417+
"newtable.csv",
418+
},
380419
}
381420

382421
for _, test := range tests {

survey_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ func TestAsk(t *testing.T) {
206206
c.ExpectString("Edit git commit message [Enter to launch editor]")
207207
c.SendLine("")
208208
time.Sleep(time.Millisecond)
209-
c.Send("iAdd editor prompt tests\x1b")
209+
c.Send("ccAdd editor prompt tests\x1b")
210210
c.SendLine(":wq!")
211211

212212
// Editor validated
@@ -221,7 +221,7 @@ func TestAsk(t *testing.T) {
221221
c.SendLine("")
222222
time.Sleep(time.Millisecond)
223223
c.ExpectString("first try")
224-
c.Send("ccAdd editor prompt tests\x1b")
224+
c.Send("ccAdd editor prompt tests, but validated\x1b")
225225
c.SendLine(":wq!")
226226

227227
// Input
@@ -253,7 +253,7 @@ func TestAsk(t *testing.T) {
253253
map[string]interface{}{
254254
"pizza": true,
255255
"commit-message": "Add editor prompt tests\n",
256-
"commit-message-validated": "Add editor prompt tests\n",
256+
"commit-message-validated": "Add editor prompt tests, but validated\n",
257257
"name": "Johnny Appleseed",
258258
/* TODO
259259
"day": []string{"Monday", "Wednesday"},

0 commit comments

Comments
 (0)