From 71eda7d37c8ef38502c360b518fcbf5960497eea Mon Sep 17 00:00:00 2001 From: Parasrah Date: Thu, 6 Jan 2022 21:54:28 -0700 Subject: [PATCH] completions: add support for completing multiple addresses as per the discussion https://lists.sr.ht/~sircmpwn/aerc/patches/15367 this handles completions in `completer/completer.go` by enabling the completer to return a `prefix` that will be prepended to the selected completion candidate. --- completer/completer.go | 43 +++++++++++++++++++++++++++--------------- doc/aerc-config.5.scd | 4 ++-- lib/ui/textinput.go | 11 ++++++----- widgets/aerc.go | 12 ++++++------ widgets/exline.go | 6 +++--- 5 files changed, 45 insertions(+), 31 deletions(-) diff --git a/completer/completer.go b/completer/completer.go index bc6c96f..251658e 100644 --- a/completer/completer.go +++ b/completer/completer.go @@ -8,6 +8,7 @@ import ( "mime" "net/mail" "os/exec" + "regexp" "strings" "github.com/google/shlex" @@ -28,8 +29,8 @@ type Completer struct { } // A CompleteFunc accepts a string to be completed and returns a slice of -// possible completions. -type CompleteFunc func(string) []string +// completions candidates with a prefix to prepend to the chosen candidate +type CompleteFunc func(string) ([]string, string) // New creates a new Completer with the specified address book command. func New(addressBookCmd string, errHandler func(error), logger *log.Logger) *Completer { @@ -50,13 +51,13 @@ func (c *Completer) ForHeader(h string) CompleteFunc { return nil } // wrap completeAddress in an error handler - return func(s string) []string { - completions, err := c.completeAddress(s) + return func(s string) ([]string, string) { + completions, prefix, err := c.completeAddress(s) if err != nil { c.handleErr(err) - return []string{} + return []string{}, "" } - return completions + return completions, prefix } } return nil @@ -73,23 +74,24 @@ func isAddressHeader(h string) bool { } // completeAddress uses the configured address book completion command to fetch -// completions for the specified string, returning a slice of completions or an -// error. -func (c *Completer) completeAddress(s string) ([]string, error) { - cmd, err := c.getAddressCmd(s) +// 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) ([]string, string, error) { + prefix, candidate := c.parseAddress(s) + cmd, err := c.getAddressCmd(candidate) if err != nil { - return nil, err + return nil, "", err } stdout, err := cmd.StdoutPipe() if err != nil { - return nil, fmt.Errorf("stdout: %v", err) + return nil, "", fmt.Errorf("stdout: %v", err) } if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("cmd start: %v", err) + return nil, "", fmt.Errorf("cmd start: %v", err) } completions, err := readCompletions(stdout) if err != nil { - return nil, fmt.Errorf("read completions: %v", err) + return nil, "", fmt.Errorf("read completions: %v", err) } // Wait returns an error if the exit status != 0, which some completion @@ -100,7 +102,18 @@ func (c *Completer) completeAddress(s string) ([]string, error) { c.logger.Printf("completion error: %v", err) } - return completions, nil + return completions, prefix, nil +} + +// parseAddress will break an address header into a prefix (containing +// the already valid addresses) and an input for completion +func (c *Completer) parseAddress(s string) (string, string) { + pattern := regexp.MustCompile(`^(.*),\s+([^,]*)$`) + matches := pattern.FindStringSubmatch(s) + if matches == nil { + return "", s + } + return matches[1] + ", ", matches[2] } // getAddressCmd constructs an exec.Cmd based on the configured command and diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index ae03074..2ef4ebc 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -318,8 +318,8 @@ These options are configured in the *[compose]* section of aerc.conf. *address-book-cmd* Specifies the command to be used to tab-complete email addresses. Any - occurrence of "%s" in the address-book-cmd will be replaced with what the - user has typed so far. + occurrence of "%s" in the address-book-cmd will be replaced with anything + the user has typed after the last comma. The command must output the completions to standard output, one completion per line. Each line must be tab-delimited, with an email address occurring as diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go index 9daae3a..cd31d26 100644 --- a/lib/ui/textinput.go +++ b/lib/ui/textinput.go @@ -24,8 +24,9 @@ type TextInput struct { scroll int text []rune change []func(ti *TextInput) - tabcomplete func(s string) []string + tabcomplete func(s string) ([]string, string) completions []string + prefix string completeIndex int completeDelay time.Duration completeDebouncer *time.Timer @@ -55,7 +56,7 @@ func (ti *TextInput) Prompt(prompt string) *TextInput { } func (ti *TextInput) TabComplete( - tabcomplete func(s string) []string, d time.Duration) *TextInput { + tabcomplete func(s string) ([]string, string), d time.Duration) *TextInput { ti.tabcomplete = tabcomplete ti.completeDelay = d return ti @@ -129,7 +130,7 @@ func (ti *TextInput) drawPopover(ctx *Context) { ti.Invalidate() }, onStem: func(stem string) { - ti.Set(stem + ti.StringRight()) + ti.Set(ti.prefix + stem + ti.StringRight()) ti.Invalidate() }, uiConfig: ti.uiConfig, @@ -251,7 +252,7 @@ func (ti *TextInput) backspace() { func (ti *TextInput) executeCompletion() { if len(ti.completions) > 0 { - ti.Set(ti.completions[ti.completeIndex] + ti.StringRight()) + ti.Set(ti.prefix + ti.completions[ti.completeIndex] + ti.StringRight()) } } @@ -286,7 +287,7 @@ func (ti *TextInput) showCompletions() { // no completer return } - ti.completions = ti.tabcomplete(ti.StringLeft()) + ti.completions, ti.prefix = ti.tabcomplete(ti.StringLeft()) ti.completeIndex = -1 ti.Invalidate() } diff --git a/widgets/aerc.go b/widgets/aerc.go index 3c52f7e..e644f82 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -448,8 +448,8 @@ func (aerc *Aerc) BeginExCommand(cmd string) { }, func() { aerc.statusbar.Pop() aerc.focus(previous) - }, func(cmd string) []string { - return aerc.complete(cmd) + }, func(cmd string) ([]string, string) { + return aerc.complete(cmd), "" }, aerc.cmdHistory) aerc.statusbar.Push(exline) aerc.focus(exline) @@ -464,8 +464,8 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) { if err != nil { aerc.PushError(err.Error()) } - }, func(cmd string) []string { - return nil // TODO: completions + }, func(cmd string) ([]string, string) { + return nil, "" // TODO: completions }) aerc.prompts.Push(p) } @@ -491,8 +491,8 @@ func (aerc *Aerc) RegisterChoices(choices []Choice) { if err != nil { aerc.PushError(err.Error()) } - }, func(cmd string) []string { - return nil // TODO: completions + }, func(cmd string) ([]string, string) { + return nil, "" // TODO: completions }) aerc.prompts.Push(p) } diff --git a/widgets/exline.go b/widgets/exline.go index dd9c928..0d245fb 100644 --- a/widgets/exline.go +++ b/widgets/exline.go @@ -12,14 +12,14 @@ type ExLine struct { ui.Invalidatable commit func(cmd string) finish func() - tabcomplete func(cmd string) []string + tabcomplete func(cmd string) ([]string, string) cmdHistory lib.History input *ui.TextInput conf *config.AercConfig } func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), finish func(), - tabcomplete func(cmd string) []string, + tabcomplete func(cmd string) ([]string, string), cmdHistory lib.History) *ExLine { input := ui.NewTextInput("", conf.Ui).Prompt(":").Set(cmd) @@ -41,7 +41,7 @@ func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), fin } func NewPrompt(conf *config.AercConfig, prompt string, commit func(text string), - tabcomplete func(cmd string) []string) *ExLine { + tabcomplete func(cmd string) ([]string, string)) *ExLine { input := ui.NewTextInput("", conf.Ui).Prompt(prompt) if conf.Ui.CompletionPopovers {