Skip to content

Commit 9a298ff

Browse files
committed
Support async custom completion closures.
Signed-off-by: Ross Goldberg <[email protected]>
1 parent d8a9695 commit 9a298ff

File tree

7 files changed

+82
-30
lines changed

7 files changed

+82
-30
lines changed

Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ extension [ParsableCommand.Type] {
380380
381381
"""
382382

383-
case .custom:
383+
case .custom, .customAsync:
384384
// Generate a call back into the command to retrieve a completions list
385385
return """
386386
\(addCompletionsFunctionName) -W\

Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ extension [ParsableCommand.Type] {
223223
results += ["-\(r)fa '(\(completeDirectoriesFunctionName))'"]
224224
case .shellCommand(let shellCommand):
225225
results += ["-\(r)fka '(\(shellCommand))'"]
226-
case .custom:
226+
case .custom, .customAsync:
227227
results += [
228228
"""
229229
-\(r)fka '(\

Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ extension [ParsableCommand.Type] {
204204
nil
205205
)
206206

207-
case .custom:
207+
case .custom, .customAsync:
208208
return (
209209
"{\(customCompleteFunctionName) \(arg.customCompletionCall(self)) \"${current_word_index}\" \"$(\(cursorIndexInCurrentWordFunctionName))\"}",
210210
nil

Sources/ArgumentParser/Parsable Properties/CompletionKind.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public struct CompletionKind {
4141
case directory
4242
case shellCommand(String)
4343
case custom(@Sendable ([String], Int, String) -> [String])
44+
case customAsync(@Sendable ([String], Int, String) async -> [String])
4445
case customDeprecated(@Sendable ([String]) -> [String])
4546
}
4647

@@ -176,6 +177,17 @@ public struct CompletionKind {
176177
CompletionKind(kind: .custom(completion))
177178
}
178179

180+
/// Generate completions using the given async closure.
181+
///
182+
/// The same as `custom(@Sendable @escaping ([String], Int, String) -> [String])`,
183+
/// except that the closure is asynchronous.
184+
@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
185+
public static func custom(
186+
_ completion: @Sendable @escaping ([String], Int, String) async -> [String]
187+
) -> CompletionKind {
188+
CompletionKind(kind: .customAsync(completion))
189+
}
190+
179191
/// Deprecated; only kept for backwards compatibility.
180192
///
181193
/// The same as `custom(@Sendable @escaping ([String], Int, String) -> [String])`,

Sources/ArgumentParser/Parsing/CommandParser.swift

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
#if swift(>=6.0)
13+
private import class Dispatch.DispatchSemaphore
1314
internal import class Foundation.ProcessInfo
1415
#else
16+
import class Dispatch.DispatchSemaphore
1517
import class Foundation.ProcessInfo
1618
#endif
1719

@@ -447,37 +449,20 @@ extension CommandParser {
447449
let completions: [String]
448450
switch argument.completion.kind {
449451
case .custom(let complete):
450-
var args = args.dropFirst(0)
451-
guard
452-
let s = args.popFirst(),
453-
let completingArgumentIndex = Int(s)
454-
else {
455-
throw ParserError.invalidState
456-
}
457-
458-
guard
459-
let arg = args.popFirst(),
460-
let cursorIndexWithinCompletingArgument = Int(arg)
461-
else {
462-
throw ParserError.invalidState
463-
}
464-
465-
let completingPrefix: String
466-
if let completingArgument = args.last {
467-
completingPrefix = String(
468-
completingArgument.prefix(cursorIndexWithinCompletingArgument)
469-
)
470-
} else if cursorIndexWithinCompletingArgument == 0 {
471-
completingPrefix = ""
472-
} else {
473-
throw ParserError.invalidState
474-
}
475-
452+
let (args, completingArgumentIndex, completingPrefix) =
453+
try parseCustomCompletionArguments(from: args)
476454
completions = complete(
477-
Array(args),
455+
args,
478456
completingArgumentIndex,
479457
completingPrefix
480458
)
459+
case .customAsync(let complete):
460+
if #available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
461+
{
462+
completions = try asyncCustomCompletions(from: args, complete: complete)
463+
} else {
464+
throw ParserError.invalidState
465+
}
481466
case .customDeprecated(let complete):
482467
completions = complete(args)
483468
default:
@@ -494,6 +479,57 @@ extension CommandParser {
494479
}
495480
}
496481

482+
private func parseCustomCompletionArguments(
483+
from args: [String]
484+
) throws -> ([String], Int, String) {
485+
var args = args.dropFirst(0)
486+
guard
487+
let s = args.popFirst(),
488+
let completingArgumentIndex = Int(s)
489+
else {
490+
throw ParserError.invalidState
491+
}
492+
493+
guard
494+
let arg = args.popFirst(),
495+
let cursorIndexWithinCompletingArgument = Int(arg)
496+
else {
497+
throw ParserError.invalidState
498+
}
499+
500+
let completingPrefix: String
501+
if let completingArgument = args.last {
502+
completingPrefix = String(
503+
completingArgument.prefix(cursorIndexWithinCompletingArgument)
504+
)
505+
} else if cursorIndexWithinCompletingArgument == 0 {
506+
completingPrefix = ""
507+
} else {
508+
throw ParserError.invalidState
509+
}
510+
511+
return (Array(args), completingArgumentIndex, completingPrefix)
512+
}
513+
514+
@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
515+
private func asyncCustomCompletions(
516+
from args: [String],
517+
complete: @escaping @Sendable ([String], Int, String) async -> [String]
518+
) throws -> [String] {
519+
let (args, completingArgumentIndex, completingPrefix) =
520+
try parseCustomCompletionArguments(from: args)
521+
var completions: [String] = []
522+
let semaphore = DispatchSemaphore(value: 0)
523+
Task {
524+
completions = await complete(
525+
args, completingArgumentIndex, completingPrefix
526+
)
527+
semaphore.signal()
528+
}
529+
semaphore.wait()
530+
return completions
531+
}
532+
497533
// MARK: Building Command Stacks
498534

499535
extension CommandParser {

Sources/ArgumentParser/Usage/DumpHelpGenerator.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ extension ArgumentInfoV0.CompletionKindV0 {
224224
self = .shellCommand(command: command)
225225
case .custom(_):
226226
self = .custom
227+
case .customAsync(_):
228+
self = .customAsync
227229
case .customDeprecated(_):
228230
self = .customDeprecated
229231
}

Sources/ArgumentParserToolInfo/ToolInfo.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ public struct ArgumentInfoV0: Codable, Hashable {
151151
case shellCommand(command: String)
152152
/// Generate completions using the given three-parameter closure.
153153
case custom
154+
/// Generate completions using the given async three-parameter closure.
155+
case customAsync
154156
/// Generate completions using the given one-parameter closure.
155157
@available(*, deprecated, message: "Use custom instead.")
156158
case customDeprecated

0 commit comments

Comments
 (0)