diff options
-rw-r--r-- | app/aerc.go | 15 | ||||
-rw-r--r-- | app/app.go | 3 | ||||
-rw-r--r-- | app/exline.go | 10 | ||||
-rw-r--r-- | commands/completion_helpers.go | 3 | ||||
-rw-r--r-- | commands/compose/send.go | 3 | ||||
-rw-r--r-- | completer/completer.go | 17 | ||||
-rw-r--r-- | lib/ui/textinput.go | 54 | ||||
-rw-r--r-- | main.go | 2 |
8 files changed, 67 insertions, 40 deletions
diff --git a/app/aerc.go b/app/aerc.go index 72a60562..12bb6893 100644 --- a/app/aerc.go +++ b/app/aerc.go @@ -1,6 +1,7 @@ package app import ( + "context" "errors" "fmt" "io" @@ -29,7 +30,7 @@ type Aerc struct { accounts map[string]*AccountView cmd func(string, *config.AccountConfig, *models.MessageInfo) error cmdHistory lib.History - complete func(cmd string) ([]opt.Completion, string) + complete func(ctx context.Context, cmd string) ([]opt.Completion, string) focused ui.Interactive grid *ui.Grid simulating int @@ -54,7 +55,7 @@ type Choice struct { func (aerc *Aerc) Init( crypto crypto.Provider, cmd func(string, *config.AccountConfig, *models.MessageInfo) error, - complete func(cmd string) ([]opt.Completion, string), cmdHistory lib.History, + complete func(ctx context.Context, cmd string) ([]opt.Completion, string), cmdHistory lib.History, deferLoop chan struct{}, ) { tabs := ui.NewTabs(func(d ui.Drawable) *config.UIConfig { @@ -317,8 +318,8 @@ func (aerc *Aerc) simulate(strokes []config.KeyStroke) { aerc.simulating -= 1 if exline, ok := aerc.focused.(*ExLine); ok { // we are still focused on the exline, turn on tab complete - exline.TabComplete(func(cmd string) ([]opt.Completion, string) { - return aerc.complete(cmd) + exline.TabComplete(func(ctx context.Context, cmd string) ([]opt.Completion, string) { + return aerc.complete(ctx, cmd) }) if complete { // force completion now @@ -633,7 +634,7 @@ func (aerc *Aerc) focus(item ui.Interactive) { func (aerc *Aerc) BeginExCommand(cmd string) { previous := aerc.focused - var tabComplete func(string) ([]opt.Completion, string) + var tabComplete func(context.Context, string) ([]opt.Completion, string) if aerc.simulating != 0 { // Don't try to draw completions for simulated events tabComplete = nil @@ -671,7 +672,7 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd string) { if err != nil { aerc.PushError(err.Error()) } - }, func(cmd string) ([]opt.Completion, string) { + }, func(ctx context.Context, cmd string) ([]opt.Completion, string) { return nil, "" // TODO: completions }) aerc.prompts.Push(p) @@ -698,7 +699,7 @@ func (aerc *Aerc) RegisterChoices(choices []Choice) { if err != nil { aerc.PushError(err.Error()) } - }, func(cmd string) ([]opt.Completion, string) { + }, func(ctx context.Context, cmd string) ([]opt.Completion, string) { return nil, "" // TODO: completions }) aerc.prompts.Push(p) @@ -1,6 +1,7 @@ package app import ( + "context" "time" "git.sr.ht/~rjarry/aerc/config" @@ -19,7 +20,7 @@ var aerc Aerc func Init( crypto crypto.Provider, cmd func(string, *config.AccountConfig, *models.MessageInfo) error, - complete func(cmd string) ([]opt.Completion, string), history lib.History, + complete func(ctx context.Context, cmd string) ([]opt.Completion, string), history lib.History, deferLoop chan struct{}, ) { aerc.Init(crypto, cmd, complete, history, deferLoop) diff --git a/app/exline.go b/app/exline.go index 5f8e4280..20be8a4b 100644 --- a/app/exline.go +++ b/app/exline.go @@ -1,6 +1,8 @@ package app import ( + "context" + "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib/ui" @@ -11,13 +13,13 @@ import ( type ExLine struct { commit func(cmd string) finish func() - tabcomplete func(cmd string) ([]opt.Completion, string) + tabcomplete func(ctx context.Context, cmd string) ([]opt.Completion, string) cmdHistory lib.History input *ui.TextInput } func NewExLine(cmd string, commit func(cmd string), finish func(), - tabcomplete func(cmd string) ([]opt.Completion, string), + tabcomplete func(ctx context.Context, cmd string) ([]opt.Completion, string), cmdHistory lib.History, ) *ExLine { input := ui.NewTextInput("", config.Ui).Prompt(":").Set(cmd) @@ -39,7 +41,7 @@ func NewExLine(cmd string, commit func(cmd string), finish func(), return exline } -func (x *ExLine) TabComplete(tabComplete func(string) ([]opt.Completion, string)) { +func (x *ExLine) TabComplete(tabComplete func(context.Context, string) ([]opt.Completion, string)) { x.input.TabComplete( tabComplete, config.Ui.CompletionDelay, @@ -49,7 +51,7 @@ func (x *ExLine) TabComplete(tabComplete func(string) ([]opt.Completion, string) } func NewPrompt(prompt string, commit func(text string), - tabcomplete func(cmd string) ([]opt.Completion, string), + tabcomplete func(ctx context.Context, cmd string) ([]opt.Completion, string), ) *ExLine { input := ui.NewTextInput("", config.Ui).Prompt(prompt) if config.Ui.CompletionPopovers { diff --git a/commands/completion_helpers.go b/commands/completion_helpers.go index 8d293a32..6017035e 100644 --- a/commands/completion_helpers.go +++ b/commands/completion_helpers.go @@ -1,6 +1,7 @@ package commands import ( + "context" "fmt" "net/mail" "strings" @@ -30,7 +31,7 @@ func GetAddress(search string) []string { }) if cmpl != nil { - addrList, _ := cmpl.ForHeader("to")(search) + addrList, _ := cmpl.ForHeader("to")(context.Background(), search) for _, full := range addrList { addr, err := mail.ParseAddress(full.Value) if err != nil { diff --git a/commands/compose/send.go b/commands/compose/send.go index 73e3e13a..f9faf1b5 100644 --- a/commands/compose/send.go +++ b/commands/compose/send.go @@ -2,6 +2,7 @@ package compose import ( "bytes" + "context" "fmt" "io" "net/url" @@ -147,7 +148,7 @@ func (s Send) Execute(args []string) error { from, rcpts, tab.Name, s.CopyTo, s.Archive, copyToReplied) } - }, func(cmd string) ([]opt.Completion, string) { + }, func(ctx context.Context, cmd string) ([]opt.Completion, string) { var comps []opt.Completion if cmd == "" { comps = append(comps, opt.Completion{Value: "y"}) diff --git a/completer/completer.go b/completer/completer.go index c5b61528..670db34d 100644 --- a/completer/completer.go +++ b/completer/completer.go @@ -2,6 +2,7 @@ package completer import ( "bufio" + "context" "errors" "fmt" "io" @@ -31,7 +32,7 @@ type Completer struct { // A CompleteFunc accepts a string to be completed and returns a slice of // completions candidates with a prefix to prepend to the chosen candidate -type CompleteFunc func(string) ([]opt.Completion, string) +type CompleteFunc func(context.Context, string) ([]opt.Completion, string) // New creates a new Completer with the specified address book command. func New(addressBookCmd string, errHandler func(error)) *Completer { @@ -51,8 +52,8 @@ func (c *Completer) ForHeader(h string) CompleteFunc { return nil } // wrap completeAddress in an error handler - return func(s string) ([]opt.Completion, string) { - completions, prefix, err := c.completeAddress(s) + return func(ctx context.Context, s string) ([]opt.Completion, string) { + completions, prefix, err := c.completeAddress(ctx, s) if err != nil { c.handleErr(err) return []opt.Completion{}, "" @@ -80,9 +81,9 @@ var tooManyLines = fmt.Errorf("returned more than %d lines", maxCompletionLines) // completeAddress uses the configured address book completion command to fetch // completions for the specified string, returning a slice of completions and // a prefix to be prepended to the selected completion, or an error. -func (c *Completer) completeAddress(s string) ([]opt.Completion, string, error) { +func (c *Completer) completeAddress(ctx context.Context, s string) ([]opt.Completion, string, error) { prefix, candidate := c.parseAddress(s) - cmd, err := c.getAddressCmd(candidate) + cmd, err := c.getAddressCmd(ctx, candidate) if err != nil { return nil, "", err } @@ -137,7 +138,7 @@ func (c *Completer) parseAddress(s string) (string, string) { // getAddressCmd constructs an exec.Cmd based on the configured command and // specified query. -func (c *Completer) getAddressCmd(s string) (*exec.Cmd, error) { +func (c *Completer) getAddressCmd(ctx context.Context, s string) (*exec.Cmd, error) { if strings.TrimSpace(c.AddressBookCmd) == "" { return nil, fmt.Errorf("no command configured") } @@ -147,9 +148,9 @@ func (c *Completer) getAddressCmd(s string) (*exec.Cmd, error) { return nil, fmt.Errorf("empty command") } if len(parts) > 1 { - return exec.Command(parts[0], parts[1:]...), nil + return exec.CommandContext(ctx, parts[0], parts[1:]...), nil } - return exec.Command(parts[0]), nil + return exec.CommandContext(ctx, parts[0]), nil } // readCompletions reads a slice of completions from r line by line. Each line 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)) { @@ -50,7 +50,7 @@ func execCommand( return err } -func getCompletions(cmdline string) ([]opt.Completion, string) { +func getCompletions(ctx context.Context, cmdline string) ([]opt.Completion, string) { // complete template terms if options, prefix, ok := commands.GetTemplateCompletion(cmdline); ok { sort.Strings(options) |