diff --git a/Examples/math/Math.swift b/Examples/math/Math.swift index 168f11458..b4160dda6 100644 --- a/Examples/math/Math.swift +++ b/Examples/math/Math.swift @@ -198,10 +198,17 @@ extension Math.Statistics { var oneOfFour: String? @Argument( - completion: .custom { _ in ["alabaster", "breakfast", "crunch", "crash"] } + completion: .custom { _, _, _ in + ["alabaster", "breakfast", "crunch", "crash"] + } ) var customArg: String? + @Argument( + completion: .custom { _ in ["alabaster", "breakfast", "crunch", "crash"] } + ) + var customDeprecatedArg: String? + @Argument(help: "A group of floating-point values to operate on.") var values: [Double] = [] @@ -222,12 +229,16 @@ extension Math.Statistics { var directory: String? @Option( - completion: .shellCommand("head -100 /usr/share/dict/words | tail -50")) + completion: .shellCommand("head -100 /usr/share/dict/words | tail -50") + ) var shell: String? @Option(completion: .custom(customCompletion)) var custom: String? + @Option(completion: .custom(customDeprecatedCompletion)) + var customDeprecated: String? + func validate() throws { if testSuccessExitCode { throw ExitCode.success @@ -248,7 +259,13 @@ extension Math.Statistics { } } -func customCompletion(_ s: [String]) -> [String] { +func customCompletion(_ s: [String], _: Int, _: Int) -> [String] { + (s.last ?? "").starts(with: "a") + ? ["aardvark", "aaaaalbert"] + : ["hello", "helicopter", "heliotrope"] +} + +func customDeprecatedCompletion(_ s: [String]) -> [String] { (s.last ?? "").starts(with: "a") ? ["aardvark", "aaaaalbert"] : ["hello", "helicopter", "heliotrope"] diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index b3268cb89..216884f7f 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -21,6 +21,22 @@ extension [ParsableCommand.Type] { return """ #!/bin/bash + \(cursorIndexInCurrentWordFunctionName)() { + local remaining="${COMP_LINE}" + + local word + for word in "${COMP_WORDS[@]::COMP_CWORD}"; do + remaining="${remaining##*([[:space:]])"${word}"*([[:space:]])}" + done + + local -ir index="$((COMP_POINT - ${#COMP_LINE} + ${#remaining}))" + if [[ "${index}" -le 0 ]]; then + printf 0 + else + printf %s "${index}" + fi + } + # positional arguments: # # - 1: the current (sub)command's count of positional arguments @@ -365,6 +381,16 @@ extension [ParsableCommand.Type] { """ case .custom: + // Generate a call back into the command to retrieve a completions list + return """ + \(addCompletionsFunctionName) -W\ + "$(\(customCompleteFunctionName) \(arg.customCompletionCall(self))\ + "${COMP_CWORD}"\ + "$(\(cursorIndexInCurrentWordFunctionName))")" + + """ + + case .customDeprecated: // Generate a call back into the command to retrieve a completions list return """ \(addCompletionsFunctionName) -W\ @@ -374,6 +400,10 @@ extension [ParsableCommand.Type] { } } + private var cursorIndexInCurrentWordFunctionName: String { + "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_cursor_index_in_current_word" + } + private var offerFlagsOptionsFunctionName: String { "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_offer_flags_options" } diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index b0a8f59bf..2168cfe2e 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -224,6 +224,15 @@ extension [ParsableCommand.Type] { case .shellCommand(let shellCommand): results += ["-\(r)fka '(\(shellCommand))'"] case .custom: + results += [ + """ + -\(r)fka '(\ + \(customCompletionFunctionName) \(arg.customCompletionCall(self)) \ + (count (\(tokensFunctionName) -pc)) (\(tokensFunctionName) -tC)\ + )' + """ + ] + case .customDeprecated: results += [ """ -\(r)fka '(\(customCompletionFunctionName) \(arg.customCompletionCall(self)))' diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index c570fb703..a6a49d1ef 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -28,12 +28,20 @@ extension [ParsableCommand.Type] { \(customCompleteFunctionName)() { local -a completions - completions=("${(@f)"$("${@}")"}") + completions=("${(@f)"$("${command_name}" "${@}" "${command_line[@]}")"}") if [[ "${#completions[@]}" -gt 1 ]]; then \(completeFunctionName) "${completions[@]:0:-1}" fi } + \(cursorIndexInCurrentWordFunctionName)() { + if [[ -z "${QIPREFIX}${IPREFIX}${PREFIX}" ]]; then + printf 0 + else + printf %s "${#${(z)LBUFFER}[-1]}" + fi + } + \(completionFunctions)\ \(completionFunctionName()) """ @@ -107,6 +115,7 @@ extension [ParsableCommand.Type] { local -r command_name="${words[1]}" local -ar command_line=("${words[@]}") + local -ir current_word_index="$((CURRENT - 1))" """ @@ -192,7 +201,13 @@ extension [ParsableCommand.Type] { case .custom: return ( - "{\(customCompleteFunctionName) \"${command_name}\" \(arg.customCompletionCall(self)) \"${command_line[@]}\"}", + "{\(customCompleteFunctionName) \(arg.customCompletionCall(self)) \"${current_word_index}\" \"$(\(cursorIndexInCurrentWordFunctionName))\"}", + nil + ) + + case .customDeprecated: + return ( + "{\(customCompleteFunctionName) \(arg.customCompletionCall(self))}", nil ) } @@ -218,6 +233,10 @@ extension [ParsableCommand.Type] { // Precondition: first is guaranteed to be non-empty "__\(first!._commandName)_custom_complete" } + + private var cursorIndexInCurrentWordFunctionName: String { + "__\(first?._commandName ?? "")_cursor_index_in_current_word" + } } extension String { diff --git a/Sources/ArgumentParser/Parsable Properties/CompletionKind.swift b/Sources/ArgumentParser/Parsable Properties/CompletionKind.swift index 03fdedcc2..2d229ba2d 100644 --- a/Sources/ArgumentParser/Parsable Properties/CompletionKind.swift +++ b/Sources/ArgumentParser/Parsable Properties/CompletionKind.swift @@ -40,7 +40,8 @@ public struct CompletionKind { case file(extensions: [String]) case directory case shellCommand(String) - case custom(@Sendable ([String]) -> [String]) + case custom(@Sendable ([String], Int, Int) -> [String]) + case customDeprecated(@Sendable ([String]) -> [String]) } internal var kind: Kind @@ -125,6 +126,12 @@ public struct CompletionKind { /// passed to Swift as `"abc\\""def"` (i.e. the Swift String's contents would /// include all 4 of the double quotes and the 2 consecutive backslashes). /// + /// The first of the two `Int` arguments is the 0-based index of the word + /// for which completions are being requested within the given `[String]`. + /// + /// The second of the two `Int` arguments is the 0-based index of the shell + /// cursor within the word for which completions are being requested. + /// /// ### bash /// /// In bash 3-, a process substitution (`<(…)`) in the command line prevents @@ -151,15 +158,40 @@ public struct CompletionKind { /// example, the shell word `"abc\\""def"` would be passed to Swift as /// `abc\def`. This is fixed in fish 4+. /// + /// In fish 3-, the cursor index is provided based on the verbatim word, not + /// based on the unquoted word, so it can be inconsistent with the unquoted + /// word that is supplied to Swift. This problem does not exist in fish 4+. + /// /// ### zsh /// /// In zsh, redirects (both their symbol and source/target) are omitted. + /// + /// In zsh, if the cursor is between a backslash and the character that it + /// escapes, the shell cursor index will be indicated as after the escaped + /// character, not as after the backslash. @preconcurrency public static func custom( - _ completion: @Sendable @escaping ([String]) -> [String] + _ completion: @Sendable @escaping ([String], Int, Int) -> [String] ) -> CompletionKind { CompletionKind(kind: .custom(completion)) } + + /// Deprecated; only kept for backwards compatibility. + /// + /// The same as `custom(@Sendable @escaping ([String], Int, Int) -> [String])`, + /// except that index arguments are not supplied. + @preconcurrency + @available( + *, + deprecated, + message: + "Provide a three-parameter closure instead. See custom(@Sendable @escaping ([String], Int, Int) -> [String])." + ) + public static func custom( + _ completion: @Sendable @escaping ([String]) -> [String] + ) -> CompletionKind { + CompletionKind(kind: .customDeprecated(completion)) + } } extension CompletionKind: Sendable {} diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index 275dde073..5c24586aa 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -373,7 +373,12 @@ extension CommandParser { func handleCustomCompletion(_ arguments: [String]) throws { // Completion functions use a custom format: // - // ---completion [ ...] -- [] + // ---completion [ ...] -- [ ...] + // + // is the 0-based index of the for which completions are being requested. + // + // is the 0-based index of the character within the before which the cursor is located. + // For an whose length is n, if the cursor is after the last element, will be set to n. // // The triple-dash prefix makes '---completion' invalid syntax for regular // arguments, so it's safe to use for this internal purpose. @@ -395,35 +400,38 @@ extension CommandParser { guard let argToMatch = args.popFirst() else { throw ParserError.invalidState } - // Completion text is optional here - let completionValues = Array(args) // Generate the argument set and parse the argument to find in the set let argset = ArgumentSet(current.element, visibility: .private, parent: nil) guard let parsedArgument = try parseIndividualArg(argToMatch, at: 0).first else { throw ParserError.invalidState } - // Look up the specified argument and retrieve its custom completion function - let completionFunction: ([String]) -> [String] - + // Look up the specified argument, then retrieve & run its custom completion function switch parsedArgument.value { case .option(let parsed): - guard let matchedArgument = argset.first(matching: parsed), - case .custom(let f) = matchedArgument.completion.kind - else { throw ParserError.invalidState } - completionFunction = f + guard let matchedArgument = argset.first(matching: parsed) else { + throw ParserError.invalidState + } + try customComplete(matchedArgument, forArguments: Array(args)) case .value(let str): - guard let key = InputKey(fullPathString: str), - let matchedArgument = argset.firstPositional(withKey: key), - case .custom(let f) = matchedArgument.completion.kind - else { throw ParserError.invalidState } - completionFunction = f + guard + let key = InputKey(fullPathString: str), + let matchedArgument = argset.firstPositional(withKey: key) + else { + throw ParserError.invalidState + } + try customComplete(matchedArgument, forArguments: Array(args)) case .terminator: throw ParserError.invalidState } + } + private func customComplete( + _ argument: ArgumentDefinition, + forArguments args: [String] + ) throws { let environment = ProcessInfo.processInfo.environment if let completionShellName = environment[ CompletionShell.shellEnvironmentVariableName] @@ -436,10 +444,38 @@ extension CommandParser { $0 = environment[CompletionShell.shellVersionEnvironmentVariableName] } + let completions: [String] + switch argument.completion.kind { + case .custom(let complete): + var args = args.dropFirst(0) + guard + let s = args.popFirst(), + let completingArgumentIndex = Int(s) + else { + throw ParserError.invalidState + } + + guard + let s = args.popFirst(), + let cursorIndexWithinCompletingArgument = Int(s) + else { + throw ParserError.invalidState + } + + completions = complete( + Array(args), + completingArgumentIndex, + cursorIndexWithinCompletingArgument + ) + case .customDeprecated(let complete): + completions = complete(args) + default: + throw ParserError.invalidState + } + // Parsing and retrieval successful! We don't want to continue with any // other parsing here, so after printing the result of the completion // function, exit with a success code. - let completions = completionFunction(completionValues) throw ParserError.completionScriptCustomResponse( CompletionShell.requesting?.format(completions: completions) ?? completions.joined(separator: "\n") @@ -472,9 +508,9 @@ extension CommandParser { return result } - func commandStack(for subcommand: ParsableCommand.Type) - -> [ParsableCommand.Type] - { + func commandStack( + for subcommand: ParsableCommand.Type + ) -> [ParsableCommand.Type] { let path = commandTree.path(to: subcommand) return path.isEmpty ? [commandTree.element] diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index dc4b6c00b..438ae411b 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -224,6 +224,8 @@ extension ArgumentInfoV0.CompletionKindV0 { self = .shellCommand(command: command) case .custom(_): self = .custom + case .customDeprecated(_): + self = .customDeprecated } } } diff --git a/Sources/ArgumentParserToolInfo/ToolInfo.swift b/Sources/ArgumentParserToolInfo/ToolInfo.swift index 926bb5156..df35f15fe 100644 --- a/Sources/ArgumentParserToolInfo/ToolInfo.swift +++ b/Sources/ArgumentParserToolInfo/ToolInfo.swift @@ -149,8 +149,11 @@ public struct ArgumentInfoV0: Codable, Hashable { case directory /// Call the given shell command to generate completions. case shellCommand(command: String) - /// Generate completions using the given closure. + /// Generate completions using the given closure including index arguments. case custom + /// Generate completions using the given closure without index arguments. + @available(*, deprecated, message: "Use custom instead.") + case customDeprecated } /// Kind of argument the ArgumentInfo describes. diff --git a/Tests/ArgumentParserExampleTests/MathExampleTests.swift b/Tests/ArgumentParserExampleTests/MathExampleTests.swift index 1a0db7a40..f1928f915 100644 --- a/Tests/ArgumentParserExampleTests/MathExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/MathExampleTests.swift @@ -114,11 +114,12 @@ final class MathExampleTests: XCTestCase { let helpText = """ OVERVIEW: Print the quantiles of the values (TBD). - USAGE: math stats quantiles [] [] [ ...] [--file ] [--directory ] [--shell ] [--custom ] + USAGE: math stats quantiles [] [] [] [ ...] [--file ] [--directory ] [--shell ] [--custom ] [--custom-deprecated ] ARGUMENTS: + A group of floating-point values to operate on. OPTIONS: @@ -126,6 +127,7 @@ final class MathExampleTests: XCTestCase { --directory --shell --custom + --custom-deprecated --version Show the version. -h, --help Show help information. @@ -251,7 +253,7 @@ extension MathExampleTests { forShell shell: CompletionShell ) throws { try AssertExecuteCommand( - command: "math ---completion stats quantiles -- --custom", + command: "math ---completion stats quantiles -- --custom 0 0", expected: shell.format(completions: [ "hello", "helicopter", @@ -263,7 +265,7 @@ extension MathExampleTests { ) try AssertExecuteCommand( - command: "math ---completion stats quantiles -- --custom h", + command: "math ---completion stats quantiles -- --custom 0 1 h", expected: shell.format(completions: [ "hello", "helicopter", @@ -275,7 +277,7 @@ extension MathExampleTests { ) try AssertExecuteCommand( - command: "math ---completion stats quantiles -- --custom a", + command: "math ---completion stats quantiles -- --custom 0 1 a", expected: shell.format(completions: [ "aardvark", "aaaaalbert", diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index b88219d2e..130f09334 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -1,5 +1,21 @@ #!/bin/bash +__math_cursor_index_in_current_word() { + local remaining="${COMP_LINE}" + + local word + for word in "${COMP_WORDS[@]::COMP_CWORD}"; do + remaining="${remaining##*([[:space:]])"${word}"*([[:space:]])}" + done + + local -ir index="$((COMP_POINT - ${#COMP_LINE} + ${#remaining}))" + if [[ "${index}" -le 0 ]]; then + printf 0 + else + printf %s "${index}" + fi +} + # positional arguments: # # - 1: the current (sub)command's count of positional arguments @@ -217,8 +233,8 @@ _math_stats_stdev() { _math_stats_quantiles() { flags=(--version -h --help) - options=(--file --directory --shell --custom) - __math_offer_flags_options 3 + options=(--file --directory --shell --custom --custom-deprecated) + __math_offer_flags_options 4 # Offer option value completions case "${prev}" in @@ -235,7 +251,11 @@ _math_stats_quantiles() { return ;; --custom) - __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- --custom)" + __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- --custom "${COMP_CWORD}" "$(__math_cursor_index_in_current_word)")" + return + ;; + --custom-deprecated) + __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- --custom-deprecated)" return ;; esac @@ -247,7 +267,11 @@ _math_stats_quantiles() { return ;; 2) - __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- customArg)" + __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- customArg "${COMP_CWORD}" "$(__math_cursor_index_in_current_word)")" + return + ;; + 3) + __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- customDeprecatedArg)" return ;; esac diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish b/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish index 4bce4f100..3abe14746 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish @@ -19,7 +19,7 @@ function __math_should_offer_completions_for -a expected_commands -a expected_po case 'stdev' __math_parse_subcommand -r 1 'version' 'h/help' case 'quantiles' - __math_parse_subcommand -r 3 'file=' 'directory=' 'shell=' 'custom=' 'version' 'h/help' + __math_parse_subcommand -r 4 'file=' 'directory=' 'shell=' 'custom=' 'custom-deprecated=' 'version' 'h/help' end case 'help' __math_parse_subcommand -r 1 'version' @@ -101,11 +101,13 @@ complete -c 'math' -n '__math_should_offer_completions_for "math stats average"' complete -c 'math' -n '__math_should_offer_completions_for "math stats stdev"' -l version -d 'Show the version.' complete -c 'math' -n '__math_should_offer_completions_for "math stats stdev"' -s h -l help -d 'Show help information.' complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 1' -fka 'alphabet alligator branch braggart' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 2' -fka '(__math_custom_completion ---completion stats quantiles -- customArg)' +complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 2' -fka '(__math_custom_completion ---completion stats quantiles -- customArg (count (__math_tokens -pc)) (__math_tokens -tC))' +complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 3' -fka '(__math_custom_completion ---completion stats quantiles -- customDeprecatedArg)' complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l file -rfa '(set -l exts \'txt\' \'md\';for p in (string match -e -- \'*/\' (commandline -t);or printf \n)*.{$exts};printf %s\n $p;end;__fish_complete_directories (commandline -t) \'\')' complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l directory -rfa '(__math_complete_directories)' complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l shell -rfka '(head -100 /usr/share/dict/words | tail -50)' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l custom -rfka '(__math_custom_completion ---completion stats quantiles -- --custom)' +complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l custom -rfka '(__math_custom_completion ---completion stats quantiles -- --custom (count (__math_tokens -pc)) (__math_tokens -tC))' +complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l custom-deprecated -rfka '(__math_custom_completion ---completion stats quantiles -- --custom-deprecated)' complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l version -d 'Show the version.' complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -s h -l help -d 'Show help information.' complete -c 'math' -n '__math_should_offer_completions_for "math help"' -l version -d 'Show the version.' diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index fe6007067..6bf3475c3 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -8,12 +8,20 @@ __math_complete() { __math_custom_complete() { local -a completions - completions=("${(@f)"$("${@}")"}") + completions=("${(@f)"$("${command_name}" "${@}" "${command_line[@]}")"}") if [[ "${#completions[@]}" -gt 1 ]]; then __math_complete "${completions[@]:0:-1}" fi } +__math_cursor_index_in_current_word() { + if [[ -z "${QIPREFIX}${IPREFIX}${PREFIX}" ]]; then + printf 0 + else + printf %s "${#${(z)LBUFFER}[-1]}" + fi +} + _math() { emulate -RL zsh -G setopt extendedglob nullglob numericglobsort @@ -29,6 +37,7 @@ _math() { local -r command_name="${words[1]}" local -ar command_line=("${words[@]}") + local -ir current_word_index="$((CURRENT - 1))" local -i ret=1 local -ar arg_specs=( @@ -147,12 +156,14 @@ _math_stats_quantiles() { local -ar math_stats_quantiles_one_of_four=('alphabet' 'alligator' 'branch' 'braggart') local -ar arg_specs=( ':one-of-four:{__math_complete "${math_stats_quantiles_one_of_four[@]}"}' - ':custom-arg:{__math_custom_complete "${command_name}" ---completion stats quantiles -- customArg "${command_line[@]}"}' + ':custom-arg:{__math_custom_complete ---completion stats quantiles -- customArg "${current_word_index}" "$(__math_cursor_index_in_current_word)"}' + ':custom-deprecated-arg:{__math_custom_complete ---completion stats quantiles -- customDeprecatedArg}' ':values:' '--file:file:_files -g '\''*.txt *.md'\''' '--directory:directory:_files -/' '--shell:shell:{local -a list;list=(${(f)"$(head -100 /usr/share/dict/words | tail -50)"});_describe "" list}' - '--custom:custom:{__math_custom_complete "${command_name}" ---completion stats quantiles -- --custom "${command_line[@]}"}' + '--custom:custom:{__math_custom_complete ---completion stats quantiles -- --custom "${current_word_index}" "$(__math_cursor_index_in_current_word)"}' + '--custom-deprecated:custom-deprecated:{__math_custom_complete ---completion stats quantiles -- --custom-deprecated}' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md index f7d37d8fa..f91c7643b 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md @@ -156,7 +156,7 @@ math stats stdev [...] [--version] [--help] Print the quantiles of the values (TBD). ``` -math stats quantiles [] [] [...] [--file=] [--directory=] [--shell=] [--custom=] [--version] [--help] +math stats quantiles [] [] [] [...] [--file=] [--directory=] [--shell=] [--custom=] [--custom-deprecated=] [--version] [--help] ``` - term **one-of-four:** @@ -165,6 +165,9 @@ math stats quantiles [] [] [...] [--file=] [] [...] [--file=:** +- term **--custom-deprecated=\:** + + - term **--version:** *Show the version.* diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathMarkdownReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathMarkdownReference().md index adcaac132..3578c578c 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathMarkdownReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathMarkdownReference().md @@ -156,7 +156,7 @@ math stats stdev [...] [--version] [--help] Print the quantiles of the values (TBD). ``` -math stats quantiles [] [] [...] [--file=] [--directory=] [--shell=] [--custom=] [--version] [--help] +math stats quantiles [] [] [] [...] [--file=] [--directory=] [--shell=] [--custom=] [--custom-deprecated=] [--version] [--help] ``` **one-of-four:** @@ -165,6 +165,9 @@ math stats quantiles [] [] [...] [--file=] [] [...] [--file=:** +**--custom-deprecated=\:** + + **--version:** *Show the version.* diff --git a/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathMultiPageManual().mdoc b/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathMultiPageManual().mdoc index e9f0e8c19..c8d90da51 100644 --- a/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathMultiPageManual().mdoc +++ b/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathMultiPageManual().mdoc @@ -228,23 +228,27 @@ and .Nm .Op Ar one-of-four .Op Ar custom-arg +.Op Ar custom-deprecated-arg .Op Ar values... .Op Fl -file Ar file .Op Fl -directory Ar directory .Op Fl -shell Ar shell .Op Fl -custom Ar custom +.Op Fl -custom-deprecated Ar custom-deprecated .Op Fl -version .Op Fl -help .Sh DESCRIPTION .Bl -tag -width 6n .It Ar one-of-four .It Ar custom-arg +.It Ar custom-deprecated-arg .It Ar values... A group of floating-point values to operate on. .It Fl -file Ar file .It Fl -directory Ar directory .It Fl -shell Ar shell .It Fl -custom Ar custom +.It Fl -custom-deprecated Ar custom-deprecated .It Fl -version Show the version. .It Fl h , -help diff --git a/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathSinglePageManual().mdoc b/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathSinglePageManual().mdoc index 3060fedeb..d36005cde 100644 --- a/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathSinglePageManual().mdoc +++ b/Tests/ArgumentParserGenerateManualTests/Snapshots/testMathSinglePageManual().mdoc @@ -75,12 +75,14 @@ Print the quantiles of the values (TBD). .Bl -tag -width 6n .It Ar one-of-four .It Ar custom-arg +.It Ar custom-deprecated-arg .It Ar values... A group of floating-point values to operate on. .It Fl -file Ar file .It Fl -directory Ar directory .It Fl -shell Ar shell .It Fl -custom Ar custom +.It Fl -custom-deprecated Ar custom-deprecated .It Fl -version Show the version. .It Fl h , -help diff --git a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift index 6b9e6b3ec..0cabf9602 100644 --- a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift +++ b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift @@ -50,7 +50,7 @@ extension CompletionScriptTests { } struct NestedArguments: ParsableArguments { - @Argument(completion: .custom { _ in candidates(prefix: "a") }) + @Argument(completion: .custom { _, _, _ in candidates(prefix: "a") }) var nestedArgument: String } @@ -74,7 +74,7 @@ extension CompletionScriptTests { @Option() var rep1: [String] @Option(name: [.short, .long]) var rep2: [String] - @Argument(completion: .custom { _ in candidates(prefix: "d") }) + @Argument(completion: .custom { _, _, _ in candidates(prefix: "d") }) var argument: String @OptionGroup var nested: NestedArguments @@ -91,7 +91,7 @@ extension CompletionScriptTests { @Option(help: #"Escaped chars: '[]\."#) var one: String - @Argument(completion: .custom { _ in candidates(prefix: "i") }) + @Argument(completion: .custom { _, _, _ in candidates(prefix: "i") }) var two: String } } @@ -143,21 +143,24 @@ extension CompletionScriptTests { extension CompletionScriptTests { struct Custom: ParsableCommand { @Option( - name: .shortAndLong, completion: .custom { _ in candidates(prefix: "e") }) + name: .shortAndLong, + completion: .custom { _, _, _ in candidates(prefix: "e") } + ) var one: String - @Argument(completion: .custom { _ in candidates(prefix: "f") }) + @Argument(completion: .custom { _, _, _ in candidates(prefix: "f") }) var two: String @Option( name: .customShort("z"), - completion: .custom { _ in candidates(prefix: "g") }) + completion: .custom { _, _, _ in candidates(prefix: "g") } + ) var three: String @OptionGroup var nested: NestedArguments struct NestedArguments: ParsableArguments { - @Argument(completion: .custom { _ in candidates(prefix: "h") }) + @Argument(completion: .custom { _, _, _ in candidates(prefix: "h") }) var four: String } } @@ -173,7 +176,7 @@ extension CompletionScriptTests { do { setenv(CompletionShell.shellEnvironmentVariableName, shell.rawValue, 1) defer { unsetenv(CompletionShell.shellEnvironmentVariableName) } - _ = try Custom.parse(["---completion", "--", arg]) + _ = try Custom.parse(["---completion", "--", arg, "0", "0"]) XCTFail("Didn't error as expected", file: file, line: line) } catch let error as CommandError { guard case .completionScriptCustomResponse(let output) = error.parserError diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 6fc57bb40..32d67dc9e 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -1,5 +1,21 @@ #!/bin/bash +__base_test_cursor_index_in_current_word() { + local remaining="${COMP_LINE}" + + local word + for word in "${COMP_WORDS[@]::COMP_CWORD}"; do + remaining="${remaining##*([[:space:]])"${word}"*([[:space:]])}" + done + + local -ir index="$((COMP_POINT - ${#COMP_LINE} + ${#remaining}))" + if [[ "${index}" -le 0 ]]; then + printf 0 + else + printf %s "${index}" + fi +} + # positional arguments: # # - 1: the current (sub)command's count of positional arguments @@ -182,11 +198,11 @@ _base_test() { # Offer positional completions case "${positional_number}" in 1) - __base_test_add_completions -W "$(__base_test_custom_complete ---completion -- argument)" + __base_test_add_completions -W "$(__base_test_custom_complete ---completion -- argument "${COMP_CWORD}" "$(__base_test_cursor_index_in_current_word)")" return ;; 2) - __base_test_add_completions -W "$(__base_test_custom_complete ---completion -- nested.nestedArgument)" + __base_test_add_completions -W "$(__base_test_custom_complete ---completion -- nested.nestedArgument "${COMP_CWORD}" "$(__base_test_cursor_index_in_current_word)")" return ;; esac @@ -228,7 +244,7 @@ _base_test_escaped_command() { # Offer positional completions case "${positional_number}" in 1) - __base_test_add_completions -W "$(__base_test_custom_complete ---completion escaped-command -- two)" + __base_test_add_completions -W "$(__base_test_custom_complete ---completion escaped-command -- two "${COMP_CWORD}" "$(__base_test_cursor_index_in_current_word)")" return ;; esac diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish index ac32cce50..29daaa2f2 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish @@ -80,13 +80,13 @@ complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l kind-counter complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l rep1 -rfka '' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -s r -l rep2 -rfka '' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 1' -fka '(__base-test_custom_completion ---completion -- argument)' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 2' -fka '(__base-test_custom_completion ---completion -- nested.nestedArgument)' +complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 1' -fka '(__base-test_custom_completion ---completion -- argument (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' +complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 2' -fka '(__base-test_custom_completion ---completion -- nested.nestedArgument (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -s h -l help -d 'Show help information.' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 3' -fa 'sub-command' -d '' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 3' -fa 'escaped-command' -d '' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 3' -fa 'help' -d 'Show subcommand help information.' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test sub-command"' -s h -l help -d 'Show help information.' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command"' -l one -d 'Escaped chars: \'[]\\.' -rfka '' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command" 1' -fka '(__base-test_custom_completion ---completion escaped-command -- two)' +complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command" 1' -fka '(__base-test_custom_completion ---completion escaped-command -- two (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command"' -s h -l help -d 'Show help information.' \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 6a046fb81..8bcb06661 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -8,12 +8,20 @@ __base-test_complete() { __base-test_custom_complete() { local -a completions - completions=("${(@f)"$("${@}")"}") + completions=("${(@f)"$("${command_name}" "${@}" "${command_line[@]}")"}") if [[ "${#completions[@]}" -gt 1 ]]; then __base-test_complete "${completions[@]:0:-1}" fi } +__base-test_cursor_index_in_current_word() { + if [[ -z "${QIPREFIX}${IPREFIX}${PREFIX}" ]]; then + printf 0 + else + printf %s "${#${(z)LBUFFER}[-1]}" + fi +} + _base-test() { emulate -RL zsh -G setopt extendedglob nullglob numericglobsort @@ -29,6 +37,7 @@ _base-test() { local -r command_name="${words[1]}" local -ar command_line=("${words[@]}") + local -ir current_word_index="$((CURRENT - 1))" local -i ret=1 local -ar __base_test_kind=('one' 'two' 'custom-three') @@ -47,8 +56,8 @@ _base-test() { '*--kind-counter' '*--rep1:rep1:' '*'{-r,--rep2}':rep2:' - ':argument:{__base-test_custom_complete "${command_name}" ---completion -- argument "${command_line[@]}"}' - ':nested-argument:{__base-test_custom_complete "${command_name}" ---completion -- nested.nestedArgument "${command_line[@]}"}' + ':argument:{__base-test_custom_complete ---completion -- argument "${current_word_index}" "$(__base-test_cursor_index_in_current_word)"}' + ':nested-argument:{__base-test_custom_complete ---completion -- nested.nestedArgument "${current_word_index}" "$(__base-test_cursor_index_in_current_word)"}' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' @@ -89,7 +98,7 @@ _base-test_escaped-command() { local -i ret=1 local -ar arg_specs=( '--one[Escaped chars: '\''\[\]\\.]:one:' - ':two:{__base-test_custom_complete "${command_name}" ---completion escaped-command -- two "${command_line[@]}"}' + ':two:{__base-test_custom_complete ---completion escaped-command -- two "${current_word_index}" "$(__base-test_cursor_index_in_current_word)"}' '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S : "${arg_specs[@]}" && ret=0 diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json index af618a847..f2f2e7559 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testMathDumpHelp().json @@ -436,6 +436,18 @@ "shouldDisplay" : true, "valueName" : "custom-arg" }, + { + "completionKind" : { + "customDeprecated" : { + + } + }, + "isOptional" : true, + "isRepeating" : false, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "custom-deprecated-arg" + }, { "abstract" : "A group of floating-point values to operate on.", "isOptional" : true, @@ -603,6 +615,28 @@ "shouldDisplay" : true, "valueName" : "custom" }, + { + "completionKind" : { + "customDeprecated" : { + + } + }, + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "custom-deprecated" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "custom-deprecated" + }, + "shouldDisplay" : true, + "valueName" : "custom-deprecated" + }, { "abstract" : "Show the version.", "isOptional" : true, diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json index edc0b3153..5a56bfc33 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testMathStatsDumpHelp().json @@ -229,6 +229,18 @@ "shouldDisplay" : true, "valueName" : "custom-arg" }, + { + "completionKind" : { + "customDeprecated" : { + + } + }, + "isOptional" : true, + "isRepeating" : false, + "kind" : "positional", + "shouldDisplay" : true, + "valueName" : "custom-deprecated-arg" + }, { "abstract" : "A group of floating-point values to operate on.", "isOptional" : true, @@ -396,6 +408,28 @@ "shouldDisplay" : true, "valueName" : "custom" }, + { + "completionKind" : { + "customDeprecated" : { + + } + }, + "isOptional" : true, + "isRepeating" : false, + "kind" : "option", + "names" : [ + { + "kind" : "long", + "name" : "custom-deprecated" + } + ], + "preferredName" : { + "kind" : "long", + "name" : "custom-deprecated" + }, + "shouldDisplay" : true, + "valueName" : "custom-deprecated" + }, { "abstract" : "Show the version.", "isOptional" : true,