Skip to content

Migrate huge chunks of sourcekit-lsp to actors/async/await #850

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 39 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9ec6149
Handle messages on a serial queue in `Connection`
ahoppen Sep 19, 2023
ce58b3b
Make `MessageHandler.handle` async
ahoppen Sep 19, 2023
09dc0bc
Make SourceKitServer an actor
ahoppen Sep 27, 2023
b36352b
Make sourcekit-lsp compatible with SDKs < macOS 13.3
ahoppen Sep 26, 2023
c4e5097
Make all the methods in `ToolchainLanguageServer` async
ahoppen Sep 21, 2023
ded0cf0
Migrate SwiftLanguageServer to an actor
ahoppen Sep 27, 2023
f1548bd
Call into the `BuildSystemManager` from `SwiftLanguageServer` to get …
ahoppen Sep 22, 2023
dd82313
Migrate `ClangLanguageServerShim` to be an actor
ahoppen Sep 27, 2023
9008a01
Call into the `BuildSystemManager` from `ClangLanguageServerShim` to …
ahoppen Sep 22, 2023
07eb45b
Wait for documents to be re-opened before setting the clangd status t…
ahoppen Sep 27, 2023
5952ddd
Fix typos
ahoppen Sep 27, 2023
34c6b81
Move semaphore signalling to block main thread until request has rece…
ahoppen Sep 29, 2023
443fc7a
Make `BuildSystemManager` an actor
ahoppen Sep 28, 2023
54e6d95
Make all methods on `BuildSystem` async
ahoppen Sep 28, 2023
8eed2e2
Migrate `SwiftPMWorkspace` to be an actor
ahoppen Sep 28, 2023
2c76561
Migrate `CompilationDatabaseBuildSystem` to be an actor
ahoppen Sep 28, 2023
5335aca
Migrate `BuildServerBuildSystem` to an actor and make methods in `Bui…
ahoppen Sep 22, 2023
ca547cf
Migrate `BSMDelegate` in BuildSystemManagerTests` to an actor
ahoppen Sep 22, 2023
ea20e19
Add an overload of `firstNonNil` that allows the default value to be …
ahoppen Sep 30, 2023
607f040
Remove tracking of `RequestCancelKey` to `CancellationToken`
ahoppen Sep 27, 2023
ebcdbb6
Make SwiftLanguageServer and ClangLanguageServerShim call directly in…
ahoppen Sep 30, 2023
8f859c5
Simplify forwarding for requests to clangd
ahoppen Sep 27, 2023
bf867ec
Call into the `BuildSystemManager` from `BSMDelegate ` in `BuildSyste…
ahoppen Sep 30, 2023
93dfc3d
Get the build settings of the main file for a given header in `BuildS…
ahoppen Sep 30, 2023
dffcc93
Change the build system to only notify delegate about changed files, …
ahoppen Sep 30, 2023
c642b37
Remove tracking of file build settings status in `SourceKitServer` an…
ahoppen Sep 30, 2023
e1548a0
Merge pull request #841 from ahoppen/ahoppen/build-settings-pull
ahoppen Oct 2, 2023
eb597fd
Remove a couple of async wrapper functions
ahoppen Sep 22, 2023
7f4e10e
Asyncify `MainFilesDelegate`
ahoppen Sep 26, 2023
abf456a
Make `reloadPackageStatusCallback` async
ahoppen Sep 26, 2023
0c30951
Inline `_handleUnknown`
ahoppen Sep 27, 2023
edfda7d
Add support for concurrent queues and dispatch barriers to `AsyncQueue`
ahoppen Oct 3, 2023
1f02b95
Shift responsibility for in-order message handling from `Connection` …
ahoppen Oct 3, 2023
1b6015f
Make the folding range request return the request result as an async …
ahoppen Oct 3, 2023
6206585
Merge pull request #849 from ahoppen/ahoppen/async-request-result-return
ahoppen Oct 3, 2023
d6101a1
Merge pull request #848 from ahoppen/ahoppen/minor-cleanups
ahoppen Oct 3, 2023
453ebfb
Make `FallbackBuildSystem` not conform to `BuildSystem` and remove no…
ahoppen Oct 2, 2023
0cccd3b
Refactor the computation of main files
ahoppen Oct 2, 2023
81dec01
Merge pull request #847 from ahoppen/ahoppen/refactor-main-file-compu…
ahoppen Oct 3, 2023
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
16 changes: 16 additions & 0 deletions Sources/LSPLogging/Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ public func orLog<R>(
}
}

/// Like `try?`, but logs the error on failure.
public func orLog<R>(
_ prefix: String = "",
level: LogLevel = .default,
logger: Logger = Logger.shared,
_ block: () async throws -> R?) async -> R?
{
do {
return try await block()
} catch {
logger.log("\(prefix)\(prefix.isEmpty ? "" : " ")\(error)", level: level)
return nil
}
}


/// Logs the time that the given block takes to execute in milliseconds.
public func logExecutionTime<R>(
_ prefix: String = #function,
Expand Down
18 changes: 0 additions & 18 deletions Sources/LSPTestSupport/AssertNoThrow.swift

This file was deleted.

105 changes: 105 additions & 0 deletions Sources/LSPTestSupport/Assertions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import XCTest

/// Same as `assertNoThrow` but executes the trailing closure.
public func assertNoThrow<T>(
_ expression: () throws -> T,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertNoThrow(try expression(), message(), file: file, line: line)
}

/// Same as `XCTAssertThrows` but executes the trailing closure.
public func assertThrowsError<T>(
_ expression: @autoclosure () async throws -> T,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath,
line: UInt = #line,
_ errorHandler: (_ error: Error) -> Void = { _ in }
) async {
let didThrow: Bool
do {
_ = try await expression()
didThrow = false
} catch {
errorHandler(error)
didThrow = true
}
if !didThrow {
XCTFail("Expression was expected to throw but did not throw", file: file, line: line)
}
}

/// Same as `XCTAssertEqual` but doesn't take autoclosures and thus `expression1`
/// and `expression2` can contain `await`.
public func assertEqual<T: Equatable>(
_ expression1: T,
_ expression2: T,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertEqual(expression1, expression2, message(), file: file, line: line)
}

/// Same as `XCTAssertNil` but doesn't take autoclosures and thus `expression`
/// can contain `await`.
public func assertNil<T: Equatable>(
_ expression: T?,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertNil(expression, message(), file: file, line: line)
}

/// Same as `XCTAssertNotNil` but doesn't take autoclosures and thus `expression`
/// can contain `await`.
public func assertNotNil<T: Equatable>(
_ expression: T?,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertNotNil(expression, message(), file: file, line: line)
}

extension XCTestCase {
private struct ExpectationNotFulfilledError: Error, CustomStringConvertible {
var expecatations: [XCTestExpectation]

var description: String {
return "One of the expectation was not fulfilled within timeout: \(expecatations.map(\.description).joined(separator: ", "))"
}
}

/// Wait for the given expectations to be fulfilled. If the expectations aren't
/// fulfilled within `timeout`, throw an error, aborting the test execution.
public func fulfillmentOfOrThrow(
_ expectations: [XCTestExpectation],
timeout: TimeInterval = defaultTimeout,
enforceOrder enforceOrderOfFulfillment: Bool = false
) async throws {
// `XCTWaiter.fulfillment` was introduced in the macOS 13.3 SDK but marked as being available on macOS 10.15.
// At the same time that XCTWaiter.fulfillment was introduced `XCTWaiter.wait` was deprecated in async contexts.
// This means that we can't write code that compiles without warnings with both the macOS 13.3 and any previous SDK.
// 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.
let started = XCTWaiter.wait(for: expectations, timeout: timeout, enforceOrder: enforceOrderOfFulfillment)
if started != .completed {
throw ExpectationNotFulfilledError(expecatations: expectations)
}
}
}
127 changes: 127 additions & 0 deletions Sources/LanguageServerProtocol/AsyncQueue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation

/// Abstraction layer so we can store a heterogeneous collection of tasks in an
/// array.
private protocol AnyTask: Sendable {
func waitForCompletion() async
}

extension Task: AnyTask where Failure == Never {
func waitForCompletion() async {
_ = await value
}
}

extension NSLock {
/// NOTE: Keep in sync with SwiftPM's 'Sources/Basics/NSLock+Extensions.swift'
func withLock<T>(_ body: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try body()
}
}

/// A queue that allows the execution of asyncronous blocks of code.
public final class AsyncQueue {
public enum QueueKind {
/// A queue that allows concurrent execution of tasks.
case concurrent

/// A queue that executes one task after the other.
case serial
}

private struct PendingTask {
/// The task that is pending.
let task: any AnyTask

/// Whether the task needs to finish executing befoer any other task can
/// start in executing in the queue.
let isBarrier: Bool

/// A unique value used to identify the task. This allows tasks to get
/// removed from `pendingTasks` again after they finished executing.
let id: UUID
}

/// Whether the queue allows concurrent execution of tasks.
private let kind: QueueKind

/// Lock guarding `pendingTasks`.
private let pendingTasksLock = NSLock()

/// Pending tasks that have not finished execution yet.
private var pendingTasks = [PendingTask]()

public init(_ kind: QueueKind) {
self.kind = kind
self.pendingTasksLock.name = "AsyncQueue"
}

/// Schedule a new closure to be executed on the queue.
///
/// If this is a serial queue, all previously added tasks are guaranteed to
/// finished executing before this closure gets executed.
///
/// If this is a barrier, all previously scheduled tasks are guaranteed to
/// finish execution before the barrier is executed and all tasks that are
/// added later will wait until the barrier finishes execution.
@discardableResult
public func async<Success: Sendable>(
priority: TaskPriority? = nil,
barrier isBarrier: Bool = false,
@_inheritActorContext operation: @escaping @Sendable () async -> Success
) -> Task<Success, Never> {
let id = UUID()

return pendingTasksLock.withLock {
// Build the list of tasks that need to finishe exeuction before this one
// can be executed
let dependencies: [PendingTask]
switch (kind, isBarrier: isBarrier) {
case (.concurrent, isBarrier: true):
// Wait for all tasks after the last barrier.
let lastBarrierIndex = pendingTasks.lastIndex(where: { $0.isBarrier }) ?? pendingTasks.startIndex
dependencies = Array(pendingTasks[lastBarrierIndex...])
case (.concurrent, isBarrier: false):
// If there is a barrier, wait for it.
dependencies = [pendingTasks.last(where: { $0.isBarrier })].compactMap { $0 }
case (.serial, _):
// We are in a serial queue. The last pending task must finish for this one to start.
dependencies = [pendingTasks.last].compactMap { $0 }
}


// Schedule the task.
let task = Task {
for dependency in dependencies {
await dependency.task.waitForCompletion()
}

let result = await operation()

pendingTasksLock.withLock {
pendingTasks.removeAll(where: { $0.id == id })
}

return result
}

pendingTasks.append(PendingTask(task: task, isBarrier: isBarrier, id: id))

return task
}
}
}
1 change: 1 addition & 0 deletions Sources/LanguageServerProtocol/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
add_library(LanguageServerProtocol STATIC
AsyncQueue.swift
Cancellation.swift
Connection.swift
CustomCodable.swift
Expand Down
30 changes: 24 additions & 6 deletions Sources/LanguageServerProtocol/Connection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,18 @@ extension Connection {
public protocol MessageHandler: AnyObject {

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

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

Expand All @@ -66,8 +75,9 @@ public final class LocalConnection {
case ready, started, closed
}

/// The queue guarding `_nextRequestID`.
let queue: DispatchQueue = DispatchQueue(label: "local-connection-queue")

var _nextRequestID: Int = 0

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

extension LocalConnection: Connection {
public func send<Notification>(_ notification: Notification) where Notification: NotificationType {
handler?.handle(notification, from: ObjectIdentifier(self))
self.handler?.handle(notification, from: ObjectIdentifier(self))
}

public func send<Request>(_ request: Request, queue: DispatchQueue, reply: @escaping (LSPResult<Request.Response>) -> Void) -> RequestID where Request: RequestType {
public func send<Request: RequestType>(
_ request: Request,
queue: DispatchQueue,
reply: @escaping (LSPResult<Request.Response>) -> Void
) -> RequestID {
let id = nextRequestID()
guard let handler = handler else {
queue.async { reply(.failure(.serverCancelled)) }

guard let handler = self.handler else {
queue.async {
reply(.failure(.serverCancelled))
}
return id
}

precondition(state == .started)
precondition(self.state == .started)
handler.handle(request, id: id, from: ObjectIdentifier(self)) { result in
queue.async {
reply(result)
}
}

return id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//===----------------------------------------------------------------------===//

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

/// Request for returning all possible code actions for a given text document and range.
///
Expand Down
Loading