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"
"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

View File

@ -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

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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 {