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.
This commit is contained in:
Parasrah 2022-01-06 21:54:28 -07:00 committed by Robin Jarry
parent b19b844a63
commit 71eda7d37c
5 changed files with 45 additions and 31 deletions

View file

@ -8,6 +8,7 @@ import (
"mime" "mime"
"net/mail" "net/mail"
"os/exec" "os/exec"
"regexp"
"strings" "strings"
"github.com/google/shlex" "github.com/google/shlex"
@ -28,8 +29,8 @@ type Completer struct {
} }
// A CompleteFunc accepts a string to be completed and returns a slice of // A CompleteFunc accepts a string to be completed and returns a slice of
// possible completions. // completions candidates with a prefix to prepend to the chosen candidate
type CompleteFunc func(string) []string type CompleteFunc func(string) ([]string, string)
// New creates a new Completer with the specified address book command. // New creates a new Completer with the specified address book command.
func New(addressBookCmd string, errHandler func(error), logger *log.Logger) *Completer { func New(addressBookCmd string, errHandler func(error), logger *log.Logger) *Completer {
@ -50,13 +51,13 @@ func (c *Completer) ForHeader(h string) CompleteFunc {
return nil return nil
} }
// wrap completeAddress in an error handler // wrap completeAddress in an error handler
return func(s string) []string { return func(s string) ([]string, string) {
completions, err := c.completeAddress(s) completions, prefix, err := c.completeAddress(s)
if err != nil { if err != nil {
c.handleErr(err) c.handleErr(err)
return []string{} return []string{}, ""
} }
return completions return completions, prefix
} }
} }
return nil return nil
@ -73,23 +74,24 @@ func isAddressHeader(h string) bool {
} }
// completeAddress uses the configured address book completion command to fetch // completeAddress uses the configured address book completion command to fetch
// completions for the specified string, returning a slice of completions or an // completions for the specified string, returning a slice of completions and
// error. // a prefix to be prepended to the selected completion, or an error.
func (c *Completer) completeAddress(s string) ([]string, error) { func (c *Completer) completeAddress(s string) ([]string, string, error) {
cmd, err := c.getAddressCmd(s) prefix, candidate := c.parseAddress(s)
cmd, err := c.getAddressCmd(candidate)
if err != nil { if err != nil {
return nil, err return nil, "", err
} }
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return nil, fmt.Errorf("stdout: %v", err) return nil, "", fmt.Errorf("stdout: %v", err)
} }
if err := cmd.Start(); err != nil { 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) completions, err := readCompletions(stdout)
if err != nil { 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 // 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) 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 // getAddressCmd constructs an exec.Cmd based on the configured command and

View file

@ -318,8 +318,8 @@ These options are configured in the *[compose]* section of aerc.conf.
*address-book-cmd* *address-book-cmd*
Specifies the command to be used to tab-complete email addresses. Any 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 occurrence of "%s" in the address-book-cmd will be replaced with anything
user has typed so far. the user has typed after the last comma.
The command must output the completions to standard output, one completion 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 per line. Each line must be tab-delimited, with an email address occurring as

View file

@ -24,8 +24,9 @@ type TextInput struct {
scroll int scroll int
text []rune text []rune
change []func(ti *TextInput) change []func(ti *TextInput)
tabcomplete func(s string) []string tabcomplete func(s string) ([]string, string)
completions []string completions []string
prefix string
completeIndex int completeIndex int
completeDelay time.Duration completeDelay time.Duration
completeDebouncer *time.Timer completeDebouncer *time.Timer
@ -55,7 +56,7 @@ func (ti *TextInput) Prompt(prompt string) *TextInput {
} }
func (ti *TextInput) TabComplete( 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.tabcomplete = tabcomplete
ti.completeDelay = d ti.completeDelay = d
return ti return ti
@ -129,7 +130,7 @@ func (ti *TextInput) drawPopover(ctx *Context) {
ti.Invalidate() ti.Invalidate()
}, },
onStem: func(stem string) { onStem: func(stem string) {
ti.Set(stem + ti.StringRight()) ti.Set(ti.prefix + stem + ti.StringRight())
ti.Invalidate() ti.Invalidate()
}, },
uiConfig: ti.uiConfig, uiConfig: ti.uiConfig,
@ -251,7 +252,7 @@ func (ti *TextInput) backspace() {
func (ti *TextInput) executeCompletion() { func (ti *TextInput) executeCompletion() {
if len(ti.completions) > 0 { 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 // no completer
return return
} }
ti.completions = ti.tabcomplete(ti.StringLeft()) ti.completions, ti.prefix = ti.tabcomplete(ti.StringLeft())
ti.completeIndex = -1 ti.completeIndex = -1
ti.Invalidate() ti.Invalidate()
} }

View file

@ -448,8 +448,8 @@ func (aerc *Aerc) BeginExCommand(cmd string) {
}, func() { }, func() {
aerc.statusbar.Pop() aerc.statusbar.Pop()
aerc.focus(previous) aerc.focus(previous)
}, func(cmd string) []string { }, func(cmd string) ([]string, string) {
return aerc.complete(cmd) return aerc.complete(cmd), ""
}, aerc.cmdHistory) }, aerc.cmdHistory)
aerc.statusbar.Push(exline) aerc.statusbar.Push(exline)
aerc.focus(exline) aerc.focus(exline)
@ -464,8 +464,8 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
if err != nil { if err != nil {
aerc.PushError(err.Error()) aerc.PushError(err.Error())
} }
}, func(cmd string) []string { }, func(cmd string) ([]string, string) {
return nil // TODO: completions return nil, "" // TODO: completions
}) })
aerc.prompts.Push(p) aerc.prompts.Push(p)
} }
@ -491,8 +491,8 @@ func (aerc *Aerc) RegisterChoices(choices []Choice) {
if err != nil { if err != nil {
aerc.PushError(err.Error()) aerc.PushError(err.Error())
} }
}, func(cmd string) []string { }, func(cmd string) ([]string, string) {
return nil // TODO: completions return nil, "" // TODO: completions
}) })
aerc.prompts.Push(p) aerc.prompts.Push(p)
} }

View file

@ -12,14 +12,14 @@ type ExLine struct {
ui.Invalidatable ui.Invalidatable
commit func(cmd string) commit func(cmd string)
finish func() finish func()
tabcomplete func(cmd string) []string tabcomplete func(cmd string) ([]string, string)
cmdHistory lib.History cmdHistory lib.History
input *ui.TextInput input *ui.TextInput
conf *config.AercConfig conf *config.AercConfig
} }
func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), finish func(), 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 { cmdHistory lib.History) *ExLine {
input := ui.NewTextInput("", conf.Ui).Prompt(":").Set(cmd) 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), 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) input := ui.NewTextInput("", conf.Ui).Prompt(prompt)
if conf.Ui.CompletionPopovers { if conf.Ui.CompletionPopovers {