fix(completion): always leave a trailing space after a task name

The protocol carries one directive per response, so a single truncated wildcard prefix cost every complete name its trailing space: a Taskfile holding `deploy-*` anywhere made `task buil<TAB>` insert `build` with the cursor stuck against it. Completing a task name is the common case and the wildcard prefix the rare one, so the polarity was backwards — and the legacy wrappers, which had no notion of an incomplete candidate, always left the space.

A wildcard prefix now gets a space it does not want, which is the accepted trade until suggestions can carry a directive of their own. Required variables keep NoSpace: there a free-form `VAR=` is the common case, not the exception.
This commit is contained in:
Valentin Maerten
2026-08-20 16:51:59 +02:00
parent 978277273e
commit 13f72a0b56
2 changed files with 8 additions and 11 deletions

View File

@@ -166,7 +166,9 @@ func TestComplete_WildcardTaskNames(t *testing.T) {
// Patterns are cut at their first `*`: `wildcard-*` and `wildcard-*-*`
// collapse into one candidate, and `*-wildcard-*` leaves nothing to insert.
require.Equal(t, []string{"build", "matches-exactly-", "release-", "start-", "s-", "wildcard-"}, values(suggs))
require.Equal(t, complete.DirectiveNoSpace|complete.DirectiveNoFileComp, dir)
// A truncated prefix costs the whole response its trailing space, so task
// names keep theirs and the wildcard prefix gets one it does not want.
require.Equal(t, complete.DirectiveNoFileComp, dir)
// Without a desc, the pattern says what the prefix stands for.
require.Contains(t, descriptions(suggs), "wildcard-*")

View File

@@ -67,7 +67,6 @@ func completeTaskNames(e *task.Executor, opts Options) ([]Suggestion, Directive)
out := make([]Suggestion, 0, len(tasks))
seen := make(map[string]bool, len(tasks))
anyPartial := false
add := func(name, desc string) {
value, partial := suggestedName(name)
// `*-wildcard-*` has no prefix, and `wildcard-*` / `wildcard-*-*` share one.
@@ -75,11 +74,9 @@ func completeTaskNames(e *task.Executor, opts Options) ([]Suggestion, Directive)
return
}
seen[value] = true
if partial {
anyPartial = true
if desc == "" && !opts.NoDescriptions {
desc = name
}
// Without a desc, a truncated pattern says what its prefix stands for.
if partial && desc == "" && !opts.NoDescriptions {
desc = name
}
out = append(out, Suggestion{Value: value, Description: desc})
}
@@ -94,10 +91,8 @@ func completeTaskNames(e *task.Executor, opts Options) ([]Suggestion, Directive)
}
}
// A truncated pattern is half a name: the cursor must stay against it.
if anyPartial {
return out, DirectiveNoSpace | DirectiveNoFileComp
}
// A single truncated wildcard prefix would otherwise cost every complete
// name its trailing space: the directive covers the whole response.
return out, DirectiveNoFileComp
}