From 5b57d24afd4e3dec029bc32528d2d6e4dc8a3e64 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Wed, 23 Oct 2024 08:51:01 -0500 Subject: textinput: make completions run async with cancellation Make the Completer interface accept a context.Context. Provide a cancellation feature on text input tab completion to cancel an inflight completion command. This is particularly useful for address book completion if the user has specified a network-accessing command, eg carddav-query. The command is started according to the completion delay, but is cancellable if another request comes in. We also check for cancellation after the request is complete to ensure we only show valid completion results. Changelog-changed: Tab completions for text fields are run asynchronously. In-flight requests are cancelled when new input arrives. Signed-off-by: Tim Culverhouse Acked-by: Robin Jarry --- lib/ui/textinput.go | 54 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 17 deletions(-) (limited to 'lib') diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go index d52859b7..08fc2c92 100644 --- a/lib/ui/textinput.go +++ b/lib/ui/textinput.go @@ -1,6 +1,7 @@ package ui import ( + "context" "math" "strings" "sync" @@ -28,7 +29,8 @@ type TextInput struct { text []vaxis.Character change []func(ti *TextInput) focusLost []func(ti *TextInput) - tabcomplete func(s string) ([]opt.Completion, string) + tabcomplete func(ctx context.Context, s string) ([]opt.Completion, string) + tabcompleteCancel context.CancelFunc completions []opt.Completion prefix string completeIndex int @@ -45,10 +47,11 @@ type TextInput struct { func NewTextInput(text string, ui *config.UIConfig) *TextInput { chars := vaxis.Characters(text) return &TextInput{ - cells: -1, - text: chars, - index: len(chars), - uiConfig: ui, + cells: -1, + text: chars, + index: len(chars), + uiConfig: ui, + tabcompleteCancel: func() {}, } } @@ -63,7 +66,7 @@ func (ti *TextInput) Prompt(prompt string) *TextInput { } func (ti *TextInput) TabComplete( - tabcomplete func(s string) ([]opt.Completion, string), + tabcomplete func(ctx context.Context, s string) ([]opt.Completion, string), d time.Duration, minChars int, key *config.KeyStroke, ) *TextInput { ti.tabcomplete = tabcomplete @@ -342,17 +345,34 @@ func (ti *TextInput) showCompletions(explicit bool) { // no completer return } - ti.completions, ti.prefix = ti.tabcomplete(ti.StringLeft()) - - if explicit && len(ti.completions) == 1 { - // automatically accept if there is only one choice - ti.completeIndex = 0 - ti.executeCompletion() - ti.invalidateCompletions() - } else { - ti.completeIndex = -1 - } - Invalidate() + if ti.tabcompleteCancel != nil { + // Cancel any inflight completions we currently have + ti.tabcompleteCancel() + } + ctx, cancel := context.WithCancel(context.Background()) + ti.tabcompleteCancel = cancel + go func() { + defer log.PanicHandler() + matches, prefix := ti.tabcomplete(ctx, ti.StringLeft()) + select { + case <-ctx.Done(): + return + default: + ti.Lock() + defer ti.Unlock() + ti.completions = matches + ti.prefix = prefix + if explicit && len(ti.completions) == 1 { + // automatically accept if there is only one choice + ti.completeIndex = 0 + ti.executeCompletion() + ti.invalidateCompletions() + } else { + ti.completeIndex = -1 + } + Invalidate() + } + }() } func (ti *TextInput) OnChange(onChange func(ti *TextInput)) { -- cgit