~rjarry/public-inbox

go-opt: Add description support to autocompletion v2 PROPOSED

This is a series of patches that introduce a new field `description`
that appear alongside arguments during autocompletion.

v1->v2:
 * return Completion struct instead of string to allow applications to
   use description however they want

Bojan Gabric (4):
  spec: add description field to arguments
  complete: include `description` in autocompletion output
  complete_test: update test cases to return `Completion` type
  complete_test: add tests for `description` field in autocompletion

 README.md        |   4 ++
 complete.go      |  30 +++++++++-----
 complete_test.go | 104 ++++++++++++++++++++++++++++-------------------
 opt.go           |   2 +-
 spec.go          |   4 ++
 5 files changed, 90 insertions(+), 54 deletions(-)

-- 
2.45.2
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~rjarry/public-inbox/patches/54866/mbox | git am -3
Learn more about email & git

[PATCH go-opt v2 1/4] spec: add description field to arguments Export this patch

This field will be used to store descriptions that will be displayed
during the autocompletion process.

Signed-off-by: Bojan Gabric <bojan@bojangabric.com>
---
 spec.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/spec.go b/spec.go
index d4960d1..1e07042 100644
--- a/spec.go
+++ b/spec.go
@@ -56,6 +56,8 @@ type optSpec struct {
	kind optKind
	// argument is required
	required bool
	// option/argument description
	description string
	// argument was seen on the command line
	seen bool
	// argument value was seen on the command line (only applies to options)
@@ -218,6 +220,8 @@ func (spec *optSpec) parseField(struc reflect.Value, t reflect.StructField) {
		spec.metavar = metavar
	}

	spec.description = t.Tag.Get("description")

	spec.defval = t.Tag.Get("default")

	switch t.Tag.Get("required") {
-- 
2.45.2

[PATCH go-opt v2 2/4] complete: include `description` in autocompletion output Export this patch

Update the autocompletion logic to return `Completion` struct:

type Completion struct {
	Value       string
	Description string
}

This will allow application that uses autocompletion to do what it wants
with the description.

Implements: https://todo.sr.ht/~rjarry/aerc/271
Signed-off-by: Bojan Gabric <bojan@bojangabric.com>
---
Not sure if the function that's passed to the "complete" argument should 
return `[]Completion`?

 README.md   |  4 ++++
 complete.go | 30 +++++++++++++++++++-----------
 opt.go      |  2 +-
 3 files changed, 24 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index e2f5590..e942094 100644
--- a/README.md
+++ b/README.md
@@ -237,6 +237,10 @@ be a method with a pointer receiver to the struct itself, takes a single
`string` argument and may return an `error` to abort parsing. The `action`
method is responsible of updating the struct.

### `description:"foobaz"`

A description that appears alongside arguments during autocompletion.

### `default:"foobaz"`

Default `string` value if not specified by the user. Will be processed by the
diff --git a/complete.go b/complete.go
index f003e88..578ca97 100644
--- a/complete.go
+++ b/complete.go
@@ -5,8 +5,13 @@ import (
	"strings"
)

func (c *CmdSpec) unseenFlags(arg string) []string {
	var flags []string
type Completion struct {
	Value       string
	Description string
}

func (c *CmdSpec) unseenFlags(arg string) []Completion {
	var flags []Completion
	for i := 0; i < len(c.opts); i++ {
		spec := &c.opts[i]
		if !spec.appliesToAlias(c.name) || spec.seen {
@@ -15,10 +20,10 @@ func (c *CmdSpec) unseenFlags(arg string) []string {
		switch spec.kind {
		case flag, option:
			if spec.short != "" && strings.HasPrefix(spec.short, arg) {
				flags = append(flags, spec.short+" ")
				flags = append(flags, Completion{Value: spec.short + " ", Description: spec.description})
			}
			if spec.long != "" && strings.HasPrefix(spec.long, arg) {
				flags = append(flags, spec.long+" ")
				flags = append(flags, Completion{Value: spec.long + " ", Description: spec.description})
			}
		}
	}
@@ -40,25 +45,29 @@ func (c *CmdSpec) nextPositional() *optSpec {
	return spec
}

func (s *optSpec) getCompletions(arg string) []string {
func (s *optSpec) getCompletions(arg string) []Completion {
	if s.complete.IsValid() {
		in := []reflect.Value{reflect.ValueOf(arg)}
		out := s.complete.Call(in)
		if res, ok := out[0].Interface().([]string); ok {
			return res
			var completions []Completion
			for _, value := range res {
				completions = append(completions, Completion{Value: value})
			}
			return completions
		}
	}
	return nil
}

func (c *CmdSpec) GetCompletions(args *Args) ([]string, string) {
func (c *CmdSpec) GetCompletions(args *Args) ([]Completion, string) {
	if args.Count() == 0 || (args.Count() == 1 && args.TrailingSpace() == "") {
		return nil, ""
	}

	var completions []string
	var completions []Completion
	var prefix string
	var flags []string
	var flags []Completion
	var last *seenArg
	var spec *optSpec

@@ -94,8 +103,7 @@ func (c *CmdSpec) GetCompletions(args *Args) ([]string, string) {
			switch {
			case (s.kind == flag || s.kind == option) && (s.short == arg || s.long == arg):
				// Current argument is precisely a flag.
				spec = nil
				completions = []string{arg + " "}
				completions = []Completion{{Value: arg + " ", Description: s.description}}
			case s.kind == option && f != "=" && strings.HasPrefix(arg, f):
				// Current argument is a long flag in the format:
				//       --flag=value
diff --git a/opt.go b/opt.go
index 610c0ba..3d9cd7e 100644
--- a/opt.go
+++ b/opt.go
@@ -148,7 +148,7 @@ func ArgsToStruct(args *Args, v any) error {
	return nil
}

func GetCompletions(cmdline string, v any) (completions []string, prefix string) {
func GetCompletions(cmdline string, v any) (completions []Completion, prefix string) {
	args := LexArgs(cmdline)
	if args.Count() == 0 {
		return nil, ""
-- 
2.45.2

[PATCH go-opt v2 3/4] complete_test: update test cases to return `Completion` type Export this patch

Update test cases to ensure that the expected completions are of type
`Completion`.

Signed-off-by: Bojan Gabric <bojan@bojangabric.com>
---
 complete_test.go | 86 +++++++++++++++++++++++++++---------------------
 1 file changed, 49 insertions(+), 37 deletions(-)

diff --git a/complete_test.go b/complete_test.go
index a210efc..d2e58f9 100644
--- a/complete_test.go
+++ b/complete_test.go
@@ -47,93 +47,105 @@ func (c *CompleteStruct) CompleteTag(arg string) []string {
func TestComplete(t *testing.T) {
	vectors := []struct {
		cmdline     string
		completions []string
		completions []opt.Completion
		prefix      string
	}{
		{
			"foo --delay 33..33.3 -n",
			[]string{"-n "},
			[]opt.Completion{{Value: "-n "}},
			"foo --delay 33..33.3 ",
		},
		{
			"foo --delay 33..33.3 -n ",
			[]string{"leonardo", "michelangelo", "rafaelo", "donatello"},
			[]opt.Completion{
				{Value: "leonardo"},
				{Value: "michelangelo"},
				{Value: "rafaelo"},
				{Value: "donatello"},
			},
			"foo --delay 33..33.3 -n ",
		},
		{
			"foo --delay 33..33.3 -n don",
			[]string{"donatello"},
			[]opt.Completion{{Value: "donatello"}},
			"foo --delay 33..33.3 -n ",
		},
		{
			"foo --delay 33..33.3 --name=",
			[]string{"leonardo", "michelangelo", "rafaelo", "donatello"},
			[]opt.Completion{
				{Value: "leonardo"},
				{Value: "michelangelo"},
				{Value: "rafaelo"},
				{Value: "donatello"},
			},
			"foo --delay 33..33.3 --name=",
		},
		{
			"foo --delay 33..33.3 --name=leo",
			[]string{"leonardo"},
			[]opt.Completion{{Value: "leonardo"}},
			"foo --delay 33..33.3 --name=",
		},
		{
			"foo --nam",
			[]string{
				"--name ",
			},
			[]opt.Completion{{Value: "--name "}},
			"foo ",
		},
		{
			"foo --delay 33..33.3 --backoff",
			[]string{
				"--backoff ",
			},
			[]opt.Completion{{Value: "--backoff "}},
			"foo --delay 33..33.3 ",
		},
		{
			"foo --delay 33..33.3 -",
			[]string{
				"-unread",
				"-sent",
				"-important",
				"-inbox",
				"-trash",
				"-n ",
				"--name ",
				"-z ",
				"-B ",
				"--backoff ",
			[]opt.Completion{
				{Value: "-unread"},
				{Value: "-sent"},
				{Value: "-important"},
				{Value: "-inbox"},
				{Value: "-trash"},
				{Value: "-n "},
				{Value: "--name "},
				{Value: "-z "},
				{Value: "-B "},
				{Value: "--backoff "},
			},
			"foo --delay 33..33.3 ",
		},
		{
			"foo --delay 33..33.3 ",
			[]string{
				"unread",
				"sent",
				"important",
				"inbox",
				"trash",
				"-n ",
				"--name ",
				"-z ",
				"-B ",
				"--backoff ",
			[]opt.Completion{
				{Value: "unread"},
				{Value: "sent"},
				{Value: "important"},
				{Value: "inbox"},
				{Value: "trash"},
				{Value: "-n "},
				{Value: "--name "},
				{Value: "-z "},
				{Value: "-B "},
				{Value: "--backoff "},
			},
			"foo --delay 33..33.3 ",
		},
		{
			"foo --delay 33..33.3 -n leonardo i",
			[]string{"important", "inbox"},
			[]opt.Completion{{Value: "important"}, {Value: "inbox"}},
			"foo --delay 33..33.3 -n leonardo ",
		},
		{
			"foo +",
			[]string{"+unread", "+sent", "+important", "+inbox", "+trash"},
			[]opt.Completion{
				{Value: "+unread"},
				{Value: "+sent"},
				{Value: "+important"},
				{Value: "+inbox"},
				{Value: "+trash"},
			},
			"foo ",
		},
		{
			"foo -i",
			[]string{"-important", "-inbox"},
			[]opt.Completion{{Value: "-important"}, {Value: "-inbox"}},
			"foo ",
		},
	}
-- 
2.45.2

[PATCH go-opt v2 4/4] complete_test: add tests for `description` field in autocompletion Export this patch

Add test cases to verify that the `description` field is correctly
integrated into the autocompletion output.

Signed-off-by: Bojan Gabric <bojan@bojangabric.com>
---
 complete_test.go | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/complete_test.go b/complete_test.go
index d2e58f9..3349e9b 100644
--- a/complete_test.go
+++ b/complete_test.go
@@ -9,11 +9,12 @@ import (
)

type CompleteStruct struct {
	Name    string   `opt:"-n,--name" required:"true" complete:"CompleteName"`
	Delay   float64  `opt:"--delay"`
	Zero    bool     `opt:"-z"`
	Backoff bool     `opt:"-B,--backoff"`
	Tags    []string `opt:"..." complete:"CompleteTag"`
	Name        string   `opt:"-n,--name" required:"true" complete:"CompleteName"`
	Delay       float64  `opt:"--delay"`
	Zero        bool     `opt:"-z"`
	Backoff     bool     `opt:"-B,--backoff"`
	Description string   `opt:"-d" description:"Argument description"`
	Tags        []string `opt:"..." complete:"CompleteTag"`
}

func (c *CompleteStruct) CompleteName(arg string) []string {
@@ -108,6 +109,7 @@ func TestComplete(t *testing.T) {
				{Value: "-z "},
				{Value: "-B "},
				{Value: "--backoff "},
				{Value: "-d ", Description: "Argument description"},
			},
			"foo --delay 33..33.3 ",
		},
@@ -124,6 +126,7 @@ func TestComplete(t *testing.T) {
				{Value: "-z "},
				{Value: "-B "},
				{Value: "--backoff "},
				{Value: "-d ", Description: "Argument description"},
			},
			"foo --delay 33..33.3 ",
		},
@@ -148,6 +151,11 @@ func TestComplete(t *testing.T) {
			[]opt.Completion{{Value: "-important"}, {Value: "-inbox"}},
			"foo ",
		},
		{
			"foo --delay 33..33.3 -d",
			[]opt.Completion{{Value: "-d ", Description: "Argument description"}},
			"foo --delay 33..33.3 ",
		},
	}

	for _, v := range vectors {
-- 
2.45.2