Completion: Do not emit multi line candidates for fish

fish splits the output of the completion function on newlines to build
the candidate list, so an option whose help text spans paragraphs added
its continuation lines as candidates of their own. Build the candidate
with the newline replacement the fish2 branch already used, so both
paths agree.
This commit is contained in:
devangpratap 2026-09-24 14:48:28 -04:00
parent becd8ed74c
commit bd3eb4c267
2 changed files with 32 additions and 2 deletions

View file

@ -52,10 +52,15 @@ func fish_output_serializer(completions []*Completions, shell_state map[string]s
n := completions[0].Delegate.NumToRemove
fm := markup.New(false) // fish freaks out if there are escape codes in the description strings
legacy_completion := shell_state["_legacy_completion"]
// fish splits the output of the completion function on newlines, so a description
// spanning multiple lines adds its continuation lines as spurious candidates.
candidate := func(m *Match) string {
return strings.ReplaceAll(m.Word+"\t"+fm.Prettify(m.Description), "\n", " ")
}
if legacy_completion == "fish2" {
for _, mg := range completions[0].Groups {
for _, m := range mg.Matches {
f("%s", strings.ReplaceAll(m.Word+"\t"+fm.Prettify(m.Description), "\n", " "))
f("%s", candidate(m))
}
}
} else if n > 0 {
@ -72,7 +77,7 @@ func fish_output_serializer(completions []*Completions, shell_state map[string]s
} else {
for _, mg := range completions[0].Groups {
for _, m := range mg.Matches {
f("echo -- %s", utils.QuoteStringForFish(m.Word+"\t"+fm.Prettify(m.Description)))
f("echo -- %s", utils.QuoteStringForFish(candidate(m)))
}
}
}

25
tools/cli/fish_test.go Normal file
View file

@ -0,0 +1,25 @@
// License: GPLv3 Copyright: 2026, Kovid Goyal, <kovid at kovidgoyal.net>
package cli
import (
"strings"
"testing"
)
func TestFishCompletionMultilineDescription(t *testing.T) {
completions := []*Completions{{Groups: []*MatchGroup{{Title: "Options", Matches: []*Match{
{Word: "--config", Description: "Specify a path to the configuration file.\n\nIf it is not specified, config files are searched for."},
}}}}}
for _, shell_state := range []map[string]string{{}, {"_legacy_completion": "fish2"}} {
data, err := fish_output_serializer(completions, shell_state)
if err != nil {
t.Fatal(err)
}
// each candidate must be a single line, since fish splits the output of the
// completion function on newlines to get the candidate list
if q := strings.TrimRight(string(data), "\n"); strings.Contains(q, "\n") {
t.Fatalf("A multi line description produced more than one candidate with shell state %#v:\n%s", shell_state, q)
}
}
}