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