/
FishCompletionsGenerator.swift
155 lines (136 loc) · 5.07 KB
/
FishCompletionsGenerator.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
struct FishCompletionsGenerator {
static func generateCompletionScript(_ type: ParsableCommand.Type) -> String {
let programName = type._commandName
let helper = """
function _swift_\(programName)_using_command
set -l cmd (commandline -opc)
if [ (count $cmd) -eq (count $argv) ]
for i in (seq (count $argv))
if [ $cmd[$i] != $argv[$i] ]
return 1
end
end
return 0
end
return 1
end
"""
let completions = generateCompletions(commandChain: [programName], [type])
.joined(separator: "\n")
return helper + completions
}
static func generateCompletions(commandChain: [String], _ commands: [ParsableCommand.Type])
-> [String]
{
let type = commands.last!
let isRootCommand = commands.count == 1
let programName = commandChain[0]
var subcommands = type.configuration.subcommands
.filter { $0.configuration.shouldDisplay }
if !subcommands.isEmpty {
if isRootCommand {
subcommands.append(HelpCommand.self)
}
}
let prefix = "complete -c \(programName) -n '_swift_\(programName)_using_command"
/// We ask each suggestion to produce 2 pieces of information
/// - Parameters
/// - ancestors: a list of "ancestor" which must be present in the current shell buffer for
/// this suggestion to be considered. This could be a combination of (nested)
/// subcommands and flags.
/// - suggestion: text for the actual suggestion
/// - Returns: A completion expression
func complete(ancestors: [String], suggestion: String) -> String {
"\(prefix) \(ancestors.joined(separator: " "))' \(suggestion)"
}
let subcommandCompletions = subcommands.map { (subcommand: ParsableCommand.Type) -> String in
let escapedAbstract = subcommand.configuration.abstract.fishEscape()
let suggestion = "-f -a '\(subcommand._commandName)' -d '\(escapedAbstract)'"
return complete(ancestors: commandChain, suggestion: suggestion)
}
let argumentCompletions = commands
.argumentsForHelp(visibility: .default)
.flatMap { $0.argumentSegments(commandChain) }
.map { complete(ancestors: $0, suggestion: $1) }
let completionsFromSubcommands = subcommands.flatMap { subcommand in
generateCompletions(commandChain: commandChain + [subcommand._commandName], [subcommand])
}
return argumentCompletions + subcommandCompletions + completionsFromSubcommands
}
}
extension String {
fileprivate func fishEscape() -> String {
self.replacingOccurrences(of: "'", with: #"\'"#)
}
}
extension Name {
fileprivate var asFishSuggestion: String {
switch self {
case .long(let longName):
return "-l \(longName)"
case .short(let shortName, _):
return "-s \(shortName)"
case .longWithSingleDash(let dashedName):
return "-o \(dashedName)"
}
}
fileprivate var asFormattedFlag: String {
switch self {
case .long(let longName):
return "--\(longName)"
case .short(let shortName, _):
return "-\(shortName)"
case .longWithSingleDash(let dashedName):
return "-\(dashedName)"
}
}
}
extension ArgumentDefinition {
fileprivate func argumentSegments(_ commandChain: [String]) -> [([String], String)] {
guard help.visibility.base == .default else { return [] }
var results = [([String], String)]()
var formattedFlags = [String]()
var flags = [String]()
switch self.kind {
case .positional, .default:
break
case .named(let names):
flags = names.map { $0.asFishSuggestion }
formattedFlags = names.map { $0.asFormattedFlag }
if !flags.isEmpty {
// add these flags to suggestions
var suggestion = "-f\(isNullary ? "" : " -r") \(flags.joined(separator: " "))"
if !help.abstract.isEmpty {
suggestion += " -d '\(help.abstract.fishEscape())'"
}
results.append((commandChain, suggestion))
}
}
if isNullary {
return results
}
// each flag alternative gets its own completion suggestion
for flag in formattedFlags {
let ancestors = commandChain + [flag]
switch self.completion.kind {
case .default:
break
case .list(let list):
results.append((ancestors, "-f -k -a '\(list.joined(separator: " "))'"))
case .file(let extensions):
let pattern = "*.{\(extensions.joined(separator: ","))}"
results.append((ancestors, "-f -a '(for i in \(pattern); echo $i;end)'"))
case .directory:
results.append((ancestors, "-f -a '(__fish_complete_directories)'"))
case .shellCommand(let shellCommand):
results.append((ancestors, "-f -a '(\(shellCommand))'"))
case .custom:
let program = commandChain[0]
let subcommands = commandChain.dropFirst().joined(separator: " ")
let suggestion = "-f -a '(command \(program) ---completion \(subcommands) -- --custom (commandline -opc)[1..-1])'"
results.append((ancestors, suggestion))
}
}
return results
}
}