summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/fish-completion.md4
-rw-r--r--integrationtests/do_scope_test.go2
-rw-r--r--internal/askcli/command_complete_uuids.go32
-rw-r--r--internal/askcli/command_complete_uuids_test.go56
-rw-r--r--internal/askcli/commands_registry.go6
-rw-r--r--internal/askcli/completion.go4
-rw-r--r--internal/askcli/completion_test.go6
-rw-r--r--internal/askcli/dispatch_test.go37
8 files changed, 138 insertions, 9 deletions
diff --git a/docs/fish-completion.md b/docs/fish-completion.md
index f55e4cf..95de56a 100644
--- a/docs/fish-completion.md
+++ b/docs/fish-completion.md
@@ -3,8 +3,8 @@
The `do` task-management CLI embeds its Fish completion script in the binary and prints it with `do fish`.
It completes the top-level `do` subcommands and the nested `do dep` operations.
-It also completes task selectors for UUID-taking commands by reading pending tasks through `do complete-uuids`, which uses the local alias cache for stable short IDs.
-Fish suggests each task's alias ID first and also keeps the raw UUID available as a fallback selector.
+It also completes task selectors for UUID-taking commands by reading pending tasks through `do complete-aliases`, which uses the local alias cache for stable short IDs.
+The `do complete-uuids` command still emits both alias and UUID lines for scripts and tests that need the full selector list.
Selector suggestions stop once a command has consumed its selector argument, and `do dep add` / `do dep rm` suggest selectors for both task positions.
When typing `do add depends:...`, Fish also completes the comma-separated dependency selector list inside the `depends:` modifier.
The script preserves the global `--json` flag.
diff --git a/integrationtests/do_scope_test.go b/integrationtests/do_scope_test.go
index f24f5ca..c87c67c 100644
--- a/integrationtests/do_scope_test.go
+++ b/integrationtests/do_scope_test.go
@@ -77,7 +77,7 @@ func hasTag(tags []string, want string) bool {
// hasSelectorLine reports whether any line in output starts with want followed
// by a tab or end-of-line. This handles the "selector\tdescription" format
-// emitted by complete-uuids for fish shell autocompletion.
+// emitted by complete-uuids / complete-aliases (tab-separated selector lines).
func hasSelectorLine(output, want string) bool {
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
line = strings.TrimSpace(line)
diff --git a/internal/askcli/command_complete_uuids.go b/internal/askcli/command_complete_uuids.go
index 8c00a7b..bc160b6 100644
--- a/internal/askcli/command_complete_uuids.go
+++ b/internal/askcli/command_complete_uuids.go
@@ -13,6 +13,18 @@ import (
const completionDescriptionMaxLen = 60
func (d *Dispatcher) handleCompleteUUIDs(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
+ return d.completeTaskSelectors(ctx, args, stdout, stderr, taskCompletionItems)
+}
+
+func (d *Dispatcher) handleCompleteAliases(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
+ return d.completeTaskSelectors(ctx, args, stdout, stderr, taskCompletionAliasItems)
+}
+
+// taskCompletionLinesFn builds tab-separated "selector\tdescription" lines for
+// completion output (full list vs alias-only for Fish).
+type taskCompletionLinesFn func(tasks []TaskExport, aliases map[string]string) []string
+
+func (d *Dispatcher) completeTaskSelectors(ctx context.Context, args []string, stdout, stderr io.Writer, lines taskCompletionLinesFn) (int, error) {
_ = args
var outBuf bytes.Buffer
code, err := d.runner.Run(ctx, []string{"status:pending", "export"}, nil, &outBuf, stderr)
@@ -31,7 +43,7 @@ func (d *Dispatcher) handleCompleteUUIDs(ctx context.Context, args []string, std
}
// Each line is "selector\tdescription" so fish shell can show the task
// summary alongside the ID in the autocompletion menu.
- for _, item := range taskCompletionItems(tasks, aliases) {
+ for _, item := range lines(tasks, aliases) {
_, _ = io.WriteString(stdout, item+"\n")
}
return 0, nil
@@ -55,6 +67,24 @@ func taskCompletionItems(tasks []TaskExport, aliases map[string]string) []string
return items
}
+// taskCompletionAliasItems returns tab-separated "selector\tdescription" lines
+// only for short alias IDs (never the raw UUID). Used by Fish completion via
+// `complete-aliases`; non-shell callers still use complete-uuids for full
+// selector lists.
+func taskCompletionAliasItems(tasks []TaskExport, aliases map[string]string) []string {
+ items := make([]string, 0, len(tasks))
+ for _, task := range tasks {
+ if task.UUID == "" {
+ continue
+ }
+ desc := truncateDescription(task.Description, completionDescriptionMaxLen)
+ if alias := displayTaskAlias(task.UUID, aliases); alias != "" && alias != task.UUID {
+ items = append(items, alias+"\t"+desc)
+ }
+ }
+ return items
+}
+
// truncateDescription shortens s to at most maxLen characters, appending "…"
// when the string is cut, so the completion hint fits on one line.
func truncateDescription(s string, maxLen int) string {
diff --git a/internal/askcli/command_complete_uuids_test.go b/internal/askcli/command_complete_uuids_test.go
index 92bf244..0859acc 100644
--- a/internal/askcli/command_complete_uuids_test.go
+++ b/internal/askcli/command_complete_uuids_test.go
@@ -146,6 +146,62 @@ func TestTaskCompletionSelectors_SkipsMissingAndDuplicateAliases(t *testing.T) {
}
}
+func TestTaskCompletionAliasItems_OnlyShortAliases(t *testing.T) {
+ tasks := []TaskExport{
+ {UUID: "uuid-1", Description: "First task"},
+ {UUID: "", Description: "Ignored"},
+ {UUID: "uuid-2", Description: "Second task"},
+ }
+ aliases := map[string]string{
+ "uuid-1": "0",
+ "uuid-2": "uuid-2", // same as UUID: no alias-only line (use complete-uuids for UUID)
+ }
+
+ got := taskCompletionAliasItems(tasks, aliases)
+ want := []string{
+ "0\tFirst task",
+ }
+ if strings.Join(got, "\n") != strings.Join(want, "\n") {
+ t.Fatalf("taskCompletionAliasItems = %v, want %v", got, want)
+ }
+}
+
+func TestHandleCompleteAliases_PrintsAliasesOnly(t *testing.T) {
+ dir := t.TempDir()
+ oldNow := nowTaskAliasCache
+ oldRoot := taskAliasCacheRoot
+ nowTaskAliasCache = func() time.Time { return time.Date(2026, 3, 26, 12, 0, 0, 0, time.UTC) }
+ taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil }
+ defer func() {
+ nowTaskAliasCache = oldNow
+ taskAliasCacheRoot = oldRoot
+ }()
+
+ d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
+ want := []string{"status:pending", "export"}
+ if strings.Join(args, " ") != strings.Join(want, " ") {
+ t.Fatalf("args = %v, want %v", args, want)
+ }
+ _, _ = io.WriteString(stdout, `[{"uuid":"uuid-1","description":"First task"},{"uuid":"uuid-2","description":"Second task"},{"uuid":""}]`)
+ return 0, nil
+ }})
+
+ var stdout, stderr bytes.Buffer
+ code, err := d.handleCompleteAliases(context.Background(), nil, &stdout, &stderr)
+ if err != nil {
+ t.Fatalf("handleCompleteAliases returned error: %v", err)
+ }
+ if code != 0 {
+ t.Fatalf("handleCompleteAliases code = %d, want 0", code)
+ }
+ if got := stdout.String(); got != "0\tFirst task\n1\tSecond task\n" {
+ t.Fatalf("stdout = %q, want alias-only tab-separated list", got)
+ }
+ if stderr.Len() != 0 {
+ t.Fatalf("stderr = %q, want empty", stderr.String())
+ }
+}
+
func TestTaskCompletionItems_IncludesDescriptions(t *testing.T) {
tasks := []TaskExport{
{UUID: "uuid-1", Description: "First task"},
diff --git a/internal/askcli/commands_registry.go b/internal/askcli/commands_registry.go
index 4d8d9ba..1c2e60d 100644
--- a/internal/askcli/commands_registry.go
+++ b/internal/askcli/commands_registry.go
@@ -204,4 +204,10 @@ func init() {
handler: wrapSimpleCommand((*Dispatcher).handleCompleteUUIDs),
includeInCompletion: false,
})
+ commandRegistry.add(commandEntry{
+ name: "complete-aliases",
+ description: "Emit short task selector list for Fish completion",
+ handler: wrapSimpleCommand((*Dispatcher).handleCompleteAliases),
+ includeInCompletion: false,
+ })
}
diff --git a/internal/askcli/completion.go b/internal/askcli/completion.go
index 3e30c6c..e7b837e 100644
--- a/internal/askcli/completion.go
+++ b/internal/askcli/completion.go
@@ -274,9 +274,9 @@ func writeFishTaskSelectorFunction(b *strings.Builder, binaryPath string) {
b.WriteString(" end\n")
b.WriteString(" set -l selectors\n")
b.WriteString(" if test -n \"$scope_prefix\"\n")
- b.WriteString(" set selectors (command $do_bin $scope_prefix complete-uuids 2>/dev/null)\n")
+ b.WriteString(" set selectors (command $do_bin $scope_prefix complete-aliases 2>/dev/null)\n")
b.WriteString(" else\n")
- b.WriteString(" set selectors (command $do_bin complete-uuids 2>/dev/null)\n")
+ b.WriteString(" set selectors (command $do_bin complete-aliases 2>/dev/null)\n")
b.WriteString(" end\n")
b.WriteString(" if test $status -ne 0\n")
b.WriteString(" return 1\n")
diff --git a/internal/askcli/completion_test.go b/internal/askcli/completion_test.go
index 012927d..5e18c09 100644
--- a/internal/askcli/completion_test.go
+++ b/internal/askcli/completion_test.go
@@ -28,8 +28,8 @@ func TestFishCompletion_IncludesCommandsAndExcludesExport(t *testing.T) {
"function __do_add_dependency_modifiers",
`set -l do_bin "do"`,
"set -l selectors",
- "set selectors (command $do_bin complete-uuids 2>/dev/null)",
- "set selectors (command $do_bin $scope_prefix complete-uuids 2>/dev/null)",
+ "set selectors (command $do_bin complete-aliases 2>/dev/null)",
+ "set selectors (command $do_bin $scope_prefix complete-aliases 2>/dev/null)",
"complete -c do -n '__do_in_uuid_context' -a '(__do_task_selectors)' -d 'Task selector'",
"complete -c do -n '__do_in_dep_uuid_context' -a '(__do_task_selectors)' -d 'Task selector'",
"complete -c do -n '__do_in_add_dep_modifier_context' -a '(__do_add_dependency_modifiers)' -d 'Task dependency'",
@@ -139,7 +139,7 @@ func TestFishCompletionFor_EmbedsBinaryPath(t *testing.T) {
script := FishCompletionFor(`/tmp/do "$HOME"`)
for _, line := range []string{
`set -l do_bin "/tmp/do \"\$HOME\""`,
- "set selectors (command $do_bin complete-uuids 2>/dev/null)",
+ "set selectors (command $do_bin complete-aliases 2>/dev/null)",
} {
if !strings.Contains(script, line) {
t.Fatalf("script missing %q", line)
diff --git a/internal/askcli/dispatch_test.go b/internal/askcli/dispatch_test.go
index 90c1b99..0e13788 100644
--- a/internal/askcli/dispatch_test.go
+++ b/internal/askcli/dispatch_test.go
@@ -90,6 +90,38 @@ func TestDispatcher_CompleteUUIDsSubcommand(t *testing.T) {
}
}
+func TestDispatcher_CompleteAliasesSubcommand(t *testing.T) {
+ dir := t.TempDir()
+ oldRoot := taskAliasCacheRoot
+ oldNow := nowTaskAliasCache
+ taskAliasCacheRoot = func() (string, error) { return filepath.Join(dir, "hexai"), nil }
+ nowTaskAliasCache = func() time.Time { return time.Date(2026, 3, 27, 12, 0, 0, 0, time.UTC) }
+ defer func() {
+ taskAliasCacheRoot = oldRoot
+ nowTaskAliasCache = oldNow
+ }()
+
+ d := NewDispatcher(&spyRunner{runFn: func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
+ if strings.Join(args, " ") != "status:pending export" {
+ t.Fatalf("args = %v, want pending export", args)
+ }
+ _, _ = io.WriteString(stdout, `[{"uuid":"uuid-1","description":"Sample task"}]`)
+ return 0, nil
+ }})
+ var stdout, stderr bytes.Buffer
+ code, err := d.Dispatch(context.Background(), []string{"complete-aliases"}, nil, &stdout, &stderr)
+ if err != nil {
+ t.Fatalf("complete-aliases returned error: %v", err)
+ }
+ if code != 0 {
+ t.Fatalf("complete-aliases code = %d, want 0", code)
+ }
+ // Fish completion uses aliases only; UUID lines are omitted.
+ if got := stdout.String(); got != "0\tSample task\n" {
+ t.Fatalf("stdout = %q, want alias-only tab-separated list", got)
+ }
+}
+
func TestDispatcher_LongHelp(t *testing.T) {
d := NewDispatcher(nil)
var stdout bytes.Buffer
@@ -323,6 +355,11 @@ func TestDispatcher_AllSubcommandsReachExecutor(t *testing.T) {
wantCalls: [][]string{{"status:pending", "export"}},
},
{
+ name: "complete-aliases",
+ args: []string{"complete-aliases"},
+ wantCalls: [][]string{{"status:pending", "export"}},
+ },
+ {
name: "info",
args: []string{"info", "test-uuid"},
wantCalls: [][]string{{"uuid:test-uuid", "export"}},