about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorParasrah <dev@parasrah.com>2022-01-06 21:54:28 -0700
committerRobin Jarry <robin@jarry.cc>2022-01-07 13:54:10 +0100
commit71eda7d37c8ef38502c360b518fcbf5960497eea (patch)
tree07b9383865f65934001378f170c942242bc0f054
parentb19b844a6326793f078b4a03eaf63ca96528796e (diff)
downloadaerc-71eda7d37c8ef38502c360b518fcbf5960497eea.tar.gz
completions: add support for completing multiple addresses HEAD master
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.
-rw-r--r--completer/completer.go43
-rw-r--r--doc/aerc-config.5.scd4
-rw-r--r--lib/ui/textinput.go11
-rw-r--r--widgets/aerc.go12
-rw-r--r--widgets/exline.go6
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 {