Skip to content

Refactor completion script generation to use ToolInfoV0 #764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
394 changes: 194 additions & 200 deletions Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift

Large diffs are not rendered by default.

113 changes: 58 additions & 55 deletions Sources/ArgumentParser/Completions/CompletionsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
//
//===----------------------------------------------------------------------===//

#if swift(>=6.0)
internal import ArgumentParserToolInfo
#else
import ArgumentParserToolInfo
#endif

/// A shell for which the parser can generate a completion script.
public struct CompletionShell: RawRepresentable, Hashable, CaseIterable {
public var rawValue: String
Expand Down Expand Up @@ -134,84 +140,81 @@ struct CompletionsGenerator {
CompletionShell._requesting.withLock { $0 = shell }
switch shell {
case .zsh:
return [command].zshCompletionScript
return ToolInfoV0(commandStack: [command]).zshCompletionScript
case .bash:
return [command].bashCompletionScript
return ToolInfoV0(commandStack: [command]).bashCompletionScript
case .fish:
return [command].fishCompletionScript
return ToolInfoV0(commandStack: [command]).fishCompletionScript
default:
fatalError("Invalid CompletionShell: \(shell)")
}
}
}

extension ArgumentDefinition {
/// Returns a string with the arguments for the callback to generate custom completions for
/// this argument.
func customCompletionCall(_ commands: [ParsableCommand.Type]) -> String {
let subcommandNames =
commands.dropFirst().map { "\($0._commandName) " }.joined()
let argumentName =
names.preferredName?.synopsisString
?? self.help.keys.first?.fullPathString
?? "---"
return "---completion \(subcommandNames)-- \(argumentName)"
extension String {
func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self {
iterationCount == 0
? self
: replacingOccurrences(of: "'", with: "'\\''")
.shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1)
}
}

extension ParsableCommand {
fileprivate static var compositeCommandName: [String] {
if let superCommandName = configuration._superCommandName {
return [superCommandName]
+ _commandName.split(separator: " ").map(String.init)
} else {
return _commandName.split(separator: " ").map(String.init)
}
func shellEscapeForVariableName() -> Self {
replacingOccurrences(of: "-", with: "_")
}
}

extension [ParsableCommand.Type] {
var positionalArguments: [ArgumentDefinition] {
guard let command = last else {
return []
}
return ArgumentSet(command, visibility: .default, parent: nil)
.filter(\.isPositional)
extension CommandInfoV0 {
var commandContext: [String] {
(superCommands ?? []) + [commandName]
}

/// Include default 'help' subcommand in nonempty subcommand list if & only if
/// no help subcommand already exists.
mutating func addHelpSubcommandIfMissing() {
if !isEmpty && !contains(where: { $0._commandName == "help" }) {
append(HelpCommand.self)
}
var initialCommand: String {
superCommands?.first ?? commandName
}
}

extension Sequence where Element == ParsableCommand.Type {
func completionFunctionName() -> String {
"_"
+ self.flatMap { $0.compositeCommandName }
.uniquingAdjacentElements()
.joined(separator: "_")
var positionalArguments: [ArgumentInfoV0] {
(arguments ?? []).filter { $0.kind == .positional }
}

var shellVariableNamePrefix: String {
flatMap { $0.compositeCommandName }
.joined(separator: "_")
.shellEscapeForVariableName()
var completionFunctionName: String {
"_" + commandContext.joined(separator: "_")
}

var completionFunctionPrefix: String {
"__\(initialCommand)"
}
}

extension String {
func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self {
iterationCount == 0
? self
: replacingOccurrences(of: "'", with: "'\\''")
.shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1)
extension ArgumentInfoV0 {
/// Returns a string with the arguments for the callback to generate custom
/// completions for this argument.
func commonCustomCompletionCall(command: CommandInfoV0) -> String {
let subcommandNames =
command.commandContext.dropFirst().map { "\($0) " }.joined()

let argumentName: String
switch kind {
case .positional:
if let index = command.positionalArguments.firstIndex(of: self) {
argumentName = "positional@\(index)"
} else {
argumentName = "---"
}
default:
argumentName = preferredName?.commonCompletionSynopsisString() ?? "---"
}
return "---completion \(subcommandNames)-- \(argumentName)"
}
}

func shellEscapeForVariableName() -> Self {
replacingOccurrences(of: "-", with: "_")
extension ArgumentInfoV0.NameInfoV0 {
func commonCompletionSynopsisString() -> String {
switch kind {
case .long:
return "--\(name)"
case .short, .longWithSingleDash:
return "-\(name)"
}
}
}
Loading
Loading