Skip to content

Commit 99e2e97

Browse files
authored
Improve fish completion (#376, #534) (#535)
A generated fish script didn't complete an option after an argument. The cause is the generated fish script doesn't accept an input text which already has arguments. For example, when the input is `repeat -`, the script can complete `--count`, but when the text is `repeat foo -`, the script cannot complete `repeat foo --count`, because the input text already has the argument "foo". To fix the issue, `FishCompletionsGenerator` got a capability that can accept a text which has arguments.
1 parent 83c0ef0 commit 99e2e97

File tree

3 files changed

+236
-154
lines changed

3 files changed

+236
-154
lines changed
Lines changed: 117 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,94 @@
11
struct FishCompletionsGenerator {
22
static func generateCompletionScript(_ type: ParsableCommand.Type) -> String {
33
let programName = type._commandName
4-
let helper = """
5-
function _swift_\(programName)_using_command
6-
set -l cmd (commandline -opc)
7-
if [ (count $cmd) -eq (count $argv) ]
8-
for i in (seq (count $argv))
9-
if [ $cmd[$i] != $argv[$i] ]
10-
return 1
11-
end
12-
end
13-
return 0
14-
end
15-
return 1
16-
end
17-
18-
"""
19-
4+
let helperFunctions = [
5+
preprocessorFunction(commandName: programName),
6+
helperFunction(commandName: programName)
7+
]
208
let completions = generateCompletions(commandChain: [programName], [type])
21-
.joined(separator: "\n")
229

23-
return helper + completions
10+
return helperFunctions.joined(separator: "\n\n") + "\n\n" + completions.joined(separator: "\n")
2411
}
12+
}
2513

26-
static func generateCompletions(commandChain: [String], _ commands: [ParsableCommand.Type])
27-
-> [String]
28-
{
14+
// MARK: - Private functions
15+
16+
extension FishCompletionsGenerator {
17+
private static func generateCompletions(commandChain: [String], _ commands: [ParsableCommand.Type]) -> [String] {
2918
let type = commands.last!
3019
let isRootCommand = commands.count == 1
3120
let programName = commandChain[0]
3221
var subcommands = type.configuration.subcommands
3322
.filter { $0.configuration.shouldDisplay }
3423

24+
if !subcommands.isEmpty && isRootCommand {
25+
subcommands.append(HelpCommand.self)
26+
}
27+
28+
let helperFunctionName = helperFunctionName(commandName: programName)
29+
30+
var prefix = "complete -c \(programName) -n '\(helperFunctionName) \"\(commandChain.joined(separator: separator))\""
3531
if !subcommands.isEmpty {
36-
if isRootCommand {
37-
subcommands.append(HelpCommand.self)
38-
}
32+
prefix += " \"\(subcommands.map { $0._commandName }.joined(separator: separator))\""
3933
}
34+
prefix += "'"
4035

41-
let prefix = "complete -c \(programName) -n '_swift_\(programName)_using_command"
42-
/// We ask each suggestion to produce 2 pieces of information
43-
/// - Parameters
44-
/// - ancestors: a list of "ancestor" which must be present in the current shell buffer for
45-
/// this suggestion to be considered. This could be a combination of (nested)
46-
/// subcommands and flags.
47-
/// - suggestion: text for the actual suggestion
48-
/// - Returns: A completion expression
49-
func complete(ancestors: [String], suggestion: String) -> String {
50-
"\(prefix) \(ancestors.joined(separator: " "))' \(suggestion)"
36+
func complete(suggestion: String) -> String {
37+
"\(prefix) \(suggestion)"
5138
}
5239

53-
let subcommandCompletions = subcommands.map { (subcommand: ParsableCommand.Type) -> String in
40+
let subcommandCompletions = subcommands.map { subcommand in
5441
let escapedAbstract = subcommand.configuration.abstract.fishEscape()
5542
let suggestion = "-f -a '\(subcommand._commandName)' -d '\(escapedAbstract)'"
56-
return complete(ancestors: commandChain, suggestion: suggestion)
43+
return complete(suggestion: suggestion)
5744
}
5845

5946
let argumentCompletions = commands
6047
.argumentsForHelp(visibility: .default)
61-
.flatMap { $0.argumentSegments(commandChain) }
62-
.map { complete(ancestors: $0, suggestion: $1) }
48+
.compactMap { $0.argumentSegments(commandChain) }
49+
.map { $0.joined(separator: " ") }
50+
.map { complete(suggestion: $0) }
6351

6452
let completionsFromSubcommands = subcommands.flatMap { subcommand in
6553
generateCompletions(commandChain: commandChain + [subcommand._commandName], [subcommand])
6654
}
6755

68-
return argumentCompletions + subcommandCompletions + completionsFromSubcommands
56+
return completionsFromSubcommands + argumentCompletions + subcommandCompletions
6957
}
7058
}
7159

72-
extension String {
73-
fileprivate func fishEscape() -> String {
74-
self.replacingOccurrences(of: "'", with: #"\'"#)
60+
extension ArgumentDefinition {
61+
fileprivate func argumentSegments(_ commandChain: [String]) -> [String]? {
62+
guard help.visibility.base == .default,
63+
!names.isEmpty
64+
else { return nil }
65+
66+
var results = names.map{ $0.asFishSuggestion }
67+
68+
if !help.abstract.isEmpty {
69+
results += ["-d '\(help.abstract.fishEscape())'"]
70+
}
71+
72+
if isNullary {
73+
return results
74+
}
75+
76+
switch completion.kind {
77+
case .default: return results
78+
case .list(let list):
79+
return results + ["-r -f -k -a '\(list.joined(separator: " "))'"]
80+
case .file(let extensions):
81+
let pattern = "*.{\(extensions.joined(separator: ","))}"
82+
return results + ["-r -f -a '(for i in \(pattern); echo $i;end)'"]
83+
case .directory:
84+
return results + ["-r -f -a '(__fish_complete_directories)'"]
85+
case .shellCommand(let shellCommand):
86+
return results + ["-r -f -a '(\(shellCommand))'"]
87+
case .custom:
88+
let program = commandChain[0]
89+
let subcommands = commandChain.dropFirst().joined(separator: " ")
90+
return results + ["-r -f -a '(command \(program) ---completion \(subcommands) -- --custom (commandline -opc)[1..-1])'"]
91+
}
7592
}
7693
}
7794

@@ -86,70 +103,70 @@ extension Name {
86103
return "-o \(dashedName)"
87104
}
88105
}
106+
}
89107

90-
fileprivate var asFormattedFlag: String {
91-
switch self {
92-
case .long(let longName):
93-
return "--\(longName)"
94-
case .short(let shortName, _):
95-
return "-\(shortName)"
96-
case .longWithSingleDash(let dashedName):
97-
return "-\(dashedName)"
98-
}
108+
extension String {
109+
fileprivate func fishEscape() -> String {
110+
replacingOccurrences(of: "'", with: #"\'"#)
99111
}
100112
}
101113

102-
extension ArgumentDefinition {
103-
fileprivate func argumentSegments(_ commandChain: [String]) -> [([String], String)] {
104-
guard help.visibility.base == .default else { return [] }
105-
106-
var results = [([String], String)]()
107-
var formattedFlags = [String]()
108-
var flags = [String]()
109-
switch self.kind {
110-
case .positional, .default:
111-
break
112-
case .named(let names):
113-
flags = names.map { $0.asFishSuggestion }
114-
formattedFlags = names.map { $0.asFormattedFlag }
115-
if !flags.isEmpty {
116-
// add these flags to suggestions
117-
var suggestion = "-f\(isNullary ? "" : " -r") \(flags.joined(separator: " "))"
118-
if !help.abstract.isEmpty {
119-
suggestion += " -d '\(help.abstract.fishEscape())'"
120-
}
121-
122-
results.append((commandChain, suggestion))
123-
}
124-
}
114+
extension FishCompletionsGenerator {
125115

126-
if isNullary {
127-
return results
128-
}
116+
private static var separator: String { " " }
129117

130-
// each flag alternative gets its own completion suggestion
131-
for flag in formattedFlags {
132-
let ancestors = commandChain + [flag]
133-
switch self.completion.kind {
134-
case .default:
135-
break
136-
case .list(let list):
137-
results.append((ancestors, "-f -k -a '\(list.joined(separator: " "))'"))
138-
case .file(let extensions):
139-
let pattern = "*.{\(extensions.joined(separator: ","))}"
140-
results.append((ancestors, "-f -a '(for i in \(pattern); echo $i;end)'"))
141-
case .directory:
142-
results.append((ancestors, "-f -a '(__fish_complete_directories)'"))
143-
case .shellCommand(let shellCommand):
144-
results.append((ancestors, "-f -a '(\(shellCommand))'"))
145-
case .custom:
146-
let program = commandChain[0]
147-
let subcommands = commandChain.dropFirst().joined(separator: " ")
148-
let suggestion = "-f -a '(command \(program) ---completion \(subcommands) -- --custom (commandline -opc)[1..-1])'"
149-
results.append((ancestors, suggestion))
150-
}
151-
}
118+
private static func preprocessorFunctionName(commandName: String) -> String {
119+
"_swift_\(commandName)_preprocessor"
120+
}
121+
122+
private static func preprocessorFunction(commandName: String) -> String {
123+
"""
124+
# A function which filters options which starts with "-" from $argv.
125+
function \(preprocessorFunctionName(commandName: commandName))
126+
set -l results
127+
for i in (seq (count $argv))
128+
switch (echo $argv[$i] | string sub -l 1)
129+
case '-'
130+
case '*'
131+
echo $argv[$i]
132+
end
133+
end
134+
end
135+
"""
136+
}
137+
138+
private static func helperFunctionName(commandName: String) -> String {
139+
"_swift_" + commandName + "_using_command"
140+
}
152141

153-
return results
142+
private static func helperFunction(commandName: String) -> String {
143+
let functionName = helperFunctionName(commandName: commandName)
144+
let preprocessorFunctionName = preprocessorFunctionName(commandName: commandName)
145+
return """
146+
function \(functionName)
147+
set -l currentCommands (\(preprocessorFunctionName) (commandline -opc))
148+
set -l expectedCommands (string split \"\(separator)\" $argv[1])
149+
set -l subcommands (string split \"\(separator)\" $argv[2])
150+
if [ (count $currentCommands) -ge (count $expectedCommands) ]
151+
for i in (seq (count $expectedCommands))
152+
if [ $currentCommands[$i] != $expectedCommands[$i] ]
153+
return 1
154+
end
155+
end
156+
if [ (count $currentCommands) -eq (count $expectedCommands) ]
157+
return 0
158+
end
159+
if [ (count $subcommands) -gt 1 ]
160+
for i in (seq (count $subcommands))
161+
if [ $currentCommands[(math (count $expectedCommands) + 1)] = $subcommands[$i] ]
162+
return 1
163+
end
164+
end
165+
end
166+
return 0
167+
end
168+
return 1
169+
end
170+
"""
154171
}
155172
}

Tests/ArgumentParserExampleTests/MathExampleTests.swift

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -545,45 +545,65 @@ _math
545545
"""
546546

547547
private let fishCompletionScriptText = """
548+
# A function which filters options which starts with \"-\" from $argv.
549+
function _swift_math_preprocessor
550+
set -l results
551+
for i in (seq (count $argv))
552+
switch (echo $argv[$i] | string sub -l 1)
553+
case '-'
554+
case '*'
555+
echo $argv[$i]
556+
end
557+
end
558+
end
559+
548560
function _swift_math_using_command
549-
set -l cmd (commandline -opc)
550-
if [ (count $cmd) -eq (count $argv) ]
551-
for i in (seq (count $argv))
552-
if [ $cmd[$i] != $argv[$i] ]
561+
set -l currentCommands (_swift_math_preprocessor (commandline -opc))
562+
set -l expectedCommands (string split \" \" $argv[1])
563+
set -l subcommands (string split \" \" $argv[2])
564+
if [ (count $currentCommands) -ge (count $expectedCommands) ]
565+
for i in (seq (count $expectedCommands))
566+
if [ $currentCommands[$i] != $expectedCommands[$i] ]
553567
return 1
554568
end
555569
end
570+
if [ (count $currentCommands) -eq (count $expectedCommands) ]
571+
return 0
572+
end
573+
if [ (count $subcommands) -gt 1 ]
574+
for i in (seq (count $subcommands))
575+
if [ $currentCommands[(math (count $expectedCommands) + 1)] = $subcommands[$i] ]
576+
return 1
577+
end
578+
end
579+
end
556580
return 0
557581
end
558582
return 1
559583
end
560-
complete -c math -n '_swift_math_using_command math' -f -l version -d 'Show the version.'
561-
complete -c math -n '_swift_math_using_command math' -f -s h -l help -d 'Show help information.'
562-
complete -c math -n '_swift_math_using_command math' -f -a 'add' -d 'Print the sum of the values.'
563-
complete -c math -n '_swift_math_using_command math' -f -a 'multiply' -d 'Print the product of the values.'
564-
complete -c math -n '_swift_math_using_command math' -f -a 'stats' -d 'Calculate descriptive statistics.'
565-
complete -c math -n '_swift_math_using_command math' -f -a 'help' -d 'Show subcommand help information.'
566-
complete -c math -n '_swift_math_using_command math add' -f -l hex-output -s x -d 'Use hexadecimal notation for the result.'
567-
complete -c math -n '_swift_math_using_command math add' -f -s h -l help -d 'Show help information.'
568-
complete -c math -n '_swift_math_using_command math multiply' -f -l hex-output -s x -d 'Use hexadecimal notation for the result.'
569-
complete -c math -n '_swift_math_using_command math multiply' -f -s h -l help -d 'Show help information.'
570-
complete -c math -n '_swift_math_using_command math stats' -f -s h -l help -d 'Show help information.'
571-
complete -c math -n '_swift_math_using_command math stats' -f -a 'average' -d 'Print the average of the values.'
572-
complete -c math -n '_swift_math_using_command math stats' -f -a 'stdev' -d 'Print the standard deviation of the values.'
573-
complete -c math -n '_swift_math_using_command math stats' -f -a 'quantiles' -d 'Print the quantiles of the values (TBD).'
574-
complete -c math -n '_swift_math_using_command math stats' -f -a 'help' -d 'Show subcommand help information.'
575-
complete -c math -n '_swift_math_using_command math stats average' -f -r -l kind -d 'The kind of average to provide.'
576-
complete -c math -n '_swift_math_using_command math stats average --kind' -f -k -a 'mean median mode'
577-
complete -c math -n '_swift_math_using_command math stats average' -f -l version -d 'Show the version.'
578-
complete -c math -n '_swift_math_using_command math stats average' -f -s h -l help -d 'Show help information.'
579-
complete -c math -n '_swift_math_using_command math stats stdev' -f -s h -l help -d 'Show help information.'
580-
complete -c math -n '_swift_math_using_command math stats quantiles' -f -r -l file
581-
complete -c math -n '_swift_math_using_command math stats quantiles --file' -f -a '(for i in *.{txt,md}; echo $i;end)'
582-
complete -c math -n '_swift_math_using_command math stats quantiles' -f -r -l directory
583-
complete -c math -n '_swift_math_using_command math stats quantiles --directory' -f -a '(__fish_complete_directories)'
584-
complete -c math -n '_swift_math_using_command math stats quantiles' -f -r -l shell
585-
complete -c math -n '_swift_math_using_command math stats quantiles --shell' -f -a '(head -100 /usr/share/dict/words | tail -50)'
586-
complete -c math -n '_swift_math_using_command math stats quantiles' -f -r -l custom
587-
complete -c math -n '_swift_math_using_command math stats quantiles --custom' -f -a '(command math ---completion stats quantiles -- --custom (commandline -opc)[1..-1])'
588-
complete -c math -n '_swift_math_using_command math stats quantiles' -f -s h -l help -d 'Show help information.'
584+
585+
complete -c math -n \'_swift_math_using_command \"math add\"\' -l hex-output -s x -d \'Use hexadecimal notation for the result.\'
586+
complete -c math -n \'_swift_math_using_command \"math add\"\' -s h -l help -d \'Show help information.\'
587+
complete -c math -n \'_swift_math_using_command \"math multiply\"\' -l hex-output -s x -d \'Use hexadecimal notation for the result.\'
588+
complete -c math -n \'_swift_math_using_command \"math multiply\"\' -s h -l help -d \'Show help information.\'
589+
complete -c math -n \'_swift_math_using_command \"math stats average\"\' -l kind -d \'The kind of average to provide.\' -r -f -k -a \'mean median mode\'
590+
complete -c math -n \'_swift_math_using_command \"math stats average\"\' -l version -d \'Show the version.\'
591+
complete -c math -n \'_swift_math_using_command \"math stats average\"\' -s h -l help -d \'Show help information.\'
592+
complete -c math -n \'_swift_math_using_command \"math stats stdev\"\' -s h -l help -d \'Show help information.\'
593+
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l file -r -f -a \'(for i in *.{txt,md}; echo $i;end)\'
594+
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l directory -r -f -a \'(__fish_complete_directories)\'
595+
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l shell -r -f -a \'(head -100 /usr/share/dict/words | tail -50)\'
596+
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l custom -r -f -a \'(command math ---completion stats quantiles -- --custom (commandline -opc)[1..-1])\'
597+
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -s h -l help -d \'Show help information.\'
598+
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -s h -l help -d \'Show help information.\'
599+
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'average\' -d \'Print the average of the values.\'
600+
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'stdev\' -d \'Print the standard deviation of the values.\'
601+
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'quantiles\' -d \'Print the quantiles of the values (TBD).\'
602+
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'help\' -d \'Show subcommand help information.\'
603+
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -l version -d \'Show the version.\'
604+
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -s h -l help -d \'Show help information.\'
605+
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -f -a \'add\' -d \'Print the sum of the values.\'
606+
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -f -a \'multiply\' -d \'Print the product of the values.\'
607+
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -f -a \'stats\' -d \'Calculate descriptive statistics.\'
608+
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -f -a \'help\' -d \'Show subcommand help information.\'
589609
"""

0 commit comments

Comments
 (0)