From cded067bc3919a77b17feedd877e4590e7c95f4a Mon Sep 17 00:00:00 2001 From: Jeffas Date: Fri, 26 Jul 2019 14:29:40 +0100 Subject: [PATCH] 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. --- commands/commands.go | 17 +++++++-- commands/ct.go | 3 ++ lib/ui/textinput.go | 84 ++++++++++++++++++++++++++++++++++++++------ widgets/exline.go | 8 +---- 4 files changed, 92 insertions(+), 20 deletions(-) diff --git a/commands/commands.go b/commands/commands.go index c6f149f..3f7fbcd 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -2,6 +2,7 @@ package commands import ( "errors" + "sort" "strings" "unicode" @@ -73,12 +74,19 @@ func (cmds *Commands) GetCompletions(aerc *widgets.Aerc, cmd string) []string { } if len(args) == 0 { - return nil + names := cmds.Names() + sort.Strings(names) + return names } - if len(args) > 1 { + if len(args) > 1 || cmd[len(cmd)-1] == ' ' { if cmd, ok := cmds.dict()[args[0]]; ok { - completions := cmd.Complete(aerc, args[1:]) + var completions []string + if len(args) > 1 { + completions = cmd.Complete(aerc, args[1:]) + } else { + completions = cmd.Complete(aerc, []string{}) + } if completions != nil && len(completions) == 0 { return nil } @@ -109,6 +117,9 @@ func (cmds *Commands) GetCompletions(aerc *widgets.Aerc, cmd string) []string { func GetFolders(aerc *widgets.Aerc, args []string) []string { out := make([]string, 0) lower_only := false + if len(args) == 0 { + return aerc.SelectedAccount().Directories().List() + } for _, rune := range args[0] { lower_only = lower_only || unicode.IsLower(rune) } diff --git a/commands/ct.go b/commands/ct.go index ab2993d..19fb63a 100644 --- a/commands/ct.go +++ b/commands/ct.go @@ -19,6 +19,9 @@ func (_ ChangeTab) Aliases() []string { } func (_ ChangeTab) Complete(aerc *widgets.Aerc, args []string) []string { + if len(args) == 0 { + return aerc.TabNames() + } out := make([]string, 0) for _, tab := range aerc.TabNames() { if strings.HasPrefix(tab, args[0]) { diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go index 2feeb84..e5a2337 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()) } } diff --git a/widgets/exline.go b/widgets/exline.go index b7b4e3d..4791ae9 100644 --- a/widgets/exline.go +++ b/widgets/exline.go @@ -20,7 +20,7 @@ func NewExLine(commit func(cmd string), cancel func(), tabcomplete func(cmd string) []string, cmdHistory lib.History) *ExLine { - input := ui.NewTextInput("").Prompt(":") + input := ui.NewTextInput("").Prompt(":").TabComplete(tabcomplete) exline := &ExLine{ cancel: cancel, commit: commit, @@ -64,12 +64,6 @@ func (ex *ExLine) Event(event tcell.Event) bool { ex.input.Focus(false) ex.cmdHistory.Reset() ex.cancel() - case tcell.KeyTab: - complete := ex.tabcomplete(ex.input.StringLeft()) - if len(complete) == 1 { - ex.input.Set(complete[0] + " " + ex.input.StringRight()) - } - ex.Invalidate() default: return ex.input.Event(event) }