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:
parent
b19b844a63
commit
71eda7d37c
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue