Skip to content

Commit 2e25499

Browse files
authored
Merge pull request #850 from apple/ahoppen/asyncification
Migrate huge chunks of sourcekit-lsp to actors/async/await
2 parents 05b3abd + 81dec01 commit 2e25499

File tree

56 files changed

+2807
-2931
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2807
-2931
lines changed

Sources/LSPLogging/Logging.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ public func orLog<R>(
6262
}
6363
}
6464

65+
/// Like `try?`, but logs the error on failure.
66+
public func orLog<R>(
67+
_ prefix: String = "",
68+
level: LogLevel = .default,
69+
logger: Logger = Logger.shared,
70+
_ block: () async throws -> R?) async -> R?
71+
{
72+
do {
73+
return try await block()
74+
} catch {
75+
logger.log("\(prefix)\(prefix.isEmpty ? "" : " ")\(error)", level: level)
76+
return nil
77+
}
78+
}
79+
80+
6581
/// Logs the time that the given block takes to execute in milliseconds.
6682
public func logExecutionTime<R>(
6783
_ prefix: String = #function,

Sources/LSPTestSupport/AssertNoThrow.swift

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import XCTest
14+
15+
/// Same as `assertNoThrow` but executes the trailing closure.
16+
public func assertNoThrow<T>(
17+
_ expression: () throws -> T,
18+
_ message: @autoclosure () -> String = "",
19+
file: StaticString = #filePath,
20+
line: UInt = #line
21+
) {
22+
XCTAssertNoThrow(try expression(), message(), file: file, line: line)
23+
}
24+
25+
/// Same as `XCTAssertThrows` but executes the trailing closure.
26+
public func assertThrowsError<T>(
27+
_ expression: @autoclosure () async throws -> T,
28+
_ message: @autoclosure () -> String = "",
29+
file: StaticString = #filePath,
30+
line: UInt = #line,
31+
_ errorHandler: (_ error: Error) -> Void = { _ in }
32+
) async {
33+
let didThrow: Bool
34+
do {
35+
_ = try await expression()
36+
didThrow = false
37+
} catch {
38+
errorHandler(error)
39+
didThrow = true
40+
}
41+
if !didThrow {
42+
XCTFail("Expression was expected to throw but did not throw", file: file, line: line)
43+
}
44+
}
45+
46+
/// Same as `XCTAssertEqual` but doesn't take autoclosures and thus `expression1`
47+
/// and `expression2` can contain `await`.
48+
public func assertEqual<T: Equatable>(
49+
_ expression1: T,
50+
_ expression2: T,
51+
_ message: @autoclosure () -> String = "",
52+
file: StaticString = #filePath,
53+
line: UInt = #line
54+
) {
55+
XCTAssertEqual(expression1, expression2, message(), file: file, line: line)
56+
}
57+
58+
/// Same as `XCTAssertNil` but doesn't take autoclosures and thus `expression`
59+
/// can contain `await`.
60+
public func assertNil<T: Equatable>(
61+
_ expression: T?,
62+
_ message: @autoclosure () -> String = "",
63+
file: StaticString = #filePath,
64+
line: UInt = #line
65+
) {
66+
XCTAssertNil(expression, message(), file: file, line: line)
67+
}
68+
69+
/// Same as `XCTAssertNotNil` but doesn't take autoclosures and thus `expression`
70+
/// can contain `await`.
71+
public func assertNotNil<T: Equatable>(
72+
_ expression: T?,
73+
_ message: @autoclosure () -> String = "",
74+
file: StaticString = #filePath,
75+
line: UInt = #line
76+
) {
77+
XCTAssertNotNil(expression, message(), file: file, line: line)
78+
}
79+
80+
extension XCTestCase {
81+
private struct ExpectationNotFulfilledError: Error, CustomStringConvertible {
82+
var expecatations: [XCTestExpectation]
83+
84+
var description: String {
85+
return "One of the expectation was not fulfilled within timeout: \(expecatations.map(\.description).joined(separator: ", "))"
86+
}
87+
}
88+
89+
/// Wait for the given expectations to be fulfilled. If the expectations aren't
90+
/// fulfilled within `timeout`, throw an error, aborting the test execution.
91+
public func fulfillmentOfOrThrow(
92+
_ expectations: [XCTestExpectation],
93+
timeout: TimeInterval = defaultTimeout,
94+
enforceOrder enforceOrderOfFulfillment: Bool = false
95+
) async throws {
96+
// `XCTWaiter.fulfillment` was introduced in the macOS 13.3 SDK but marked as being available on macOS 10.15.
97+
// At the same time that XCTWaiter.fulfillment was introduced `XCTWaiter.wait` was deprecated in async contexts.
98+
// This means that we can't write code that compiles without warnings with both the macOS 13.3 and any previous SDK.
99+
// Accepting the warning here when compiling with macOS 13.3 or later is the only thing that I know of that we can do here.
100+
let started = XCTWaiter.wait(for: expectations, timeout: timeout, enforceOrder: enforceOrderOfFulfillment)
101+
if started != .completed {
102+
throw ExpectationNotFulfilledError(expecatations: expectations)
103+
}
104+
}
105+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
/// Abstraction layer so we can store a heterogeneous collection of tasks in an
16+
/// array.
17+
private protocol AnyTask: Sendable {
18+
func waitForCompletion() async
19+
}
20+
21+
extension Task: AnyTask where Failure == Never {
22+
func waitForCompletion() async {
23+
_ = await value
24+
}
25+
}
26+
27+
extension NSLock {
28+
/// NOTE: Keep in sync with SwiftPM's 'Sources/Basics/NSLock+Extensions.swift'
29+
func withLock<T>(_ body: () throws -> T) rethrows -> T {
30+
lock()
31+
defer { unlock() }
32+
return try body()
33+
}
34+
}
35+
36+
/// A queue that allows the execution of asyncronous blocks of code.
37+
public final class AsyncQueue {
38+
public enum QueueKind {
39+
/// A queue that allows concurrent execution of tasks.
40+
case concurrent
41+
42+
/// A queue that executes one task after the other.
43+
case serial
44+
}
45+
46+
private struct PendingTask {
47+
/// The task that is pending.
48+
let task: any AnyTask
49+
50+
/// Whether the task needs to finish executing befoer any other task can
51+
/// start in executing in the queue.
52+
let isBarrier: Bool
53+
54+
/// A unique value used to identify the task. This allows tasks to get
55+
/// removed from `pendingTasks` again after they finished executing.
56+
let id: UUID
57+
}
58+
59+
/// Whether the queue allows concurrent execution of tasks.
60+
private let kind: QueueKind
61+
62+
/// Lock guarding `pendingTasks`.
63+
private let pendingTasksLock = NSLock()
64+
65+
/// Pending tasks that have not finished execution yet.
66+
private var pendingTasks = [PendingTask]()
67+
68+
public init(_ kind: QueueKind) {
69+
self.kind = kind
70+
self.pendingTasksLock.name = "AsyncQueue"
71+
}
72+
73+
/// Schedule a new closure to be executed on the queue.
74+
///
75+
/// If this is a serial queue, all previously added tasks are guaranteed to
76+
/// finished executing before this closure gets executed.
77+
///
78+
/// If this is a barrier, all previously scheduled tasks are guaranteed to
79+
/// finish execution before the barrier is executed and all tasks that are
80+
/// added later will wait until the barrier finishes execution.
81+
@discardableResult
82+
public func async<Success: Sendable>(
83+
priority: TaskPriority? = nil,
84+
barrier isBarrier: Bool = false,
85+
@_inheritActorContext operation: @escaping @Sendable () async -> Success
86+
) -> Task<Success, Never> {
87+
let id = UUID()
88+
89+
return pendingTasksLock.withLock {
90+
// Build the list of tasks that need to finishe exeuction before this one
91+
// can be executed
92+
let dependencies: [PendingTask]
93+
switch (kind, isBarrier: isBarrier) {
94+
case (.concurrent, isBarrier: true):
95+
// Wait for all tasks after the last barrier.
96+
let lastBarrierIndex = pendingTasks.lastIndex(where: { $0.isBarrier }) ?? pendingTasks.startIndex
97+
dependencies = Array(pendingTasks[lastBarrierIndex...])
98+
case (.concurrent, isBarrier: false):
99+
// If there is a barrier, wait for it.
100+
dependencies = [pendingTasks.last(where: { $0.isBarrier })].compactMap { $0 }
101+
case (.serial, _):
102+
// We are in a serial queue. The last pending task must finish for this one to start.
103+
dependencies = [pendingTasks.last].compactMap { $0 }
104+
}
105+
106+
107+
// Schedule the task.
108+
let task = Task {
109+
for dependency in dependencies {
110+
await dependency.task.waitForCompletion()
111+
}
112+
113+
let result = await operation()
114+
115+
pendingTasksLock.withLock {
116+
pendingTasks.removeAll(where: { $0.id == id })
117+
}
118+
119+
return result
120+
}
121+
122+
pendingTasks.append(PendingTask(task: task, isBarrier: isBarrier, id: id))
123+
124+
return task
125+
}
126+
}
127+
}

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
add_library(LanguageServerProtocol STATIC
2+
AsyncQueue.swift
23
Cancellation.swift
34
Connection.swift
45
CustomCodable.swift

Sources/LanguageServerProtocol/Connection.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,18 @@ extension Connection {
4242
public protocol MessageHandler: AnyObject {
4343

4444
/// Handle a notification without a reply.
45+
///
46+
/// The method should return as soon as the notification has been sufficiently
47+
/// handled to avoid out-of-order requests, e.g. once the notification has
48+
/// been forwarded to clangd.
4549
func handle(_ params: some NotificationType, from clientID: ObjectIdentifier)
4650

4751
/// Handle a request and (asynchronously) receive a reply.
52+
///
53+
/// The method should return as soon as the request has been sufficiently
54+
/// handled to avoid out-of-order requests, e.g. once the corresponding
55+
/// request has been sent to sourcekitd. The actual semantic computation
56+
/// should occur after the method returns and report the result via `reply`.
4857
func handle<Request: RequestType>(_ params: Request, id: RequestID, from clientID: ObjectIdentifier, reply: @escaping (LSPResult<Request.Response>) -> Void)
4958
}
5059

@@ -66,8 +75,9 @@ public final class LocalConnection {
6675
case ready, started, closed
6776
}
6877

78+
/// The queue guarding `_nextRequestID`.
6979
let queue: DispatchQueue = DispatchQueue(label: "local-connection-queue")
70-
80+
7181
var _nextRequestID: Int = 0
7282

7383
var state: State = .ready
@@ -104,22 +114,30 @@ public final class LocalConnection {
104114

105115
extension LocalConnection: Connection {
106116
public func send<Notification>(_ notification: Notification) where Notification: NotificationType {
107-
handler?.handle(notification, from: ObjectIdentifier(self))
117+
self.handler?.handle(notification, from: ObjectIdentifier(self))
108118
}
109119

110-
public func send<Request>(_ request: Request, queue: DispatchQueue, reply: @escaping (LSPResult<Request.Response>) -> Void) -> RequestID where Request: RequestType {
120+
public func send<Request: RequestType>(
121+
_ request: Request,
122+
queue: DispatchQueue,
123+
reply: @escaping (LSPResult<Request.Response>) -> Void
124+
) -> RequestID {
111125
let id = nextRequestID()
112-
guard let handler = handler else {
113-
queue.async { reply(.failure(.serverCancelled)) }
126+
127+
guard let handler = self.handler else {
128+
queue.async {
129+
reply(.failure(.serverCancelled))
130+
}
114131
return id
115132
}
116133

117-
precondition(state == .started)
134+
precondition(self.state == .started)
118135
handler.handle(request, id: id, from: ObjectIdentifier(self)) { result in
119136
queue.async {
120137
reply(result)
121138
}
122139
}
140+
123141
return id
124142
}
125143
}

Sources/LanguageServerProtocol/Requests/CodeActionRequest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
public typealias CodeActionProviderCompletion = (LSPResult<[CodeAction]>) -> Void
14-
public typealias CodeActionProvider = (CodeActionRequest, @escaping CodeActionProviderCompletion) -> Void
14+
public typealias CodeActionProvider = (CodeActionRequest, @escaping CodeActionProviderCompletion) async -> Void
1515

1616
/// Request for returning all possible code actions for a given text document and range.
1717
///

0 commit comments

Comments
 (0)