Skip to content

Commit d55e2a2

Browse files
authored
Merge pull request #1007 from ahoppen/ahoppen/concurrent-map
Add a `concurrentMap` function to `Collection`
2 parents e1612d5 + a73d0df commit d55e2a2

File tree

3 files changed

+79
-53
lines changed

3 files changed

+79
-53
lines changed

Sources/SKSupport/AsyncUtils.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import Foundation
14+
1315
public extension Task {
1416
/// Awaits the value of the result.
1517
///
@@ -65,3 +67,38 @@ public func withCancellableCheckedThrowingContinuation<Handle, Result>(
6567
onCancel: callCancel
6668
)
6769
}
70+
71+
extension Collection {
72+
/// Transforms all elements in the collection concurrently and returns the transformed collection.
73+
public func concurrentMap<TransformedElement>(
74+
maxConcurrentTasks: Int = ProcessInfo.processInfo.processorCount,
75+
_ transform: @escaping (Element) async -> TransformedElement
76+
) async -> [TransformedElement] {
77+
let indexedResults = await withTaskGroup(of: (index: Int, element: TransformedElement).self) { taskGroup in
78+
var indexedResults: [(index: Int, element: TransformedElement)] = []
79+
for (index, element) in self.enumerated() {
80+
if index >= maxConcurrentTasks {
81+
// Wait for one item to finish being transformed so we don't exceed the maximum number of concurrent tasks.
82+
if let (index, transformedElement) = await taskGroup.next() {
83+
indexedResults.append((index, transformedElement))
84+
}
85+
}
86+
taskGroup.addTask {
87+
return (index, await transform(element))
88+
}
89+
}
90+
91+
// Wait for all remaining elements to be transformed.
92+
for await (index, transformedElement) in taskGroup {
93+
indexedResults.append((index, transformedElement))
94+
}
95+
return indexedResults
96+
}
97+
return Array<TransformedElement>(unsafeUninitializedCapacity: indexedResults.count) { buffer, count in
98+
for (index, transformedElement) in indexedResults {
99+
(buffer.baseAddress! + index).initialize(to: transformedElement)
100+
}
101+
count = indexedResults.count
102+
}
103+
}
104+
}

Sources/SourceKitLSP/Rename.swift

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -317,45 +317,44 @@ extension SourceKitServer {
317317

318318
// Now, call `editsToRename(locations:in:oldName:newName:)` on the language service to convert these ranges into
319319
// edits.
320-
await withTaskGroup(of: (DocumentURI, [TextEdit])?.self) { taskGroup in
321-
for (url, renameLocations) in locationsByFile {
320+
let urisAndEdits =
321+
await locationsByFile
322+
.filter { changes[DocumentURI($0.key)] == nil }
323+
.concurrentMap { (url: URL, renameLocations: [RenameLocation]) -> (DocumentURI, [TextEdit])? in
322324
let uri = DocumentURI(url)
323-
if changes[uri] != nil {
324-
// We already have edits for this document provided by the language service, so we don't need to compute
325-
// rename ranges for it.
326-
continue
325+
// Create a document snapshot to operate on. If the document is open, load it from the document manager,
326+
// otherwise conjure one from the file on disk. We need the file in memory to perform UTF-8 to UTF-16 column
327+
// conversions.
328+
// We should technically infer the language for the from-disk snapshot. But `editsToRename` doesn't care
329+
// about it, so defaulting to Swift is good enough for now
330+
// If we fail to get edits for one file, log an error and continue but don't fail rename completely.
331+
guard
332+
let snapshot = (try? self.documentManager.latestSnapshot(uri))
333+
?? (try? DocumentSnapshot(url, language: .swift))
334+
else {
335+
logger.error("Failed to get document snapshot for \(uri.forLogging)")
336+
return nil
327337
}
328-
taskGroup.addTask {
329-
// Create a document snapshot to operate on. If the document is open, load it from the document manager,
330-
// otherwise conjure one from the file on disk. We need the file in memory to perform UTF-8 to UTF-16 column
331-
// conversions.
332-
// We should technically infer the language for the from-disk snapshot. But `editsToRename` doesn't care
333-
// about it, so defaulting to Swift is good enough for now
334-
// If we fail to get edits for one file, log an error and continue but don't fail rename completely.
335-
guard
336-
let snapshot = (try? self.documentManager.latestSnapshot(uri))
337-
?? (try? DocumentSnapshot(url, language: .swift))
338-
else {
339-
logger.error("Failed to get document snapshot for \(uri.forLogging)")
340-
return nil
341-
}
342-
do {
343-
let edits = try await languageService.editsToRename(
344-
locations: renameLocations,
345-
in: snapshot,
346-
oldName: oldName,
347-
newName: request.newName
348-
)
349-
return (uri, edits)
350-
} catch {
351-
logger.error("Failed to get edits for \(uri.forLogging): \(error.forLogging)")
352-
return nil
353-
}
338+
do {
339+
let edits = try await languageService.editsToRename(
340+
locations: renameLocations,
341+
in: snapshot,
342+
oldName: oldName,
343+
newName: request.newName
344+
)
345+
return (uri, edits)
346+
} catch {
347+
logger.error("Failed to get edits for \(uri.forLogging): \(error.forLogging)")
348+
return nil
354349
}
355-
}
356-
for await case let (uri, textEdits)? in taskGroup where !textEdits.isEmpty {
357-
precondition(changes[uri] == nil, "We should not create tasks for URIs that already have edits")
358-
changes[uri] = textEdits
350+
}.compactMap { $0 }
351+
for (uri, editsForUri) in urisAndEdits {
352+
precondition(
353+
changes[uri] == nil,
354+
"We should have only computed edits for URIs that didn't have edits from the initial rename request"
355+
)
356+
if !editsForUri.isEmpty {
357+
changes[uri] = editsForUri
359358
}
360359
}
361360
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -647,24 +647,14 @@ extension SwiftLanguageServer {
647647
guard providers.isEmpty == false else {
648648
return []
649649
}
650-
let codeActions = await withTaskGroup(of: [CodeAction].self) { taskGroup in
651-
for provider in providers {
652-
taskGroup.addTask {
653-
do {
654-
return try await provider(req)
655-
} catch {
656-
// Ignore any providers that failed to provide refactoring actions.
657-
return []
658-
}
659-
}
660-
}
661-
var results: [CodeAction] = []
662-
for await taskResults in taskGroup {
663-
results += taskResults
650+
return await providers.concurrentMap { provider in
651+
do {
652+
return try await provider(req)
653+
} catch {
654+
// Ignore any providers that failed to provide refactoring actions.
655+
return []
664656
}
665-
return results
666-
}
667-
return codeActions
657+
}.flatMap { $0 }
668658
}
669659

670660
func retrieveRefactorCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] {

0 commit comments

Comments
 (0)