From cded067bc3919a77b17feedd877e4590e7c95f4a Mon Sep 17 00:00:00 2001 From: Jeffas Date: Fri, 26 Jul 2019 14:29:40 +0100 Subject: Add tab completion to textinputs This adds tab completion to textinput components. They can be configured with a completion function. This function is called when the user presses . The first completion is initially shown to the user inserted into the text. Repeated presses of or cycle through the completions list. The completions list is invalidated when any other non-tab-like key is pressed. Also changed is some logic for current completion generation so that all available commands are returned when is pressed with no current text and similarly for arguments of commands. --- lib/ui/textinput.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 10 deletions(-) (limited to 'lib/ui/textinput.go') diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go index 2feeb842..e5a23379 100644 --- a/lib/ui/textinput.go +++ b/lib/ui/textinput.go @@ -5,20 +5,23 @@ import ( "github.com/mattn/go-runewidth" ) -// TODO: Attach history and tab completion providers +// TODO: Attach history providers // TODO: scrolling type TextInput struct { Invalidatable - cells int - ctx *Context - focus bool - index int - password bool - prompt string - scroll int - text []rune - change []func(ti *TextInput) + cells int + ctx *Context + focus bool + index int + password bool + prompt string + scroll int + text []rune + change []func(ti *TextInput) + tabcomplete func(s string) []string + completions []string + completeIndex int } // Creates a new TextInput. TextInputs will render a "textbox" in the entire @@ -42,6 +45,12 @@ func (ti *TextInput) Prompt(prompt string) *TextInput { return ti } +func (ti *TextInput) TabComplete( + tabcomplete func(s string) []string) *TextInput { + ti.tabcomplete = tabcomplete + return ti +} + func (ti *TextInput) String() string { return string(ti.text) } @@ -161,6 +170,41 @@ func (ti *TextInput) backspace() { } } +func (ti *TextInput) nextCompletion() { + if ti.completions == nil { + if ti.tabcomplete == nil { + return + } + ti.completions = ti.tabcomplete(ti.StringLeft()) + ti.completeIndex = 0 + } else { + ti.completeIndex++ + if ti.completeIndex >= len(ti.completions) { + ti.completeIndex = 0 + } + } + if len(ti.completions) > 0 { + ti.Set(ti.completions[ti.completeIndex] + ti.StringRight()) + } +} + +func (ti *TextInput) previousCompletion() { + if ti.completions == nil || len(ti.completions) == 0 { + return + } + ti.completeIndex-- + if ti.completeIndex < 0 { + ti.completeIndex = len(ti.completions) - 1 + } + if len(ti.completions) > 0 { + ti.Set(ti.completions[ti.completeIndex] + ti.StringRight()) + } +} + +func (ti *TextInput) invalidateCompletions() { + ti.completions = nil +} + func (ti *TextInput) onChange() { for _, change := range ti.change { change(ti) @@ -176,32 +220,52 @@ func (ti *TextInput) Event(event tcell.Event) bool { case *tcell.EventKey: switch event.Key() { case tcell.KeyBackspace, tcell.KeyBackspace2: + ti.invalidateCompletions() ti.backspace() case tcell.KeyCtrlD, tcell.KeyDelete: + ti.invalidateCompletions() ti.deleteChar() case tcell.KeyCtrlB, tcell.KeyLeft: + ti.invalidateCompletions() if ti.index > 0 { ti.index-- ti.ensureScroll() ti.Invalidate() } case tcell.KeyCtrlF, tcell.KeyRight: + ti.invalidateCompletions() if ti.index < len(ti.text) { ti.index++ ti.ensureScroll() ti.Invalidate() } case tcell.KeyCtrlA, tcell.KeyHome: + ti.invalidateCompletions() ti.index = 0 ti.ensureScroll() ti.Invalidate() case tcell.KeyCtrlE, tcell.KeyEnd: + ti.invalidateCompletions() ti.index = len(ti.text) ti.ensureScroll() ti.Invalidate() case tcell.KeyCtrlW: + ti.invalidateCompletions() ti.deleteWord() + case tcell.KeyTab: + if ti.tabcomplete != nil { + ti.nextCompletion() + } else { + ti.insert('\t') + } + ti.Invalidate() + case tcell.KeyBacktab: + if ti.tabcomplete != nil { + ti.previousCompletion() + } + ti.Invalidate() case tcell.KeyRune: + ti.invalidateCompletions() ti.insert(event.Rune()) } } -- cgit