From b22af35eb17ed417803e34bb8cbf65591b8df17d Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Sat, 30 Sep 2023 10:09:59 -0700 Subject: [PATCH] Revert asyncificaiton changes The asyncification changes caused some non-deterministic test failures. I believe that some of these are due to race conditions that are the result of the partial transition to actors. Instead of merging the asyncification piece by piece, I will collect the changes asyncification changes in a branch and then qualify that branch througougly (running CI multiple times) before merging it into `main`. --- Sources/LSPLogging/Logging.swift | 16 - Sources/LSPTestSupport/AssertNoThrow.swift | 18 + Sources/LSPTestSupport/Assertions.swift | 105 -- .../LanguageServerProtocol/AsyncQueue.swift | 87 -- Sources/LanguageServerProtocol/CMakeLists.txt | 1 - .../LanguageServerProtocol/Connection.swift | 59 +- Sources/LanguageServerProtocol/Message.swift | 10 +- .../Requests/CodeActionRequest.swift | 2 +- .../JSONRPCConnection.swift | 39 +- Sources/SKCore/BuildServerBuildSystem.swift | 88 +- Sources/SKCore/BuildSystem.swift | 29 +- Sources/SKCore/BuildSystemDelegate.swift | 8 +- Sources/SKCore/BuildSystemManager.swift | 383 +++--- .../CompilationDatabaseBuildSystem.swift | 93 +- Sources/SKCore/FallbackBuildSystem.swift | 12 +- .../SKSwiftPMWorkspace/SwiftPMWorkspace.swift | 108 +- .../SKSwiftPMTestWorkspace.swift | 24 +- .../SKTestSupport/SKTibsTestWorkspace.swift | 30 +- ...itServer+WorkspaceForDocumentOnQueue.swift | 22 + Sources/SourceKitLSP/CMakeLists.txt | 1 - .../Clang/ClangLanguageServer.swift | 382 ++--- Sources/SourceKitLSP/Sequence+AsyncMap.swift | 43 - Sources/SourceKitLSP/SourceKitServer.swift | 1029 +++++++------- .../SourceKitLSP/Swift/CodeCompletion.swift | 21 +- Sources/SourceKitLSP/Swift/CursorInfo.swift | 37 +- .../Swift/ExpressionTypeInfo.swift | 28 +- .../SourceKitLSP/Swift/OpenInterface.swift | 50 +- .../Swift/SemanticRefactoring.swift | 84 +- .../Swift/SwiftLanguageServer.swift | 1224 +++++++++-------- .../SourceKitLSP/Swift/VariableTypeInfo.swift | 42 +- .../ToolchainLanguageServer.swift | 69 +- Sources/SourceKitLSP/Workspace.swift | 41 +- Sources/sourcekit-lsp/SourceKitLSP.swift | 2 +- .../BuildServerBuildSystemTests.swift | 52 +- .../SKCoreTests/BuildSystemManagerTests.swift | 360 +++-- .../CompilationDatabaseTests.swift | 54 +- .../FallbackBuildSystemTests.swift | 34 +- .../SwiftPMWorkspaceTests.swift | 149 +- .../SourceKitDTests/CrashRecoveryTests.swift | 66 +- .../SourceKitLSPTests/BuildSystemTests.swift | 158 +-- .../CallHierarchyTests.swift | 4 +- Tests/SourceKitLSPTests/CodeActionTests.swift | 28 +- .../CompilationDatabaseTests.swift | 6 +- .../ExecuteCommandTests.swift | 8 +- .../SourceKitLSPTests/FoldingRangeTests.swift | 40 +- .../ImplementationTests.swift | 4 +- Tests/SourceKitLSPTests/LocalClangTests.swift | 50 +- Tests/SourceKitLSPTests/LocalSwiftTests.swift | 49 +- .../MainFilesProviderTests.swift | 8 +- .../SemanticTokensTests.swift | 4 +- Tests/SourceKitLSPTests/SourceKitTests.swift | 60 +- .../SwiftInterfaceTests.swift | 16 +- .../SwiftPMIntegration.swift | 14 +- .../TypeHierarchyTests.swift | 4 +- Tests/SourceKitLSPTests/WorkspaceTests.swift | 38 +- 55 files changed, 2548 insertions(+), 2845 deletions(-) create mode 100644 Sources/LSPTestSupport/AssertNoThrow.swift delete mode 100644 Sources/LSPTestSupport/Assertions.swift delete mode 100644 Sources/LanguageServerProtocol/AsyncQueue.swift create mode 100644 Sources/SKTestSupport/SourceKitServer+WorkspaceForDocumentOnQueue.swift delete mode 100644 Sources/SourceKitLSP/Sequence+AsyncMap.swift diff --git a/Sources/LSPLogging/Logging.swift b/Sources/LSPLogging/Logging.swift index 7b2643fc..7eb7baed 100644 --- a/Sources/LSPLogging/Logging.swift +++ b/Sources/LSPLogging/Logging.swift @@ -62,22 +62,6 @@ public func orLog( } } -/// Like `try?`, but logs the error on failure. -public func orLog( - _ 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( _ prefix: String = #function, diff --git a/Sources/LSPTestSupport/AssertNoThrow.swift b/Sources/LSPTestSupport/AssertNoThrow.swift new file mode 100644 index 00000000..0bf4ccac --- /dev/null +++ b/Sources/LSPTestSupport/AssertNoThrow.swift @@ -0,0 +1,18 @@ +//===----------------------------------------------------------------------===// +// +// 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 `XCTAssertThrows` but executes the trailing closure. +public func assertNoThrow(_ expression: () throws -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) { + XCTAssertNoThrow(try expression(), message(), file: file, line: line) +} diff --git a/Sources/LSPTestSupport/Assertions.swift b/Sources/LSPTestSupport/Assertions.swift deleted file mode 100644 index 9e2ab4d4..00000000 --- a/Sources/LSPTestSupport/Assertions.swift +++ /dev/null @@ -1,105 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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( - _ 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( - _ 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( - _ 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( - _ 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( - _ 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) - } - } -} diff --git a/Sources/LanguageServerProtocol/AsyncQueue.swift b/Sources/LanguageServerProtocol/AsyncQueue.swift deleted file mode 100644 index f113266b..00000000 --- a/Sources/LanguageServerProtocol/AsyncQueue.swift +++ /dev/null @@ -1,87 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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(_ body: () throws -> T) rethrows -> T { - lock() - defer { unlock() } - return try body() - } -} - -/// A serial queue that allows the execution of asyncronous blocks of code. -public final class AsyncQueue { - /// Lock guarding `lastTask`. - private let lastTaskLock = NSLock() - - /// The last scheduled task if it hasn't finished yet. - /// - /// Any newly scheduled tasks need to await this task to ensure that tasks are - /// executed syncronously. - /// - /// `id` is a unique value to identify the task. This allows us to set `lastTask` - /// to `nil` if the queue runs empty. - private var lastTask: (task: AnyTask, id: UUID)? - - public init() { - self.lastTaskLock.name = "AsyncQueue.lastTaskLock" - } - - /// Schedule a new closure to be executed on the queue. - /// - /// All previously added tasks are guaranteed to finished executing before - /// this closure gets executed. - @discardableResult - public func async( - priority: TaskPriority? = nil, - @_inheritActorContext operation: @escaping @Sendable () async -> Success - ) -> Task { - let id = UUID() - - return lastTaskLock.withLock { - let task = Task(priority: priority) { [previousLastTask = lastTask] in - await previousLastTask?.task.waitForCompletion() - - defer { - lastTaskLock.withLock { - // If we haven't queued a new task since enquing this one, we can clear - // last task. - if self.lastTask?.id == id { - self.lastTask = nil - } - } - } - - return await operation() - } - - lastTask = (task, id) - - return task - } - } -} diff --git a/Sources/LanguageServerProtocol/CMakeLists.txt b/Sources/LanguageServerProtocol/CMakeLists.txt index bac242a7..0f77f6d2 100644 --- a/Sources/LanguageServerProtocol/CMakeLists.txt +++ b/Sources/LanguageServerProtocol/CMakeLists.txt @@ -1,5 +1,4 @@ add_library(LanguageServerProtocol STATIC - AsyncQueue.swift Cancellation.swift Connection.swift CustomCodable.swift diff --git a/Sources/LanguageServerProtocol/Connection.swift b/Sources/LanguageServerProtocol/Connection.swift index a3562c3e..cee4e346 100644 --- a/Sources/LanguageServerProtocol/Connection.swift +++ b/Sources/LanguageServerProtocol/Connection.swift @@ -42,19 +42,10 @@ 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) async + 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(_ params: Request, id: RequestID, from clientID: ObjectIdentifier, reply: @escaping (LSPResult) -> Void) async + func handle(_ params: Request, id: RequestID, from clientID: ObjectIdentifier, reply: @escaping (LSPResult) -> Void) } /// A connection between two message handlers in the same process. @@ -75,20 +66,8 @@ public final class LocalConnection { case ready, started, closed } - /// The queue guarding `_nextRequestID`. let queue: DispatchQueue = DispatchQueue(label: "local-connection-queue") - /// The queue on which all messages (notifications, requests, responses) are - /// handled. - /// - /// The queue is blocked until the message has been sufficiently handled to - /// avoid out-of-order handling of messages. For sourcekitd, this means that - /// a request has been sent to sourcekitd and for clangd, this means that we - /// have forwarded the request to clangd. - /// - /// The actual semantic handling of the message happens off this queue. - let messageHandlingQueue: AsyncQueue = AsyncQueue() - var _nextRequestID: Int = 0 var state: State = .ready @@ -125,34 +104,22 @@ public final class LocalConnection { extension LocalConnection: Connection { public func send(_ notification: Notification) where Notification: NotificationType { - messageHandlingQueue.async { - await self.handler?.handle(notification, from: ObjectIdentifier(self)) - } + handler?.handle(notification, from: ObjectIdentifier(self)) } - public func send( - _ request: Request, - queue: DispatchQueue, - reply: @escaping (LSPResult) -> Void - ) -> RequestID { + public func send(_ request: Request, queue: DispatchQueue, reply: @escaping (LSPResult) -> Void) -> RequestID where Request: RequestType { let id = nextRequestID() - - messageHandlingQueue.async { - guard let handler = self.handler else { - queue.async { - reply(.failure(.serverCancelled)) - } - return - } - - precondition(self.state == .started) - await handler.handle(request, id: id, from: ObjectIdentifier(self)) { result in - queue.async { - reply(result) - } - } + guard let handler = handler else { + queue.async { reply(.failure(.serverCancelled)) } + return id } + precondition(state == .started) + handler.handle(request, id: id, from: ObjectIdentifier(self)) { result in + queue.async { + reply(result) + } + } return id } } diff --git a/Sources/LanguageServerProtocol/Message.swift b/Sources/LanguageServerProtocol/Message.swift index 6ee2291a..f4bbf4da 100644 --- a/Sources/LanguageServerProtocol/Message.swift +++ b/Sources/LanguageServerProtocol/Message.swift @@ -28,7 +28,7 @@ public protocol _RequestType: MessageType { id: RequestID, connection: Connection, reply: @escaping (LSPResult, RequestID) -> Void - ) async + ) } /// A request, which must have a unique `method` name as well as an associated response type. @@ -54,16 +54,16 @@ extension RequestType { id: RequestID, connection: Connection, reply: @escaping (LSPResult, RequestID) -> Void - ) async { - await handler.handle(self, id: id, from: ObjectIdentifier(connection)) { response in + ) { + handler.handle(self, id: id, from: ObjectIdentifier(connection)) { response in reply(response.map({ $0 as ResponseType }), id) } } } extension NotificationType { - public func _handle(_ handler: MessageHandler, connection: Connection) async { - await handler.handle(self, from: ObjectIdentifier(connection)) + public func _handle(_ handler: MessageHandler, connection: Connection) { + handler.handle(self, from: ObjectIdentifier(connection)) } } diff --git a/Sources/LanguageServerProtocol/Requests/CodeActionRequest.swift b/Sources/LanguageServerProtocol/Requests/CodeActionRequest.swift index 205456f0..4a777f62 100644 --- a/Sources/LanguageServerProtocol/Requests/CodeActionRequest.swift +++ b/Sources/LanguageServerProtocol/Requests/CodeActionRequest.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// public typealias CodeActionProviderCompletion = (LSPResult<[CodeAction]>) -> Void -public typealias CodeActionProvider = (CodeActionRequest, @escaping CodeActionProviderCompletion) async -> Void +public typealias CodeActionProvider = (CodeActionRequest, @escaping CodeActionProviderCompletion) -> Void /// Request for returning all possible code actions for a given text document and range. /// diff --git a/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift b/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift index 883e039e..330d61b2 100644 --- a/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift +++ b/Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift @@ -24,23 +24,8 @@ import LSPLogging public final class JSONRPCConnection { var receiveHandler: MessageHandler? = nil - - /// The queue on which we read the data let queue: DispatchQueue = DispatchQueue(label: "jsonrpc-queue", qos: .userInitiated) - - /// The queue on which we send data. let sendQueue: DispatchQueue = DispatchQueue(label: "jsonrpc-send-queue", qos: .userInitiated) - - /// The queue on which all messages (notifications, requests, responses) are - /// handled. - /// - /// The queue is blocked until the message has been sufficiently handled to - /// avoid out-of-order handling of messages. For sourcekitd, this means that - /// a request has been sent to sourcekitd and for clangd, this means that we - /// have forwarded the request to clangd. - /// - /// The actual semantic handling of the message happens off this queue. - let messageHandlingQueue: AsyncQueue = AsyncQueue() let receiveIO: DispatchIO let sendIO: DispatchIO let messageRegistry: MessageRegistry @@ -70,9 +55,7 @@ public final class JSONRPCConnection { /// The set of currently outstanding outgoing requests along with information about how to decode and handle their responses. var outstandingRequests: [RequestID: OutstandingRequest] = [:] - /// A handler that will be called asyncronously when the connection is being - /// closed. - var closeHandler: (() async -> Void)! = nil + var closeHandler: (() -> Void)! = nil public init( protocol messageRegistry: MessageRegistry, @@ -122,10 +105,8 @@ public final class JSONRPCConnection { ioGroup.notify(queue: queue) { [weak self] in guard let self = self else { return } - Task { - await self.closeHandler() - self.receiveHandler = nil // break retain cycle - } + self.closeHandler() + self.receiveHandler = nil // break retain cycle } // We cannot assume the client will send us bytes in packets of any particular size, so set the lower limit to 1. @@ -143,7 +124,7 @@ public final class JSONRPCConnection { /// Start processing `inFD` and send messages to `receiveHandler`. /// /// - parameter receiveHandler: The message handler to invoke for requests received on the `inFD`. - public func start(receiveHandler: MessageHandler, closeHandler: @escaping () async -> Void = {}) { + public func start(receiveHandler: MessageHandler, closeHandler: @escaping () -> Void = {}) { precondition(state == .created) state = .running self.receiveHandler = receiveHandler @@ -282,16 +263,12 @@ public final class JSONRPCConnection { func handle(_ message: JSONRPCMessage) { switch message { case .notification(let notification): - messageHandlingQueue.async { - await notification._handle(self.receiveHandler!, connection: self) - } + notification._handle(receiveHandler!, connection: self) case .request(let request, id: let id): let semaphore: DispatchSemaphore? = syncRequests ? .init(value: 0) : nil - messageHandlingQueue.async { - await request._handle(self.receiveHandler!, id: id, connection: self) { (response, id) in - self.sendReply(response, id: id) - semaphore?.signal() - } + request._handle(receiveHandler!, id: id, connection: self) { (response, id) in + self.sendReply(response, id: id) + semaphore?.signal() } semaphore?.wait() diff --git a/Sources/SKCore/BuildServerBuildSystem.swift b/Sources/SKCore/BuildServerBuildSystem.swift index 4f7e3a66..7bbef467 100644 --- a/Sources/SKCore/BuildServerBuildSystem.swift +++ b/Sources/SKCore/BuildServerBuildSystem.swift @@ -43,7 +43,10 @@ func executable(_ name: String) -> String { /// /// Provides build settings from a build server launched based on a /// `buildServer.json` configuration file provided in the repo root. -public actor BuildServerBuildSystem: MessageHandler { +public final class BuildServerBuildSystem: MessageHandler { + /// The handler's request queue. `delegate` will always be called on this queue. + public let queue: DispatchQueue = DispatchQueue(label: "language-server-queue", qos: .userInitiated) + let projectRoot: AbsolutePath let buildFolder: AbsolutePath? let serverConfig: BuildServerConfig @@ -62,15 +65,7 @@ public actor BuildServerBuildSystem: MessageHandler { /// Delegate to handle any build system events. public weak var delegate: BuildSystemDelegate? - /// - Note: Needed to set the delegate from a different actor isolation context - public func setDelegate(_ delegate: BuildSystemDelegate?) async { - self.delegate = delegate - } - - /// The build settings that have been received from the build server. - private var buildSettings: [DocumentURI: FileBuildSettings] = [:] - - public init(projectRoot: AbsolutePath, buildFolder: AbsolutePath?, fileSystem: FileSystem = localFileSystem) async throws { + public init(projectRoot: AbsolutePath, buildFolder: AbsolutePath?, fileSystem: FileSystem = localFileSystem) throws { let configPath = projectRoot.appending(component: "buildServer.json") let config = try loadBuildServerConfig(path: configPath, fileSystem: fileSystem) #if os(Windows) @@ -92,11 +87,12 @@ public actor BuildServerBuildSystem: MessageHandler { /// Creates a build system using the Build Server Protocol config. /// /// - Returns: nil if `projectRoot` has no config or there is an error parsing it. - public init?(projectRoot: AbsolutePath?, buildSetup: BuildSetup) async { + public convenience init?(projectRoot: AbsolutePath?, buildSetup: BuildSetup) + { if projectRoot == nil { return nil } do { - try await self.init(projectRoot: projectRoot!, buildFolder: buildSetup.path) + try self.init(projectRoot: projectRoot!, buildFolder: buildSetup.path) } catch _ as FileSystemError { // config file was missing, no build server for this workspace return nil @@ -167,11 +163,13 @@ public actor BuildServerBuildSystem: MessageHandler { /// the build server has sent us a notification. /// /// We need to notify the delegate about any updated build settings. - public func handle(_ params: some NotificationType, from clientID: ObjectIdentifier) async { - if let params = params as? BuildTargetsChangedNotification { - await self.handleBuildTargetsChanged(Notification(params, clientID: clientID)) - } else if let params = params as? FileOptionsChangedNotification { - await self.handleFileOptionsChanged(Notification(params, clientID: clientID)) + public func handle(_ params: some NotificationType, from clientID: ObjectIdentifier) { + queue.async { + if let params = params as? BuildTargetsChangedNotification { + self.handleBuildTargetsChanged(Notification(params, clientID: clientID)) + } else if let params = params as? FileOptionsChangedNotification { + self.handleFileOptionsChanged(Notification(params, clientID: clientID)) + } } } @@ -184,29 +182,20 @@ public actor BuildServerBuildSystem: MessageHandler { from clientID: ObjectIdentifier, reply: @escaping (LSPResult) -> Void ) { - reply(.failure(ResponseError.methodNotFound(R.method))) + queue.async { + reply(.failure(ResponseError.methodNotFound(R.method))) + } } - func handleBuildTargetsChanged(_ notification: LanguageServerProtocol.Notification) async { - await self.delegate?.buildTargetsChanged(notification.params.changes) + func handleBuildTargetsChanged(_ notification: LanguageServerProtocol.Notification) { + self.delegate?.buildTargetsChanged(notification.params.changes) } - func handleFileOptionsChanged(_ notification: LanguageServerProtocol.Notification) async { + func handleFileOptionsChanged(_ notification: LanguageServerProtocol.Notification) { let result = notification.params.updatedOptions let settings = FileBuildSettings( compilerArguments: result.options, workingDirectory: result.workingDirectory) - await self.buildSettingsChanged(for: notification.params.uri, settings: settings) - } - - /// Record the new build settings for the given document and inform the delegate - /// about the changed build settings. - private func buildSettingsChanged(for document: DocumentURI, settings: FileBuildSettings?) async { - buildSettings[document] = settings - if let settings { - await self.delegate?.fileBuildSettingsChanged([document: .modified(settings)]) - } else { - await self.delegate?.fileBuildSettingsChanged([document: .removedOrUnavailable]) - } + self.delegate?.fileBuildSettingsChanged([notification.params.uri: .modified(settings)]) } } @@ -219,26 +208,29 @@ private func readReponseDataKey(data: LSPAny?, key: String) -> String? { return nil } -extension BuildServerBuildSystem: BuildSystem { - /// The build settings for the given file. - /// - /// Returns `nil` if no build settings have been received from the build - /// server yet or if no build settings are available for this file. - public func buildSettings(for document: DocumentURI, language: Language) async throws -> FileBuildSettings? { - return buildSettings[document] +extension BuildServerBuildSystem { + /// Exposed for *testing*. + public func _settings(for uri: DocumentURI) -> FileBuildSettings? { + if let response = try? self.buildServer?.sendSync(SourceKitOptions(uri: uri)) { + return FileBuildSettings( + compilerArguments: response.options, + workingDirectory: response.workingDirectory) + } + return nil } +} + +extension BuildServerBuildSystem: BuildSystem { public func registerForChangeNotifications(for uri: DocumentURI, language: Language) { let request = RegisterForChanges(uri: uri, action: .register) _ = self.buildServer?.send(request, queue: requestQueue, reply: { result in - Task { - if let error = result.failure { - log("error registering \(uri): \(error)", level: .error) - - // BuildServer registration failed, so tell our delegate that no build - // settings are available. - await self.buildSettingsChanged(for: uri, settings: nil) - } + if let error = result.failure { + log("error registering \(uri): \(error)", level: .error) + + // BuildServer registration failed, so tell our delegate that no build + // settings are available. + self.delegate?.fileBuildSettingsChanged([uri: .removedOrUnavailable]) } }) } diff --git a/Sources/SKCore/BuildSystem.swift b/Sources/SKCore/BuildSystem.swift index 25488ada..dee92d51 100644 --- a/Sources/SKCore/BuildSystem.swift +++ b/Sources/SKCore/BuildSystem.swift @@ -39,30 +39,17 @@ public enum FileHandlingCapability: Comparable { public protocol BuildSystem: AnyObject { /// The path to the raw index store data, if any. - var indexStorePath: AbsolutePath? { get async } + var indexStorePath: AbsolutePath? { get } /// The path to put the index database, if any. - var indexDatabasePath: AbsolutePath? { get async } + var indexDatabasePath: AbsolutePath? { get } /// Path remappings for remapping index data for local use. - var indexPrefixMappings: [PathPrefixMapping] { get async } + var indexPrefixMappings: [PathPrefixMapping] { get } /// Delegate to handle any build system events such as file build settings /// initial reports as well as changes. - var delegate: BuildSystemDelegate? { get async } - - /// Set the build system's delegate. - /// - /// - Note: Needed so we can set the delegate from a different actor isolation - /// context. - func setDelegate(_ delegate: BuildSystemDelegate?) async - - /// Retrieve build settings for the given document with the given source - /// language. - /// - /// Returns `nil` if the build system can't provide build settings for this - /// file or if it hasn't computed build settings for the file yet. - func buildSettings(for document: DocumentURI, language: Language) async throws -> FileBuildSettings? + var delegate: BuildSystemDelegate? { get set } /// Register the given file for build-system level change notifications, such /// as command line flag changes, dependency changes, etc. @@ -70,16 +57,16 @@ public protocol BuildSystem: AnyObject { /// IMPORTANT: When first receiving a register request, the `BuildSystem` MUST asynchronously /// inform its delegate of any initial settings for the given file via the /// `fileBuildSettingsChanged` method, even if unavailable. - func registerForChangeNotifications(for: DocumentURI, language: Language) async + func registerForChangeNotifications(for: DocumentURI, language: Language) /// Unregister the given file for build-system level change notifications, /// such as command line flag changes, dependency changes, etc. - func unregisterForChangeNotifications(for: DocumentURI) async + func unregisterForChangeNotifications(for: DocumentURI) /// Called when files in the project change. - func filesDidChange(_ events: [FileEvent]) async + func filesDidChange(_ events: [FileEvent]) - func fileHandlingCapability(for uri: DocumentURI) async -> FileHandlingCapability + func fileHandlingCapability(for uri: DocumentURI) -> FileHandlingCapability } public let buildTargetsNotSupported = diff --git a/Sources/SKCore/BuildSystemDelegate.swift b/Sources/SKCore/BuildSystemDelegate.swift index 948a861a..670dabf1 100644 --- a/Sources/SKCore/BuildSystemDelegate.swift +++ b/Sources/SKCore/BuildSystemDelegate.swift @@ -18,14 +18,14 @@ public protocol BuildSystemDelegate: AnyObject { /// /// The callee should request new sources and outputs for the build targets of /// interest. - func buildTargetsChanged(_ changes: [BuildTargetEvent]) async + func buildTargetsChanged(_ changes: [BuildTargetEvent]) /// Notify the delegate that the given files' build settings have changed. /// /// The delegate should cache the new build settings for any of the given /// files that they are interested in. func fileBuildSettingsChanged( - _ changedFiles: [DocumentURI: FileBuildSettingsChange]) async + _ changedFiles: [DocumentURI: FileBuildSettingsChange]) /// Notify the delegate that the dependencies of the given files have changed /// and that ASTs may need to be refreshed. If the given set is empty, assume @@ -33,10 +33,10 @@ public protocol BuildSystemDelegate: AnyObject { /// /// The callee should refresh ASTs unless it is able to determine that a /// refresh is not necessary. - func filesDependenciesUpdated(_ changedFiles: Set) async + func filesDependenciesUpdated(_ changedFiles: Set) /// Notify the delegate that the file handling capability of this build system /// for some file has changed. The delegate should discard any cached file /// handling capability. - func fileHandlingCapabilityChanged() async + func fileHandlingCapabilityChanged() } diff --git a/Sources/SKCore/BuildSystemManager.swift b/Sources/SKCore/BuildSystemManager.swift index 953f6247..1e811f6d 100644 --- a/Sources/SKCore/BuildSystemManager.swift +++ b/Sources/SKCore/BuildSystemManager.swift @@ -85,7 +85,14 @@ extension MainFileStatus { /// Since some `BuildSystem`s may require a bit of a time to compute their arguments asynchronously, /// this class has a configurable `buildSettings` timeout which denotes the amount of time to give /// the build system before applying the fallback arguments. -public actor BuildSystemManager { +public final class BuildSystemManager { + + /// Queue for processing asynchronous work and mutual exclusion for shared state. + let queue: DispatchQueue = DispatchQueue(label: "\(BuildSystemManager.self)-queue") + + /// Queue for asynchronous notifications. + let notifyQueue: DispatchQueue = DispatchQueue(label: "\(BuildSystemManager.self)-notify") + /// The set of watched files, along with their main file and language. var watchedFiles: [DocumentURI: (mainFile: DocumentURI, language: Language)] = [:] @@ -111,103 +118,63 @@ public actor BuildSystemManager { /// Create a BuildSystemManager that wraps the given build system. The new /// manager will modify the delegate of the underlying build system. public init(buildSystem: BuildSystem?, fallbackBuildSystem: FallbackBuildSystem?, - mainFilesProvider: MainFilesProvider?, fallbackSettingsTimeout: DispatchTimeInterval = .seconds(3)) async { - let buildSystemHasDelegate = await buildSystem?.delegate != nil - precondition(!buildSystemHasDelegate) + mainFilesProvider: MainFilesProvider?, fallbackSettingsTimeout: DispatchTimeInterval = .seconds(3)) { + precondition(buildSystem?.delegate == nil) self.buildSystem = buildSystem self.fallbackBuildSystem = fallbackBuildSystem self._mainFilesProvider = mainFilesProvider self.fallbackSettingsTimeout = fallbackSettingsTimeout - await self.buildSystem?.setDelegate(self) + self.buildSystem?.delegate = self } - public func filesDidChange(_ events: [FileEvent]) async { - await self.buildSystem?.filesDidChange(events) - self.fallbackBuildSystem?.filesDidChange(events) + public func filesDidChange(_ events: [FileEvent]) { + queue.async { + self.buildSystem?.filesDidChange(events) + self.fallbackBuildSystem?.filesDidChange(events) + } } } extension BuildSystemManager { public var delegate: BuildSystemDelegate? { - get { _delegate } - set { _delegate = newValue } - } - - /// - Note: Needed so we can set the delegate from a different isolation context. - public func setDelegate(_ delegate: BuildSystemDelegate?) { - self.delegate = delegate + get { queue.sync { _delegate } } + set { queue.sync { _delegate = newValue } } } public var mainFilesProvider: MainFilesProvider? { - get { _mainFilesProvider} - set { _mainFilesProvider = newValue } + get { queue.sync { _mainFilesProvider} } + set { queue.sync { _mainFilesProvider = newValue } } } - /// - Note: Needed so we can set the delegate from a different isolation context. - public func setMainFilesProvider(_ mainFilesProvider: MainFilesProvider?) { - self.mainFilesProvider = mainFilesProvider - } + public func registerForChangeNotifications(for uri: DocumentURI, language: Language) { + return queue.async { + log("registerForChangeNotifications(\(uri.pseudoPath))") + let mainFile: DocumentURI - /// Get the build settings for the given document, assuming it has the given - /// language. - /// - /// Returns `nil` if no build settings are available in the build system and - /// no fallback build settings can be computed. - /// - /// `isFallback` is `true` if the build settings couldn't be computed and - /// fallback settings are used. These fallback settings are most likely not - /// correct and provide limited semantic functionality. - public func buildSettings( - for document: DocumentURI, - language: Language - ) async -> (buildSettings: FileBuildSettings, isFallback: Bool)? { - do { - // FIXME: (async) We should only wait `fallbackSettingsTimeout` for build - // settings and return fallback afterwards. I am not sure yet, how best to - // implement that with Swift concurrency. - // For now, this should be fine because all build systems return - // very quickly from `settings(for:language:)`. - if let settings = try await buildSystem?.buildSettings(for: document, language: language) { - return (buildSettings: settings, isFallback: false) + if let watchedFile = self.watchedFiles[uri] { + mainFile = watchedFile.mainFile + } else { + let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) + mainFile = chooseMainFile(for: uri, from: mainFiles ?? []) + self.watchedFiles[uri] = (mainFile, language) } - } catch { - log("Getting build settings failed: \(error)") - } - if let settings = fallbackBuildSystem?.buildSettings(for: document, language: language) { - // If there is no build system and we only have the fallback build system, - // we will never get real build settings. Consider the build settings - // non-fallback. - return (buildSettings: settings, isFallback: buildSystem != nil) - } else { - return nil - } - } - public func registerForChangeNotifications(for uri: DocumentURI, language: Language) async { - log("registerForChangeNotifications(\(uri.pseudoPath))") - let mainFile: DocumentURI + let newStatus = self.cachedStatusOrRegisterForSettings(for: mainFile, language: language) - if let watchedFile = self.watchedFiles[uri] { - mainFile = watchedFile.mainFile - } else { - let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) - mainFile = chooseMainFile(for: uri, from: mainFiles ?? []) - self.watchedFiles[uri] = (mainFile, language) - } - - let newStatus = await self.cachedStatusOrRegisterForSettings(for: mainFile, language: language) - - if let mainChange = newStatus.buildSettingsChange, - let delegate = self._delegate { - let change = self.convert(change: mainChange, ofMainFile: mainFile, to: uri) - await delegate.fileBuildSettingsChanged([uri: change]) + if let mainChange = newStatus.buildSettingsChange, + let delegate = self._delegate { + let change = self.convert(change: mainChange, ofMainFile: mainFile, to: uri) + self.notifyQueue.async { + delegate.fileBuildSettingsChanged([uri: change]) + } + } } } /// Return settings for `file` based on the `change` settings for `mainFile`. /// /// This is used when inferring arguments for header files (e.g. main file is a `.m` while file is a` .h`). - nonisolated func convert( + func convert( change: FileBuildSettingsChange, ofMainFile mainFile: DocumentURI, to file: DocumentURI @@ -222,13 +189,12 @@ extension BuildSystemManager { } } - /// Handle a request for `FileBuildSettings` on `mainFile`. - /// - /// Updates and returns the new `MainFileStatus` for `mainFile`. + /// *Must be called on queue*. Handle a request for `FileBuildSettings` on + /// `mainFile`. Updates and returns the new `MainFileStatus` for `mainFile`. func cachedStatusOrRegisterForSettings( for mainFile: DocumentURI, language: Language - ) async -> MainFileStatus { + ) -> MainFileStatus { // If we already have a status for the main file, use that. // Don't update any existing timeout. if let status = self.mainFileStatuses[mainFile] { @@ -240,23 +206,23 @@ extension BuildSystemManager { if let buildSystem = self.buildSystem { // Register the timeout if it's applicable. if let fallback = self.fallbackBuildSystem { - Task { - try await Task.sleep(nanoseconds: UInt64(self.fallbackSettingsTimeout.nanoseconds()!)) - await self.handleFallbackTimer(for: mainFile, language: language, fallback) + self.queue.asyncAfter(deadline: DispatchTime.now() + self.fallbackSettingsTimeout) { [weak self] in + guard let self = self else { return } + self.handleFallbackTimer(for: mainFile, language: language, fallback) } } // Intentionally register with the `BuildSystem` after setting the fallback to allow for // testing of the fallback system triggering before the `BuildSystem` can reply (e.g. if a // fallback time of 0 is specified). - await buildSystem.registerForChangeNotifications(for: mainFile, language: language) + buildSystem.registerForChangeNotifications(for: mainFile, language: language) newStatus = .waiting } else if let fallback = self.fallbackBuildSystem { // Only have a fallback build system. We consider it be a primary build // system that functions synchronously. - if let settings = fallback.buildSettings(for: mainFile, language: language) { + if let settings = fallback.settings(for: mainFile, language) { newStatus = .primary(settings) } else { newStatus = .unsupported @@ -264,23 +230,13 @@ extension BuildSystemManager { } else { // Don't have any build systems. newStatus = .unsupported } - - if let status = self.mainFileStatuses[mainFile] { - // Since we await above, another call to `cachedStatusOrRegisterForSettings` - // might have set the main file status of `mainFile`. If this race happened, - // return the value set by the concurrently executing function. This is safe - // since all calls from this function are either side-effect free or - // idempotent. - return status - } - self.mainFileStatuses[mainFile] = newStatus return newStatus } - /// Update and notify our delegate for the given main file changes if they are - /// convertible into `FileBuildSettingsChange`. - func updateAndNotifyStatuses(changes: [DocumentURI: MainFileStatus]) async { + /// *Must be called on queue*. Update and notify our delegate for the given + /// main file changes if they are convertable into `FileBuildSettingsChange`. + func updateAndNotifyStatuses(changes: [DocumentURI: MainFileStatus]) { var changedWatchedFiles = [DocumentURI: FileBuildSettingsChange]() for (mainFile, status) in changes { let watches = self.watchedFiles.filter { $1.mainFile == mainFile } @@ -307,184 +263,173 @@ extension BuildSystemManager { } if !changedWatchedFiles.isEmpty, let delegate = self._delegate { - await delegate.fileBuildSettingsChanged(changedWatchedFiles) + self.notifyQueue.async { + delegate.fileBuildSettingsChanged(changedWatchedFiles) + } } } - /// Handle the fallback timer firing for a given `mainFile`. - /// - /// Since this doesn't occur immediately it's possible that the `mainFile` is - /// no longer referenced or is referenced by multiple watched files. + /// *Must be called on queue*. Handle the fallback timer firing for a given + /// `mainFile`. Since this doesn't occur immediately it's possible that the + /// `mainFile` is no longer referenced or is referenced by multiple watched + /// files. func handleFallbackTimer( for mainFile: DocumentURI, language: Language, _ fallback: FallbackBuildSystem - ) async { + ) { // There won't be a current status if it's unreferenced by any watched file. - // Similarly, if the status isn't `waiting` then there's nothing to do. + // Simiarly, if the status isn't `waiting` then there's nothing to do. guard let status = self.mainFileStatuses[mainFile], status == .waiting else { return } - if let settings = fallback.buildSettings(for: mainFile, language: language) { - await self.updateAndNotifyStatuses(changes: [mainFile: .waitingUsingFallback(settings)]) + if let settings = fallback.settings(for: mainFile, language) { + self.updateAndNotifyStatuses(changes: [mainFile: .waitingUsingFallback(settings)]) } else { // Keep the status as waiting. } } - public func unregisterForChangeNotifications(for uri: DocumentURI) async { - guard let mainFile = self.watchedFiles[uri]?.mainFile else { - log("Unbalanced calls for registerForChangeNotifications and unregisterForChangeNotifications", level: .warning) - return + public func unregisterForChangeNotifications(for uri: DocumentURI) { + queue.async { + guard let mainFile = self.watchedFiles[uri]?.mainFile else { + log("Unbalanced calls for registerForChangeNotifications and unregisterForChangeNotifications", level: .warning) + return + } + self.watchedFiles[uri] = nil + self.checkUnreferencedMainFile(mainFile) } - self.watchedFiles[uri] = nil - await self.checkUnreferencedMainFile(mainFile) } - /// If the given main file is no longer referenced by any watched files, - /// remove it and unregister it at the underlying build system. - func checkUnreferencedMainFile(_ mainFile: DocumentURI) async { + /// *Must be called on queue*. If the given main file is no longer referenced + /// by any watched files, remove it and unregister it at the underlying + /// build system. + func checkUnreferencedMainFile(_ mainFile: DocumentURI) { if !self.watchedFiles.values.lazy.map({ $0.mainFile }).contains(mainFile) { // This was the last reference to the main file. Remove it. - await self.buildSystem?.unregisterForChangeNotifications(for: mainFile) + self.buildSystem?.unregisterForChangeNotifications(for: mainFile) self.mainFileStatuses[mainFile] = nil } } - public func fileHandlingCapability(for uri: DocumentURI) async -> FileHandlingCapability { - return max( - await buildSystem?.fileHandlingCapability(for: uri) ?? .unhandled, - fallbackBuildSystem?.fileHandlingCapability(for: uri) ?? .unhandled - ) + public func fileHandlingCapability(for uri: DocumentURI) -> FileHandlingCapability { + return max(buildSystem?.fileHandlingCapability(for: uri) ?? .unhandled, fallbackBuildSystem?.fileHandlingCapability(for: uri) ?? .unhandled) } } extension BuildSystemManager: BuildSystemDelegate { - // FIXME: (async) Make this method isolated once `BuildSystemDelegate` has ben asyncified - public nonisolated func fileBuildSettingsChanged(_ changes: [DocumentURI: FileBuildSettingsChange]) { - Task { - await fileBuildSettingsChangedImpl(changes) - } - } - public func fileBuildSettingsChangedImpl(_ changes: [DocumentURI: FileBuildSettingsChange]) async { - let statusChanges: [DocumentURI: MainFileStatus] = - changes.reduce(into: [:]) { (result, entry) in - let mainFile = entry.key - let settingsChange = entry.value - let watches = self.watchedFiles.filter { $1.mainFile == mainFile } - guard let firstWatch = watches.first else { - // Possible notification after the file was unregistered. Ignore. - return - } - let newStatus: MainFileStatus + public func fileBuildSettingsChanged(_ changes: [DocumentURI: FileBuildSettingsChange]) { + queue.async { + let statusChanges: [DocumentURI: MainFileStatus] = + changes.reduce(into: [:]) { (result, entry) in + let mainFile = entry.key + let settingsChange = entry.value + let watches = self.watchedFiles.filter { $1.mainFile == mainFile } + guard let firstWatch = watches.first else { + // Possible notification after the file was unregistered. Ignore. + return + } + let newStatus: MainFileStatus - if let newSettings = settingsChange.newSettings { - newStatus = settingsChange.isFallback ? .fallback(newSettings) : .primary(newSettings) - } else if let fallback = self.fallbackBuildSystem { - // FIXME: we need to stop threading the language everywhere, or we need the build system - // itself to pass it in here. Or alternatively cache the fallback settings/language earlier? - let language = firstWatch.value.language - if let settings = fallback.buildSettings(for: mainFile, language: language) { - newStatus = .fallback(settings) + if let newSettings = settingsChange.newSettings { + newStatus = settingsChange.isFallback ? .fallback(newSettings) : .primary(newSettings) + } else if let fallback = self.fallbackBuildSystem { + // FIXME: we need to stop threading the language everywhere, or we need the build system + // itself to pass it in here. Or alteratively cache the fallback settings/language earlier? + let language = firstWatch.value.language + if let settings = fallback.settings(for: mainFile, language) { + newStatus = .fallback(settings) + } else { + newStatus = .unsupported + } } else { newStatus = .unsupported } - } else { - newStatus = .unsupported + result[mainFile] = newStatus } - result[mainFile] = newStatus - } - await self.updateAndNotifyStatuses(changes: statusChanges) - } - - // FIXME: (async) Make this method isolated once `BuildSystemDelegate` has ben asyncified - public nonisolated func filesDependenciesUpdated(_ changedFiles: Set) { - Task { - await filesDependenciesUpdatedImpl(changedFiles) + self.updateAndNotifyStatuses(changes: statusChanges) } } - public func filesDependenciesUpdatedImpl(_ changedFiles: Set) async { - // Empty changes --> assume everything has changed. - guard !changedFiles.isEmpty else { + public func filesDependenciesUpdated(_ changedFiles: Set) { + queue.async { + // Empty changes --> assume everything has changed. + guard !changedFiles.isEmpty else { + if let delegate = self._delegate { + self.notifyQueue.async { + delegate.filesDependenciesUpdated(changedFiles) + } + } + return + } + + // Need to map the changed main files back into changed watch files. + let changedWatchedFiles = self.watchedFiles.filter { changedFiles.contains($1.mainFile) } + let newChangedFiles = Set(changedWatchedFiles.map { $0.key }) + if let delegate = self._delegate, !newChangedFiles.isEmpty { + self.notifyQueue.async { + delegate.filesDependenciesUpdated(newChangedFiles) + } + } + } + } + + public func buildTargetsChanged(_ changes: [BuildTargetEvent]) { + queue.async { if let delegate = self._delegate { - await delegate.filesDependenciesUpdated(changedFiles) + self.notifyQueue.async { + delegate.buildTargetsChanged(changes) + } } - return - } - - // Need to map the changed main files back into changed watch files. - let changedWatchedFiles = self.watchedFiles.filter { changedFiles.contains($1.mainFile) } - let newChangedFiles = Set(changedWatchedFiles.map { $0.key }) - if let delegate = self._delegate, !newChangedFiles.isEmpty { - await delegate.filesDependenciesUpdated(newChangedFiles) } } - // FIXME: (async) Make this method isolated once `BuildSystemDelegate` has ben asyncified - public nonisolated func buildTargetsChanged(_ changes: [BuildTargetEvent]) { - Task { - await buildTargetsChangedImpl(changes) - } - } - - public func buildTargetsChangedImpl(_ changes: [BuildTargetEvent]) async { - if let delegate = self._delegate { - await delegate.buildTargetsChanged(changes) - } - } - - // FIXME: (async) Make this method isolated once `BuildSystemDelegate` has ben asyncified - public nonisolated func fileHandlingCapabilityChanged() { - Task { - await fileHandlingCapabilityChangedImpl() - } - } - - public func fileHandlingCapabilityChangedImpl() async { - if let delegate = self._delegate { - await delegate.fileHandlingCapabilityChanged() + public func fileHandlingCapabilityChanged() { + queue.async { + if let delegate = self._delegate { + self.notifyQueue.async { + delegate.fileHandlingCapabilityChanged() + } + } } } } extension BuildSystemManager: MainFilesDelegate { - // FIXME: (async) Make this method isolated once `MainFilesDelegate` has ben asyncified - public nonisolated func mainFilesChanged() { - Task { - await mainFilesChangedImpl() - } - } // FIXME: Consider debouncing/limiting this, seems to trigger often during a build. - public func mainFilesChangedImpl() async { - let origWatched = self.watchedFiles - self.watchedFiles = [:] - var buildSettingsChanges = [DocumentURI: FileBuildSettingsChange]() + public func mainFilesChanged() { + queue.async { + let origWatched = self.watchedFiles + self.watchedFiles = [:] + var buildSettingsChanges = [DocumentURI: FileBuildSettingsChange]() - for (uri, state) in origWatched { - let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) ?? [] - let newMainFile = chooseMainFile(for: uri, previous: state.mainFile, from: mainFiles) - let language = state.language + for (uri, state) in origWatched { + let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) ?? [] + let newMainFile = chooseMainFile(for: uri, previous: state.mainFile, from: mainFiles) + let language = state.language - self.watchedFiles[uri] = (newMainFile, language) + self.watchedFiles[uri] = (newMainFile, language) - if state.mainFile != newMainFile { - log("main file for '\(uri)' changed old: '\(state.mainFile)' -> new: '\(newMainFile)'", level: .info) - await self.checkUnreferencedMainFile(state.mainFile) + if state.mainFile != newMainFile { + log("main file for '\(uri)' changed old: '\(state.mainFile)' -> new: '\(newMainFile)'", level: .info) + self.checkUnreferencedMainFile(state.mainFile) - let newStatus = await self.cachedStatusOrRegisterForSettings( - for: newMainFile, language: language) - if let change = newStatus.buildSettingsChange { - let newChange = self.convert(change: change, ofMainFile: newMainFile, to: uri) - buildSettingsChanges[uri] = newChange + let newStatus = self.cachedStatusOrRegisterForSettings( + for: newMainFile, language: language) + if let change = newStatus.buildSettingsChange { + let newChange = self.convert(change: change, ofMainFile: newMainFile, to: uri) + buildSettingsChanges[uri] = newChange + } } } - } - if let delegate = self._delegate, !buildSettingsChanges.isEmpty { - await delegate.fileBuildSettingsChanged(buildSettingsChanges) + if let delegate = self._delegate, !buildSettingsChanges.isEmpty { + self.notifyQueue.async { + delegate.fileBuildSettingsChanged(buildSettingsChanges) + } + } } } } @@ -493,12 +438,16 @@ extension BuildSystemManager { /// *For Testing* Returns the main file used for `uri`, if this is a registered file. public func _cachedMainFile(for uri: DocumentURI) -> DocumentURI? { - watchedFiles[uri]?.mainFile + queue.sync { + watchedFiles[uri]?.mainFile + } } /// *For Testing* Returns the main file used for `uri`, if this is a registered file. public func _cachedMainFileSettings(for uri: DocumentURI) -> FileBuildSettings?? { - mainFileStatuses[uri]?.buildSettings + queue.sync { + mainFileStatuses[uri]?.buildSettings + } } } diff --git a/Sources/SKCore/CompilationDatabaseBuildSystem.swift b/Sources/SKCore/CompilationDatabaseBuildSystem.swift index 3425d474..10735178 100644 --- a/Sources/SKCore/CompilationDatabaseBuildSystem.swift +++ b/Sources/SKCore/CompilationDatabaseBuildSystem.swift @@ -25,10 +25,18 @@ import var TSCBasic.localFileSystem /// /// Provides build settings from a `CompilationDatabase` found by searching a project. For now, only /// one compilation database, located at the project root. -public actor CompilationDatabaseBuildSystem { +public final class CompilationDatabaseBuildSystem { + + /// Queue guarding the following properties: + /// - `compdb` + /// - `watchedFiles` + /// - `_indexStorePath` + let queue: DispatchQueue = .init(label: "CompilationDatabaseBuildSystem.queue", qos: .userInitiated) + /// The compilation database. var compdb: CompilationDatabase? = nil { didSet { + dispatchPrecondition(condition: .onQueue(queue)) // Build settings have changed and thus the index store path might have changed. // Recompute it on demand. _indexStorePath = nil @@ -38,10 +46,6 @@ public actor CompilationDatabaseBuildSystem { /// Delegate to handle any build system events. public weak var delegate: BuildSystemDelegate? = nil - public func setDelegate(_ delegate: BuildSystemDelegate?) async { - self.delegate = delegate - } - let projectRoot: AbsolutePath? let fileSystem: FileSystem @@ -52,22 +56,24 @@ public actor CompilationDatabaseBuildSystem { private var _indexStorePath: AbsolutePath? public var indexStorePath: AbsolutePath? { - if let indexStorePath = _indexStorePath { - return indexStorePath - } + return queue.sync { + if let indexStorePath = _indexStorePath { + return indexStorePath + } - if let allCommands = self.compdb?.allCommands { - for command in allCommands { - let args = command.commandLine - for i in args.indices.reversed() { - if args[i] == "-index-store-path" && i != args.endIndex - 1 { - _indexStorePath = try? AbsolutePath(validating: args[i+1]) - return _indexStorePath + if let allCommands = self.compdb?.allCommands { + for command in allCommands { + let args = command.commandLine + for i in args.indices.reversed() { + if args[i] == "-index-store-path" && i != args.endIndex - 1 { + _indexStorePath = try? AbsolutePath(validating: args[i+1]) + return _indexStorePath + } } } } + return nil } - return nil } public init(projectRoot: AbsolutePath? = nil, fileSystem: FileSystem = localFileSystem) { @@ -87,32 +93,36 @@ extension CompilationDatabaseBuildSystem: BuildSystem { public var indexPrefixMappings: [PathPrefixMapping] { return [] } - public func buildSettings(for document: DocumentURI, language: Language) async throws -> FileBuildSettings? { - return self.settings(for: document) - } + public func registerForChangeNotifications(for uri: DocumentURI, language: Language) { + queue.async { + self.watchedFiles[uri] = language - public func registerForChangeNotifications(for uri: DocumentURI, language: Language) async { - self.watchedFiles[uri] = language + guard let delegate = self.delegate else { return } - guard let delegate = self.delegate else { return } - - let settings = self.settings(for: uri) - await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) + let settings = self.settings(for: uri) + delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) + } } /// We don't support change watching. public func unregisterForChangeNotifications(for uri: DocumentURI) { - self.watchedFiles[uri] = nil + queue.async { + self.watchedFiles[uri] = nil + } } + /// Must be invoked on `queue`. private func database(for url: URL) -> CompilationDatabase? { + dispatchPrecondition(condition: .onQueue(queue)) if let path = try? AbsolutePath(validating: url.path) { return database(for: path) } return compdb } + /// Must be invoked on `queue`. private func database(for path: AbsolutePath) -> CompilationDatabase? { + dispatchPrecondition(condition: .onQueue(queue)) if compdb == nil { var dir = path while !dir.isRoot { @@ -142,7 +152,10 @@ extension CompilationDatabaseBuildSystem: BuildSystem { /// The compilation database has been changed on disk. /// Reload it and notify the delegate about build setting changes. - private func reloadCompilationDatabase() async { + /// Must be called on `queue`. + private func reloadCompilationDatabase() { + dispatchPrecondition(condition: .onQueue(queue)) + guard let projectRoot = self.projectRoot else { return } self.compdb = tryLoadCompilationDatabase(directory: projectRoot, self.fileSystem) @@ -156,13 +169,15 @@ extension CompilationDatabaseBuildSystem: BuildSystem { changedFiles[uri] = .removedOrUnavailable } } - await delegate.fileBuildSettingsChanged(changedFiles) + delegate.fileBuildSettingsChanged(changedFiles) } } - public func filesDidChange(_ events: [FileEvent]) async { - if events.contains(where: { self.fileEventShouldTriggerCompilationDatabaseReload(event: $0) }) { - await self.reloadCompilationDatabase() + public func filesDidChange(_ events: [FileEvent]) { + queue.async { + if events.contains(where: { self.fileEventShouldTriggerCompilationDatabaseReload(event: $0) }) { + self.reloadCompilationDatabase() + } } } @@ -170,16 +185,20 @@ extension CompilationDatabaseBuildSystem: BuildSystem { guard let fileUrl = uri.fileURL else { return .unhandled } - if database(for: fileUrl) != nil { - return .handled - } else { - return .unhandled + return queue.sync { + if database(for: fileUrl) != nil { + return .handled + } else { + return .unhandled + } } } } extension CompilationDatabaseBuildSystem { + /// Must be invoked on `queue`. private func settings(for uri: DocumentURI) -> FileBuildSettings? { + dispatchPrecondition(condition: .onQueue(queue)) guard let url = uri.fileURL else { // We can't determine build settings for non-file URIs. return nil @@ -193,6 +212,8 @@ extension CompilationDatabaseBuildSystem { /// Exposed for *testing*. public func _settings(for uri: DocumentURI) -> FileBuildSettings? { - return self.settings(for: uri) + return queue.sync { + return self.settings(for: uri) + } } } diff --git a/Sources/SKCore/FallbackBuildSystem.swift b/Sources/SKCore/FallbackBuildSystem.swift index c03bfb44..30183e04 100644 --- a/Sources/SKCore/FallbackBuildSystem.swift +++ b/Sources/SKCore/FallbackBuildSystem.swift @@ -38,17 +38,13 @@ public final class FallbackBuildSystem: BuildSystem { /// Delegate to handle any build system events. public weak var delegate: BuildSystemDelegate? = nil - public func setDelegate(_ delegate: BuildSystemDelegate?) async { - self.delegate = delegate - } - public var indexStorePath: AbsolutePath? { return nil } public var indexDatabasePath: AbsolutePath? { return nil } public var indexPrefixMappings: [PathPrefixMapping] { return [] } - public func buildSettings(for uri: DocumentURI, language: Language) -> FileBuildSettings? { + public func settings(for uri: DocumentURI, _ language: Language) -> FileBuildSettings? { switch language { case .swift: return settingsSwift(uri.pseudoPath) @@ -62,9 +58,9 @@ public final class FallbackBuildSystem: BuildSystem { public func registerForChangeNotifications(for uri: DocumentURI, language: Language) { guard let delegate = self.delegate else { return } - let settings = self.buildSettings(for: uri, language: language) - Task { - await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) + let settings = self.settings(for: uri, language) + DispatchQueue.global().async { + delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) } } diff --git a/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift b/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift index df39d785..2ca18dc5 100644 --- a/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift +++ b/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift @@ -48,7 +48,7 @@ public enum ReloadPackageStatus { /// This class implements the `BuildSystem` interface to provide the build settings for a Swift /// Package Manager (SwiftPM) package. The settings are determined by loading the Package.swift /// manifest using `libSwiftPM` and constructing a build plan using the default (debug) parameters. -public actor SwiftPMWorkspace { +public final class SwiftPMWorkspace { public enum Error: Swift.Error { @@ -62,10 +62,6 @@ public actor SwiftPMWorkspace { /// Delegate to handle any build system events. public weak var delegate: SKCore.BuildSystemDelegate? = nil - public func setDelegate(_ delegate: SKCore.BuildSystemDelegate?) async { - self.delegate = delegate - } - let workspacePath: TSCAbsolutePath let packageRoot: TSCAbsolutePath /// *Public for testing* @@ -82,6 +78,14 @@ public actor SwiftPMWorkspace { /// mapped to the language the delegate specified when registering for change notifications. var watchedFiles: [DocumentURI: Language] = [:] + /// Queue guarding the following properties: + /// - `delegate` + /// - `watchedFiles` + /// - `packageGraph` + /// - `fileToTarget` + /// - `sourceDirToTarget` + let queue: DispatchQueue = .init(label: "SwiftPMWorkspace.queue", qos: .utility) + /// This callback is informed when `reloadPackage` starts and ends executing. var reloadPackageStatusCallback: (ReloadPackageStatus) -> Void @@ -100,7 +104,8 @@ public actor SwiftPMWorkspace { fileSystem: FileSystem = localFileSystem, buildSetup: BuildSetup, reloadPackageStatusCallback: @escaping (ReloadPackageStatus) -> Void = { _ in } - ) async throws { + ) throws + { self.workspacePath = workspacePath self.fileSystem = fileSystem @@ -152,7 +157,7 @@ public actor SwiftPMWorkspace { self.packageGraph = try PackageGraph(rootPackages: [], dependencies: [], binaryArtifacts: [:]) self.reloadPackageStatusCallback = reloadPackageStatusCallback - try await reloadPackage() + try reloadPackage() } /// Creates a build system using the Swift Package Manager, if this workspace is a package. @@ -160,14 +165,15 @@ public actor SwiftPMWorkspace { /// - Parameters: /// - reloadPackageStatusCallback: Will be informed when `reloadPackage` starts and ends executing. /// - Returns: nil if `workspacePath` is not part of a package or there is an error. - public init?( + public convenience init?( url: URL, toolchainRegistry: ToolchainRegistry, buildSetup: BuildSetup, reloadPackageStatusCallback: @escaping (ReloadPackageStatus) -> Void - ) async { + ) + { do { - try await self.init( + try self.init( workspacePath: try TSCAbsolutePath(validating: url.path), toolchainRegistry: toolchainRegistry, fileSystem: localFileSystem, @@ -188,7 +194,8 @@ extension SwiftPMWorkspace { /// (Re-)load the package settings by parsing the manifest and resolving all the targets and /// dependencies. - func reloadPackage() async throws { + /// Must only be called on `queue` or from the initializer. + func reloadPackage() throws { reloadPackageStatusCallback(.start) defer { reloadPackageStatusCallback(.end) @@ -245,15 +252,15 @@ extension SwiftPMWorkspace { var changedFiles: [DocumentURI: FileBuildSettingsChange] = [:] for (uri, language) in self.watchedFiles { orLog { - if let settings = try self.buildSettings(for: uri, language: language) { + if let settings = try self.settings(for: uri, language) { changedFiles[uri] = FileBuildSettingsChange(settings) } else { changedFiles[uri] = .removedOrUnavailable } } } - await delegate.fileBuildSettingsChanged(changedFiles) - await delegate.fileHandlingCapabilityChanged() + delegate.fileBuildSettingsChanged(changedFiles) + delegate.fileHandlingCapabilityChanged() } } @@ -278,10 +285,17 @@ extension SwiftPMWorkspace: SKCore.BuildSystem { for uri: DocumentURI, _ language: Language) throws -> FileBuildSettings? { - try self.buildSettings(for: uri, language: language) + return try queue.sync { + try self.settings(for: uri, language) + } } - public func buildSettings(for uri: DocumentURI, language: Language) throws -> FileBuildSettings? { + /// Must only be called on `queue`. + private func settings( + for uri: DocumentURI, + _ language: Language) throws -> FileBuildSettings? + { + dispatchPrecondition(condition: .onQueue(queue)) guard let url = uri.fileURL else { // We can't determine build settings for non-file URIs. return nil @@ -305,32 +319,38 @@ extension SwiftPMWorkspace: SKCore.BuildSystem { return nil } - public func registerForChangeNotifications(for uri: DocumentURI, language: Language) async { - assert(self.watchedFiles[uri] == nil, "Registered twice for change notifications of the same URI") - guard let delegate = self.delegate else { return } - self.watchedFiles[uri] = language + public func registerForChangeNotifications(for uri: DocumentURI, language: Language) { + queue.async { + assert(self.watchedFiles[uri] == nil, "Registered twice for change notifications of the same URI") + guard let delegate = self.delegate else { return } + self.watchedFiles[uri] = language - var settings: FileBuildSettings? = nil - do { - settings = try self.buildSettings(for: uri, language: language) - } catch { - log("error computing settings: \(error)") - } - if let settings = settings { - await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) - } else { - await delegate.fileBuildSettingsChanged([uri: .removedOrUnavailable]) + var settings: FileBuildSettings? = nil + do { + settings = try self.settings(for: uri, language) + } catch { + log("error computing settings: \(error)") + } + if let settings = settings { + delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) + } else { + delegate.fileBuildSettingsChanged([uri: .removedOrUnavailable]) + } } } /// Unregister the given file for build-system level change notifications, such as command /// line flag changes, dependency changes, etc. public func unregisterForChangeNotifications(for uri: DocumentURI) { - self.watchedFiles[uri] = nil + queue.async { + self.watchedFiles[uri] = nil + } } /// Returns the resolved target description for the given file, if one is known. + /// Must only be called on `queue`. private func targetDescription(for file: AbsolutePath) throws -> TargetBuildDescription? { + dispatchPrecondition(condition: .onQueue(queue)) if let td = fileToTarget[file] { return td } @@ -366,11 +386,13 @@ extension SwiftPMWorkspace: SKCore.BuildSystem { } } - public func filesDidChange(_ events: [FileEvent]) async { - if events.contains(where: { self.fileEventShouldTriggerPackageReload(event: $0) }) { - await orLog { - // TODO: It should not be necessary to reload the entire package just to get build settings for one file. - try await self.reloadPackage() + public func filesDidChange(_ events: [FileEvent]) { + queue.async { + if events.contains(where: { self.fileEventShouldTriggerPackageReload(event: $0) }) { + orLog { + // TODO: It should not be necessary to reload the entire package just to get build settings for one file. + try self.reloadPackage() + } } } } @@ -379,10 +401,12 @@ extension SwiftPMWorkspace: SKCore.BuildSystem { guard let fileUrl = uri.fileURL else { return .unhandled } - if (try? targetDescription(for: AbsolutePath(validating: fileUrl.path))) != nil { - return .handled - } else { - return .unhandled + return self.queue.sync { + if (try? targetDescription(for: AbsolutePath(validating: fileUrl.path))) != nil { + return .handled + } else { + return .unhandled + } } } } @@ -410,7 +434,9 @@ extension SwiftPMWorkspace { } /// Retrieve settings for a package manifest (Package.swift). + /// Must only be called on `queue`. private func settings(forPackageManifest path: AbsolutePath) throws -> FileBuildSettings? { + dispatchPrecondition(condition: .onQueue(queue)) func impl(_ path: AbsolutePath) -> FileBuildSettings? { for package in packageGraph.packages where path == package.manifest.path { let compilerArgs = workspace.interpreterFlags(for: package.path) + [path.pathString] @@ -428,7 +454,9 @@ extension SwiftPMWorkspace { } /// Retrieve settings for a given header file. + /// Must only be called on `queue`. private func settings(forHeader path: AbsolutePath, _ language: Language) throws -> FileBuildSettings? { + dispatchPrecondition(condition: .onQueue(queue)) func impl(_ path: AbsolutePath) throws -> FileBuildSettings? { var dir = path.parentDirectory while !dir.isRoot { diff --git a/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift b/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift index f1187c3c..f5b97dd0 100644 --- a/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift +++ b/Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift @@ -26,12 +26,6 @@ import func TSCBasic.resolveSymlinks import struct TSCBasic.AbsolutePath import struct PackageModel.BuildFlags -fileprivate extension SourceKitServer { - func addWorkspace(_ workspace: Workspace) { - self._workspaces.append(workspace) - } -} - public final class SKSwiftPMTestWorkspace { /// The directory containing the original, unmodified project. @@ -58,7 +52,7 @@ public final class SKSwiftPMTestWorkspace { public var sk: TestClient { testServer.client } /// When `testServer` is not `nil`, the workspace will be opened in that server, otherwise a new server will be created for the workspace - public init(projectDir: URL, tmpDir: URL, toolchain: Toolchain, testServer: TestSourceKitServer? = nil) async throws { + public init(projectDir: URL, tmpDir: URL, toolchain: Toolchain, testServer: TestSourceKitServer? = nil) throws { self.testServer = testServer ?? TestSourceKitServer(connectionKind: .local) self.projectDir = URL( @@ -85,26 +79,26 @@ public final class SKSwiftPMTestWorkspace { let buildPath = try AbsolutePath(validating: buildDir.path) let buildSetup = BuildSetup(configuration: .debug, path: buildPath, flags: BuildFlags()) - let swiftpm = try await SwiftPMWorkspace( + let swiftpm = try SwiftPMWorkspace( workspacePath: sourcePath, toolchainRegistry: ToolchainRegistry.shared, buildSetup: buildSetup) let libIndexStore = try IndexStoreLibrary(dylibPath: toolchain.libIndexStore!.pathString) - try fm.createDirectory(atPath: await swiftpm.indexStorePath!.pathString, withIntermediateDirectories: true) + try fm.createDirectory(atPath: swiftpm.indexStorePath!.pathString, withIntermediateDirectories: true) let indexDelegate = SourceKitIndexDelegate() self.index = try IndexStoreDB( - storePath: await swiftpm.indexStorePath!.pathString, + storePath: swiftpm.indexStorePath!.pathString, databasePath: tmpDir.path, library: libIndexStore, delegate: indexDelegate, listenToUnitEvents: false) let server = self.testServer.server! - let workspace = await Workspace( + let workspace = Workspace( documentManager: DocumentManager(), rootUri: DocumentURI(sources.rootDirectory), capabilityRegistry: CapabilityRegistry(clientCapabilities: ClientCapabilities()), @@ -113,8 +107,8 @@ public final class SKSwiftPMTestWorkspace { underlyingBuildSystem: swiftpm, index: index, indexDelegate: indexDelegate) - await workspace.buildSystemManager.setDelegate(server) - await server.addWorkspace(workspace) + workspace.buildSystemManager.delegate = server + server._workspaces.append(workspace) } deinit { @@ -164,10 +158,10 @@ extension SKSwiftPMTestWorkspace { extension XCTestCase { - public func staticSourceKitSwiftPMWorkspace(name: String, server: TestSourceKitServer? = nil) async throws -> SKSwiftPMTestWorkspace? { + public func staticSourceKitSwiftPMWorkspace(name: String, server: TestSourceKitServer? = nil) throws -> SKSwiftPMTestWorkspace? { let testDirName = testDirectoryName let toolchain = ToolchainRegistry.shared.default! - let workspace = try await SKSwiftPMTestWorkspace( + let workspace = try SKSwiftPMTestWorkspace( projectDir: XCTestCase.sklspInputsDirectory.appendingPathComponent(name, isDirectory: true), tmpDir: URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent("sk-test-data/\(testDirName)/\(name)", isDirectory: true), diff --git a/Sources/SKTestSupport/SKTibsTestWorkspace.swift b/Sources/SKTestSupport/SKTibsTestWorkspace.swift index 7b7f033a..dce0d79e 100644 --- a/Sources/SKTestSupport/SKTibsTestWorkspace.swift +++ b/Sources/SKTestSupport/SKTibsTestWorkspace.swift @@ -27,12 +27,6 @@ import struct PackageModel.BuildFlags public typealias URL = Foundation.URL -fileprivate extension SourceKitServer { - func setWorkspaces(_ workspaces: [Workspace]) { - self._workspaces = workspaces - } -} - public final class SKTibsTestWorkspace { public let tibsWorkspace: TibsTestWorkspace @@ -51,7 +45,7 @@ public final class SKTibsTestWorkspace { toolchain: Toolchain, clientCapabilities: ClientCapabilities, testServer: TestSourceKitServer? = nil - ) async throws + ) throws { self.testServer = testServer ?? TestSourceKitServer(connectionKind: .local) self.tibsWorkspace = try TibsTestWorkspace( @@ -61,7 +55,7 @@ public final class SKTibsTestWorkspace { removeTmpDir: removeTmpDir, toolchain: TibsToolchain(toolchain)) - try await initWorkspace(clientCapabilities: clientCapabilities) + try initWorkspace(clientCapabilities: clientCapabilities) } public init( @@ -70,7 +64,7 @@ public final class SKTibsTestWorkspace { toolchain: Toolchain, clientCapabilities: ClientCapabilities, testServer: TestSourceKitServer? = nil - ) async throws { + ) throws { self.testServer = testServer ?? TestSourceKitServer(connectionKind: .local) self.tibsWorkspace = try TibsTestWorkspace( @@ -78,16 +72,16 @@ public final class SKTibsTestWorkspace { tmpDir: tmpDir, toolchain: TibsToolchain(toolchain)) - try await initWorkspace(clientCapabilities: clientCapabilities) + try initWorkspace(clientCapabilities: clientCapabilities) } - func initWorkspace(clientCapabilities: ClientCapabilities) async throws { + func initWorkspace(clientCapabilities: ClientCapabilities) throws { let buildPath = try AbsolutePath(validating: builder.buildRoot.path) let buildSystem = CompilationDatabaseBuildSystem(projectRoot: buildPath) let indexDelegate = SourceKitIndexDelegate() tibsWorkspace.delegate = indexDelegate - let workspace = await Workspace( + let workspace = Workspace( documentManager: DocumentManager(), rootUri: DocumentURI(sources.rootDirectory), capabilityRegistry: CapabilityRegistry(clientCapabilities: clientCapabilities), @@ -97,8 +91,8 @@ public final class SKTibsTestWorkspace { index: index, indexDelegate: indexDelegate) - await workspace.buildSystemManager.setDelegate(testServer.server!) - await testServer.server!.setWorkspaces([workspace]) + workspace.buildSystemManager.delegate = testServer.server! + testServer.server!._workspaces = [workspace] } } @@ -137,9 +131,9 @@ extension XCTestCase { tmpDir: URL? = nil, removeTmpDir: Bool = true, server: TestSourceKitServer? = nil - ) async throws -> SKTibsTestWorkspace? { + ) throws -> SKTibsTestWorkspace? { let testDirName = testDirectoryName - let workspace = try await SKTibsTestWorkspace( + let workspace = try SKTibsTestWorkspace( immutableProjectDir: XCTestCase.sklspInputsDirectory .appendingPathComponent(name, isDirectory: true), persistentBuildDir: XCTestCase.productsDirectory @@ -169,9 +163,9 @@ extension XCTestCase { name: String, clientCapabilities: ClientCapabilities = .init(), tmpDir: URL? = nil - ) async throws -> SKTibsTestWorkspace? { + ) throws -> SKTibsTestWorkspace? { let testDirName = testDirectoryName - let workspace = try await SKTibsTestWorkspace( + let workspace = try SKTibsTestWorkspace( projectDir: XCTestCase.sklspInputsDirectory.appendingPathComponent(name, isDirectory: true), tmpDir: tmpDir ?? URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent("sk-test-data/\(testDirName)", isDirectory: true), diff --git a/Sources/SKTestSupport/SourceKitServer+WorkspaceForDocumentOnQueue.swift b/Sources/SKTestSupport/SourceKitServer+WorkspaceForDocumentOnQueue.swift new file mode 100644 index 00000000..92cfec86 --- /dev/null +++ b/Sources/SKTestSupport/SourceKitServer+WorkspaceForDocumentOnQueue.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2022 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 LanguageServerProtocol +import SourceKitLSP + +public extension SourceKitServer { + func workspaceForDocumentOnQueue(uri: DocumentURI) -> Workspace? { + self.queue.sync { + return self.workspaceForDocument(uri: uri) + } + } +} diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt index 9fb6ab1a..306d7b84 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -5,7 +5,6 @@ add_library(SourceKitLSP STATIC DocumentTokens.swift IndexStoreDB+MainFilesProvider.swift RangeAdjuster.swift - Sequence+AsyncMap.swift SourceKitIndexDelegate.swift SourceKitLSPCommandMetadata.swift SourceKitServer+Options.swift diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift index e500a972..afd83446 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift @@ -40,10 +40,11 @@ extension NSLock { /// ``ClangLangaugeServerShim`` conforms to ``MessageHandler`` to receive /// requests and notifications **from** clangd, not from the editor, and it will /// forward these requests and notifications to the editor. -actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler { - // FIXME: (async) Remove once `Connection.send` has been asyncified. - /// The queue on which clangd calls us back. - public let clangdCommunicationQueue: DispatchQueue = DispatchQueue(label: "language-server-queue", qos: .userInitiated) +final class ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler { + /// The server's request queue. + /// + /// All incoming requests start on this queue, but should reply or move to another queue as soon as possible to avoid blocking. + public let queue: DispatchQueue = DispatchQueue(label: "language-server-queue", qos: .userInitiated) /// The connection to the client. In the case of `ClangLanguageServerShim`, /// the client is always a ``SourceKitServer``, which will forward the request @@ -64,10 +65,18 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler { let clangdOptions: [String] + /// Resolved build settings by file. Must be accessed with the `lock`. + private var buildSettingsByFile: [DocumentURI: ClangBuildSettings] = [:] + + /// Lock protecting `buildSettingsByFile`. + private var lock: NSLock = NSLock() + /// The current state of the `clangd` language server. /// Changing the property automatically notified the state change handlers. private var state: LanguageServerState { didSet { + // `state` must only be set from `queue`. + dispatchPrecondition(condition: .onQueue(queue)) for handler in stateChangeHandlers { handler(oldValue, state) } @@ -89,16 +98,11 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler { private var initializeRequest: InitializeRequest? /// The workspace this `ClangLanguageServer` was opened for. - /// /// `clangd` doesn't have support for multi-root workspaces, so we need to start a separate `clangd` instance for every workspace root. - private let workspace: WeakWorkspace + private weak var workspace: Workspace? /// A callback with which `ClangLanguageServer` can request its owner to reopen all documents in case it has crashed. - private let reopenDocuments: (ToolchainLanguageServer) async -> Void - - /// The documents that have been opened and which language they have been - /// opened with. - private var openDocuments: [DocumentURI: Language] = [:] + private let reopenDocuments: (ToolchainLanguageServer) -> Void /// While `clangd` is running, its PID. #if os(Windows) @@ -114,62 +118,36 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler { toolchain: Toolchain, options: SourceKitServer.Options, workspace: Workspace, - reopenDocuments: @escaping (ToolchainLanguageServer) async -> Void, - workspaceForDocument: @escaping (DocumentURI) async -> Workspace? - ) async throws { + reopenDocuments: @escaping (ToolchainLanguageServer) -> Void + ) throws { guard let clangdPath = toolchain.clangd else { return nil } self.clangPath = toolchain.clang self.clangdPath = clangdPath self.clangdOptions = options.clangdOptions - self.workspace = WeakWorkspace(workspace) + self.workspace = workspace self.reopenDocuments = reopenDocuments self.state = .connected self.client = client try startClangdProcesss() } - private func buildSettings(for document: DocumentURI) async -> ClangBuildSettings? { - guard let workspace = workspace.value, let language = openDocuments[document] else { - return nil - } - guard let settings = await workspace.buildSystemManager.buildSettings(for: document, language: language) else { - return nil - } - return ClangBuildSettings(settings.buildSettings, clangPath: clangdPath, isFallback: settings.isFallback) - } - - nonisolated func canHandle(workspace: Workspace) -> Bool { + func canHandle(workspace: Workspace) -> Bool { // We launch different clangd instance for each workspace because clangd doesn't have multi-root workspace support. - return workspace === self.workspace.value + return workspace === self.workspace } func addStateChangeHandler(handler: @escaping (LanguageServerState, LanguageServerState) -> Void) { - self.stateChangeHandlers.append(handler) - } - - /// Called after the `clangd` process exits. - /// - /// Restarts `clangd` if it has crashed. - /// - /// - Parameter terminationStatus: The exit code of `clangd`. - private func handleClangdTermination(terminationStatus: Int32) { -#if os(Windows) - self.hClangd = INVALID_HANDLE_VALUE -#else - self.clangdPid = nil -#endif - if terminationStatus != 0 { - self.state = .connectionInterrupted - self.restartClangd() + queue.async { + self.stateChangeHandlers.append(handler) } } /// Start the `clangd` process, either on creation of the `ClangLanguageServerShim` or after `clangd` has crashed. private func startClangdProcesss() throws { - // Since we are starting a new clangd process, reset the list of open document - openDocuments = [:] + // Since we are starting a new clangd process, reset the build settings we have transmitted to clangd + buildSettingsByFile = [:] let usToClangd: Pipe = Pipe() let clangdToUs: Pipe = Pipe() @@ -201,8 +179,16 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler { log("clangd exited: \(process.terminationReason) \(process.terminationStatus)") connectionToClangd.close() guard let self = self else { return } - Task { - await self.handleClangdTermination(terminationStatus: process.terminationStatus) + self.queue.async { +#if os(Windows) + self.hClangd = INVALID_HANDLE_VALUE +#else + self.clangdPid = nil +#endif + if process.terminationStatus != 0 { + self.state = .connectionInterrupted + self.restartClangd() + } } } try process.run() @@ -216,52 +202,57 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler { /// Restart `clangd` after it has crashed. /// Delays restarting of `clangd` in case there is a crash loop. private func restartClangd() { - precondition(self.state == .connectionInterrupted) - - precondition(self.clangRestartScheduled == false) - self.clangRestartScheduled = true - - guard let initializeRequest = self.initializeRequest else { - log("clangd crashed before it was sent an InitializeRequest.", level: .error) - return - } - - let restartDelay: Int - if let lastClangdRestart = self.lastClangdRestart, Date().timeIntervalSince(lastClangdRestart) < 30 { - log("clangd has already been restarted in the last 30 seconds. Delaying another restart by 10 seconds.", level: .info) - restartDelay = 10 - } else { - restartDelay = 0 - } - self.lastClangdRestart = Date() - - Task { - try await Task.sleep(nanoseconds: UInt64(restartDelay) * 1_000_000_000) - self.clangRestartScheduled = false - do { - try self.startClangdProcesss() - // FIXME: We assume that clangd will return the same capabilites after restarting. - // Theoretically they could have changed and we would need to inform SourceKitServer about them. - // But since SourceKitServer more or less ignores them right now anyway, this should be fine for now. - _ = try self.initializeSync(initializeRequest) - self.clientInitialized(InitializedNotification()) - await self.reopenDocuments(self) - self.state = .connected - } catch { - log("Failed to restart clangd after a crash.", level: .error) + queue.async { + precondition(self.state == .connectionInterrupted) + + precondition(self.clangRestartScheduled == false) + self.clangRestartScheduled = true + + guard let initializeRequest = self.initializeRequest else { + log("clangd crashed before it was sent an InitializeRequest.", level: .error) + return } + + let restartDelay: Int + if let lastClangdRestart = self.lastClangdRestart, Date().timeIntervalSince(lastClangdRestart) < 30 { + log("clangd has already been restarted in the last 30 seconds. Delaying another restart by 10 seconds.", level: .info) + restartDelay = 10 + } else { + restartDelay = 0 } + self.lastClangdRestart = Date() + + DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + .seconds(restartDelay)) { + self.clangRestartScheduled = false + do { + try self.startClangdProcesss() + // FIXME: We assume that clangd will return the same capabilites after restarting. + // Theoretically they could have changed and we would need to inform SourceKitServer about them. + // But since SourceKitServer more or less ignores them right now anyway, this should be fine for now. + _ = try self.initializeSync(initializeRequest) + self.clientInitialized(InitializedNotification()) + self.reopenDocuments(self) + self.queue.async { + self.state = .connected + } + } catch { + log("Failed to restart clangd after a crash.", level: .error) + } + } + } } /// Handler for notifications received **from** clangd, ie. **clangd** is /// sending a notification that's intended for the editor. /// /// We should either handle it ourselves or forward it to the client. - func handle(_ params: some NotificationType, from clientID: ObjectIdentifier) async { - if let publishDiags = params as? PublishDiagnosticsNotification { - await self.publishDiagnostics(Notification(publishDiags, clientID: clientID)) - } else if clientID == ObjectIdentifier(self.clangd) { - self.client.send(params) + func handle(_ params: some NotificationType, from clientID: ObjectIdentifier) { + queue.async { + if let publishDiags = params as? PublishDiagnosticsNotification { + self.publishDiagnostics(Notification(publishDiags, clientID: clientID)) + } else if clientID == ObjectIdentifier(self.clangd) { + self.client.send(params) + } } } @@ -275,14 +266,16 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler { from clientID: ObjectIdentifier, reply: @escaping (LSPResult) -> Void ) { - let request = Request(params, id: id, clientID: clientID, cancellation: CancellationToken(), reply: { result in - reply(result) - }) + queue.async { + let request = Request(params, id: id, clientID: clientID, cancellation: CancellationToken(), reply: { result in + reply(result) + }) - if request.clientID == ObjectIdentifier(self.clangd) { - self.forwardRequest(request, to: self.client) - } else { - request.reply(.failure(ResponseError.methodNotFound(R.method))) + if request.clientID == ObjectIdentifier(self.clangd) { + self.forwardRequest(request, to: self.client) + } else { + request.reply(.failure(ResponseError.methodNotFound(R.method))) + } } } @@ -304,7 +297,7 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler { to: Connection, _ handler: ((LSPResult) -> Void)? = nil) { - let id = to.send(request.params, queue: clangdCommunicationQueue) { result in + let id = to.send(request.params, queue: queue) { result in handler?(result) request.reply(result) } @@ -313,24 +306,40 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler { } } + /// Forward the given `notification` to `clangd` by asynchronously switching to `queue` for a thread-safe access to `clangd`. + private func forwardNotificationToClangdOnQueue(_ notification: Notification) where Notification: NotificationType { + queue.async { + self.clangd.send(notification) + } + } + func _crash() { - // Since `clangd` doesn't have a method to crash it, kill it. + self.queue.async { + // Since `clangd` doesn't have a method to crash it, kill it. #if os(Windows) - if self.hClangd != INVALID_HANDLE_VALUE { - // FIXME(compnerd) this is a bad idea - we can potentially deadlock the - // process if a kobject is a pending state. Unfortunately, the - // `OpenProcess(PROCESS_TERMINATE, ...)`, `CreateRemoteThread`, - // `ExitProcess` dance, while safer, can also indefinitely hang as - // `CreateRemoteThread` may not be serviced depending on the state of - // the process. This just attempts to terminate the process, risking a - // deadlock and resource leaks. - _ = TerminateProcess(self.hClangd, 0) - } + if self.hClangd != INVALID_HANDLE_VALUE { + // FIXME(compnerd) this is a bad idea - we can potentially deadlock the + // process if a kobject is a pending state. Unfortunately, the + // `OpenProcess(PROCESS_TERMINATE, ...)`, `CreateRemoteThread`, + // `ExitProcess` dance, while safer, can also indefinitely hang as + // `CreateRemoteThread` may not be serviced depending on the state of + // the process. This just attempts to terminate the process, risking a + // deadlock and resource leaks. + _ = TerminateProcess(self.hClangd, 0) + } #else - if let pid = self.clangdPid { - kill(pid, SIGKILL) - } + if let pid = self.clangdPid { + kill(pid, SIGKILL) + } #endif + } + } + + /// Forward the given `request` to `clangd` by asynchronously switching to `queue` for a thread-safe access to `clangd`. + private func forwardRequestToClangdOnQueue(_ request: Request, _ handler: ((LSPResult) -> Void)? = nil) { + queue.async { + self.forwardRequest(request, to: self.clangd, handler) + } } } @@ -340,27 +349,19 @@ extension ClangLanguageServerShim { /// Intercept clangd's `PublishDiagnosticsNotification` to withold it if we're using fallback /// build settings. - func publishDiagnostics(_ note: Notification) async { + func publishDiagnostics(_ note: Notification) { let params = note.params - // Technically, the publish diagnostics notification could still originate - // from when we opened the file with fallback build settings and we could - // have received real build settings since, which haven't been acknowledged - // by clangd yet. - // - // Since there is no way to tell which build settings clangd used to generate - // the diagnostics, there's no good way to resolve this race. For now, this - // should be good enough since the time in which the race may occur is pretty - // short and we expect clangd to send us new diagnostics with the updated - // non-fallback settings very shortly after, which will override the - // incorrect result, making it very temporary. - let buildSettings = await self.buildSettings(for: params.uri) - if buildSettings?.isFallback ?? true { - // Fallback: send empty publish notification instead. - client.send(PublishDiagnosticsNotification( - uri: params.uri, version: params.version, diagnostics: [])) - } else { - client.send(note.params) + let buildSettings = self.lock.withLock { + return self.buildSettingsByFile[params.uri] } + let isFallback = buildSettings?.isFallback ?? true + guard isFallback else { + client.send(note.params) + return + } + // Fallback: send empty publish notification instead. + client.send(PublishDiagnosticsNotification( + uri: params.uri, version: params.version, diagnostics: [])) } } @@ -370,51 +371,48 @@ extension ClangLanguageServerShim { extension ClangLanguageServerShim { func initializeSync(_ initialize: InitializeRequest) throws -> InitializeResult { - // Store the initialize request so we can replay it in case clangd crashes - self.initializeRequest = initialize - - let result = try clangd.sendSync(initialize) - self.capabilities = result.capabilities - return result + return try queue.sync { + // Store the initialize request so we can replay it in case clangd crashes + self.initializeRequest = initialize + + let result = try clangd.sendSync(initialize) + self.capabilities = result.capabilities + return result + } } public func clientInitialized(_ initialized: InitializedNotification) { - clangd.send(initialized) + forwardNotificationToClangdOnQueue(initialized) } - public func shutdown() async { - await withCheckedContinuation { continuation in - _ = clangd.send(ShutdownRequest(), queue: self.clangdCommunicationQueue) { [weak self] _ in - guard let self else { return } - Task { - await self.clangd.send(ExitNotification()) - if let localConnection = self.client as? LocalConnection { - localConnection.close() - } - continuation.resume() + public func shutdown(callback: @escaping () -> Void) { + queue.async { + _ = self.clangd.send(ShutdownRequest(), queue: self.queue) { [weak self] _ in + self?.clangd.send(ExitNotification()) + if let localConnection = self?.client as? LocalConnection { + localConnection.close() } + callback() } } } // MARK: - Text synchronization - public func openDocument(_ note: DidOpenTextDocumentNotification) async { - openDocuments[note.textDocument.uri] = note.textDocument.language - // Send clangd the build settings for the new file. We need to do this before - // sending the open notification, so that the initial diagnostics already - // have build settings. - await documentUpdatedBuildSettings(note.textDocument.uri, change: .removedOrUnavailable) - clangd.send(note) + public func openDocument(_ note: DidOpenTextDocumentNotification) { + forwardNotificationToClangdOnQueue(note) } public func closeDocument(_ note: DidCloseTextDocumentNotification) { - openDocuments[note.textDocument.uri] = nil - clangd.send(note) + forwardNotificationToClangdOnQueue(note) + + // Don't clear cached build settings since we've already informed clangd of the settings for the + // file; if we clear the build settings here we should give clangd dummy build settings to make + // sure we're in sync. } public func changeDocument(_ note: DidChangeTextDocumentNotification) { - clangd.send(note) + forwardNotificationToClangdOnQueue(note) } public func willSaveDocument(_ note: WillSaveTextDocumentNotification) { @@ -422,23 +420,31 @@ extension ClangLanguageServerShim { } public func didSaveDocument(_ note: DidSaveTextDocumentNotification) { - clangd.send(note) + forwardNotificationToClangdOnQueue(note) } // MARK: - Build System Integration - public func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) async { + public func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) { guard let url = uri.fileURL else { // FIXME: The clang workspace can probably be reworked to support non-file URIs. log("Received updated build settings for non-file URI '\(uri)'. Ignoring the update.") return } - let clangBuildSettings = await self.buildSettings(for: uri) + let clangBuildSettings = ClangBuildSettings(change: change, clangPath: self.clangPath) logAsync(level: clangBuildSettings == nil ? .warning : .debug) { _ in let settingsStr = clangBuildSettings == nil ? "nil" : clangBuildSettings!.compilerArgs.description return "settings for \(uri): \(settingsStr)" } + let changed = lock.withLock { () -> Bool in + let prevBuildSettings = self.buildSettingsByFile[uri] + guard clangBuildSettings != prevBuildSettings else { return false } + self.buildSettingsByFile[uri] = clangBuildSettings + return true + } + guard changed else { return } + // The compile command changed, send over the new one. // FIXME: what should we do if we no longer have valid build settings? if @@ -448,7 +454,7 @@ extension ClangLanguageServerShim { let note = DidChangeConfigurationNotification(settings: .clangd( ClangWorkspaceSettings( compilationDatabaseChanges: [pathString: compileCommand]))) - clangd.send(note) + forwardNotificationToClangdOnQueue(note) } } @@ -460,7 +466,7 @@ extension ClangLanguageServerShim { textDocument: VersionedTextDocumentIdentifier(uri, version: 0), contentChanges: [], forceRebuild: true) - clangd.send(note) + forwardNotificationToClangdOnQueue(note) } // MARK: - Text Document @@ -469,82 +475,88 @@ extension ClangLanguageServerShim { /// Returns true if the `ToolchainLanguageServer` will take ownership of the request. public func definition(_ req: Request) -> Bool { // We handle it to provide jump-to-header support for #import/#include. - self.forwardRequest(req, to: self.clangd) + forwardRequestToClangdOnQueue(req) return true } /// Returns true if the `ToolchainLanguageServer` will take ownership of the request. public func declaration(_ req: Request) -> Bool { // We handle it to provide jump-to-header support for #import/#include. - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) return true } func completion(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func hover(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func symbolInfo(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func documentSymbolHighlight(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func documentSymbol(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func documentColor(_ req: Request) { - if self.capabilities?.colorProvider?.isSupported == true { - forwardRequest(req, to: clangd) - } else { - req.reply(.success([])) + queue.async { + if self.capabilities?.colorProvider?.isSupported == true { + self.forwardRequestToClangdOnQueue(req) + } else { + req.reply(.success([])) + } } } func documentSemanticTokens(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func documentSemanticTokensDelta(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func documentSemanticTokensRange(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func colorPresentation(_ req: Request) { - if self.capabilities?.colorProvider?.isSupported == true { - forwardRequest(req, to: clangd) - } else { - req.reply(.success([])) + queue.async { + if self.capabilities?.colorProvider?.isSupported == true { + self.forwardRequestToClangdOnQueue(req) + } else { + req.reply(.success([])) + } } } func codeAction(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func inlayHint(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func documentDiagnostic(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } func foldingRange(_ req: Request) { - if self.capabilities?.foldingRangeProvider?.isSupported == true { - forwardRequest(req, to: clangd) - } else { - req.reply(.success(nil)) + queue.async { + if self.capabilities?.foldingRangeProvider?.isSupported == true { + self.forwardRequestToClangdOnQueue(req) + } else { + req.reply(.success(nil)) + } } } @@ -555,7 +567,7 @@ extension ClangLanguageServerShim { // MARK: - Other func executeCommand(_ req: Request) { - forwardRequest(req, to: clangd) + forwardRequestToClangdOnQueue(req) } } diff --git a/Sources/SourceKitLSP/Sequence+AsyncMap.swift b/Sources/SourceKitLSP/Sequence+AsyncMap.swift deleted file mode 100644 index 32fc1917..00000000 --- a/Sources/SourceKitLSP/Sequence+AsyncMap.swift +++ /dev/null @@ -1,43 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -extension Sequence { - /// Just like `Sequence.map` but allows an `async` transform function. - func asyncMap( - _ transform: (Element) async throws -> T - ) async rethrows -> [T] { - var result: [T] = [] - result.reserveCapacity(self.underestimatedCount) - - for element in self { - try await result.append(transform(element)) - } - - return result - } - - /// Just like `Sequence.compactMap` but allows an `async` transform function. - func asyncCompactMap( - _ transform: (Element) async throws -> T? - ) async rethrows -> [T] { - var result: [T] = [] - - for element in self { - if let transformed = try await transform(element) { - result.append(transformed) - } - } - - return result - } -} - diff --git a/Sources/SourceKitLSP/SourceKitServer.swift b/Sources/SourceKitLSP/SourceKitServer.swift index 28f62a42..cd842947 100644 --- a/Sources/SourceKitLSP/SourceKitServer.swift +++ b/Sources/SourceKitLSP/SourceKitServer.swift @@ -27,6 +27,10 @@ import var TSCBasic.localFileSystem public typealias URL = Foundation.URL +private struct WeakWorkspace { + weak var value: Workspace? +} + /// Exhaustive enumeration of all toolchain language servers known to SourceKit-LSP. enum LanguageServerType: Hashable { case clangd @@ -55,11 +59,11 @@ enum LanguageServerType: Hashable { } /// Keeps track of the state to send work done progress updates to the client -final actor WorkDoneProgressState { +final class WorkDoneProgressState { private enum State { /// No `WorkDoneProgress` has been created. case noProgress - /// We have sent the request to create a `WorkDoneProgress` but haven’t received a response yet. + /// We have sent the request to create a `WorkDoneProgress` but haven’t received a respose yet. case creating /// A `WorkDoneProgress` has been created. case created @@ -90,12 +94,15 @@ final actor WorkDoneProgressState { /// Start a new task, creating a new `WorkDoneProgress` if none is running right now. /// /// - Parameter server: The server that is used to create the `WorkDoneProgress` on the client + /// + /// - Important: Must be called on `server.queue`. func startProgress(server: SourceKitServer) { + dispatchPrecondition(condition: .onQueue(server.queue)) activeTasks += 1 if state == .noProgress { state = .creating // Discard the handle. We don't support cancellation of the creation of a work done progress. - _ = server.client.send(CreateWorkDoneProgressRequest(token: token), queue: server.clientCommunicationQueue) { result in + _ = server.client.send(CreateWorkDoneProgressRequest(token: token), queue: server.queue) { result in if result.success != nil { if self.activeTasks == 0 { // ActiveTasks might have been decreased while we created the `WorkDoneProgress` @@ -117,7 +124,10 @@ final actor WorkDoneProgressState { /// If this drops the active task count to 0, the work done progress is ended on the client. /// /// - Parameter server: The server that is used to send and update of the `WorkDoneProgress` to the client + /// + /// - Important: Must be called on `server.queue`. func endProgress(server: SourceKitServer) { + dispatchPrecondition(condition: .onQueue(server.queue)) assert(activeTasks > 0, "Unbalanced startProgress/endProgress calls") activeTasks -= 1 if state == .created && activeTasks == 0 { @@ -131,10 +141,11 @@ final actor WorkDoneProgressState { /// This is the client-facing language server implementation, providing indexing, multiple-toolchain /// and cross-language support. Requests may be dispatched to language-specific services or handled /// centrally, but this is transparent to the client. -public actor SourceKitServer { - // FIXME: (async) We can remove this if we migrate client.send to be async and it thus doesn't take a queue anymore. - /// The queue on which we communicate with the client. - public let clientCommunicationQueue: DispatchQueue = DispatchQueue(label: "language-server-queue", qos: .userInitiated) +public final class SourceKitServer { + /// The server's request queue. + /// + /// All incoming requests start on this queue, but should reply or move to another queue as soon as possible to avoid blocking. + public let queue: DispatchQueue = DispatchQueue(label: "language-server-queue", qos: .userInitiated) public struct RequestCancelKey: Hashable { public var client: ObjectIdentifier @@ -176,10 +187,16 @@ public actor SourceKitServer { /// Caches which workspace a document with the given URI should be opened in. /// Must only be accessed from `queue`. - private var uriToWorkspaceCache: [DocumentURI: WeakWorkspace] = [:] + private var uriToWorkspaceCache: [DocumentURI: WeakWorkspace] = [:] { + didSet { + dispatchPrecondition(condition: .onQueue(queue)) + } + } + /// Must only be accessed from `queue`. private var workspaces: [Workspace] = [] { didSet { + dispatchPrecondition(condition: .onQueue(queue)) uriToWorkspaceCache = [:] } } @@ -187,10 +204,14 @@ public actor SourceKitServer { /// **Public for testing** public var _workspaces: [Workspace] { get { - return self.workspaces + return queue.sync { + return self.workspaces + } } set { - self.workspaces = newValue + queue.sync { + self.workspaces = newValue + } } } @@ -200,6 +221,7 @@ public actor SourceKitServer { /// Creates a language server for the given client. public init(client: Connection, fileSystem: FileSystem = localFileSystem, options: Options, onExit: @escaping () -> Void = {}) { + self.fs = fileSystem self.toolchainRegistry = ToolchainRegistry.shared self.options = options @@ -208,7 +230,8 @@ public actor SourceKitServer { self.client = client } - public func workspaceForDocument(uri: DocumentURI) async -> Workspace? { + public func workspaceForDocument(uri: DocumentURI) -> Workspace? { + dispatchPrecondition(condition: .onQueue(queue)) if workspaces.count == 1 { // Special handling: If there is only one workspace, open all files in it. // This retains the behavior of SourceKit-LSP before it supported multiple workspaces. @@ -223,24 +246,24 @@ public actor SourceKitServer { // If there is a tie, use the workspace that occurred first in the list. var bestWorkspace: (workspace: Workspace?, fileHandlingCapability: FileHandlingCapability) = (nil, .unhandled) for workspace in workspaces { - let fileHandlingCapability = await workspace.buildSystemManager.fileHandlingCapability(for: uri) + let fileHandlingCapability = workspace.buildSystemManager.fileHandlingCapability(for: uri) if fileHandlingCapability > bestWorkspace.fileHandlingCapability { bestWorkspace = (workspace, fileHandlingCapability) } } - uriToWorkspaceCache[uri] = WeakWorkspace(bestWorkspace.workspace) + uriToWorkspaceCache[uri] = WeakWorkspace(value: bestWorkspace.workspace) return bestWorkspace.workspace } /// Execute `notificationHandler` once the document that it concerns is ready - /// and has the initial build settings. These build settings might still be + /// and has the intial build settings. These build settings might still be /// incomplete or fallback settings. private func withReadyDocument( for notification: Notification, - notificationHandler: @escaping (Notification, ToolchainLanguageServer) async -> Void - ) async { + notificationHandler: @escaping (Notification, ToolchainLanguageServer) -> Void + ) { let doc = notification.params.textDocument.uri - guard let workspace = await self.workspaceForDocument(uri: doc) else { + guard let workspace = self.workspaceForDocument(uri: doc) else { return } @@ -252,26 +275,26 @@ public actor SourceKitServer { // If the document is ready, we can handle it right now. guard !self.documentsReady.contains(doc) else { - await notificationHandler(notification, languageService) + notificationHandler(notification, languageService) return } // Not ready to handle it, we'll queue it and handle it later. self.documentToPendingQueue[doc, default: DocumentNotificationRequestQueue()].add(operation: { - await notificationHandler(notification, languageService) + notificationHandler(notification, languageService) }) } /// Execute `notificationHandler` once the document that it concerns is ready - /// and has the initial build settings. These build settings might still be + /// and has the intial build settings. These build settings might still be /// incomplete or fallback settings. private func withReadyDocument( for request: Request, - requestHandler: @escaping (Request, Workspace, ToolchainLanguageServer) async -> Void, + requestHandler: @escaping (Request, Workspace, ToolchainLanguageServer) -> Void, fallback: RequestType.Response - ) async { + ) { let doc = request.params.textDocument.uri - guard let workspace = await self.workspaceForDocument(uri: doc) else { + guard let workspace = self.workspaceForDocument(uri: doc) else { return request.reply(.failure(.workspaceNotOpen(doc))) } @@ -283,13 +306,13 @@ public actor SourceKitServer { // If the document is ready, we can handle it right now. guard !self.documentsReady.contains(doc) else { - await requestHandler(request, workspace, languageService) + requestHandler(request, workspace, languageService) return } // Not ready to handle it, we'll queue it and handle it later. self.documentToPendingQueue[doc, default: DocumentNotificationRequestQueue()].add(operation: { - await requestHandler(request, workspace, languageService) + requestHandler(request, workspace, languageService) }, cancellationHandler: { request.reply(fallback) }) @@ -303,7 +326,7 @@ public actor SourceKitServer { } // Unknown requests from a language server are passed on to the client. - let id = client.send(req.params, queue: clientCommunicationQueue) { result in + let id = client.send(req.params, queue: queue) { result in req.reply(result) } req.cancellationToken.addCancellationHandler { @@ -348,78 +371,59 @@ public actor SourceKitServer { } /// After the language service has crashed, send `DidOpenTextDocumentNotification`s to a newly instantiated language service for previously open documents. - func reopenDocuments(for languageService: ToolchainLanguageServer) async { - for documentUri in self.documentManager.openDocuments { - guard let workspace = await self.workspaceForDocument(uri: documentUri) else { - continue - } - guard workspace.documentService[documentUri] === languageService else { - continue - } - guard let snapshot = self.documentManager.latestSnapshot(documentUri) else { - // The document has been closed since we retrieved its URI. We don't care about it anymore. - continue - } + func reopenDocuments(for languageService: ToolchainLanguageServer) { + queue.async { + for documentUri in self.documentManager.openDocuments { + guard let workspace = self.workspaceForDocument(uri: documentUri) else { + continue + } + guard workspace.documentService[documentUri] === languageService else { + continue + } + guard let snapshot = self.documentManager.latestSnapshot(documentUri) else { + // The document has been closed since we retrieved its URI. We don't care about it anymore. + continue + } - // Close the document properly in the document manager and build system manager to start with a clean sheet when re-opening it. - let closeNotification = DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(documentUri)) - await self.closeDocument(closeNotification, workspace: workspace) + // Close the docuemnt properly in the document manager and build system manager to start with a clean sheet when re-opening it. + let closeNotification = DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(documentUri)) + self.closeDocument(closeNotification, workspace: workspace) - let textDocument = TextDocumentItem(uri: documentUri, - language: snapshot.document.language, - version: snapshot.version, - text: snapshot.text) - await self.openDocument(DidOpenTextDocumentNotification(textDocument: textDocument), workspace: workspace) - } - } + let textDocument = TextDocumentItem(uri: documentUri, + language: snapshot.document.language, + version: snapshot.version, + text: snapshot.text) + self.openDocument(DidOpenTextDocumentNotification(textDocument: textDocument), workspace: workspace) - /// If a language service of type `serverType` that can handle `workspace` has - /// already been started, return it, otherwise return `nil`. - private func existingLanguageService(_ serverType: LanguageServerType, workspace: Workspace) -> ToolchainLanguageServer? { - for languageService in languageServices[serverType, default: []] { - if languageService.canHandle(workspace: workspace) { - return languageService } } - return nil } func languageService( for toolchain: Toolchain, _ language: Language, in workspace: Workspace - ) async -> ToolchainLanguageServer? { + ) -> ToolchainLanguageServer? { guard let serverType = LanguageServerType(language: language) else { return nil } // Pick the first language service that can handle this workspace. - if let languageService = existingLanguageService(serverType, workspace: workspace) { - return languageService + for languageService in languageServices[serverType, default: []] { + if languageService.canHandle(workspace: workspace) { + return languageService + } } // Start a new service. - return await orLog("failed to start language service", level: .error) { - let service = try await SourceKitLSP.languageService( - for: toolchain, - serverType, - options: options, - client: self, - in: workspace, - reopenDocuments: { [weak self] toolchainLanguageServer in - await self?.reopenDocuments(for: toolchainLanguageServer) - }, - workspaceForDocument: { [weak self] document in - guard let self else { return nil } - return await self.workspaceForDocument(uri: document) - } - ) - - guard let service else { + return orLog("failed to start language service", level: .error) { + guard let service = try SourceKitLSP.languageService( + for: toolchain, serverType, options: options, client: self, in: workspace, reopenDocuments: { [weak self] in self?.reopenDocuments(for: $0) }) + else { return nil } let pid = Int(ProcessInfo.processInfo.processIdentifier) - let resp = try await service.initializeSync(InitializeRequest( + let resp = try service.initializeSync(InitializeRequest( processId: pid, rootPath: nil, rootURI: workspace.rootUri, @@ -445,17 +449,7 @@ public actor SourceKitServer { fatalError("non-incremental update not implemented") } - await service.clientInitialized(InitializedNotification()) - - if let concurrentlyInitializedService = existingLanguageService(serverType, workspace: workspace) { - // Since we 'await' above, another call to languageService might have - // happened concurrently, passed the `existingLanguageService` check at - // the top and started initializing another language service. - // If this race happened, just shut down our server and return the - // other one. - await service.shutdown() - return concurrentlyInitializedService - } + service.clientInitialized(InitializedNotification()) languageServices[serverType, default: []].append(service) return service @@ -463,30 +457,23 @@ public actor SourceKitServer { } /// **Public for testing purposes only** - public func _languageService(for uri: DocumentURI, _ language: Language, in workspace: Workspace) async -> ToolchainLanguageServer? { - return await languageService(for: uri, language, in: workspace) + public func _languageService(for uri: DocumentURI, _ language: Language, in workspace: Workspace) -> ToolchainLanguageServer? { + return languageService(for: uri, language, in: workspace) } - func languageService(for uri: DocumentURI, _ language: Language, in workspace: Workspace) async -> ToolchainLanguageServer? { + func languageService(for uri: DocumentURI, _ language: Language, in workspace: Workspace) -> ToolchainLanguageServer? { if let service = workspace.documentService[uri] { return service } guard let toolchain = toolchain(for: uri, language), - let service = await languageService(for: toolchain, language, in: workspace) + let service = languageService(for: toolchain, language, in: workspace) else { return nil } log("Using toolchain \(toolchain.displayName) (\(toolchain.identifier)) for \(uri)") - if let concurrentlySetService = workspace.documentService[uri] { - // Since we await the construction of `service`, another call to this - // function might have happened and raced us, setting - // `workspace.documentServices[uri]`. If this is the case, return the - // existing value and discard the service that we just retrieved. - return concurrentlySetService - } workspace.documentService[uri] = service return service } @@ -495,121 +482,121 @@ public actor SourceKitServer { // MARK: - MessageHandler extension SourceKitServer: MessageHandler { - public func handle(_ params: some NotificationType, from clientID: ObjectIdentifier) async { - let notification = Notification(params, clientID: clientID) - self._logNotification(notification) + public func handle(_ params: N, from clientID: ObjectIdentifier) { + queue.async { - switch notification { - case let notification as Notification: - self.clientInitialized(notification) - case let notification as Notification: - self.cancelRequest(notification) - case let notification as Notification: - await self.exit(notification) - case let notification as Notification: - await self.openDocument(notification) - case let notification as Notification: - await self.closeDocument(notification) - case let notification as Notification: - await self.changeDocument(notification) - case let notification as Notification: - await self.didChangeWorkspaceFolders(notification) - case let notification as Notification: - await self.didChangeWatchedFiles(notification) - case let notification as Notification: - await self.withReadyDocument(for: notification, notificationHandler: self.willSaveDocument) - case let notification as Notification: - await self.withReadyDocument(for: notification, notificationHandler: self.didSaveDocument) - default: - self._handleUnknown(notification) + let notification = Notification(params, clientID: clientID) + self._logNotification(notification) + + switch notification { + case let notification as Notification: + self.clientInitialized(notification) + case let notification as Notification: + self.cancelRequest(notification) + case let notification as Notification: + self.exit(notification) + case let notification as Notification: + self.openDocument(notification) + case let notification as Notification: + self.closeDocument(notification) + case let notification as Notification: + self.changeDocument(notification) + case let notification as Notification: + self.didChangeWorkspaceFolders(notification) + case let notification as Notification: + self.didChangeWatchedFiles(notification) + case let notification as Notification: + self.withReadyDocument(for: notification, notificationHandler: self.willSaveDocument) + case let notification as Notification: + self.withReadyDocument(for: notification, notificationHandler: self.didSaveDocument) + default: + self._handleUnknown(notification) + } } } - public func handle(_ params: R, id: RequestID, from clientID: ObjectIdentifier, reply: @escaping (LSPResult) -> Void) async { - let cancellationToken = CancellationToken() - let key = RequestCancelKey(client: clientID, request: id) + public func handle(_ params: R, id: RequestID, from clientID: ObjectIdentifier, reply: @escaping (LSPResult) -> Void) { + queue.async { - self.requestCancellation[key] = cancellationToken + let cancellationToken = CancellationToken() + let key = RequestCancelKey(client: clientID, request: id) - let request = Request(params, id: id, clientID: clientID, cancellation: cancellationToken, reply: { [weak self] result in - if let self { - Task { - await self.stopTrackingCancellationKey(key) + self.requestCancellation[key] = cancellationToken + + let request = Request(params, id: id, clientID: clientID, cancellation: cancellationToken, reply: { [weak self] result in + self?.queue.async { + self?.requestCancellation[key] = nil } - } - reply(result) - if let self { - Task { - await self._logResponse(result, id: id, method: R.method) - } - } - }) + reply(result) + self?._logResponse(result, id: id, method: R.method) + }) - self._logRequest(request) + self._logRequest(request) - switch request { - case let request as Request: - await self.initialize(request) - case let request as Request: - await self.shutdown(request) - case let request as Request: - self.workspaceSymbols(request) - case let request as Request: - self.pollIndex(request) - case let request as Request: - await self.executeCommand(request) - case let request as Request: - await self.incomingCalls(request) - case let request as Request: - await self.outgoingCalls(request) - case let request as Request: - await self.supertypes(request) - case let request as Request: - await self.subtypes(request) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.completion, fallback: CompletionList(isIncomplete: false, items: [])) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.hover, fallback: nil) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.openInterface, fallback: nil) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.declaration, fallback: nil) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.definition, fallback: .locations([])) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.references, fallback: []) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.implementation, fallback: .locations([])) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.prepareCallHierarchy, fallback: []) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.prepareTypeHierarchy, fallback: []) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.symbolInfo, fallback: []) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.documentSymbolHighlight, fallback: nil) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.foldingRange, fallback: nil) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.documentSymbol, fallback: nil) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.documentColor, fallback: []) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.documentSemanticTokens, fallback: nil) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.documentSemanticTokensDelta, fallback: nil) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.documentSemanticTokensRange, fallback: nil) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.colorPresentation, fallback: []) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.codeAction, fallback: nil) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.inlayHint, fallback: []) - case let request as Request: - await self.withReadyDocument(for: request, requestHandler: self.documentDiagnostic, fallback: .full(.init(items: []))) - default: - self._handleUnknown(request) + switch request { + case let request as Request: + self.initialize(request) + case let request as Request: + self.shutdown(request) + case let request as Request: + self.workspaceSymbols(request) + case let request as Request: + self.pollIndex(request) + case let request as Request: + self.executeCommand(request) + case let request as Request: + self.incomingCalls(request) + case let request as Request: + self.outgoingCalls(request) + case let request as Request: + self.supertypes(request) + case let request as Request: + self.subtypes(request) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.completion, fallback: CompletionList(isIncomplete: false, items: [])) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.hover, fallback: nil) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.openInterface, fallback: nil) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.declaration, fallback: nil) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.definition, fallback: .locations([])) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.references, fallback: []) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.implementation, fallback: .locations([])) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.prepareCallHierarchy, fallback: []) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.prepareTypeHierarchy, fallback: []) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.symbolInfo, fallback: []) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.documentSymbolHighlight, fallback: nil) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.foldingRange, fallback: nil) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.documentSymbol, fallback: nil) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.documentColor, fallback: []) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.documentSemanticTokens, fallback: nil) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.documentSemanticTokensDelta, fallback: nil) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.documentSemanticTokensRange, fallback: nil) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.colorPresentation, fallback: []) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.codeAction, fallback: nil) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.inlayHint, fallback: []) + case let request as Request: + self.withReadyDocument(for: request, requestHandler: self.documentDiagnostic, fallback: .full(.init(items: []))) + default: + self._handleUnknown(request) + } } } @@ -648,8 +635,7 @@ extension SourceKitServer: MessageHandler { // MARK: - Build System Delegate extension SourceKitServer: BuildSystemDelegate { - // FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified - public nonisolated func buildTargetsChanged(_ changes: [BuildTargetEvent]) { + public func buildTargetsChanged(_ changes: [BuildTargetEvent]) { // TODO: do something with these changes once build target support is in place } @@ -664,117 +650,92 @@ extension SourceKitServer: BuildSystemDelegate { return documentManager.openDocuments.intersection(changes) } - // FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified - /// Non-async variant that executes `fileBuildSettingsChangedImpl` in a new task. - public nonisolated func fileBuildSettingsChanged(_ changedFiles: [DocumentURI: FileBuildSettingsChange]) { - Task { - await self.fileBuildSettingsChangedImpl(changedFiles) - } - } - /// Handle a build settings change notification from the `BuildSystem`. /// This has two primary cases: /// - Initial settings reported for a given file, now we can fully open it /// - Changed settings for an already open file - public func fileBuildSettingsChangedImpl( + public func fileBuildSettingsChanged( _ changedFiles: [DocumentURI: FileBuildSettingsChange] - ) async { - for (uri, change) in changedFiles { - // Non-ready documents should be considered open even though we haven't - // opened it with the language service yet. - guard self.documentManager.openDocuments.contains(uri) else { continue } + ) { + queue.async { + for (uri, change) in changedFiles { + // Non-ready documents should be considered open even though we haven't + // opened it with the language service yet. + guard self.documentManager.openDocuments.contains(uri) else { continue } - guard let workspace = await self.workspaceForDocument(uri: uri) else { - continue - } - guard self.documentsReady.contains(uri) else { - // Case 1: initial settings for a given file. Now we can process our backlog. - log("Initial build settings received for opened file \(uri)") + guard let workspace = self.workspaceForDocument(uri: uri) else { + continue + } + guard self.documentsReady.contains(uri) else { + // Case 1: initial settings for a given file. Now we can process our backlog. + log("Initial build settings received for opened file \(uri)") - guard let service = workspace.documentService[uri] else { - // Unexpected: we should have an existing language service if we've registered for - // change notifications for an opened but non-ready document. - log("No language service for build settings change to non-ready file \(uri)", - level: .error) + guard let service = workspace.documentService[uri] else { + // Unexpected: we should have an existing language service if we've registered for + // change notifications for an opened but non-ready document. + log("No language service for build settings change to non-ready file \(uri)", + level: .error) - // We're in an odd state, cancel pending requests if we have any. - self.documentToPendingQueue[uri]?.cancelAll() + // We're in an odd state, cancel pending requests if we have any. + self.documentToPendingQueue[uri]?.cancelAll() + self.documentToPendingQueue[uri] = nil + continue + } + + // Notify the language server so it can apply the proper arguments. + service.documentUpdatedBuildSettings(uri, change: change) + + // Catch up on any queued notifications and requests. + self.documentToPendingQueue[uri]?.handleAll() self.documentToPendingQueue[uri] = nil + self.documentsReady.insert(uri) continue } - // Notify the language server so it can apply the proper arguments. - await service.documentUpdatedBuildSettings(uri, change: change) - - // Catch up on any queued notifications and requests. - while !(documentToPendingQueue[uri]?.queue.isEmpty ?? true) { - // We need to run this loop until convergence since new closures can - // get added to `documentToPendingQueue` while we are awaiting the - // result of a `task.operation()`. - let pendingQueue = documentToPendingQueue[uri]?.queue ?? [] - documentToPendingQueue[uri]?.queue = [] - for task in pendingQueue { - await task.operation() - } + // Case 2: changed settings for an already open file. + log("Build settings changed for opened file \(uri)") + if let service = workspace.documentService[uri] { + service.documentUpdatedBuildSettings(uri, change: change) } - self.documentToPendingQueue[uri] = nil - self.documentsReady.insert(uri) - continue } - - // Case 2: changed settings for an already open file. - log("Build settings changed for opened file \(uri)") - if let service = workspace.documentService[uri] { - await service.documentUpdatedBuildSettings(uri, change: change) - } - } - } - - // FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified - public nonisolated func filesDependenciesUpdated(_ changedFiles: Set) { - Task { - await filesDependenciesUpdatedImpl(changedFiles) } } /// Handle a dependencies updated notification from the `BuildSystem`. /// We inform the respective language services as long as the given file is open /// (not queued for opening). - public func filesDependenciesUpdatedImpl(_ changedFiles: Set) async { - // Split the changedFiles into the workspaces they belong to. - // Then invoke affectedOpenDocumentsForChangeSet for each workspace with its affected files. - let changedFilesAndWorkspace = await changedFiles.asyncMap { - return (uri: $0, workspace: await self.workspaceForDocument(uri: $0)) - } - for workspace in self.workspaces { - let changedFilesForWorkspace = Set(changedFilesAndWorkspace.filter({ $0.workspace === workspace }).map(\.uri)) - if changedFilesForWorkspace.isEmpty { - continue - } - for uri in self.affectedOpenDocumentsForChangeSet(changedFilesForWorkspace, self.documentManager) { - // Make sure the document is ready - otherwise the language service won't - // know about the document yet. - guard self.documentsReady.contains(uri) else { + public func filesDependenciesUpdated(_ changedFiles: Set) { + queue.async { + // Split the changedFiles into the workspaces they belong to. + // Then invoke affectedOpenDocumentsForChangeSet for each workspace with its affected files. + let changedFilesAndWorkspace = changedFiles.map({ + return (uri: $0, workspace: self.workspaceForDocument(uri: $0)) + }) + for workspace in self.workspaces { + let changedFilesForWorkspace = Set(changedFilesAndWorkspace.filter({ $0.workspace === workspace }).map(\.uri)) + if changedFilesForWorkspace.isEmpty { continue } - log("Dependencies updated for opened file \(uri)") - if let service = workspace.documentService[uri] { - await service.documentDependenciesUpdated(uri) + for uri in self.affectedOpenDocumentsForChangeSet(changedFilesForWorkspace, self.documentManager) { + // Make sure the document is ready - otherwise the language service won't + // know about the document yet. + guard self.documentsReady.contains(uri) else { + continue + } + log("Dependencies updated for opened file \(uri)") + if let service = workspace.documentService[uri] { + service.documentDependenciesUpdated(uri) + } } } } } - // FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified - public nonisolated func fileHandlingCapabilityChanged() { - Task { - await fileHandlingCapabilityChangedImpl() + public func fileHandlingCapabilityChanged() { + queue.async { + self.uriToWorkspaceCache = [:] } } - - public func fileHandlingCapabilityChangedImpl() { - self.uriToWorkspaceCache = [:] - } } // MARK: - Request and notification handling @@ -784,12 +745,12 @@ extension SourceKitServer { // MARK: - General /// Creates a workspace at the given `uri`. - private func createWorkspace(uri: DocumentURI) async -> Workspace? { + private func workspace(uri: DocumentURI) -> Workspace? { guard let capabilityRegistry = capabilityRegistry else { log("Cannot open workspace before server is initialized") return nil } - return try? await Workspace( + return try? Workspace( documentManager: self.documentManager, rootUri: uri, capabilityRegistry: capabilityRegistry, @@ -801,24 +762,19 @@ extension SourceKitServer { // Client doesn’t support work done progress return } - // FIXME: (async) This can cause out-of-order notifications to be sent to the editor - // if the scheduled tasks change order. - // Make `reloadPackageStatusCallback` async and shift the responsibility for - // guaranteeing in-order calls to `reloadPackageStatusCallback` to - // `SwiftPMWorkspace.reloadPackage` once that method is async. - Task { + self.queue.async { switch status { case .start: - await self.packageLoadingWorkDoneProgress.startProgress(server: self) + self.packageLoadingWorkDoneProgress.startProgress(server: self) case .end: - await self.packageLoadingWorkDoneProgress.endProgress(server: self) + self.packageLoadingWorkDoneProgress.endProgress(server: self) } } } ) } - func initialize(_ req: Request) async { + func initialize(_ req: Request) { if case .dictionary(let options) = req.params.initializationOptions { if case .bool(let listenToUnitEvents) = options["listenToUnitEvents"] { self.options.indexOptions.listenToUnitEvents = listenToUnitEvents @@ -842,44 +798,41 @@ extension SourceKitServer { capabilityRegistry = CapabilityRegistry(clientCapabilities: req.params.capabilities) - if let workspaceFolders = req.params.workspaceFolders { - self.workspaces += await workspaceFolders.asyncCompactMap { await self.createWorkspace(uri: $0.uri) } - } else if let uri = req.params.rootURI { - if let workspace = await self.createWorkspace(uri: uri) { - self.workspaces.append(workspace) + // Any messages sent before initialize returns are expected to fail, so this will run before + // the first "supported" request. Run asynchronously to hide the latency of setting up the + // build system and index. + queue.async { + if let workspaceFolders = req.params.workspaceFolders { + self.workspaces.append(contentsOf: workspaceFolders.compactMap({ self.workspace(uri: $0.uri) })) + } else if let uri = req.params.rootURI { + if let workspace = self.workspace(uri: uri) { + self.workspaces.append(workspace) + } + } else if let path = req.params.rootPath { + if let workspace = self.workspace(uri: DocumentURI(URL(fileURLWithPath: path))) { + self.workspaces.append(workspace) + } } - } else if let path = req.params.rootPath { - if let workspace = await self.createWorkspace(uri: DocumentURI(URL(fileURLWithPath: path))) { - self.workspaces.append(workspace) - } - } - if self.workspaces.isEmpty { - log("no workspace found", level: .warning) - - let workspace = await Workspace( - documentManager: self.documentManager, - rootUri: req.params.rootURI, - capabilityRegistry: self.capabilityRegistry!, - toolchainRegistry: self.toolchainRegistry, - buildSetup: self.options.buildSetup, - underlyingBuildSystem: nil, - index: nil, - indexDelegate: nil - ) - - // Another workspace might have been added while we awaited the - // construction of the workspace above. If that race happened, just - // discard the workspace we created here since `workspaces` now isn't - // empty anymore. if self.workspaces.isEmpty { + log("no workspace found", level: .warning) + + let workspace = Workspace( + documentManager: self.documentManager, + rootUri: req.params.rootURI, + capabilityRegistry: self.capabilityRegistry!, + toolchainRegistry: self.toolchainRegistry, + buildSetup: self.options.buildSetup, + underlyingBuildSystem: nil, + index: nil, + indexDelegate: nil) self.workspaces.append(workspace) } - } - assert(!self.workspaces.isEmpty) - for workspace in self.workspaces { - await workspace.buildSystemManager.setDelegate(self) + assert(!self.workspaces.isEmpty) + for workspace in self.workspaces { + workspace.buildSystemManager.delegate = self + } } req.reply(InitializeResult(capabilities: @@ -998,7 +951,7 @@ extension SourceKitServer { _ registry: CapabilityRegistry ) { let req = RegisterCapabilityRequest(registrations: [registration]) - let _ = client.send(req, queue: clientCommunicationQueue) { result in + let _ = client.send(req, queue: queue) { result in if let error = result.failure { log("Failed to dynamically register for \(registration.method): \(error)", level: .error) registry.remove(registration: registration) @@ -1015,62 +968,61 @@ extension SourceKitServer { requestCancellation[key]?.cancel() } - /// Stop keeping track of the cancellation handler for the given cancellation key. - func stopTrackingCancellationKey(_ key: RequestCancelKey) { - requestCancellation[key] = nil - } - /// The server is about to exit, and the server should flush any buffered state. /// /// The server shall not be used to handle more requests (other than possibly /// `shutdown` and `exit`) and should attempt to flush any buffered state /// immediately, such as sending index changes to disk. - public func prepareForExit() async { + public func prepareForExit() { // Note: this method should be safe to call multiple times, since we want to // be resilient against multiple possible shutdown sequences, including // pipe failure. - // Theoretically, new workspaces could be added while we are awaiting inside - // the loop. But since we are currently exiting, it doesn't make sense for - // the client to open new workspaces. + // Close the index, which will flush to disk. + self.queue.sync { + self._prepareForExit() + } + } + + func _prepareForExit() { + dispatchPrecondition(condition: .onQueue(queue)) + // Note: this method should be safe to call multiple times, since we want to + // be resilient against multiple possible shutdown sequences, including + // pipe failure. + + // Close the index, which will flush to disk. for workspace in self.workspaces { - await workspace.buildSystemManager.setMainFilesProvider(nil) - // Close the index, which will flush to disk. + workspace.buildSystemManager.mainFilesProvider = nil workspace.index = nil // Break retain cycle with the BSM. - await workspace.buildSystemManager.setDelegate(nil) + workspace.buildSystemManager.delegate = nil } } - func shutdown(_ request: Request) async { - await prepareForExit() - - await withTaskGroup(of: Void.self) { taskGroup in - for service in languageServices.values.flatMap({ $0 }) { - taskGroup.addTask { - await service.shutdown() - } + func shutdown(_ request: Request) { + _prepareForExit() + let shutdownGroup = DispatchGroup() + for service in languageServices.values.flatMap({ $0 }) { + shutdownGroup.enter() + service.shutdown() { + shutdownGroup.leave() } } - - // We have a semantic guarantee that no request or notification should be - // sent to an LSP server after the shutdown request. Thus, there's no chance - // that a new language service has been started during the above 'await' - // call. languageServices = [:] - // Wait for all services to shut down before sending the shutdown response. // Otherwise we might terminate sourcekit-lsp while it still has open // connections to the toolchain servers, which could send messages to // sourcekit-lsp while it is being deallocated, causing crashes. - request.reply(VoidResponse()) + shutdownGroup.notify(queue: self.queue) { + request.reply(VoidResponse()) + } } - func exit(_ notification: Notification) async { + func exit(_ notification: Notification) { // Should have been called in shutdown, but allow misbehaving clients. - await prepareForExit() + _prepareForExit() // Call onExit only once, and hop off queue to allow the handler to call us back. let onExit = self.onExit @@ -1082,16 +1034,16 @@ extension SourceKitServer { // MARK: - Text synchronization - func openDocument(_ note: Notification) async { + func openDocument(_ note: Notification) { let uri = note.params.textDocument.uri - guard let workspace = await workspaceForDocument(uri: uri) else { + guard let workspace = workspaceForDocument(uri: uri) else { log("received open notification for file '\(uri)' without a corresponding workspace, ignoring...", level: .error) return } - await openDocument(note.params, workspace: workspace) + openDocument(note.params, workspace: workspace) } - private func openDocument(_ note: DidOpenTextDocumentNotification, workspace: Workspace) async { + private func openDocument(_ note: DidOpenTextDocumentNotification, workspace: Workspace) { // Immediately open the document even if the build system isn't ready. This is important since // we check that the document is open when we receive messages from the build system. documentManager.open(note) @@ -1101,46 +1053,46 @@ extension SourceKitServer { let language = textDocument.language // If we can't create a service, this document is unsupported and we can bail here. - guard let service = await languageService(for: uri, language, in: workspace) else { + guard let service = languageService(for: uri, language, in: workspace) else { return } - await workspace.buildSystemManager.registerForChangeNotifications(for: uri, language: language) + workspace.buildSystemManager.registerForChangeNotifications(for: uri, language: language) // If the document is ready, we can immediately send the notification. guard !documentsReady.contains(uri) else { - await service.openDocument(note) + service.openDocument(note) return } // Need to queue the open call so we can handle it when ready. self.documentToPendingQueue[uri, default: DocumentNotificationRequestQueue()].add(operation: { - await service.openDocument(note) + service.openDocument(note) }) } - func closeDocument(_ note: Notification) async { + func closeDocument(_ note: Notification) { let uri = note.params.textDocument.uri - guard let workspace = await workspaceForDocument(uri: uri) else { + guard let workspace = workspaceForDocument(uri: uri) else { log("received close notification for file '\(uri)' without a corresponding workspace, ignoring...", level: .error) return } - await self.closeDocument(note.params, workspace: workspace) + self.closeDocument(note.params, workspace: workspace) } - func closeDocument(_ note: DidCloseTextDocumentNotification, workspace: Workspace) async { + func closeDocument(_ note: DidCloseTextDocumentNotification, workspace: Workspace) { // Immediately close the document. We need to be sure to clear our pending work queue in case // the build system still isn't ready. documentManager.close(note) let uri = note.textDocument.uri - await workspace.buildSystemManager.unregisterForChangeNotifications(for: uri) + workspace.buildSystemManager.unregisterForChangeNotifications(for: uri) // If the document is ready, we can close it now. guard !documentsReady.contains(uri) else { self.documentsReady.remove(uri) - await workspace.documentService[uri]?.closeDocument(note) + workspace.documentService[uri]?.closeDocument(note) return } @@ -1150,10 +1102,10 @@ extension SourceKitServer { self.documentToPendingQueue[uri] = nil } - func changeDocument(_ note: Notification) async { + func changeDocument(_ note: Notification) { let uri = note.params.textDocument.uri - guard let workspace = await workspaceForDocument(uri: uri) else { + guard let workspace = workspaceForDocument(uri: uri) else { log("received change notification for file '\(uri)' without a corresponding workspace, ignoring...", level: .error) return } @@ -1161,45 +1113,35 @@ extension SourceKitServer { // If the document is ready, we can handle the change right now. guard !documentsReady.contains(uri) else { documentManager.edit(note.params) - await workspace.documentService[uri]?.changeDocument(note.params) + workspace.documentService[uri]?.changeDocument(note.params) return } // Need to queue the change call so we can handle it when ready. self.documentToPendingQueue[uri, default: DocumentNotificationRequestQueue()].add(operation: { self.documentManager.edit(note.params) - await workspace.documentService[uri]?.changeDocument(note.params) + workspace.documentService[uri]?.changeDocument(note.params) }) } func willSaveDocument( _ note: Notification, languageService: ToolchainLanguageServer - ) async { - await languageService.willSaveDocument(note.params) + ) { + languageService.willSaveDocument(note.params) } func didSaveDocument( _ note: Notification, languageService: ToolchainLanguageServer - ) async { - await languageService.didSaveDocument(note.params) + ) { + languageService.didSaveDocument(note.params) } - func didChangeWorkspaceFolders(_ note: Notification) async { - // There is a theoretical race condition here: While we await in this function, - // the open documents or workspaces could have changed. Because of this, - // we might close a document in a workspace that is no longer responsible - // for it. - // In practice, it is fine: sourcekit-lsp will not handle any new messages - // while we are executing this function and thus there's no risk of - // documents or workspaces changing. To hit the race condition, you need - // to invoke the API of `SourceKitServer` directly and open documents - // while this function is executing. Even in such an API use case, hitting - // that race condition seems very unlikely. + func didChangeWorkspaceFolders(_ note: Notification) { var preChangeWorkspaces: [DocumentURI: Workspace] = [:] for docUri in self.documentManager.openDocuments { - preChangeWorkspaces[docUri] = await self.workspaceForDocument(uri: docUri) + preChangeWorkspaces[docUri] = self.workspaceForDocument(uri: docUri) } if let removed = note.params.event.removed { self.workspaces.removeAll { workspace in @@ -1209,9 +1151,9 @@ extension SourceKitServer { } } if let added = note.params.event.added { - let newWorkspaces = await added.asyncCompactMap { await self.createWorkspace(uri: $0.uri) } + let newWorkspaces = added.compactMap({ self.workspace(uri: $0.uri) }) for workspace in newWorkspaces { - await workspace.buildSystemManager.setDelegate(self) + workspace.buildSystemManager.delegate = self } self.workspaces.append(contentsOf: newWorkspaces) } @@ -1220,18 +1162,18 @@ extension SourceKitServer { // the old workspace and open it in the new workspace. for docUri in self.documentManager.openDocuments { let oldWorkspace = preChangeWorkspaces[docUri] - let newWorkspace = await self.workspaceForDocument(uri: docUri) + let newWorkspace = self.workspaceForDocument(uri: docUri) if newWorkspace !== oldWorkspace { guard let snapshot = documentManager.latestSnapshot(docUri) else { continue } if let oldWorkspace = oldWorkspace { - await self.closeDocument(DidCloseTextDocumentNotification( + self.closeDocument(DidCloseTextDocumentNotification( textDocument: TextDocumentIdentifier(docUri) ), workspace: oldWorkspace) } if let newWorkspace = newWorkspace { - await self.openDocument(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( + self.openDocument(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( uri: docUri, language: snapshot.document.language, version: snapshot.version, @@ -1242,14 +1184,15 @@ extension SourceKitServer { } } - func didChangeWatchedFiles(_ note: Notification) async { + func didChangeWatchedFiles(_ note: Notification) { + dispatchPrecondition(condition: .onQueue(queue)) // We can't make any assumptions about which file changes a particular build // system is interested in. Just because it doesn't have build settings for // a file doesn't mean a file can't affect the build system's build settings // (e.g. Package.swift doesn't have build settings but affects build // settings). Inform the build system about all file changes. for workspace in workspaces { - await workspace.buildSystemManager.filesDidChange(note.params.changes) + workspace.buildSystemManager.filesDidChange(note.params.changes) } } @@ -1259,29 +1202,30 @@ extension SourceKitServer { _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.completion(req) + ) { + languageService.completion(req) } func hover( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.hover(req) + ) { + languageService.hover(req) } func openInterface( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.openInterface(req) + ) { + languageService.openInterface(req) } /// Find all symbols in the workspace that include a string in their name. /// - returns: An array of SymbolOccurrences that match the string. func findWorkspaceSymbols(matching: String) -> [SymbolOccurrence] { + dispatchPrecondition(condition: .onQueue(queue)) // Ignore short queries since they are: // - noisy and slow, since they can match many symbols // - normally unintentional, triggered when the user types slowly or if the editor doesn't @@ -1344,80 +1288,80 @@ extension SourceKitServer { _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.symbolInfo(req) + ) { + languageService.symbolInfo(req) } func documentSymbolHighlight( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.documentSymbolHighlight(req) + ) { + languageService.documentSymbolHighlight(req) } func foldingRange( _ req: Request, workspace: Workspace, - languageService: ToolchainLanguageServer) async { - await languageService.foldingRange(req) + languageService: ToolchainLanguageServer) { + languageService.foldingRange(req) } func documentSymbol( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.documentSymbol(req) + ) { + languageService.documentSymbol(req) } func documentColor( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.documentColor(req) + ) { + languageService.documentColor(req) } func documentSemanticTokens( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.documentSemanticTokens(req) + ) { + languageService.documentSemanticTokens(req) } func documentSemanticTokensDelta( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.documentSemanticTokensDelta(req) + ) { + languageService.documentSemanticTokensDelta(req) } func documentSemanticTokensRange( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.documentSemanticTokensRange(req) + ) { + languageService.documentSemanticTokensRange(req) } func colorPresentation( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.colorPresentation(req) + ) { + languageService.colorPresentation(req) } - func executeCommand(_ req: Request) async { + func executeCommand(_ req: Request) { guard let uri = req.params.textDocument?.uri else { log("attempted to perform executeCommand request without an url!", level: .error) req.reply(nil) return } - guard let workspace = await workspaceForDocument(uri: uri) else { + guard let workspace = workspaceForDocument(uri: uri) else { req.reply(.failure(.workspaceNotOpen(uri))) return } @@ -1428,51 +1372,42 @@ extension SourceKitServer { // If the document isn't yet ready, queue the request. guard self.documentsReady.contains(uri) else { - let operation = { [weak self] in + self.documentToPendingQueue[uri, default: DocumentNotificationRequestQueue()].add(operation: { + [weak self] in guard let self = self else { return } - // FIXME: (async) This might cause out-of order requests if tasks of the - // same `documentToPendingQueue` are executed out-of-order. To fix this, we should - // always wait for build settings before handling a request and remove - // documentToPendingQueue. - Task { - await self.fowardExecuteCommand(req, languageService: languageService) - } - } - let cancellationHandler = { + self.fowardExecuteCommand(req, languageService: languageService) + }, cancellationHandler: { req.reply(nil) - } - - self.documentToPendingQueue[uri, default: DocumentNotificationRequestQueue()] - .add(operation: operation, cancellationHandler: cancellationHandler) + }) return } - await self.fowardExecuteCommand(req, languageService: languageService) + self.fowardExecuteCommand(req, languageService: languageService) } func fowardExecuteCommand( _ req: Request, languageService: ToolchainLanguageServer - ) async { + ) { let params = req.params let executeCommand = ExecuteCommandRequest(command: params.command, arguments: params.argumentsWithoutSourceKitMetadata) - let callback = { (result: Result) in + let callback = callbackOnQueue(self.queue) { (result: Result) in req.reply(result) } let request = Request(executeCommand, id: req.id, clientID: ObjectIdentifier(self), cancellation: req.cancellationToken, reply: callback) - await languageService.executeCommand(request) + languageService.executeCommand(request) } func codeAction( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { + ) { let codeAction = CodeActionRequest(range: req.params.range, context: req.params.context, textDocument: req.params.textDocument) - let callback = { (result: Result) in + let callback = callbackOnQueue(self.queue) { (result: Result) in switch result { case .success(let reply): req.reply(req.params.injectMetadata(toResponse: reply)) @@ -1482,23 +1417,23 @@ extension SourceKitServer { } let request = Request(codeAction, id: req.id, clientID: ObjectIdentifier(self), cancellation: req.cancellationToken, reply: callback) - await languageService.codeAction(request) + languageService.codeAction(request) } func inlayHint( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.inlayHint(req) + ) { + languageService.inlayHint(req) } func documentDiagnostic( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - await languageService.documentDiagnostic(req) + ) { + languageService.documentDiagnostic(req) } /// Converts a location from the symbol index to an LSP location. @@ -1567,8 +1502,8 @@ extension SourceKitServer { _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { - guard await languageService.declaration(req) else { + ) { + guard languageService.declaration(req) else { return req.reply(.locations([])) } } @@ -1577,59 +1512,58 @@ extension SourceKitServer { _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { + ) { let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position) - let index = await self.workspaceForDocument(uri: req.params.textDocument.uri)?.index - let callback = { (result: LSPResult) -> Void in - Task { - // If this symbol is a module then generate a textual interface - if case .success(let symbols) = result, let symbol = symbols.first, symbol.kind == .module, let name = symbol.name { - await self.respondWithInterface(req, moduleName: name, symbolUSR: nil, languageService: languageService) + let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index + let callback = callbackOnQueue(self.queue) { (result: LSPResult) in + + // If this symbol is a module then generate a textual interface + if case .success(let symbols) = result, let symbol = symbols.first, symbol.kind == .module, let name = symbol.name { + self.respondWithInterface(req, moduleName: name, symbolUSR: nil, languageService: languageService) + return + } + + let extractedResult = self.extractIndexedOccurrences(result: result, index: index, useLocalFallback: true) { (usr, index) in + log("performing indexed jump-to-def with usr \(usr)") + var occurs = index.occurrences(ofUSR: usr, roles: [.definition]) + if occurs.isEmpty { + occurs = index.occurrences(ofUSR: usr, roles: [.declaration]) + } + return occurs + } + + switch extractedResult { + case .success(let resolved): + // if first resolved location is in `.swiftinterface` file. Use moduleName to return + // textual interface + if let firstResolved = resolved.first, + let moduleName = firstResolved.occurrence?.location.moduleName, + firstResolved.location.uri.fileURL?.pathExtension == "swiftinterface" { + self.respondWithInterface( + req, + moduleName: moduleName, + symbolUSR: firstResolved.occurrence?.symbol.usr, + languageService: languageService + ) return } - - let extractedResult = self.extractIndexedOccurrences(result: result, index: index, useLocalFallback: true) { (usr, index) in - log("performing indexed jump-to-def with usr \(usr)") - var occurs = index.occurrences(ofUSR: usr, roles: [.definition]) - if occurs.isEmpty { - occurs = index.occurrences(ofUSR: usr, roles: [.declaration]) - } - return occurs - } - - switch extractedResult { - case .success(let resolved): - // if first resolved location is in `.swiftinterface` file. Use moduleName to return - // textual interface - if let firstResolved = resolved.first, - let moduleName = firstResolved.occurrence?.location.moduleName, - firstResolved.location.uri.fileURL?.pathExtension == "swiftinterface" { - await self.respondWithInterface( - req, - moduleName: moduleName, - symbolUSR: firstResolved.occurrence?.symbol.usr, - languageService: languageService - ) - return - } - let locs = resolved.map(\.location) - // If we're unable to handle the definition request using our index, see if the - // language service can handle it (e.g. clangd can provide AST based definitions). - guard locs.isEmpty else { - req.reply(.locations(locs)) - return - } - let handled = await languageService.definition(req) - guard !handled else { return } - req.reply(.locations([])) - case .failure(let error): - req.reply(.failure(error)) + let locs = resolved.map(\.location) + // If we're unable to handle the definition request using our index, see if the + // language service can handle it (e.g. clangd can provide AST based definitions). + guard locs.isEmpty else { + req.reply(.locations(locs)) + return } + let handled = languageService.definition(req) + guard !handled else { return } + req.reply(.locations([])) + case .failure(let error): + req.reply(.failure(error)) } } let request = Request(symbolInfo, id: req.id, clientID: ObjectIdentifier(self), cancellation: req.cancellationToken, reply: callback) - await languageService.symbolInfo(request) + languageService.symbolInfo(request) } func respondWithInterface( @@ -1637,7 +1571,7 @@ extension SourceKitServer { moduleName: String, symbolUSR: String?, languageService: ToolchainLanguageServer - ) async { + ) { let openInterface = OpenInterfaceRequest(textDocument: req.params.textDocument, name: moduleName, symbolUSR: symbolUSR) let request = Request(openInterface, id: req.id, clientID: ObjectIdentifier(self), cancellation: req.cancellationToken, reply: { (result: Result) in @@ -1652,17 +1586,17 @@ extension SourceKitServer { req.reply(.failure(error)) } }) - await languageService.openInterface(request) + languageService.openInterface(request) } func implementation( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { + ) { let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position) - let index = await self.workspaceForDocument(uri: req.params.textDocument.uri)?.index - let callback = { (result: LSPResult) in + let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index + let callback = callbackOnQueue(self.queue) { (result: LSPResult) in let extractedResult = self.extractIndexedOccurrences(result: result, index: index) { (usr, index) in var occurs = index.occurrences(ofUSR: usr, roles: .baseOf) if occurs.isEmpty { @@ -1675,17 +1609,17 @@ extension SourceKitServer { } let request = Request(symbolInfo, id: req.id, clientID: ObjectIdentifier(self), cancellation: req.cancellationToken, reply: callback) - await languageService.symbolInfo(request) + languageService.symbolInfo(request) } func references( _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { + ) { let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position) - let index = await self.workspaceForDocument(uri: req.params.textDocument.uri)?.index - let callback = { (result: LSPResult) in + let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index + let callback = callbackOnQueue(self.queue) { (result: LSPResult) in let extractedResult = self.extractIndexedOccurrences(result: result, index: index) { (usr, index) in log("performing indexed jump-to-def with usr \(usr)") var roles: SymbolRole = [.reference] @@ -1699,7 +1633,7 @@ extension SourceKitServer { } let request = Request(symbolInfo, id: req.id, clientID: ObjectIdentifier(self), cancellation: req.cancellationToken, reply: callback) - await languageService.symbolInfo(request) + languageService.symbolInfo(request) } private func indexToLSPCallHierarchyItem( @@ -1727,10 +1661,10 @@ extension SourceKitServer { _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { + ) { let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position) - let index = await self.workspaceForDocument(uri: req.params.textDocument.uri)?.index - let callback = { (result: LSPResult) in + let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index + let callback = callbackOnQueue(self.queue) { (result: LSPResult) in // For call hierarchy preparation we only locate the definition let extractedResult = self.extractIndexedOccurrences(result: result, index: index) { (usr, index) in index.occurrences(ofUSR: usr, roles: [.definition, .declaration]) @@ -1752,7 +1686,7 @@ extension SourceKitServer { } let request = Request(symbolInfo, id: req.id, clientID: ObjectIdentifier(self), cancellation: req.cancellationToken, reply: callback) - await languageService.symbolInfo(request) + languageService.symbolInfo(request) } /// Extracts our implementation-specific data about a call hierarchy @@ -1760,7 +1694,7 @@ extension SourceKitServer { /// /// - Parameter data: The opaque data structure to extract /// - Returns: The extracted data if successful or nil otherwise - private nonisolated func extractCallHierarchyItemData(_ rawData: LSPAny?) -> (uri: DocumentURI, usr: String)? { + private func extractCallHierarchyItemData(_ rawData: LSPAny?) -> (uri: DocumentURI, usr: String)? { guard case let .dictionary(data) = rawData, case let .string(uriString) = data["uri"], case let .string(usr) = data["usr"] else { @@ -1772,9 +1706,9 @@ extension SourceKitServer { ) } - func incomingCalls(_ req: Request) async { + func incomingCalls(_ req: Request) { guard let data = extractCallHierarchyItemData(req.params.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index else { + let index = self.workspaceForDocument(uri: data.uri)?.index else { req.reply([]) return } @@ -1802,9 +1736,9 @@ extension SourceKitServer { req.reply(calls) } - func outgoingCalls(_ req: Request) async { + func outgoingCalls(_ req: Request) { guard let data = extractCallHierarchyItemData(req.params.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index else { + let index = self.workspaceForDocument(uri: data.uri)?.index else { req.reply([]) return } @@ -1885,13 +1819,13 @@ extension SourceKitServer { _ req: Request, workspace: Workspace, languageService: ToolchainLanguageServer - ) async { + ) { let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position) - guard let index = await self.workspaceForDocument(uri: req.params.textDocument.uri)?.index else { + guard let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index else { req.reply([]) return } - let callback = { (result: LSPResult) in + let callback = callbackOnQueue(self.queue) { (result: LSPResult) in // For type hierarchy preparation we only locate the definition let extractedResult = self.extractIndexedOccurrences(result: result, index: index) { (usr, index) in index.occurrences(ofUSR: usr, roles: [.definition, .declaration]) @@ -1914,7 +1848,7 @@ extension SourceKitServer { } let request = Request(symbolInfo, id: req.id, clientID: ObjectIdentifier(self), cancellation: req.cancellationToken, reply: callback) - await languageService.symbolInfo(request) + languageService.symbolInfo(request) } /// Extracts our implementation-specific data about a type hierarchy @@ -1922,7 +1856,7 @@ extension SourceKitServer { /// /// - Parameter data: The opaque data structure to extract /// - Returns: The extracted data if successful or nil otherwise - private nonisolated func extractTypeHierarchyItemData(_ rawData: LSPAny?) -> (uri: DocumentURI, usr: String)? { + private func extractTypeHierarchyItemData(_ rawData: LSPAny?) -> (uri: DocumentURI, usr: String)? { guard case let .dictionary(data) = rawData, case let .string(uriString) = data["uri"], case let .string(usr) = data["usr"] else { @@ -1934,9 +1868,9 @@ extension SourceKitServer { ) } - func supertypes(_ req: Request) async { + func supertypes(_ req: Request) { guard let data = extractTypeHierarchyItemData(req.params.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index else { + let index = self.workspaceForDocument(uri: data.uri)?.index else { req.reply([]) return } @@ -1975,9 +1909,9 @@ extension SourceKitServer { req.reply(types) } - func subtypes(_ req: Request) async { + func subtypes(_ req: Request) { guard let data = extractTypeHierarchyItemData(req.params.item.data), - let index = await self.workspaceForDocument(uri: data.uri)?.index else { + let index = self.workspaceForDocument(uri: data.uri)?.index else { req.reply([]) return } @@ -2015,6 +1949,17 @@ extension SourceKitServer { } } +private func callbackOnQueue( + _ queue: DispatchQueue, + _ callback: @escaping (LSPResult) -> Void +) -> (LSPResult) -> Void { + return { (result: LSPResult) in + queue.async { + callback(result) + } + } +} + /// Creates a new connection from `client` to a service for `language` if available, and launches /// the service. Does *not* send the initialization request. /// @@ -2026,18 +1971,16 @@ func languageService( options: SourceKitServer.Options, client: MessageHandler, in workspace: Workspace, - reopenDocuments: @escaping (ToolchainLanguageServer) async -> Void, - workspaceForDocument: @escaping (DocumentURI) async -> Workspace? -) async throws -> ToolchainLanguageServer? { + reopenDocuments: @escaping (ToolchainLanguageServer) -> Void +) throws -> ToolchainLanguageServer? { let connectionToClient = LocalConnection() - let server = try await languageServerType.serverType.init( + let server = try languageServerType.serverType.init( client: connectionToClient, toolchain: toolchain, options: options, workspace: workspace, - reopenDocuments: reopenDocuments, - workspaceForDocument: workspaceForDocument + reopenDocuments: reopenDocuments ) connectionToClient.start(handler: client) return server @@ -2103,8 +2046,8 @@ extension SymbolOccurrence { /// Simple struct for pending notifications/requests, including a cancellation handler. /// For convenience the notifications/request handlers are type erased via wrapping. -fileprivate struct NotificationRequestOperation { - let operation: () async -> Void +private struct NotificationRequestOperation { + let operation: () -> Void let cancellationHandler: (() -> Void)? } @@ -2112,14 +2055,22 @@ fileprivate struct NotificationRequestOperation { /// on `BuildSystem` operations such as fetching build settings. /// /// Note: This is not thread safe. Must be called from the `SourceKitServer.queue`. -fileprivate struct DocumentNotificationRequestQueue { - fileprivate var queue = [NotificationRequestOperation]() +private struct DocumentNotificationRequestQueue { + private var queue = [NotificationRequestOperation]() /// Add an operation to the end of the queue. - mutating func add(operation: @escaping () async -> Void, cancellationHandler: (() -> Void)? = nil) { + mutating func add(operation: @escaping () -> Void, cancellationHandler: (() -> Void)? = nil) { queue.append(NotificationRequestOperation(operation: operation, cancellationHandler: cancellationHandler)) } + /// Invoke all operations in the queue. + mutating func handleAll() { + for task in queue { + task.operation() + } + queue = [] + } + /// Cancel all operations in the queue. No-op for operations without a cancellation /// handler. mutating func cancelAll() { diff --git a/Sources/SourceKitLSP/Swift/CodeCompletion.swift b/Sources/SourceKitLSP/Swift/CodeCompletion.swift index bdce13c5..89ba7b2c 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletion.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletion.swift @@ -17,7 +17,8 @@ import Foundation extension SwiftLanguageServer { - public func completion(_ req: Request) async { + /// Must be called on `queue`. + func _completion(_ req: Request) { guard let snapshot = documentManager.latestSnapshot(req.params.textDocument.uri) else { log("failed to find snapshot for url \(req.params.textDocument.uri)") req.reply(CompletionList(isIncomplete: true, items: [])) @@ -39,13 +40,14 @@ extension SwiftLanguageServer { let options = req.params.sourcekitlspOptions ?? serverOptions.completionOptions if options.serverSideFiltering { - await _completionWithServerFiltering(offset: offset, completionPos: completionPos, snapshot: snapshot, request: req, options: options) + _completionWithServerFiltering(offset: offset, completionPos: completionPos, snapshot: snapshot, request: req, options: options) } else { - await _completionWithClientFiltering(offset: offset, completionPos: completionPos, snapshot: snapshot, request: req, options: options) + _completionWithClientFiltering(offset: offset, completionPos: completionPos, snapshot: snapshot, request: req, options: options) } } - func _completionWithServerFiltering(offset: Int, completionPos: Position, snapshot: DocumentSnapshot, request req: Request, options: SKCompletionOptions) async { + /// Must be called on `queue`. + func _completionWithServerFiltering(offset: Int, completionPos: Position, snapshot: DocumentSnapshot, request req: Request, options: SKCompletionOptions) { guard let start = snapshot.indexOf(utf8Offset: offset), let end = snapshot.index(of: req.params.position) else { log("invalid completion position \(req.params.position)") @@ -73,7 +75,7 @@ extension SwiftLanguageServer { snapshot: snapshot, utf8Offset: offset, position: completionPos, - compileCommand: await buildSettings(for: snapshot.document.uri)) + compileCommand: commandsByFile[snapshot.document.uri]) currentCompletionSession?.close() currentCompletionSession = session @@ -82,7 +84,8 @@ extension SwiftLanguageServer { session.update(filterText: filterText, position: req.params.position, in: snapshot, options: options, completion: req.reply) } - func _completionWithClientFiltering(offset: Int, completionPos: Position, snapshot: DocumentSnapshot, request req: Request, options: SKCompletionOptions) async { + /// Must be called on `queue`. + func _completionWithClientFiltering(offset: Int, completionPos: Position, snapshot: DocumentSnapshot, request req: Request, options: SKCompletionOptions) { let skreq = SKDRequestDictionary(sourcekitd: sourcekitd) skreq[keys.request] = requests.codecomplete skreq[keys.offset] = offset @@ -94,7 +97,7 @@ extension SwiftLanguageServer { skreq[keys.codecomplete_options] = skreqOptions // FIXME: SourceKit should probably cache this for us. - if let compileCommand = await buildSettings(for: snapshot.document.uri) { + if let compileCommand = commandsByFile[snapshot.document.uri] { skreq[keys.compilerargs] = compileCommand.compilerArgs } @@ -117,7 +120,7 @@ extension SwiftLanguageServer { _ = handle } - nonisolated func completionsFromSKDResponse( + func completionsFromSKDResponse( _ completions: SKDResponseArray, in snapshot: DocumentSnapshot, completionPos: Position, @@ -217,7 +220,7 @@ extension SwiftLanguageServer { return Position(line: pos.line, utf16index: adjustedOffset) } - private nonisolated func computeCompletionTextEdit(completionPos: Position, requestPosition: Position, utf8CodeUnitsToErase: Int, newText: String, snapshot: DocumentSnapshot) -> TextEdit { + private func computeCompletionTextEdit(completionPos: Position, requestPosition: Position, utf8CodeUnitsToErase: Int, newText: String, snapshot: DocumentSnapshot) -> TextEdit { let textEditRangeStart: Position // Compute the TextEdit diff --git a/Sources/SourceKitLSP/Swift/CursorInfo.swift b/Sources/SourceKitLSP/Swift/CursorInfo.swift index 7701896e..ae58d5e3 100644 --- a/Sources/SourceKitLSP/Swift/CursorInfo.swift +++ b/Sources/SourceKitLSP/Swift/CursorInfo.swift @@ -71,20 +71,12 @@ extension CursorInfoError: CustomStringConvertible { extension SwiftLanguageServer { - /// Provides detailed information about a symbol under the cursor, if any. - /// - /// Wraps the information returned by sourcekitd's `cursor_info` request, such as symbol name, - /// USR, and declaration location. This request does minimal processing of the result. - /// - /// - Parameters: - /// - url: Document URL in which to perform the request. Must be an open document. - /// - range: The position range within the document to lookup the symbol at. - /// - completion: Completion block to asynchronously receive the CursorInfo, or error. - func cursorInfo( + /// Must be called on self.queue. + func _cursorInfo( _ uri: DocumentURI, _ range: Range, additionalParameters appendAdditionalParameters: ((SKDRequestDictionary) -> Void)? = nil, - _ completion: @escaping (Swift.Result) -> Void) async + _ completion: @escaping (Swift.Result) -> Void) { guard let snapshot = documentManager.latestSnapshot(uri) else { return completion(.failure(.unknownDocument(uri))) @@ -105,7 +97,7 @@ extension SwiftLanguageServer { skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath // FIXME: SourceKit should probably cache this for us. - if let compileCommand = await self.buildSettings(for: uri) { + if let compileCommand = self.commandsByFile[uri] { skreq[keys.compilerargs] = compileCommand.compilerArgs } @@ -155,4 +147,25 @@ extension SwiftLanguageServer { // FIXME: cancellation _ = handle } + + /// Provides detailed information about a symbol under the cursor, if any. + /// + /// Wraps the information returned by sourcekitd's `cursor_info` request, such as symbol name, + /// USR, and declaration location. This request does minimal processing of the result. + /// + /// - Parameters: + /// - url: Document URL in which to perform the request. Must be an open document. + /// - range: The position range within the document to lookup the symbol at. + /// - completion: Completion block to asynchronously receive the CursorInfo, or error. + func cursorInfo( + _ uri: DocumentURI, + _ range: Range, + additionalParameters appendAdditionalParameters: ((SKDRequestDictionary) -> Void)? = nil, + _ completion: @escaping (Swift.Result) -> Void) + { + self.queue.async { + self._cursorInfo(uri, range, + additionalParameters: appendAdditionalParameters, completion) + } + } } diff --git a/Sources/SourceKitLSP/Swift/ExpressionTypeInfo.swift b/Sources/SourceKitLSP/Swift/ExpressionTypeInfo.swift index 68aac791..c46d7933 100644 --- a/Sources/SourceKitLSP/Swift/ExpressionTypeInfo.swift +++ b/Sources/SourceKitLSP/Swift/ExpressionTypeInfo.swift @@ -49,15 +49,13 @@ enum ExpressionTypeInfoError: Error, Equatable { } extension SwiftLanguageServer { - /// Provides typed expressions in a document. - /// - /// - Parameters: - /// - url: Document URL in which to perform the request. Must be an open document. - /// - completion: Completion block to asynchronously receive the ExpressionTypeInfos, or error. - func expressionTypeInfos( + /// Must be called on self.queue. + private func _expressionTypeInfos( _ uri: DocumentURI, _ completion: @escaping (Swift.Result<[ExpressionTypeInfo], ExpressionTypeInfoError>) -> Void - ) async { + ) { + dispatchPrecondition(condition: .onQueue(queue)) + guard let snapshot = documentManager.latestSnapshot(uri) else { return completion(.failure(.unknownDocument(uri))) } @@ -69,7 +67,7 @@ extension SwiftLanguageServer { skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath // FIXME: SourceKit should probably cache this for us. - if let compileCommand = await self.buildSettings(for: uri) { + if let compileCommand = self.commandsByFile[uri] { skreq[keys.compilerargs] = compileCommand.compilerArgs } @@ -100,4 +98,18 @@ extension SwiftLanguageServer { // FIXME: cancellation _ = handle } + + /// Provides typed expressions in a document. + /// + /// - Parameters: + /// - url: Document URL in which to perform the request. Must be an open document. + /// - completion: Completion block to asynchronously receive the ExpressionTypeInfos, or error. + func expressionTypeInfos( + _ uri: DocumentURI, + _ completion: @escaping (Swift.Result<[ExpressionTypeInfo], ExpressionTypeInfoError>) -> Void + ) { + queue.async { + self._expressionTypeInfos(uri, completion) + } + } } diff --git a/Sources/SourceKitLSP/Swift/OpenInterface.swift b/Sources/SourceKitLSP/Swift/OpenInterface.swift index 0d36cf94..a3539141 100644 --- a/Sources/SourceKitLSP/Swift/OpenInterface.swift +++ b/Sources/SourceKitLSP/Swift/OpenInterface.swift @@ -25,33 +25,35 @@ struct FindUSRInfo { } extension SwiftLanguageServer { - public func openInterface(_ request: LanguageServerProtocol.Request) async { + public func openInterface(_ request: LanguageServerProtocol.Request) { let uri = request.params.textDocument.uri let moduleName = request.params.moduleName let name = request.params.name let symbol = request.params.symbolUSR - let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent("\(name).swiftinterface") - let interfaceDocURI = DocumentURI(interfaceFilePath) - // has interface already been generated - if let snapshot = self.documentManager.latestSnapshot(interfaceDocURI) { - self._findUSRAndRespond(request: request, uri: interfaceDocURI, snapshot: snapshot, symbol: symbol) - } else { - // generate interface - await self._openInterface(request: request, uri: uri, name: moduleName, interfaceURI: interfaceDocURI) { result in - switch result { - case .success(let interfaceInfo): - do { - // write to file - try interfaceInfo.contents.write(to: interfaceFilePath, atomically: true, encoding: String.Encoding.utf8) - // store snapshot - let snapshot = try self.documentManager.open(interfaceDocURI, language: .swift, version: 0, text: interfaceInfo.contents) - self._findUSRAndRespond(request: request, uri: interfaceDocURI, snapshot: snapshot, symbol: symbol) - } catch { - request.reply(.failure(ResponseError.unknown(error.localizedDescription))) + self.queue.async { + let interfaceFilePath = self.generatedInterfacesPath.appendingPathComponent("\(name).swiftinterface") + let interfaceDocURI = DocumentURI(interfaceFilePath) + // has interface already been generated + if let snapshot = self.documentManager.latestSnapshot(interfaceDocURI) { + self._findUSRAndRespond(request: request, uri: interfaceDocURI, snapshot: snapshot, symbol: symbol) + } else { + // generate interface + self._openInterface(request: request, uri: uri, name: moduleName, interfaceURI: interfaceDocURI) { result in + switch result { + case .success(let interfaceInfo): + do { + // write to file + try interfaceInfo.contents.write(to: interfaceFilePath, atomically: true, encoding: String.Encoding.utf8) + // store snapshot + let snapshot = try self.documentManager.open(interfaceDocURI, language: .swift, version: 0, text: interfaceInfo.contents) + self._findUSRAndRespond(request: request, uri: interfaceDocURI, snapshot: snapshot, symbol: symbol) + } catch { + request.reply(.failure(ResponseError.unknown(error.localizedDescription))) + } + case .failure(let error): + log("open interface failed: \(error)", level: .warning) + request.reply(.failure(ResponseError(error))) } - case .failure(let error): - log("open interface failed: \(error)", level: .warning) - request.reply(.failure(ResponseError(error))) } } } @@ -69,7 +71,7 @@ extension SwiftLanguageServer { uri: DocumentURI, name: String, interfaceURI: DocumentURI, - completion: @escaping (Swift.Result) -> Void) async { + completion: @escaping (Swift.Result) -> Void) { let keys = self.keys let skreq = SKDRequestDictionary(sourcekitd: sourcekitd) skreq[keys.request] = requests.editor_open_interface @@ -79,7 +81,7 @@ extension SwiftLanguageServer { } skreq[keys.name] = interfaceURI.pseudoPath skreq[keys.synthesizedextensions] = 1 - if let compileCommand = await self.buildSettings(for: uri) { + if let compileCommand = self.commandsByFile[uri] { skreq[keys.compilerargs] = compileCommand.compilerArgs } diff --git a/Sources/SourceKitLSP/Swift/SemanticRefactoring.swift b/Sources/SourceKitLSP/Swift/SemanticRefactoring.swift index 700cc720..720f802c 100644 --- a/Sources/SourceKitLSP/Swift/SemanticRefactoring.swift +++ b/Sources/SourceKitLSP/Swift/SemanticRefactoring.swift @@ -126,52 +126,54 @@ extension SwiftLanguageServer { /// - completion: Completion block to asynchronously receive the SemanticRefactoring data, or error. func semanticRefactoring( _ refactorCommand: SemanticRefactorCommand, - _ completion: @escaping (Result) -> Void) async + _ completion: @escaping (Result) -> Void) { let keys = self.keys - let uri = refactorCommand.textDocument.uri - guard let snapshot = self.documentManager.latestSnapshot(uri) else { - return completion(.failure(.unknownDocument(uri))) - } - guard let offsetRange = snapshot.utf8OffsetRange(of: refactorCommand.positionRange) else { - return completion(.failure(.failedToRetrieveOffset(refactorCommand.positionRange))) - } - let line = refactorCommand.positionRange.lowerBound.line - let utf16Column = refactorCommand.positionRange.lowerBound.utf16index - guard let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column) else { - return completion(.failure(.invalidRange(refactorCommand.positionRange))) - } - - let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd) - skreq[keys.request] = self.requests.semantic_refactoring - // Preferred name for e.g. an extracted variable. - // Empty string means sourcekitd chooses a name automatically. - skreq[keys.name] = "" - skreq[keys.sourcefile] = uri.pseudoPath - // LSP is zero based, but this request is 1 based. - skreq[keys.line] = line + 1 - skreq[keys.column] = utf8Column + 1 - skreq[keys.length] = offsetRange.count - skreq[keys.actionuid] = self.sourcekitd.api.uid_get_from_cstr(refactorCommand.actionString)! - - // FIXME: SourceKit should probably cache this for us. - if let compileCommand = await self.buildSettings(for: snapshot.document.uri) { - skreq[keys.compilerargs] = compileCommand.compilerArgs - } - - let handle = self.sourcekitd.send(skreq, self.queue) { [weak self] result in - guard let self = self else { return } - guard let dict = result.success else { - return completion(.failure(.responseError(ResponseError(result.failure!)))) + queue.async { + let uri = refactorCommand.textDocument.uri + guard let snapshot = self.documentManager.latestSnapshot(uri) else { + return completion(.failure(.unknownDocument(uri))) } - guard let refactor = SemanticRefactoring(refactorCommand.title, dict, snapshot, self.keys) else { - return completion(.failure(.noEditsNeeded(uri))) + guard let offsetRange = snapshot.utf8OffsetRange(of: refactorCommand.positionRange) else { + return completion(.failure(.failedToRetrieveOffset(refactorCommand.positionRange))) + } + let line = refactorCommand.positionRange.lowerBound.line + let utf16Column = refactorCommand.positionRange.lowerBound.utf16index + guard let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column) else { + return completion(.failure(.invalidRange(refactorCommand.positionRange))) } - completion(.success(refactor)) - } - // FIXME: cancellation - _ = handle + let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd) + skreq[keys.request] = self.requests.semantic_refactoring + // Preferred name for e.g. an extracted variable. + // Empty string means sourcekitd chooses a name automatically. + skreq[keys.name] = "" + skreq[keys.sourcefile] = uri.pseudoPath + // LSP is zero based, but this request is 1 based. + skreq[keys.line] = line + 1 + skreq[keys.column] = utf8Column + 1 + skreq[keys.length] = offsetRange.count + skreq[keys.actionuid] = self.sourcekitd.api.uid_get_from_cstr(refactorCommand.actionString)! + + // FIXME: SourceKit should probably cache this for us. + if let compileCommand = self.commandsByFile[snapshot.document.uri] { + skreq[keys.compilerargs] = compileCommand.compilerArgs + } + + let handle = self.sourcekitd.send(skreq, self.queue) { [weak self] result in + guard let self = self else { return } + guard let dict = result.success else { + return completion(.failure(.responseError(ResponseError(result.failure!)))) + } + guard let refactor = SemanticRefactoring(refactorCommand.title, dict, snapshot, self.keys) else { + return completion(.failure(.noEditsNeeded(uri))) + } + completion(.success(refactor)) + } + + // FIXME: cancellation + _ = handle + } } } diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift index 2ef2ce7e..900fd525 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift @@ -94,16 +94,9 @@ public struct SwiftCompileCommand: Equatable { } } -public actor SwiftLanguageServer: ToolchainLanguageServer { +public final class SwiftLanguageServer: ToolchainLanguageServer { - // FIXME: (async) We can delete this after - // - CodeCompletionSession is an actor - // - sourcekitd.send is async - // - client.send is async - /// The queue on which we want to be called back. This includes - /// - Completion callback from sourcekitd - /// - Sending requests to the editor - /// - Guarding the state of `CodeCompletionSession` + /// The server's request queue, used to serialize requests and responses to `sourcekitd`. public let queue: DispatchQueue = DispatchQueue(label: "swift-language-server-queue", qos: .userInitiated) let client: LocalConnection @@ -124,12 +117,14 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { var currentCompletionSession: CodeCompletionSession? = nil + var commandsByFile: [DocumentURI: SwiftCompileCommand] = [:] + /// *For Testing* public var reusedNodeCallback: ReusedNodeCallback? - nonisolated var keys: sourcekitd_keys { return sourcekitd.keys } - nonisolated var requests: sourcekitd_requests { return sourcekitd.requests } - nonisolated var values: sourcekitd_values { return sourcekitd.values } + var keys: sourcekitd_keys { return sourcekitd.keys } + var requests: sourcekitd_requests { return sourcekitd.requests } + var values: sourcekitd_values { return sourcekitd.values } var enablePublishDiagnostics: Bool { // Since LSP 3.17.0, diagnostics can be reported through pull-based requests, @@ -141,6 +136,8 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { private var state: LanguageServerState { didSet { + // `state` must only be set from `queue`. + dispatchPrecondition(condition: .onQueue(queue)) for handler in stateChangeHandlers { handler(oldValue, state) } @@ -150,13 +147,7 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { private var stateChangeHandlers: [(_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void] = [] /// A callback with which `SwiftLanguageServer` can request its owner to reopen all documents in case it has crashed. - private let reopenDocuments: (ToolchainLanguageServer) async -> Void - - /// Get the workspace that the document with the given URI belongs to. - /// - /// This is used to find the `BuildSystemManager` that is able to deliver - /// build settings for this document. - private let workspaceForDocument: (DocumentURI) async -> Workspace? + private let reopenDocuments: (ToolchainLanguageServer) -> Void /// Creates a language server for the given client using the sourcekitd dylib specified in `toolchain`. /// `reopenDocuments` is a closure that will be called if sourcekitd crashes and the `SwiftLanguageServer` asks its parent server to reopen all of its documents. @@ -166,8 +157,7 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { toolchain: Toolchain, options: SourceKitServer.Options, workspace: Workspace, - reopenDocuments: @escaping (ToolchainLanguageServer) async -> Void, - workspaceForDocument: @escaping (DocumentURI) async -> Workspace? + reopenDocuments: @escaping (ToolchainLanguageServer) -> Void ) throws { guard let sourcekitd = toolchain.sourcekitd else { return nil } self.client = client @@ -177,35 +167,28 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { self.documentManager = DocumentManager() self.state = .connected self.reopenDocuments = reopenDocuments - self.workspaceForDocument = workspaceForDocument self.generatedInterfacesPath = options.generatedInterfacesPath.asURL try FileManager.default.createDirectory(at: generatedInterfacesPath, withIntermediateDirectories: true) } - func buildSettings(for document: DocumentURI) async -> SwiftCompileCommand? { - guard let workspace = await self.workspaceForDocument(document) else { - return nil - } - if let settings = await workspace.buildSystemManager.buildSettings(for: document, language: .swift) { - return SwiftCompileCommand(settings.buildSettings, isFallback: settings.isFallback) - } else { - return nil - } - } - - public nonisolated func canHandle(workspace: Workspace) -> Bool { + public func canHandle(workspace: Workspace) -> Bool { // We have a single sourcekitd instance for all workspaces. return true } public func addStateChangeHandler(handler: @escaping (_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void) { - self.stateChangeHandlers.append(handler) + queue.async { + self.stateChangeHandlers.append(handler) + } } /// Updates the lexical tokens for the given `snapshot`. + /// Must be called on `self.queue`. private func updateSyntacticTokens( for snapshot: DocumentSnapshot ) { + dispatchPrecondition(condition: .onQueue(queue)) + let uri = snapshot.document.uri let docTokens = updateSyntaxTree(for: snapshot) @@ -244,10 +227,13 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { } /// Updates the semantic tokens for the given `snapshot`. + /// Must be called on `self.queue`. private func updateSemanticTokens( response: SKDResponseDictionary, for snapshot: DocumentSnapshot ) { + dispatchPrecondition(condition: .onQueue(queue)) + let uri = snapshot.document.uri let docTokens = updatedSemanticTokens(response: response, for: snapshot) @@ -305,15 +291,10 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { /// Register the diagnostics returned from sourcekitd in `currentDiagnostics` /// and returns the corresponding LSP diagnostics. - /// - /// If `isFromFallbackBuildSettings` is `true`, then only parse diagnostics are - /// stored and any semantic diagnostics are ignored since they are probably - /// incorrect in the absence of build settings. private func registerDiagnostics( sourcekitdDiagnostics: SKDResponseArray?, snapshot: DocumentSnapshot, - stage: DiagnosticStage, - isFromFallbackBuildSettings: Bool + stage: DiagnosticStage ) -> [Diagnostic] { let supportsCodeDescription = capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport @@ -329,7 +310,7 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { old: currentDiagnostics[snapshot.document.uri] ?? [], new: newDiags, stage: stage, - isFallback: isFromFallbackBuildSettings + isFallback: self.commandsByFile[snapshot.document.uri]?.isFallback ?? true ) currentDiagnostics[snapshot.document.uri] = result @@ -339,6 +320,8 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { /// Publish diagnostics for the given `snapshot`. We withhold semantic diagnostics if we are using /// fallback arguments. + /// + /// Should be called on self.queue. func publishDiagnostics( response: SKDResponseDictionary, for snapshot: DocumentSnapshot, @@ -356,8 +339,7 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { let diagnostics = registerDiagnostics( sourcekitdDiagnostics: response[keys.diagnostics], snapshot: snapshot, - stage: stage, - isFromFallbackBuildSettings: compileCommand?.isFallback ?? true + stage: stage ) client.send( @@ -369,11 +351,13 @@ public actor SwiftLanguageServer: ToolchainLanguageServer { ) } - func handleDocumentUpdate(uri: DocumentURI) async { + /// Should be called on self.queue. + func handleDocumentUpdate(uri: DocumentURI) { + dispatchPrecondition(condition: .onQueue(queue)) guard let snapshot = documentManager.latestSnapshot(uri) else { return } - let compileCommand = await self.buildSettings(for: uri) + let compileCommand = self.commandsByFile[uri] // Make the magic 0,0 replacetext request to update diagnostics and semantic tokens. @@ -443,13 +427,16 @@ extension SwiftLanguageServer { // Nothing to do. } - public func shutdown() async { - if let session = self.currentCompletionSession { - session.close() - self.currentCompletionSession = nil + public func shutdown(callback: @escaping () -> Void) { + queue.async { + if let session = self.currentCompletionSession { + session.close() + self.currentCompletionSession = nil + } + self.sourcekitd.removeNotificationHandler(self) + self.client.close() + callback() } - self.sourcekitd.removeNotificationHandler(self) - self.client.close() } /// Tell sourcekitd to crash itself. For testing purposes only. @@ -461,6 +448,7 @@ extension SwiftLanguageServer { // MARK: - Build System Integration + /// Should be called on self.queue. private func reopenDocument(_ snapshot: DocumentSnapshot, _ compileCmd: SwiftCompileCommand?) { let keys = self.keys let uri = snapshot.document.uri @@ -488,123 +476,143 @@ extension SwiftLanguageServer { self.updateSyntacticTokens(for: snapshot) } - public func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) async { - // We may not have a snapshot if this is called just before `openDocument`. - guard let snapshot = self.documentManager.latestSnapshot(uri) else { - return - } + public func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) { + self.queue.async { + let compileCommand = SwiftCompileCommand(change: change) + // Confirm that the compile commands actually changed, otherwise we don't need to do anything. + // This includes when the compiler arguments are the same but the command is no longer + // considered to be fallback. + guard self.commandsByFile[uri] != compileCommand else { + return + } + self.commandsByFile[uri] = compileCommand - // Close and re-open the document internally to inform sourcekitd to update the compile - // command. At the moment there's no better way to do this. - self.reopenDocument(snapshot, await self.buildSettings(for: uri)) + // We may not have a snapshot if this is called just before `openDocument`. + guard let snapshot = self.documentManager.latestSnapshot(uri) else { + return + } + + // Close and re-open the document internally to inform sourcekitd to update the compile + // command. At the moment there's no better way to do this. + self.reopenDocument(snapshot, compileCommand) + } } - public func documentDependenciesUpdated(_ uri: DocumentURI) async { - guard let snapshot = self.documentManager.latestSnapshot(uri) else { - return - } + public func documentDependenciesUpdated(_ uri: DocumentURI) { + self.queue.async { + guard let snapshot = self.documentManager.latestSnapshot(uri) else { + return + } - // Forcefully reopen the document since the `BuildSystem` has informed us - // that the dependencies have changed and the AST needs to be reloaded. - await self.reopenDocument(snapshot, self.buildSettings(for: uri)) + // Forcefully reopen the document since the `BuildSystem` has informed us + // that the dependencies have changed and the AST needs to be reloaded. + self.reopenDocument(snapshot, self.commandsByFile[uri]) + } } // MARK: - Text synchronization - public func openDocument(_ note: DidOpenTextDocumentNotification) async { + public func openDocument(_ note: DidOpenTextDocumentNotification) { let keys = self.keys - guard let snapshot = self.documentManager.open(note) else { - // Already logged failure. - return + self.queue.async { + guard let snapshot = self.documentManager.open(note) else { + // Already logged failure. + return + } + + let uri = snapshot.document.uri + let req = SKDRequestDictionary(sourcekitd: self.sourcekitd) + req[keys.request] = self.requests.editor_open + req[keys.name] = note.textDocument.uri.pseudoPath + req[keys.sourcetext] = snapshot.text + + let compileCommand = self.commandsByFile[uri] + + if let compilerArgs = compileCommand?.compilerArgs { + req[keys.compilerargs] = compilerArgs + } + + guard let dict = try? self.sourcekitd.sendSync(req) else { + // Already logged failure. + return + } + self.publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand) + self.updateSyntacticTokens(for: snapshot) } - - let uri = snapshot.document.uri - let req = SKDRequestDictionary(sourcekitd: self.sourcekitd) - req[keys.request] = self.requests.editor_open - req[keys.name] = note.textDocument.uri.pseudoPath - req[keys.sourcetext] = snapshot.text - - let compileCommand = await self.buildSettings(for: uri) - - if let compilerArgs = compileCommand?.compilerArgs { - req[keys.compilerargs] = compilerArgs - } - - guard let dict = try? self.sourcekitd.sendSync(req) else { - // Already logged failure. - return - } - self.publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand) - self.updateSyntacticTokens(for: snapshot) } public func closeDocument(_ note: DidCloseTextDocumentNotification) { let keys = self.keys - self.documentManager.close(note) + self.queue.async { + self.documentManager.close(note) - let uri = note.textDocument.uri + let uri = note.textDocument.uri - let req = SKDRequestDictionary(sourcekitd: self.sourcekitd) - req[keys.request] = self.requests.editor_close - req[keys.name] = uri.pseudoPath + let req = SKDRequestDictionary(sourcekitd: self.sourcekitd) + req[keys.request] = self.requests.editor_close + req[keys.name] = uri.pseudoPath - // Clear settings that should not be cached for closed documents. - self.currentDiagnostics[uri] = nil + // Clear settings that should not be cached for closed documents. + self.commandsByFile[uri] = nil + self.currentDiagnostics[uri] = nil - _ = try? self.sourcekitd.sendSync(req) + _ = try? self.sourcekitd.sendSync(req) + } } - public func changeDocument(_ note: DidChangeTextDocumentNotification) async { + public func changeDocument(_ note: DidChangeTextDocumentNotification) { let keys = self.keys var edits: [IncrementalEdit] = [] - var lastResponse: SKDResponseDictionary? = nil + self.queue.async { + var lastResponse: SKDResponseDictionary? = nil - let snapshot = self.documentManager.edit(note) { - (before: DocumentSnapshot, edit: TextDocumentContentChangeEvent) in - let req = SKDRequestDictionary(sourcekitd: self.sourcekitd) - req[keys.request] = self.requests.editor_replacetext - req[keys.name] = note.textDocument.uri.pseudoPath + let snapshot = self.documentManager.edit(note) { + (before: DocumentSnapshot, edit: TextDocumentContentChangeEvent) in + let req = SKDRequestDictionary(sourcekitd: self.sourcekitd) + req[keys.request] = self.requests.editor_replacetext + req[keys.name] = note.textDocument.uri.pseudoPath - if let range = edit.range { - guard let offset = before.utf8Offset(of: range.lowerBound), - let end = before.utf8Offset(of: range.upperBound) - else { - fatalError("invalid edit \(range)") + if let range = edit.range { + guard let offset = before.utf8Offset(of: range.lowerBound), + let end = before.utf8Offset(of: range.upperBound) + else { + fatalError("invalid edit \(range)") + } + + let length = end - offset + req[keys.offset] = offset + req[keys.length] = length + + edits.append(IncrementalEdit(offset: offset, length: length, replacementLength: edit.text.utf8.count)) + } else { + // Full text + let length = before.text.utf8.count + req[keys.offset] = 0 + req[keys.length] = length + + edits.append(IncrementalEdit(offset: 0, length: length, replacementLength: edit.text.utf8.count)) } - let length = end - offset - req[keys.offset] = offset - req[keys.length] = length + req[keys.sourcetext] = edit.text + lastResponse = try? self.sourcekitd.sendSync(req) - edits.append(IncrementalEdit(offset: offset, length: length, replacementLength: edit.text.utf8.count)) - } else { - // Full text - let length = before.text.utf8.count - req[keys.offset] = 0 - req[keys.length] = length - - edits.append(IncrementalEdit(offset: 0, length: length, replacementLength: edit.text.utf8.count)) + self.adjustDiagnosticRanges(of: note.textDocument.uri, for: edit) + } updateDocumentTokens: { (after: DocumentSnapshot) in + if lastResponse != nil { + return self.updateSyntaxTree(for: after, with: ConcurrentEdits(fromSequential: edits)) + } else { + return DocumentTokens() + } } - req[keys.sourcetext] = edit.text - lastResponse = try? self.sourcekitd.sendSync(req) - - self.adjustDiagnosticRanges(of: note.textDocument.uri, for: edit) - } updateDocumentTokens: { (after: DocumentSnapshot) in - if lastResponse != nil { - return self.updateSyntaxTree(for: after, with: ConcurrentEdits(fromSequential: edits)) - } else { - return DocumentTokens() + if let dict = lastResponse, let snapshot = snapshot { + let compileCommand = self.commandsByFile[note.textDocument.uri] + self.publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand) } } - - if let dict = lastResponse, let snapshot = snapshot { - let compileCommand = await self.buildSettings(for: note.textDocument.uri) - self.publishDiagnostics(response: dict, for: snapshot, compileCommand: compileCommand) - } } public func willSaveDocument(_ note: WillSaveTextDocumentNotification) { @@ -628,10 +636,16 @@ extension SwiftLanguageServer { return false } - public func hover(_ req: Request) async { + public func completion(_ req: Request) { + queue.async { + self._completion(req) + } + } + + public func hover(_ req: Request) { let uri = req.params.textDocument.uri let position = req.params.position - await cursorInfo(uri, position..) async { + public func symbolInfo(_ req: Request) { let uri = req.params.textDocument.uri let position = req.params.position - await cursorInfo(uri, position..) -> Void ) { + dispatchPrecondition(condition: .onQueue(queue)) + guard let snapshot = self.documentManager.latestSnapshot(uri) else { let msg = "failed to find snapshot for url \(uri)" log(msg) @@ -774,6 +791,15 @@ extension SwiftLanguageServer { _ = handle } + public func documentSymbols( + _ uri: DocumentURI, + _ completion: @escaping (Result<[DocumentSymbol], ResponseError>) -> Void + ) { + queue.async { + self._documentSymbols(uri, completion) + } + } + public func documentSymbol(_ req: Request) { documentSymbols(req.params.textDocument.uri) { result in req.reply(result.map { .documentSymbols($0) }) @@ -783,118 +809,122 @@ extension SwiftLanguageServer { public func documentColor(_ req: Request) { let keys = self.keys - guard let snapshot = self.documentManager.latestSnapshot(req.params.textDocument.uri) else { - log("failed to find snapshot for url \(req.params.textDocument.uri)") - req.reply([]) - return - } - - let helperDocumentName = "DocumentColor:" + snapshot.document.uri.pseudoPath - let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd) - skreq[keys.request] = self.requests.editor_open - skreq[keys.name] = helperDocumentName - skreq[keys.sourcetext] = snapshot.text - skreq[keys.syntactic_only] = 1 - - let handle = self.sourcekitd.send(skreq, self.queue) { [weak self] result in - guard let self = self else { return } - - defer { - let closeHelperReq = SKDRequestDictionary(sourcekitd: self.sourcekitd) - closeHelperReq[keys.request] = self.requests.editor_close - closeHelperReq[keys.name] = helperDocumentName - _ = self.sourcekitd.send(closeHelperReq, .global(qos: .utility), reply: { _ in }) - } - - guard let dict = result.success else { - req.reply(.failure(ResponseError(result.failure!))) + queue.async { + guard let snapshot = self.documentManager.latestSnapshot(req.params.textDocument.uri) else { + log("failed to find snapshot for url \(req.params.textDocument.uri)") + req.reply([]) return } - guard let results: SKDResponseArray = dict[self.keys.substructure] else { - return req.reply([]) - } + let helperDocumentName = "DocumentColor:" + snapshot.document.uri.pseudoPath + let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd) + skreq[keys.request] = self.requests.editor_open + skreq[keys.name] = helperDocumentName + skreq[keys.sourcetext] = snapshot.text + skreq[keys.syntactic_only] = 1 - func colorInformation(dict: SKDResponseDictionary) -> ColorInformation? { - guard let kind: sourcekitd_uid_t = dict[self.keys.kind], - kind == self.values.expr_object_literal, - let name: String = dict[self.keys.name], - name == "colorLiteral", - let offset: Int = dict[self.keys.offset], - let start: Position = snapshot.positionOf(utf8Offset: offset), - let length: Int = dict[self.keys.length], - let end: Position = snapshot.positionOf(utf8Offset: offset + length), - let substructure: SKDResponseArray = dict[self.keys.substructure] else { - return nil + let handle = self.sourcekitd.send(skreq, self.queue) { [weak self] result in + guard let self = self else { return } + + defer { + let closeHelperReq = SKDRequestDictionary(sourcekitd: self.sourcekitd) + closeHelperReq[keys.request] = self.requests.editor_close + closeHelperReq[keys.name] = helperDocumentName + _ = self.sourcekitd.send(closeHelperReq, .global(qos: .utility), reply: { _ in }) } - var red, green, blue, alpha: Double? - substructure.forEach{ (i: Int, value: SKDResponseDictionary) in - guard let name: String = value[self.keys.name], - let bodyoffset: Int = value[self.keys.bodyoffset], - let bodylength: Int = value[self.keys.bodylength] else { + + guard let dict = result.success else { + req.reply(.failure(ResponseError(result.failure!))) + return + } + + guard let results: SKDResponseArray = dict[self.keys.substructure] else { + return req.reply([]) + } + + func colorInformation(dict: SKDResponseDictionary) -> ColorInformation? { + guard let kind: sourcekitd_uid_t = dict[self.keys.kind], + kind == self.values.expr_object_literal, + let name: String = dict[self.keys.name], + name == "colorLiteral", + let offset: Int = dict[self.keys.offset], + let start: Position = snapshot.positionOf(utf8Offset: offset), + let length: Int = dict[self.keys.length], + let end: Position = snapshot.positionOf(utf8Offset: offset + length), + let substructure: SKDResponseArray = dict[self.keys.substructure] else { + return nil + } + var red, green, blue, alpha: Double? + substructure.forEach{ (i: Int, value: SKDResponseDictionary) in + guard let name: String = value[self.keys.name], + let bodyoffset: Int = value[self.keys.bodyoffset], + let bodylength: Int = value[self.keys.bodylength] else { + return true + } + let view = snapshot.text.utf8 + let bodyStart = view.index(view.startIndex, offsetBy: bodyoffset) + let bodyEnd = view.index(view.startIndex, offsetBy: bodyoffset+bodylength) + let value = String(view[bodyStart.. [ColorInformation] { - var result: [ColorInformation] = [] - array.forEach { (i: Int, value: SKDResponseDictionary) in - if let documentSymbol = colorInformation(dict: value) { - result.append(documentSymbol) - } else if let substructure: SKDResponseArray = value[self.keys.substructure] { - result += colorInformation(array: substructure) + func colorInformation(array: SKDResponseArray) -> [ColorInformation] { + var result: [ColorInformation] = [] + array.forEach { (i: Int, value: SKDResponseDictionary) in + if let documentSymbol = colorInformation(dict: value) { + result.append(documentSymbol) + } else if let substructure: SKDResponseArray = value[self.keys.substructure] { + result += colorInformation(array: substructure) + } + return true } - return true + return result } - return result - } - req.reply(colorInformation(array: results)) + req.reply(colorInformation(array: results)) + } + // FIXME: cancellation + _ = handle } - // FIXME: cancellation - _ = handle } public func documentSemanticTokens(_ req: Request) { let uri = req.params.textDocument.uri - guard let snapshot = self.documentManager.latestSnapshot(uri) else { - log("failed to find snapshot for uri \(uri)") - req.reply(DocumentSemanticTokensResponse(data: [])) - return + queue.async { + guard let snapshot = self.documentManager.latestSnapshot(uri) else { + log("failed to find snapshot for uri \(uri)") + req.reply(DocumentSemanticTokensResponse(data: [])) + return + } + + let tokens = snapshot.mergedAndSortedTokens() + let encodedTokens = tokens.lspEncoded + + req.reply(DocumentSemanticTokensResponse(data: encodedTokens)) } - - let tokens = snapshot.mergedAndSortedTokens() - let encodedTokens = tokens.lspEncoded - - req.reply(DocumentSemanticTokensResponse(data: encodedTokens)) } public func documentSemanticTokensDelta(_ req: Request) { @@ -906,16 +936,18 @@ extension SwiftLanguageServer { let uri = req.params.textDocument.uri let range = req.params.range - guard let snapshot = self.documentManager.latestSnapshot(uri) else { - log("failed to find snapshot for uri \(uri)") - req.reply(DocumentSemanticTokensResponse(data: [])) - return + queue.async { + guard let snapshot = self.documentManager.latestSnapshot(uri) else { + log("failed to find snapshot for uri \(uri)") + req.reply(DocumentSemanticTokensResponse(data: [])) + return + } + + let tokens = snapshot.mergedAndSortedTokens(in: range) + let encodedTokens = tokens.lspEncoded + + req.reply(DocumentSemanticTokensResponse(data: encodedTokens)) } - - let tokens = snapshot.mergedAndSortedTokens(in: range) - let encodedTokens = tokens.lspEncoded - - req.reply(DocumentSemanticTokensResponse(data: encodedTokens)) } public func colorPresentation(_ req: Request) { @@ -928,303 +960,293 @@ extension SwiftLanguageServer { req.reply([presentation]) } - public func documentSymbolHighlight(_ req: Request) async { + public func documentSymbolHighlight(_ req: Request) { let keys = self.keys - guard let snapshot = self.documentManager.latestSnapshot(req.params.textDocument.uri) else { - log("failed to find snapshot for url \(req.params.textDocument.uri)") - req.reply(nil) - return - } - - guard let offset = snapshot.utf8Offset(of: req.params.position) else { - log("invalid position \(req.params.position)") - req.reply(nil) - return - } - - let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd) - skreq[keys.request] = self.requests.relatedidents - skreq[keys.offset] = offset - skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath - - // FIXME: SourceKit should probably cache this for us. - if let compileCommand = await self.buildSettings(for: snapshot.document.uri) { - skreq[keys.compilerargs] = compileCommand.compilerArgs - } - - let handle = self.sourcekitd.send(skreq, self.queue) { [weak self] result in - guard let self = self else { return } - guard let dict = result.success else { - req.reply(.failure(ResponseError(result.failure!))) - return - } - - guard let results: SKDResponseArray = dict[self.keys.results] else { - return req.reply([]) - } - - var highlights: [DocumentHighlight] = [] - - results.forEach { _, value in - if let offset: Int = value[self.keys.offset], - let start: Position = snapshot.positionOf(utf8Offset: offset), - let length: Int = value[self.keys.length], - let end: Position = snapshot.positionOf(utf8Offset: offset + length) - { - highlights.append(DocumentHighlight( - range: start..) async { - let foldingRangeCapabilities = capabilityRegistry.clientCapabilities.textDocument?.foldingRange - let uri = req.params.textDocument.uri - guard var snapshot = self.documentManager.latestSnapshot(uri) else { - log("failed to find snapshot for url \(req.params.textDocument.uri)") - req.reply(nil) - return - } - - // FIXME: (async) We might not have computed the syntax tree yet. Wait until we have a syntax tree. - // Really, getting the syntax tree should be an async operation. - while snapshot.tokens.syntaxTree == nil { - try? await Task.sleep(nanoseconds: 1_000_000) - if let newSnapshot = documentManager.latestSnapshot(uri) { - snapshot = newSnapshot - } else { + queue.async { + guard let snapshot = self.documentManager.latestSnapshot(req.params.textDocument.uri) else { log("failed to find snapshot for url \(req.params.textDocument.uri)") req.reply(nil) return } - } - guard let sourceFile = snapshot.tokens.syntaxTree else { - log("no lexical structure available for url \(req.params.textDocument.uri)") - req.reply(nil) - return - } - - final class FoldingRangeFinder: SyntaxVisitor { - private let snapshot: DocumentSnapshot - /// Some ranges might occur multiple times. - /// E.g. for `print("hi")`, `"hi"` is both the range of all call arguments and the range the first argument in the call. - /// It doesn't make sense to report them multiple times, so use a `Set` here. - private var ranges: Set - /// The client-imposed limit on the number of folding ranges it would - /// prefer to recieve from the LSP server. If the value is `nil`, there - /// is no preset limit. - private var rangeLimit: Int? - /// If `true`, the client is only capable of folding entire lines. If - /// `false` the client can handle folding ranges. - private var lineFoldingOnly: Bool - - init(snapshot: DocumentSnapshot, rangeLimit: Int?, lineFoldingOnly: Bool) { - self.snapshot = snapshot - self.ranges = [] - self.rangeLimit = rangeLimit - self.lineFoldingOnly = lineFoldingOnly - super.init(viewMode: .sourceAccurate) + guard let offset = snapshot.utf8Offset(of: req.params.position) else { + log("invalid position \(req.params.position)") + req.reply(nil) + return } - override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind { - // Index comments, so we need to see at least '/*', or '//'. - if node.leadingTriviaLength.utf8Length > 2 { - self.addTrivia(from: node, node.leadingTrivia) - } + let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd) + skreq[keys.request] = self.requests.relatedidents + skreq[keys.offset] = offset + skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath - if node.trailingTriviaLength.utf8Length > 2 { - self.addTrivia(from: node, node.trailingTrivia) - } - - return .visitChildren + // FIXME: SourceKit should probably cache this for us. + if let compileCommand = self.commandsByFile[snapshot.document.uri] { + skreq[keys.compilerargs] = compileCommand.compilerArgs } - private func addTrivia(from node: TokenSyntax, _ trivia: Trivia) { - let pieces = trivia.pieces - var start = node.position.utf8Offset - /// The index of the trivia piece we are currently inspecting. - var index = 0 + let handle = self.sourcekitd.send(skreq, self.queue) { [weak self] result in + guard let self = self else { return } + guard let dict = result.success else { + req.reply(.failure(ResponseError(result.failure!))) + return + } - while index < pieces.count { - let piece = pieces[index] - defer { - start += pieces[index].sourceLength.utf8Length - index += 1 + guard let results: SKDResponseArray = dict[self.keys.results] else { + return req.reply([]) + } + + var highlights: [DocumentHighlight] = [] + + results.forEach { _, value in + if let offset: Int = value[self.keys.offset], + let start: Position = snapshot.positionOf(utf8Offset: offset), + let length: Int = value[self.keys.length], + let end: Position = snapshot.positionOf(utf8Offset: offset + length) + { + highlights.append(DocumentHighlight( + range: start.. 1 || hasSeenNewline { - // More than one newline is separating the two line comment blocks. - // We have reached the end of this block of line comments. - break LOOP - } - hasSeenNewline = true - case .spaces, .tabs: - // We allow spaces and tabs because the comments might be indented - continue - case .lineComment, .docLineComment: - // We have found a new line comment in this block. Commit it. - index = lookaheadIndex - start = lookaheadStart - hasSeenNewline = false - default: - // We assume that any other trivia piece terminates the block - // of line comments. - break LOOP - } - } - _ = self.addFoldingRange( - start: lineCommentBlockStart, - end: start + pieces[index].sourceLength.utf8Length, - kind: .comment - ) - default: - break - } - } - } - - override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.statements.position.utf8Offset, - end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset) - } - - override func visit(_ node: MemberBlockSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.members.position.utf8Offset, - end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset) - } - - override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.statements.position.utf8Offset, - end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset) - } - - override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.accessors.position.utf8Offset, - end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset) - } - - override func visit(_ node: SwitchExprSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.cases.position.utf8Offset, - end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset) - } - - override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.arguments.position.utf8Offset, - end: node.arguments.endPosition.utf8Offset) - } - - override func visit(_ node: SubscriptCallExprSyntax) -> SyntaxVisitorContinueKind { - return self.addFoldingRange( - start: node.arguments.position.utf8Offset, - end: node.arguments.endPosition.utf8Offset) - } - - __consuming func finalize() -> Set { - return self.ranges - } - - private func addFoldingRange(start: Int, end: Int, kind: FoldingRangeKind? = nil) -> SyntaxVisitorContinueKind { - if let limit = self.rangeLimit, self.ranges.count >= limit { - return .skipChildren + return true } - guard let start: Position = snapshot.positionOf(utf8Offset: start), - let end: Position = snapshot.positionOf(utf8Offset: end) else { - log("folding range failed to retrieve position of \(snapshot.document.uri): \(start)-\(end)", level: .warning) - return .visitChildren - } - let range: FoldingRange - if lineFoldingOnly { - // Since the client cannot fold less than a single line, if the - // fold would span 1 line there's no point in reporting it. - guard end.line > start.line else { - return .visitChildren - } - - // If the client only supports folding full lines, don't report - // the end of the range since there's nothing they could do with it. - range = FoldingRange(startLine: start.line, - startUTF16Index: nil, - endLine: end.line, - endUTF16Index: nil, - kind: kind) - } else { - range = FoldingRange(startLine: start.line, - startUTF16Index: start.utf16index, - endLine: end.line, - endUTF16Index: end.utf16index, - kind: kind) - } - ranges.insert(range) - return .visitChildren + req.reply(highlights) } + + // FIXME: cancellation + _ = handle } - - // If the limit is less than one, do nothing. - if let limit = foldingRangeCapabilities?.rangeLimit, limit <= 0 { - req.reply([]) - return - } - - let rangeFinder = FoldingRangeFinder( - snapshot: snapshot, - rangeLimit: foldingRangeCapabilities?.rangeLimit, - lineFoldingOnly: foldingRangeCapabilities?.lineFoldingOnly ?? false) - rangeFinder.walk(sourceFile) - let ranges = rangeFinder.finalize() - - req.reply(ranges.sorted()) } - public func codeAction(_ req: Request) async { + public func foldingRange(_ req: Request) { + let foldingRangeCapabilities = capabilityRegistry.clientCapabilities.textDocument?.foldingRange + queue.async { + guard let snapshot = self.documentManager.latestSnapshot(req.params.textDocument.uri) else { + log("failed to find snapshot for url \(req.params.textDocument.uri)") + req.reply(nil) + return + } + + guard let sourceFile = snapshot.tokens.syntaxTree else { + log("no lexical structure available for url \(req.params.textDocument.uri)") + req.reply(nil) + return + } + + final class FoldingRangeFinder: SyntaxVisitor { + private let snapshot: DocumentSnapshot + /// Some ranges might occur multiple times. + /// E.g. for `print("hi")`, `"hi"` is both the range of all call arguments and the range the first argument in the call. + /// It doesn't make sense to report them multiple times, so use a `Set` here. + private var ranges: Set + /// The client-imposed limit on the number of folding ranges it would + /// prefer to recieve from the LSP server. If the value is `nil`, there + /// is no preset limit. + private var rangeLimit: Int? + /// If `true`, the client is only capable of folding entire lines. If + /// `false` the client can handle folding ranges. + private var lineFoldingOnly: Bool + + init(snapshot: DocumentSnapshot, rangeLimit: Int?, lineFoldingOnly: Bool) { + self.snapshot = snapshot + self.ranges = [] + self.rangeLimit = rangeLimit + self.lineFoldingOnly = lineFoldingOnly + super.init(viewMode: .sourceAccurate) + } + + override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind { + // Index comments, so we need to see at least '/*', or '//'. + if node.leadingTriviaLength.utf8Length > 2 { + self.addTrivia(from: node, node.leadingTrivia) + } + + if node.trailingTriviaLength.utf8Length > 2 { + self.addTrivia(from: node, node.trailingTrivia) + } + + return .visitChildren + } + + private func addTrivia(from node: TokenSyntax, _ trivia: Trivia) { + let pieces = trivia.pieces + var start = node.position.utf8Offset + /// The index of the trivia piece we are currently inspecting. + var index = 0 + + while index < pieces.count { + let piece = pieces[index] + defer { + start += pieces[index].sourceLength.utf8Length + index += 1 + } + switch piece { + case .blockComment: + _ = self.addFoldingRange( + start: start, + end: start + piece.sourceLength.utf8Length, + kind: .comment + ) + case .docBlockComment: + _ = self.addFoldingRange( + start: start, + end: start + piece.sourceLength.utf8Length, + kind: .comment + ) + case .lineComment, .docLineComment: + let lineCommentBlockStart = start + + // Keep scanning the upcoming trivia pieces to find the end of the + // block of line comments. + // As we find a new end of the block comment, we set `index` and + // `start` to `lookaheadIndex` and `lookaheadStart` resp. to + // commit the newly found end. + var lookaheadIndex = index + var lookaheadStart = start + var hasSeenNewline = false + LOOP: while lookaheadIndex < pieces.count { + let piece = pieces[lookaheadIndex] + defer { + lookaheadIndex += 1 + lookaheadStart += piece.sourceLength.utf8Length + } + switch piece { + case .newlines(let count), .carriageReturns(let count), .carriageReturnLineFeeds(let count): + if count > 1 || hasSeenNewline { + // More than one newline is separating the two line comment blocks. + // We have reached the end of this block of line comments. + break LOOP + } + hasSeenNewline = true + case .spaces, .tabs: + // We allow spaces and tabs because the comments might be indented + continue + case .lineComment, .docLineComment: + // We have found a new line comment in this block. Commit it. + index = lookaheadIndex + start = lookaheadStart + hasSeenNewline = false + default: + // We assume that any other trivia piece terminates the block + // of line comments. + break LOOP + } + } + _ = self.addFoldingRange( + start: lineCommentBlockStart, + end: start + pieces[index].sourceLength.utf8Length, + kind: .comment + ) + default: + break + } + } + } + + override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind { + return self.addFoldingRange( + start: node.statements.position.utf8Offset, + end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset) + } + + override func visit(_ node: MemberBlockSyntax) -> SyntaxVisitorContinueKind { + return self.addFoldingRange( + start: node.members.position.utf8Offset, + end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset) + } + + override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + return self.addFoldingRange( + start: node.statements.position.utf8Offset, + end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset) + } + + override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind { + return self.addFoldingRange( + start: node.accessors.position.utf8Offset, + end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset) + } + + override func visit(_ node: SwitchExprSyntax) -> SyntaxVisitorContinueKind { + return self.addFoldingRange( + start: node.cases.position.utf8Offset, + end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset) + } + + override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind { + return self.addFoldingRange( + start: node.arguments.position.utf8Offset, + end: node.arguments.endPosition.utf8Offset) + } + + override func visit(_ node: SubscriptCallExprSyntax) -> SyntaxVisitorContinueKind { + return self.addFoldingRange( + start: node.arguments.position.utf8Offset, + end: node.arguments.endPosition.utf8Offset) + } + + __consuming func finalize() -> Set { + return self.ranges + } + + private func addFoldingRange(start: Int, end: Int, kind: FoldingRangeKind? = nil) -> SyntaxVisitorContinueKind { + if let limit = self.rangeLimit, self.ranges.count >= limit { + return .skipChildren + } + + guard let start: Position = snapshot.positionOf(utf8Offset: start), + let end: Position = snapshot.positionOf(utf8Offset: end) else { + log("folding range failed to retrieve position of \(snapshot.document.uri): \(start)-\(end)", level: .warning) + return .visitChildren + } + let range: FoldingRange + if lineFoldingOnly { + // Since the client cannot fold less than a single line, if the + // fold would span 1 line there's no point in reporting it. + guard end.line > start.line else { + return .visitChildren + } + + // If the client only supports folding full lines, don't report + // the end of the range since there's nothing they could do with it. + range = FoldingRange(startLine: start.line, + startUTF16Index: nil, + endLine: end.line, + endUTF16Index: nil, + kind: kind) + } else { + range = FoldingRange(startLine: start.line, + startUTF16Index: start.utf16index, + endLine: end.line, + endUTF16Index: end.utf16index, + kind: kind) + } + ranges.insert(range) + return .visitChildren + } + } + + // If the limit is less than one, do nothing. + if let limit = foldingRangeCapabilities?.rangeLimit, limit <= 0 { + req.reply([]) + return + } + + let rangeFinder = FoldingRangeFinder( + snapshot: snapshot, + rangeLimit: foldingRangeCapabilities?.rangeLimit, + lineFoldingOnly: foldingRangeCapabilities?.lineFoldingOnly ?? false) + rangeFinder.walk(sourceFile) + let ranges = rangeFinder.finalize() + + req.reply(ranges.sorted()) + } + } + + public func codeAction(_ req: Request) { let providersAndKinds: [(provider: CodeActionProvider, kind: CodeActionKind)] = [ (retrieveRefactorCodeActions, .refactor), (retrieveQuickFixCodeActions, .quickFix) @@ -1232,7 +1254,7 @@ extension SwiftLanguageServer { let wantedActionKinds = req.params.context.only let providers = providersAndKinds.filter { wantedActionKinds?.contains($0.1) != false } let codeActionCapabilities = capabilityRegistry.clientCapabilities.textDocument?.codeAction - await retrieveCodeActions(req, providers: providers.map { $0.provider }) { result in + retrieveCodeActions(req, providers: providers.map { $0.provider }) { result in switch result { case .success(let codeActions): let response = CodeActionRequestResponse(codeActions: codeActions, @@ -1244,45 +1266,36 @@ extension SwiftLanguageServer { } } - func retrieveCodeActions(_ req: Request, providers: [CodeActionProvider], completion: @escaping CodeActionProviderCompletion) async { + func retrieveCodeActions(_ req: Request, providers: [CodeActionProvider], completion: @escaping CodeActionProviderCompletion) { guard providers.isEmpty == false else { completion(.success([])) return } - let codeActions = await withTaskGroup(of: [CodeAction].self) { taskGroup in - for provider in providers { - taskGroup.addTask { - // FIXME: (async) Migrate `CodeActionProvider` to be async so that we - // don't need to do the `withCheckedContinuation` dance here. - await withCheckedContinuation { continuation in - Task { - await provider(req.params) { - switch $0 { - case .success(let actions): - continuation.resume(returning: actions) - case .failure: - continuation.resume(returning: []) - } - } - } + var codeActions = [CodeAction]() + let dispatchGroup = DispatchGroup() + (0.. Void) = { skreq in skreq[self.keys.retrieve_refactor_actions] = 1 } - await cursorInfo( + _cursorInfo( params.textDocument.uri, params.range, additionalParameters: additionalCursorInfoParameters) @@ -1370,9 +1383,9 @@ extension SwiftLanguageServer { completion(.success(codeActions)) } - public func inlayHint(_ req: Request) async { + public func inlayHint(_ req: Request) { let uri = req.params.textDocument.uri - await variableTypeInfos(uri, req.params.range) { infosResult in + variableTypeInfos(uri, req.params.range) { infosResult in do { let infos = try infosResult.get() let hints = infos @@ -1404,10 +1417,13 @@ extension SwiftLanguageServer { } } - public func documentDiagnostic( + // Must be called on self.queue + public func _documentDiagnostic( _ uri: DocumentURI, _ completion: @escaping (Result<[Diagnostic], ResponseError>) -> Void - ) async { + ) { + dispatchPrecondition(condition: .onQueue(queue)) + guard let snapshot = documentManager.latestSnapshot(uri) else { let msg = "failed to find snapshot for url \(uri)" log(msg) @@ -1421,12 +1437,8 @@ extension SwiftLanguageServer { skreq[keys.sourcefile] = snapshot.document.uri.pseudoPath // FIXME: SourceKit should probably cache this for us. - let areFallbackBuildSettings: Bool - if let buildSettings = await self.buildSettings(for: uri) { - skreq[keys.compilerargs] = buildSettings.compilerArgs - areFallbackBuildSettings = buildSettings.isFallback - } else { - areFallbackBuildSettings = true + if let compileCommand = self.commandsByFile[uri] { + skreq[keys.compilerargs] = compileCommand.compilerArgs } let handle = self.sourcekitd.send(skreq, self.queue) { response in @@ -1437,8 +1449,7 @@ extension SwiftLanguageServer { let diagnostics = self.registerDiagnostics( sourcekitdDiagnostics: dict[keys.diagnostics], snapshot: snapshot, - stage: .sema, - isFromFallbackBuildSettings: areFallbackBuildSettings + stage: .sema ) completion(.success(diagnostics)) @@ -1447,10 +1458,19 @@ extension SwiftLanguageServer { // FIXME: cancellation _ = handle } + + public func documentDiagnostic( + _ uri: DocumentURI, + _ completion: @escaping (Result<[Diagnostic], ResponseError>) -> Void + ) { + self.queue.async { + self._documentDiagnostic(uri, completion) + } + } - public func documentDiagnostic(_ req: Request) async { + public func documentDiagnostic(_ req: Request) { let uri = req.params.textDocument.uri - await documentDiagnostic(req.params.textDocument.uri) { result in + documentDiagnostic(req.params.textDocument.uri) { result in switch result { case .success(let diagnostics): req.reply(.full(.init(items: diagnostics))) @@ -1463,7 +1483,7 @@ extension SwiftLanguageServer { } } - public func executeCommand(_ req: Request) async { + public func executeCommand(_ req: Request) { let params = req.params //TODO: If there's support for several types of commands, we might need to structure this similarly to the code actions request. guard let swiftCommand = params.swiftCommand(ofType: SemanticRefactorCommand.self) else { @@ -1472,7 +1492,7 @@ extension SwiftLanguageServer { return req.reply(.failure(.unknown(message))) } let uri = swiftCommand.textDocument.uri - await semanticRefactoring(swiftCommand) { result in + semanticRefactoring(swiftCommand) { result in switch result { case .success(let refactor): let edit = refactor.edit @@ -1518,34 +1538,32 @@ extension SwiftLanguageServer { } extension SwiftLanguageServer: SKDNotificationHandler { - // FIXME: (async) Make this method isolated once `SKDNotificationHandler` has ben asyncified - public nonisolated func notification(_ notification: SKDResponse) { - Task { - await notificationImpl(notification) - } - } - - public func notificationImpl(_ notification: SKDResponse) async { + public func notification(_ notification: SKDResponse) { // Check if we need to update our `state` based on the contents of the notification. - if notification.value?[self.keys.notification] == self.values.notification_sema_enabled { - self.state = .connected - } + // Execute the entire code block on `queue` because we need to switch to `queue` anyway to + // check `state` in the second `if`. Moving `queue.async` up ensures we only need to switch + // queues once and makes the code inside easier to read. + self.queue.async { + if notification.value?[self.keys.notification] == self.values.notification_sema_enabled { + self.state = .connected + } - if self.state == .connectionInterrupted { - // If we get a notification while we are restoring the connection, it means that the server has restarted. - // We still need to wait for semantic functionality to come back up. - self.state = .semanticFunctionalityDisabled + if self.state == .connectionInterrupted { + // If we get a notification while we are restoring the connection, it means that the server has restarted. + // We still need to wait for semantic functionality to come back up. + self.state = .semanticFunctionalityDisabled - // Ask our parent to re-open all of our documents. - await self.reopenDocuments(self) - } + // Ask our parent to re-open all of our documents. + self.reopenDocuments(self) + } - if case .connectionInterrupted = notification.error { - self.state = .connectionInterrupted + if case .connectionInterrupted = notification.error { + self.state = .connectionInterrupted - // We don't have any open documents anymore after sourcekitd crashed. - // Reset the document manager to reflect that. - self.documentManager = DocumentManager() + // We don't have any open documents anymore after sourcekitd crashed. + // Reset the document manager to reflect that. + self.documentManager = DocumentManager() + } } guard let dict = notification.value else { @@ -1559,31 +1577,33 @@ extension SwiftLanguageServer: SKDNotificationHandler { kind == self.values.notification_documentupdate, let name: String = dict[self.keys.name] { - let uri: DocumentURI + self.queue.async { + let uri: DocumentURI - // Paths are expected to be absolute; on Windows, this means that the - // path is either drive letter prefixed (and thus `PathGetDriveNumberW` - // will provide the driver number OR it is a UNC path and `PathIsUNCW` - // will return `true`. On Unix platforms, the path will start with `/` - // which takes care of both a regular absolute path and a POSIX - // alternate root path. + // Paths are expected to be absolute; on Windows, this means that the + // path is either drive letter prefixed (and thus `PathGetDriveNumberW` + // will provide the driver number OR it is a UNC path and `PathIsUNCW` + // will return `true`. On Unix platforms, the path will start with `/` + // which takes care of both a regular absolute path and a POSIX + // alternate root path. - // TODO: this is not completely portable, e.g. MacOS 9 HFS paths are - // unhandled. + // TODO: this is not completely portable, e.g. MacOS 9 HFS paths are + // unhandled. #if os(Windows) - let isPath: Bool = name.withCString(encodedAs: UTF16.self) { - !PathIsURLW($0) - } + let isPath: Bool = name.withCString(encodedAs: UTF16.self) { + !PathIsURLW($0) + } #else - let isPath: Bool = name.starts(with: "/") + let isPath: Bool = name.starts(with: "/") #endif - if isPath { - // If sourcekitd returns us a path, translate it back into a URL - uri = DocumentURI(URL(fileURLWithPath: name)) - } else { - uri = DocumentURI(string: name) + if isPath { + // If sourcekitd returns us a path, translate it back into a URL + uri = DocumentURI(URL(fileURLWithPath: name)) + } else { + uri = DocumentURI(string: name) + } + self.handleDocumentUpdate(uri: uri) } - await self.handleDocumentUpdate(uri: uri) } } } diff --git a/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift b/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift index 77efce40..81225554 100644 --- a/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift +++ b/Sources/SourceKitLSP/Swift/VariableTypeInfo.swift @@ -84,29 +84,16 @@ enum VariableTypeInfoError: Error, Equatable { } extension SwiftLanguageServer { - /// Provides typed variable declarations in a document. - /// - /// - Parameters: - /// - url: Document URL in which to perform the request. Must be an open document. - /// - completion: Completion block to asynchronously receive the VariableTypeInfos, or error. - func variableTypeInfos( + /// Must be called on self.queue. + private func _variableTypeInfos( _ uri: DocumentURI, _ range: Range? = nil, _ completion: @escaping (Swift.Result<[VariableTypeInfo], VariableTypeInfoError>) -> Void - ) async { - guard var snapshot = documentManager.latestSnapshot(uri) else { - return completion(.failure(.unknownDocument(uri))) - } + ) { + dispatchPrecondition(condition: .onQueue(queue)) - // FIXME: (async) We might not have computed the syntax tree yet. Wait until we have a syntax tree. - // Really, getting the syntax tree should be an async operation. - while snapshot.tokens.syntaxTree == nil { - try? await Task.sleep(nanoseconds: 1_000_000) - if let newSnapshot = documentManager.latestSnapshot(uri) { - snapshot = newSnapshot - } else { - return completion(.failure(.unknownDocument(uri))) - } + guard let snapshot = documentManager.latestSnapshot(uri) else { + return completion(.failure(.unknownDocument(uri))) } let keys = self.keys @@ -123,7 +110,7 @@ extension SwiftLanguageServer { } // FIXME: SourceKit should probably cache this for us - if let compileCommand = await self.buildSettings(for: uri) { + if let compileCommand = self.commandsByFile[uri] { skreq[keys.compilerargs] = compileCommand.compilerArgs } @@ -154,4 +141,19 @@ extension SwiftLanguageServer { // FIXME: cancellation _ = handle } + + /// Provides typed variable declarations in a document. + /// + /// - Parameters: + /// - url: Document URL in which to perform the request. Must be an open document. + /// - completion: Completion block to asynchronously receive the VariableTypeInfos, or error. + func variableTypeInfos( + _ uri: DocumentURI, + _ range: Range? = nil, + _ completion: @escaping (Swift.Result<[VariableTypeInfo], VariableTypeInfoError>) -> Void + ) { + queue.async { + self._variableTypeInfos(uri, range, completion) + } + } } diff --git a/Sources/SourceKitLSP/ToolchainLanguageServer.swift b/Sources/SourceKitLSP/ToolchainLanguageServer.swift index a6cd77da..23482a62 100644 --- a/Sources/SourceKitLSP/ToolchainLanguageServer.swift +++ b/Sources/SourceKitLSP/ToolchainLanguageServer.swift @@ -34,77 +34,74 @@ public protocol ToolchainLanguageServer: AnyObject { toolchain: Toolchain, options: SourceKitServer.Options, workspace: Workspace, - reopenDocuments: @escaping (ToolchainLanguageServer) async -> Void, - workspaceForDocument: @escaping (DocumentURI) async -> Workspace? - ) async throws + reopenDocuments: @escaping (ToolchainLanguageServer) -> Void + ) throws /// Returns `true` if this instance of the language server can handle opening documents in `workspace`. - /// /// If this returns `false`, a new language server will be started for `workspace`. func canHandle(workspace: Workspace) -> Bool // MARK: - Lifetime - func initializeSync(_ initialize: InitializeRequest) async throws -> InitializeResult - func clientInitialized(_ initialized: InitializedNotification) async - - /// Shut the server down and return once the server has finished shutting down - func shutdown() async + func initializeSync(_ initialize: InitializeRequest) throws -> InitializeResult + func clientInitialized(_ initialized: InitializedNotification) + /// `callback` will be called when the server has finished shutting down. + func shutdown(callback: @escaping () -> Void) /// Add a handler that is called whenever the state of the language server changes. - func addStateChangeHandler(handler: @escaping (_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void) async + func addStateChangeHandler(handler: @escaping (_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void) // MARK: - Text synchronization /// Sent to open up a document on the Language Server. /// This may be called before or after a corresponding /// `documentUpdatedBuildSettings` call for the same document. - func openDocument(_ note: DidOpenTextDocumentNotification) async + func openDocument(_ note: DidOpenTextDocumentNotification) /// Sent to close a document on the Language Server. - func closeDocument(_ note: DidCloseTextDocumentNotification) async - func changeDocument(_ note: DidChangeTextDocumentNotification) async - func willSaveDocument(_ note: WillSaveTextDocumentNotification) async - func didSaveDocument(_ note: DidSaveTextDocumentNotification) async + func closeDocument(_ note: DidCloseTextDocumentNotification) + func changeDocument(_ note: DidChangeTextDocumentNotification) + func willSaveDocument(_ note: WillSaveTextDocumentNotification) + func didSaveDocument(_ note: DidSaveTextDocumentNotification) // MARK: - Build System Integration /// Sent when the `BuildSystem` has resolved build settings, such as for the intial build settings /// or when the settings have changed (e.g. modified build system files). This may be sent before /// the respective `DocumentURI` has been opened. - func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) async + func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) /// Sent when the `BuildSystem` has detected that dependencies of the given file have changed /// (e.g. header files, swiftmodule files, other compiler input files). - func documentDependenciesUpdated(_ uri: DocumentURI) async + func documentDependenciesUpdated(_ uri: DocumentURI) // MARK: - Text Document - func completion(_ req: Request) async - func hover(_ req: Request) async - func symbolInfo(_ request: Request) async - func openInterface(_ request: Request) async + func completion(_ req: Request) + func hover(_ req: Request) + func symbolInfo(_ request: Request) + func openInterface(_ request: Request) /// Returns true if the `ToolchainLanguageServer` will take ownership of the request. - func definition(_ request: Request) async -> Bool - func declaration(_ request: Request) async -> Bool + func definition(_ request: Request) -> Bool + func declaration(_ request: Request) -> Bool - func documentSymbolHighlight(_ req: Request) async - func foldingRange(_ req: Request) async - func documentSymbol(_ req: Request) async - func documentColor(_ req: Request) async - func documentSemanticTokens(_ req: Request) async - func documentSemanticTokensDelta(_ req: Request) async - func documentSemanticTokensRange(_ req: Request) async - func colorPresentation(_ req: Request) async - func codeAction(_ req: Request) async - func inlayHint(_ req: Request) async - func documentDiagnostic(_ req: Request) async + func documentSymbolHighlight(_ req: Request) + func foldingRange(_ req: Request) + func documentSymbol(_ req: Request) + func documentColor(_ req: Request) + func documentSemanticTokens(_ req: Request) + func documentSemanticTokensDelta(_ req: Request) + func documentSemanticTokensRange(_ req: Request) + func colorPresentation(_ req: Request) + func codeAction(_ req: Request) + func inlayHint(_ req: Request) + func documentDiagnostic(_ req: Request) // MARK: - Other - func executeCommand(_ req: Request) async + func executeCommand(_ req: Request) /// Crash the language server. Should be used for crash recovery testing only. - func _crash() async + func _crash() } diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 646a1587..9884daa6 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -19,15 +19,6 @@ import SKSwiftPMWorkspace import struct TSCBasic.AbsolutePath -/// Same as `??` but allows the right-hand side of the operator to 'await'. -fileprivate func firstNonNil(_ optional: T?, _ defaultValue: @autoclosure () async throws -> T) async rethrows -> T { - if let optional { - return optional - } - return try await defaultValue() -} - - /// Represents the configuration and state of a project or combination of projects being worked on /// together. /// @@ -66,18 +57,19 @@ public final class Workspace { buildSetup: BuildSetup, underlyingBuildSystem: BuildSystem?, index: IndexStoreDB?, - indexDelegate: SourceKitIndexDelegate? - ) async { + indexDelegate: SourceKitIndexDelegate?) + { self.documentManager = documentManager self.buildSetup = buildSetup self.rootUri = rootUri self.capabilityRegistry = capabilityRegistry self.index = index - self.buildSystemManager = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: underlyingBuildSystem, fallbackBuildSystem: FallbackBuildSystem(buildSetup: buildSetup), mainFilesProvider: index) - indexDelegate?.registerMainFileChanged(buildSystemManager) + indexDelegate?.registerMainFileChanged(bsm) + self.buildSystemManager = bsm } /// Creates a workspace for a given root `URL`, inferring the `ExternalWorkspace` if possible. @@ -94,12 +86,12 @@ public final class Workspace { buildSetup: BuildSetup, indexOptions: IndexOptions = IndexOptions(), reloadPackageStatusCallback: @escaping (ReloadPackageStatus) -> Void - ) async throws { + ) throws { var buildSystem: BuildSystem? = nil if let rootUrl = rootUri.fileURL, let rootPath = try? AbsolutePath(validating: rootUrl.path) { - if let buildServer = await BuildServerBuildSystem(projectRoot: rootPath, buildSetup: buildSetup) { + if let buildServer = BuildServerBuildSystem(projectRoot: rootPath, buildSetup: buildSetup) { buildSystem = buildServer - } else if let swiftpm = await SwiftPMWorkspace( + } else if let swiftpm = SwiftPMWorkspace( url: rootUrl, toolchainRegistry: toolchainRegistry, buildSetup: buildSetup, @@ -118,14 +110,14 @@ public final class Workspace { var index: IndexStoreDB? = nil var indexDelegate: SourceKitIndexDelegate? = nil - if let storePath = await firstNonNil(indexOptions.indexStorePath, await buildSystem?.indexStorePath), - let dbPath = await firstNonNil(indexOptions.indexDatabasePath, await buildSystem?.indexDatabasePath), + if let storePath = indexOptions.indexStorePath ?? buildSystem?.indexStorePath, + let dbPath = indexOptions.indexDatabasePath ?? buildSystem?.indexDatabasePath, let libPath = toolchainRegistry.default?.libIndexStore { do { let lib = try IndexStoreLibrary(dylibPath: libPath.pathString) indexDelegate = SourceKitIndexDelegate() - let prefixMappings = await firstNonNil(indexOptions.indexPrefixMappings, await buildSystem?.indexPrefixMappings) ?? [] + let prefixMappings = indexOptions.indexPrefixMappings ?? buildSystem?.indexPrefixMappings ?? [] index = try IndexStoreDB( storePath: storePath.pathString, databasePath: dbPath.pathString, @@ -139,7 +131,7 @@ public final class Workspace { } } - await self.init( + self.init( documentManager: documentManager, rootUri: rootUri, capabilityRegistry: capabilityRegistry, @@ -151,15 +143,6 @@ public final class Workspace { } } -/// Wrapper around a workspace that isn't being retained. -struct WeakWorkspace { - weak var value: Workspace? - - init(_ value: Workspace? = nil) { - self.value = value - } -} - public struct IndexOptions { /// Override the index-store-path provided by the build system. diff --git a/Sources/sourcekit-lsp/SourceKitLSP.swift b/Sources/sourcekit-lsp/SourceKitLSP.swift index 68d754f4..bb1ee77a 100644 --- a/Sources/sourcekit-lsp/SourceKitLSP.swift +++ b/Sources/sourcekit-lsp/SourceKitLSP.swift @@ -204,7 +204,7 @@ struct SourceKitLSP: ParsableCommand { clientConnection.close() }) clientConnection.start(receiveHandler: server, closeHandler: { - await server.prepareForExit() + server.prepareForExit() // FIXME: keep the FileHandle alive until we close the connection to // workaround SR-13822. withExtendedLifetime(realStdoutHandle) {} diff --git a/Tests/SKCoreTests/BuildServerBuildSystemTests.swift b/Tests/SKCoreTests/BuildServerBuildSystemTests.swift index 7e2bcacd..aafaa689 100644 --- a/Tests/SKCoreTests/BuildServerBuildSystemTests.swift +++ b/Tests/SKCoreTests/BuildServerBuildSystemTests.swift @@ -27,21 +27,39 @@ final class BuildServerBuildSystemTests: XCTestCase { } let buildFolder = try! AbsolutePath(validating: NSTemporaryDirectory()) - func testServerInitialize() async throws { - let buildSystem = try await BuildServerBuildSystem(projectRoot: root, buildFolder: buildFolder) + func testServerInitialize() throws { + let buildSystem = try BuildServerBuildSystem(projectRoot: root, buildFolder: buildFolder) - assertEqual( - await buildSystem.indexDatabasePath, + XCTAssertEqual( + buildSystem.indexDatabasePath, try AbsolutePath(validating: "some/index/db/path", relativeTo: root) ) - assertEqual( - await buildSystem.indexStorePath, + XCTAssertEqual( + buildSystem.indexStorePath, try AbsolutePath(validating: "some/index/store/path", relativeTo: root) ) } - func testFileRegistration() async throws { - let buildSystem = try await BuildServerBuildSystem(projectRoot: root, buildFolder: buildFolder) + func testSettings() throws { +#if os(Windows) + try XCTSkipIf(true, "hang") +#endif + let buildSystem = try BuildServerBuildSystem(projectRoot: root, buildFolder: buildFolder) + + // test settings with a response + let fileURL = URL(fileURLWithPath: "/path/to/some/file.swift") + let settings = buildSystem._settings(for: DocumentURI(fileURL)) + XCTAssertNotNil(settings) + XCTAssertEqual(settings?.compilerArguments, ["-a", "-b"]) + XCTAssertEqual(settings?.workingDirectory, fileURL.deletingLastPathComponent().path) + + // test error + let missingFileURL = URL(fileURLWithPath: "/path/to/some/missingfile.missing") + XCTAssertNil(buildSystem._settings(for: DocumentURI(missingFileURL))) + } + + func testFileRegistration() throws { + let buildSystem = try BuildServerBuildSystem(projectRoot: root, buildFolder: buildFolder) let fileUrl = URL(fileURLWithPath: "/some/file/path") let expectation = XCTestExpectation(description: "\(fileUrl) settings updated") @@ -50,14 +68,14 @@ final class BuildServerBuildSystemTests: XCTestCase { // BuildSystemManager has a weak reference to delegate. Keep it alive. _fixLifetime(buildSystemDelegate) } - await buildSystem.setDelegate(buildSystemDelegate) - await buildSystem.registerForChangeNotifications(for: DocumentURI(fileUrl), language: .swift) + buildSystem.delegate = buildSystemDelegate + buildSystem.registerForChangeNotifications(for: DocumentURI(fileUrl), language: .swift) XCTAssertEqual(XCTWaiter.wait(for: [expectation], timeout: defaultTimeout), .completed) } - func testBuildTargetsChanged() async throws { - let buildSystem = try await BuildServerBuildSystem(projectRoot: root, buildFolder: buildFolder) + func testBuildTargetsChanged() throws { + let buildSystem = try BuildServerBuildSystem(projectRoot: root, buildFolder: buildFolder) let fileUrl = URL(fileURLWithPath: "/some/file/path") let expectation = XCTestExpectation(description: "target changed") @@ -71,10 +89,14 @@ final class BuildServerBuildSystemTests: XCTestCase { // BuildSystemManager has a weak reference to delegate. Keep it alive. _fixLifetime(buildSystemDelegate) } - await buildSystem.setDelegate(buildSystemDelegate) - await buildSystem.registerForChangeNotifications(for: DocumentURI(fileUrl), language: .swift) + buildSystem.delegate = buildSystemDelegate + buildSystem.registerForChangeNotifications(for: DocumentURI(fileUrl), language: .swift) - try await fulfillmentOfOrThrow([expectation]) + let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout) + guard result == .completed else { + XCTFail("error \(result) waiting for targets changed notification") + return + } } } diff --git a/Tests/SKCoreTests/BuildSystemManagerTests.swift b/Tests/SKCoreTests/BuildSystemManagerTests.swift index dacb9db1..837133b4 100644 --- a/Tests/SKCoreTests/BuildSystemManagerTests.swift +++ b/Tests/SKCoreTests/BuildSystemManagerTests.swift @@ -19,7 +19,7 @@ import XCTest final class BuildSystemManagerTests: XCTestCase { - func testMainFiles() async { + func testMainFiles() { let a = DocumentURI(string: "bsm:a") let b = DocumentURI(string: "bsm:b") let c = DocumentURI(string: "bsm:c") @@ -33,26 +33,26 @@ final class BuildSystemManagerTests: XCTestCase { d: Set([d]), ] - let bsm = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: FallbackBuildSystem(buildSetup: .default), fallbackBuildSystem: nil, mainFilesProvider: mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. - await assertEqual(bsm._cachedMainFile(for: a), nil) - await assertEqual(bsm._cachedMainFile(for: b), nil) - await assertEqual(bsm._cachedMainFile(for: c), nil) - await assertEqual(bsm._cachedMainFile(for: d), nil) + XCTAssertEqual(bsm._cachedMainFile(for: a), nil) + XCTAssertEqual(bsm._cachedMainFile(for: b), nil) + XCTAssertEqual(bsm._cachedMainFile(for: c), nil) + XCTAssertEqual(bsm._cachedMainFile(for: d), nil) - await bsm.registerForChangeNotifications(for: a, language: .c) - await bsm.registerForChangeNotifications(for: b, language: .c) - await bsm.registerForChangeNotifications(for: c, language: .c) - await bsm.registerForChangeNotifications(for: d, language: .c) - await assertEqual(bsm._cachedMainFile(for: a), c) - let bMain = await bsm._cachedMainFile(for: b) + bsm.registerForChangeNotifications(for: a, language: .c) + bsm.registerForChangeNotifications(for: b, language: .c) + bsm.registerForChangeNotifications(for: c, language: .c) + bsm.registerForChangeNotifications(for: d, language: .c) + XCTAssertEqual(bsm._cachedMainFile(for: a), c) + let bMain = bsm._cachedMainFile(for: b) XCTAssert(Set([c, d]).contains(bMain)) - await assertEqual(bsm._cachedMainFile(for: c), c) - await assertEqual(bsm._cachedMainFile(for: d), d) + XCTAssertEqual(bsm._cachedMainFile(for: c), c) + XCTAssertEqual(bsm._cachedMainFile(for: d), d) mainFiles.mainFiles = [ a: Set([a]), @@ -61,197 +61,197 @@ final class BuildSystemManagerTests: XCTestCase { d: Set([d]), ] - await assertEqual(bsm._cachedMainFile(for: a), c) - await assertEqual(bsm._cachedMainFile(for: b), bMain) - await assertEqual(bsm._cachedMainFile(for: c), c) - await assertEqual(bsm._cachedMainFile(for: d), d) + XCTAssertEqual(bsm._cachedMainFile(for: a), c) + XCTAssertEqual(bsm._cachedMainFile(for: b), bMain) + XCTAssertEqual(bsm._cachedMainFile(for: c), c) + XCTAssertEqual(bsm._cachedMainFile(for: d), d) - await bsm.mainFilesChangedImpl() + bsm.mainFilesChanged() - await assertEqual(bsm._cachedMainFile(for: a), a) - await assertEqual(bsm._cachedMainFile(for: b), bMain) // never changes to a - await assertEqual(bsm._cachedMainFile(for: c), c) - await assertEqual(bsm._cachedMainFile(for: d), d) + XCTAssertEqual(bsm._cachedMainFile(for: a), a) + XCTAssertEqual(bsm._cachedMainFile(for: b), bMain) // never changes to a + XCTAssertEqual(bsm._cachedMainFile(for: c), c) + XCTAssertEqual(bsm._cachedMainFile(for: d), d) - await bsm.unregisterForChangeNotifications(for: a) - await assertEqual(bsm._cachedMainFile(for: a), nil) - await assertEqual(bsm._cachedMainFile(for: b), bMain) // never changes to a - await assertEqual(bsm._cachedMainFile(for: c), c) - await assertEqual(bsm._cachedMainFile(for: d), d) + bsm.unregisterForChangeNotifications(for: a) + XCTAssertEqual(bsm._cachedMainFile(for: a), nil) + XCTAssertEqual(bsm._cachedMainFile(for: b), bMain) // never changes to a + XCTAssertEqual(bsm._cachedMainFile(for: c), c) + XCTAssertEqual(bsm._cachedMainFile(for: d), d) - await bsm.unregisterForChangeNotifications(for: b) - await bsm.mainFilesChangedImpl() - await bsm.unregisterForChangeNotifications(for: c) - await bsm.unregisterForChangeNotifications(for: d) - await assertEqual(bsm._cachedMainFile(for: a), nil) - await assertEqual(bsm._cachedMainFile(for: b), nil) - await assertEqual(bsm._cachedMainFile(for: c), nil) - await assertEqual(bsm._cachedMainFile(for: d), nil) + bsm.unregisterForChangeNotifications(for: b) + bsm.mainFilesChanged() + bsm.unregisterForChangeNotifications(for: c) + bsm.unregisterForChangeNotifications(for: d) + XCTAssertEqual(bsm._cachedMainFile(for: a), nil) + XCTAssertEqual(bsm._cachedMainFile(for: b), nil) + XCTAssertEqual(bsm._cachedMainFile(for: c), nil) + XCTAssertEqual(bsm._cachedMainFile(for: d), nil) } - func testSettingsMainFile() async throws { + func testSettingsMainFile() { let a = DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider() mainFiles.mainFiles = [a: Set([a])] let bs = ManualBuildSystem() - let bsm = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: bs, fallbackBuildSystem: nil, mainFilesProvider: mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. - let del = await BSMDelegate(bsm) + let del = BSMDelegate(bsm) bs.map[a] = FileBuildSettings(compilerArguments: ["x"]) let initial = expectation(description: "initial settings") - await del.setExpected([(a, bs.map[a]!, initial, #file, #line)]) - await bsm.registerForChangeNotifications(for: a, language: .swift) - try await fulfillmentOfOrThrow([initial]) + del.expected = [(a, bs.map[a]!, initial, #file, #line)] + bsm.registerForChangeNotifications(for: a, language: .swift) + wait(for: [initial], timeout: defaultTimeout, enforceOrder: true) bs.map[a] = nil let changed = expectation(description: "changed settings") - await del.setExpected([(a, nil, changed, #file, #line)]) + del.expected = [(a, nil, changed, #file, #line)] bsm.fileBuildSettingsChanged([a: .removedOrUnavailable]) - try await fulfillmentOfOrThrow([changed]) + wait(for: [changed], timeout: defaultTimeout, enforceOrder: true) } - func testSettingsMainFileInitialNil() async throws { + func testSettingsMainFileInitialNil() { let a = DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider() mainFiles.mainFiles = [a: Set([a])] let bs = ManualBuildSystem() - let bsm = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: bs, fallbackBuildSystem: nil, mainFilesProvider: mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. - let del = await BSMDelegate(bsm) + let del = BSMDelegate(bsm) let initial = expectation(description: "initial settings") - await del.setExpected([(a, nil, initial, #file, #line)]) - await bsm.registerForChangeNotifications(for: a, language: .swift) - try await fulfillmentOfOrThrow([initial]) + del.expected = [(a, nil, initial, #file, #line)] + bsm.registerForChangeNotifications(for: a, language: .swift) + wait(for: [initial], timeout: defaultTimeout, enforceOrder: true) bs.map[a] = FileBuildSettings(compilerArguments: ["x"]) let changed = expectation(description: "changed settings") - await del.setExpected([(a, bs.map[a]!, changed, #file, #line)]) + del.expected = [(a, bs.map[a]!, changed, #file, #line)] bsm.fileBuildSettingsChanged([a: .modified(bs.map[a]!)]) - try await fulfillmentOfOrThrow([changed]) + wait(for: [changed], timeout: defaultTimeout, enforceOrder: true) } - func testSettingsMainFileWithFallback() async throws { + func testSettingsMainFileWithFallback() { let a = DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider() mainFiles.mainFiles = [a: Set([a])] let bs = ManualBuildSystem() let fallback = FallbackBuildSystem(buildSetup: .default) - let bsm = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: bs, fallbackBuildSystem: fallback, mainFilesProvider: mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. - let del = await BSMDelegate(bsm) - let fallbackSettings = fallback.buildSettings(for: a, language: .swift) + let del = BSMDelegate(bsm) + let fallbackSettings = fallback.settings(for: a, .swift) let initial = expectation(description: "initial fallback settings") - await del.setExpected([(a, fallbackSettings, initial, #file, #line)]) - await bsm.registerForChangeNotifications(for: a, language: .swift) - try await fulfillmentOfOrThrow([initial]) + del.expected = [(a, fallbackSettings, initial, #file, #line)] + bsm.registerForChangeNotifications(for: a, language: .swift) + wait(for: [initial], timeout: defaultTimeout, enforceOrder: true) bs.map[a] = FileBuildSettings(compilerArguments: ["non-fallback", "args"]) let changed = expectation(description: "changed settings") - await del.setExpected([(a, bs.map[a]!, changed, #file, #line)]) + del.expected = [(a, bs.map[a]!, changed, #file, #line)] bsm.fileBuildSettingsChanged([a: .modified(bs.map[a]!)]) - try await fulfillmentOfOrThrow([changed]) + wait(for: [changed], timeout: defaultTimeout, enforceOrder: true) bs.map[a] = nil let revert = expectation(description: "revert to fallback settings") - await del.setExpected([(a, fallbackSettings, revert, #file, #line)]) + del.expected = [(a, fallbackSettings, revert, #file, #line)] bsm.fileBuildSettingsChanged([a: .removedOrUnavailable]) - try await fulfillmentOfOrThrow([revert]) + wait(for: [revert], timeout: defaultTimeout, enforceOrder: true) } - func testSettingsMainFileInitialIntersect() async throws { + func testSettingsMainFileInitialIntersect() { let a = DocumentURI(string: "bsm:a.swift") let b = DocumentURI(string: "bsm:b.swift") let mainFiles = ManualMainFilesProvider() mainFiles.mainFiles = [a: Set([a]), b: Set([b])] let bs = ManualBuildSystem() - let bsm = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: bs, fallbackBuildSystem: nil, mainFilesProvider: mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. - let del = await BSMDelegate(bsm) + let del = BSMDelegate(bsm) bs.map[a] = FileBuildSettings(compilerArguments: ["x"]) bs.map[b] = FileBuildSettings(compilerArguments: ["y"]) let initial = expectation(description: "initial settings") - await del.setExpected([(a, bs.map[a]!, initial, #file, #line)]) - await bsm.registerForChangeNotifications(for: a, language: .swift) - try await fulfillmentOfOrThrow([initial]) + del.expected = [(a, bs.map[a]!, initial, #file, #line)] + bsm.registerForChangeNotifications(for: a, language: .swift) + wait(for: [initial], timeout: defaultTimeout, enforceOrder: true) let initialB = expectation(description: "initial settings") - await del.setExpected([(b, bs.map[b]!, initialB, #file, #line)]) - await bsm.registerForChangeNotifications(for: b, language: .swift) - try await fulfillmentOfOrThrow([initialB]) + del.expected = [(b, bs.map[b]!, initialB, #file, #line)] + bsm.registerForChangeNotifications(for: b, language: .swift) + wait(for: [initialB], timeout: defaultTimeout, enforceOrder: true) bs.map[a] = FileBuildSettings(compilerArguments: ["xx"]) bs.map[b] = FileBuildSettings(compilerArguments: ["yy"]) let changed = expectation(description: "changed settings") - await del.setExpected([(a, bs.map[a]!, changed, #file, #line)]) + del.expected = [(a, bs.map[a]!, changed, #file, #line)] bsm.fileBuildSettingsChanged([a: .modified(bs.map[a]!)]) - try await fulfillmentOfOrThrow([changed]) + wait(for: [changed], timeout: defaultTimeout, enforceOrder: true) // Test multiple changes. bs.map[a] = FileBuildSettings(compilerArguments: ["xxx"]) bs.map[b] = FileBuildSettings(compilerArguments: ["yyy"]) let changedBothA = expectation(description: "changed setting a") let changedBothB = expectation(description: "changed setting b") - await del.setExpected([ + del.expected = [ (a, bs.map[a]!, changedBothA, #file, #line), (b, bs.map[b]!, changedBothB, #file, #line), - ]) + ] bsm.fileBuildSettingsChanged([ a:. modified(bs.map[a]!), b: .modified(bs.map[b]!) ]) - try await fulfillmentOfOrThrow([changedBothA, changedBothB]) + wait(for: [changedBothA, changedBothB], timeout: defaultTimeout, enforceOrder: false) } - func testSettingsMainFileUnchanged() async throws { + func testSettingsMainFileUnchanged() { let a = DocumentURI(string: "bsm:a.swift") let b = DocumentURI(string: "bsm:b.swift") let mainFiles = ManualMainFilesProvider() mainFiles.mainFiles = [a: Set([a]), b: Set([b])] let bs = ManualBuildSystem() - let bsm = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: bs, fallbackBuildSystem: nil, mainFilesProvider: mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. - let del = await BSMDelegate(bsm) + let del = BSMDelegate(bsm) bs.map[a] = FileBuildSettings(compilerArguments: ["a"]) bs.map[b] = FileBuildSettings(compilerArguments: ["b"]) let initialA = expectation(description: "initial settings a") - await del.setExpected([(a, bs.map[a]!, initialA, #file, #line)]) - await bsm.registerForChangeNotifications(for: a, language: .swift) - try await fulfillmentOfOrThrow([initialA]) + del.expected = [(a, bs.map[a]!, initialA, #file, #line)] + bsm.registerForChangeNotifications(for: a, language: .swift) + wait(for: [initialA], timeout: defaultTimeout, enforceOrder: true) let initialB = expectation(description: "initial settings b") - await del.setExpected([(b, bs.map[b]!, initialB, #file, #line)]) - await bsm.registerForChangeNotifications(for: b, language: .swift) - try await fulfillmentOfOrThrow([initialB]) + del.expected = [(b, bs.map[b]!, initialB, #file, #line)] + bsm.registerForChangeNotifications(for: b, language: .swift) + wait(for: [initialB], timeout: defaultTimeout, enforceOrder: true) bs.map[a] = nil bs.map[b] = nil let changed = expectation(description: "changed settings") - await del.setExpected([(b, nil, changed, #file, #line)]) + del.expected = [(b, nil, changed, #file, #line)] bsm.fileBuildSettingsChanged([ b: .removedOrUnavailable ]) - try await fulfillmentOfOrThrow([changed]) + wait(for: [changed], timeout: defaultTimeout, enforceOrder: true) } - func testSettingsHeaderChangeMainFile() async throws { + func testSettingsHeaderChangeMainFile() { let h = DocumentURI(string: "bsm:header.h") let cpp1 = DocumentURI(string: "bsm:main.cpp") let cpp2 = DocumentURI(string: "bsm:other.cpp") @@ -263,51 +263,51 @@ final class BuildSystemManagerTests: XCTestCase { ] let bs = ManualBuildSystem() - let bsm = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: bs, fallbackBuildSystem: nil, mainFilesProvider: mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. - let del = await BSMDelegate(bsm) + let del = BSMDelegate(bsm) bs.map[cpp1] = FileBuildSettings(compilerArguments: ["C++ 1"]) bs.map[cpp2] = FileBuildSettings(compilerArguments: ["C++ 2"]) let initial = expectation(description: "initial settings via cpp1") - await del.setExpected([(h, bs.map[cpp1]!, initial, #file, #line)]) - await bsm.registerForChangeNotifications(for: h, language: .c) - try await fulfillmentOfOrThrow([initial]) + del.expected = [(h, bs.map[cpp1]!, initial, #file, #line)] + bsm.registerForChangeNotifications(for: h, language: .c) + wait(for: [initial], timeout: defaultTimeout, enforceOrder: true) mainFiles.mainFiles[h] = Set([cpp2]) let changed = expectation(description: "changed settings to cpp2") - await del.setExpected([(h, bs.map[cpp2]!, changed, #file, #line)]) - await bsm.mainFilesChangedImpl() - try await fulfillmentOfOrThrow([changed]) + del.expected = [(h, bs.map[cpp2]!, changed, #file, #line)] + bsm.mainFilesChanged() + wait(for: [changed], timeout: defaultTimeout, enforceOrder: true) let changed2 = expectation(description: "still cpp2, no update") changed2.isInverted = true - await del.setExpected([(h, nil, changed2, #file, #line)]) - await bsm.mainFilesChangedImpl() - try await fulfillmentOfOrThrow([changed2], timeout: 1) + del.expected = [(h, nil, changed2, #file, #line)] + bsm.mainFilesChanged() + wait(for: [changed2], timeout: 1, enforceOrder: true) mainFiles.mainFiles[h] = Set([cpp1, cpp2]) let changed3 = expectation(description: "added main file, no update") changed3.isInverted = true - await del.setExpected([(h, nil, changed3, #file, #line)]) - await bsm.mainFilesChangedImpl() - try await fulfillmentOfOrThrow([changed3], timeout: 1) + del.expected = [(h, nil, changed3, #file, #line)] + bsm.mainFilesChanged() + wait(for: [changed3], timeout: 1, enforceOrder: true) mainFiles.mainFiles[h] = Set([]) let changed4 = expectation(description: "changed settings to []") - await del.setExpected([(h, nil, changed4, #file, #line)]) - await bsm.mainFilesChangedImpl() - try await fulfillmentOfOrThrow([changed4]) + del.expected = [(h, nil, changed4, #file, #line)] + bsm.mainFilesChanged() + wait(for: [changed4], timeout: defaultTimeout, enforceOrder: true) } - func testSettingsOneMainTwoHeader() async throws { + func testSettingsOneMainTwoHeader() { let h1 = DocumentURI(string: "bsm:header1.h") let h2 = DocumentURI(string: "bsm:header2.h") let cpp = DocumentURI(string: "bsm:main.cpp") @@ -318,12 +318,12 @@ final class BuildSystemManagerTests: XCTestCase { ] let bs = ManualBuildSystem() - let bsm = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: bs, fallbackBuildSystem: nil, mainFilesProvider: mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. - let del = await BSMDelegate(bsm) + let del = BSMDelegate(bsm) let cppArg = "C++ Main File" bs.map[cpp] = FileBuildSettings(compilerArguments: [cppArg, cpp.pseudoPath]) @@ -332,17 +332,17 @@ final class BuildSystemManagerTests: XCTestCase { let initial2 = expectation(description: "initial settings h2 via cpp") let expectedArgsH1 = FileBuildSettings(compilerArguments: ["-xc++", cppArg, h1.pseudoPath]) let expectedArgsH2 = FileBuildSettings(compilerArguments: ["-xc++", cppArg, h2.pseudoPath]) - await del.setExpected([ + del.expected = [ (h1, expectedArgsH1, initial1, #file, #line), (h2, expectedArgsH2, initial2, #file, #line), - ]) + ] - await bsm.registerForChangeNotifications(for: h1, language: .c) - await bsm.registerForChangeNotifications(for: h2, language: .c) + bsm.registerForChangeNotifications(for: h1, language: .c) + bsm.registerForChangeNotifications(for: h2, language: .c) // Since the registration is async, it's possible that they get grouped together // since they are backed by the same underlying cpp file. - try await fulfillmentOfOrThrow([initial1, initial2]) + wait(for: [initial1, initial2], timeout: defaultTimeout, enforceOrder: false) let newCppArg = "New C++ Main File" bs.map[cpp] = FileBuildSettings(compilerArguments: [newCppArg, cpp.pseudoPath]) @@ -350,28 +350,28 @@ final class BuildSystemManagerTests: XCTestCase { let changed2 = expectation(description: "initial settings h2 via cpp") let newArgsH1 = FileBuildSettings(compilerArguments: ["-xc++", newCppArg, h1.pseudoPath]) let newArgsH2 = FileBuildSettings(compilerArguments: ["-xc++", newCppArg, h2.pseudoPath]) - await del.setExpected([ + del.expected = [ (h1, newArgsH1, changed1, #file, #line), (h2, newArgsH2, changed2, #file, #line), - ]) + ] bsm.fileBuildSettingsChanged([cpp: .modified(bs.map[cpp]!)]) - try await fulfillmentOfOrThrow([changed1, changed2]) + wait(for: [changed1, changed2], timeout: defaultTimeout, enforceOrder: false) } - func testSettingsChangedAfterUnregister() async throws { + func testSettingsChangedAfterUnregister() { let a = DocumentURI(string: "bsm:a.swift") let b = DocumentURI(string: "bsm:b.swift") let c = DocumentURI(string: "bsm:c.swift") let mainFiles = ManualMainFilesProvider() mainFiles.mainFiles = [a: Set([a]), b: Set([b]), c: Set([c])] let bs = ManualBuildSystem() - let bsm = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: bs, fallbackBuildSystem: nil, mainFilesProvider: mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. - let del = await BSMDelegate(bsm) + let del = BSMDelegate(bsm) bs.map[a] = FileBuildSettings(compilerArguments: ["a"]) bs.map[b] = FileBuildSettings(compilerArguments: ["b"]) @@ -380,27 +380,27 @@ final class BuildSystemManagerTests: XCTestCase { let initialA = expectation(description: "initial settings a") let initialB = expectation(description: "initial settings b") let initialC = expectation(description: "initial settings c") - await del.setExpected([ + del.expected = [ (a, bs.map[a]!, initialA, #file, #line), (b, bs.map[b]!, initialB, #file, #line), (c, bs.map[c]!, initialC, #file, #line), - ]) - await bsm.registerForChangeNotifications(for: a, language: .swift) - await bsm.registerForChangeNotifications(for: b, language: .swift) - await bsm.registerForChangeNotifications(for: c, language: .swift) - try await fulfillmentOfOrThrow([initialA, initialB, initialC]) + ] + bsm.registerForChangeNotifications(for: a, language: .swift) + bsm.registerForChangeNotifications(for: b, language: .swift) + bsm.registerForChangeNotifications(for: c, language: .swift) + wait(for: [initialA, initialB, initialC], timeout: defaultTimeout, enforceOrder: false) bs.map[a] = FileBuildSettings(compilerArguments: ["new-a"]) bs.map[b] = FileBuildSettings(compilerArguments: ["new-b"]) bs.map[c] = FileBuildSettings(compilerArguments: ["new-c"]) let changedB = expectation(description: "changed settings b") - await del.setExpected([ + del.expected = [ (b, bs.map[b]!, changedB, #file, #line), - ]) + ] - await bsm.unregisterForChangeNotifications(for: a) - await bsm.unregisterForChangeNotifications(for: c) + bsm.unregisterForChangeNotifications(for: a) + bsm.unregisterForChangeNotifications(for: c) // At this point only b is registered, but that can race with notifications, // so ensure nothing bad happens and we still get the notification for b. bsm.fileBuildSettingsChanged([ @@ -409,44 +409,44 @@ final class BuildSystemManagerTests: XCTestCase { c: .modified(bs.map[c]!) ]) - try await fulfillmentOfOrThrow([changedB]) + wait(for: [changedB], timeout: defaultTimeout, enforceOrder: false) } - func testDependenciesUpdated() async throws { + func testDependenciesUpdated() { let a = DocumentURI(string: "bsm:a.swift") let mainFiles = ManualMainFilesProvider() mainFiles.mainFiles = [a: Set([a])] class DepUpdateDuringRegistrationBS: ManualBuildSystem { - override func registerForChangeNotifications(for uri: DocumentURI, language: Language) async { - await delegate?.filesDependenciesUpdated([uri]) - await super.registerForChangeNotifications(for: uri, language: language) + override func registerForChangeNotifications(for uri: DocumentURI, language: Language) { + delegate?.filesDependenciesUpdated([uri]) + super.registerForChangeNotifications(for: uri, language: language) } } let bs = DepUpdateDuringRegistrationBS() - let bsm = await BuildSystemManager( + let bsm = BuildSystemManager( buildSystem: bs, fallbackBuildSystem: nil, mainFilesProvider: mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. - let del = await BSMDelegate(bsm) + let del = BSMDelegate(bsm) bs.map[a] = FileBuildSettings(compilerArguments: ["x"]) let initial = expectation(description: "initial settings") - await del.setExpected([(a, bs.map[a]!, initial, #file, #line)]) + del.expected = [(a, bs.map[a]!, initial, #file, #line)] let depUpdate1 = expectation(description: "dependencies update during registration") - await del.setExpectedDependenciesUpdate([(a, depUpdate1, #file, #line)]) + del.expectedDependenciesUpdate = [(a, depUpdate1, #file, #line)] - await bsm.registerForChangeNotifications(for: a, language: .swift) - try await fulfillmentOfOrThrow([initial, depUpdate1]) + bsm.registerForChangeNotifications(for: a, language: .swift) + wait(for: [initial, depUpdate1], timeout: defaultTimeout, enforceOrder: false) let depUpdate2 = expectation(description: "dependencies update 2") - await del.setExpectedDependenciesUpdate([(a, depUpdate2, #file, #line)]) + del.expectedDependenciesUpdate = [(a, depUpdate2, #file, #line)] bsm.filesDependenciesUpdated([a]) - try await fulfillmentOfOrThrow([depUpdate2]) + wait(for: [depUpdate2], timeout: defaultTimeout, enforceOrder: false) } } @@ -475,17 +475,13 @@ class ManualBuildSystem: BuildSystem { var delegate: BuildSystemDelegate? = nil - func setDelegate(_ delegate: SKCore.BuildSystemDelegate?) async { - self.delegate = delegate - } - - func buildSettings(for uri: DocumentURI, language: Language) -> FileBuildSettings? { + func settings(for uri: DocumentURI, _ language: Language) -> FileBuildSettings? { return map[uri] } - func registerForChangeNotifications(for uri: DocumentURI, language: Language) async { - let settings = self.buildSettings(for: uri, language: language) - await self.delegate?.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) + func registerForChangeNotifications(for uri: DocumentURI, language: Language) { + let settings = self.settings(for: uri, language) + self.delegate?.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) } func unregisterForChangeNotifications(for: DocumentURI) { @@ -507,59 +503,45 @@ class ManualBuildSystem: BuildSystem { } /// A `BuildSystemDelegate` setup for testing. -private actor BSMDelegate: BuildSystemDelegate { - fileprivate typealias ExpectedBuildSettingChangedCall = (uri: DocumentURI, settings: FileBuildSettings?, expectation: XCTestExpectation, file: StaticString, line: UInt) - fileprivate typealias ExpectedDependenciesUpdatedCall = (uri: DocumentURI, expectation: XCTestExpectation, file: StaticString, line: UInt) - +private final class BSMDelegate: BuildSystemDelegate { let queue: DispatchQueue = DispatchQueue(label: "\(BSMDelegate.self)") unowned let bsm: BuildSystemManager - var expected: [ExpectedBuildSettingChangedCall] = [] - - /// - Note: Needed to set `expected` outside of the actor's isolation context. - func setExpected(_ expected: [ExpectedBuildSettingChangedCall]) { - self.expected = expected - } - + var expected: [(uri: DocumentURI, settings: FileBuildSettings?, expectation: XCTestExpectation, file: StaticString, line: UInt)] = [] var expectedDependenciesUpdate: [(uri: DocumentURI, expectation: XCTestExpectation, file: StaticString, line: UInt)] = [] - /// - Note: Needed to set `expected` outside of the actor's isolation context. - func setExpectedDependenciesUpdate(_ expectedDependenciesUpdated: [ExpectedDependenciesUpdatedCall]) { - self.expectedDependenciesUpdate = expectedDependenciesUpdated - } - - init(_ bsm: BuildSystemManager) async { + init(_ bsm: BuildSystemManager) { self.bsm = bsm - // Actor initializers can't directly leave their executor. Moving the call - // of `bsm.setDelegate` into a closure works around that limitation. rdar://116221716 - await { - await bsm.setDelegate(self) - }() + bsm.delegate = self } func fileBuildSettingsChanged(_ changes: [DocumentURI: FileBuildSettingsChange]) { - for (uri, change) in changes { - guard let expected = expected.first(where: { $0.uri == uri }) else { - XCTFail("unexpected settings change for \(uri)") - continue - } + queue.sync { + for (uri, change) in changes { + guard let expected = expected.first(where: { $0.uri == uri }) else { + XCTFail("unexpected settings change for \(uri)") + continue + } - XCTAssertEqual(uri, expected.uri, file: expected.file, line: expected.line) - let settings = change.newSettings - XCTAssertEqual(settings, expected.settings, file: expected.file, line: expected.line) - expected.expectation.fulfill() + XCTAssertEqual(uri, expected.uri, file: expected.file, line: expected.line) + let settings = change.newSettings + XCTAssertEqual(settings, expected.settings, file: expected.file, line: expected.line) + expected.expectation.fulfill() + } } } func buildTargetsChanged(_ changes: [BuildTargetEvent]) {} func filesDependenciesUpdated(_ changedFiles: Set) { - for uri in changedFiles { - guard let expected = expectedDependenciesUpdate.first(where: { $0.uri == uri }) else { - XCTFail("unexpected filesDependenciesUpdated for \(uri)") - continue - } + queue.sync { + for uri in changedFiles { + guard let expected = expectedDependenciesUpdate.first(where: { $0.uri == uri }) else { + XCTFail("unexpected filesDependenciesUpdated for \(uri)") + continue + } - XCTAssertEqual(uri, expected.uri, file: expected.file, line: expected.line) - expected.expectation.fulfill() + XCTAssertEqual(uri, expected.uri, file: expected.file, line: expected.line) + expected.expectation.fulfill() + } } } diff --git a/Tests/SKCoreTests/CompilationDatabaseTests.swift b/Tests/SKCoreTests/CompilationDatabaseTests.swift index c4b6f383..fbfa7b4c 100644 --- a/Tests/SKCoreTests/CompilationDatabaseTests.swift +++ b/Tests/SKCoreTests/CompilationDatabaseTests.swift @@ -207,8 +207,8 @@ final class CompilationDatabaseTests: XCTestCase { ]) } - func testCompilationDatabaseBuildSystem() async throws { - try await checkCompilationDatabaseBuildSystem(""" + func testCompilationDatabaseBuildSystem() throws { + try checkCompilationDatabaseBuildSystem(""" [ { "file": "/a/a.swift", @@ -217,23 +217,23 @@ final class CompilationDatabaseTests: XCTestCase { } ] """) { buildSystem in - let settings = await buildSystem._settings(for: DocumentURI(URL(fileURLWithPath: "/a/a.swift"))) + let settings = buildSystem._settings(for: DocumentURI(URL(fileURLWithPath: "/a/a.swift"))) XCTAssertNotNil(settings) XCTAssertEqual(settings?.workingDirectory, "/a") XCTAssertEqual(settings?.compilerArguments, ["-swift-version", "4", "/a/a.swift"]) - assertNil(await buildSystem.indexStorePath) - assertNil(await buildSystem.indexDatabasePath) + XCTAssertNil(buildSystem.indexStorePath) + XCTAssertNil(buildSystem.indexDatabasePath) } } - func testCompilationDatabaseBuildSystemIndexStoreSwift0() async throws { - try await checkCompilationDatabaseBuildSystem("[]") { buildSystem in - assertNil(await buildSystem.indexStorePath) + func testCompilationDatabaseBuildSystemIndexStoreSwift0() throws { + try checkCompilationDatabaseBuildSystem("[]") { buildSystem in + XCTAssertNil(buildSystem.indexStorePath) } } - func testCompilationDatabaseBuildSystemIndexStoreSwift1() async throws { - try await checkCompilationDatabaseBuildSystem(""" + func testCompilationDatabaseBuildSystemIndexStoreSwift1() throws { + try checkCompilationDatabaseBuildSystem(""" [ { "file": "/a/a.swift", @@ -242,13 +242,13 @@ final class CompilationDatabaseTests: XCTestCase { } ] """) { buildSystem in - assertEqual(URL(fileURLWithPath: await buildSystem.indexStorePath?.pathString ?? "").path, "/b") - assertEqual(URL(fileURLWithPath: await buildSystem.indexDatabasePath?.pathString ?? "").path, "/IndexDatabase") + XCTAssertEqual(URL(fileURLWithPath: buildSystem.indexStorePath?.pathString ?? "").path, "/b") + XCTAssertEqual(URL(fileURLWithPath: buildSystem.indexDatabasePath?.pathString ?? "").path, "/IndexDatabase") } } - func testCompilationDatabaseBuildSystemIndexStoreSwift2() async throws { - try await checkCompilationDatabaseBuildSystem(""" + func testCompilationDatabaseBuildSystemIndexStoreSwift2() throws { + try checkCompilationDatabaseBuildSystem(""" [ { "file": "/a/a.swift", @@ -267,12 +267,12 @@ final class CompilationDatabaseTests: XCTestCase { } ] """) { buildSystem in - await assertEqual(buildSystem.indexStorePath, try AbsolutePath(validating: "/b")) + XCTAssertEqual(buildSystem.indexStorePath, try AbsolutePath(validating: "/b")) } } - func testCompilationDatabaseBuildSystemIndexStoreSwift3() async throws { - try await checkCompilationDatabaseBuildSystem(""" + func testCompilationDatabaseBuildSystemIndexStoreSwift3() throws { + try checkCompilationDatabaseBuildSystem(""" [ { "file": "/a/a.swift", @@ -281,12 +281,12 @@ final class CompilationDatabaseTests: XCTestCase { } ] """) { buildSystem in - assertEqual(await buildSystem.indexStorePath, try AbsolutePath(validating: "/b")) + XCTAssertEqual(buildSystem.indexStorePath, try AbsolutePath(validating: "/b")) } } - func testCompilationDatabaseBuildSystemIndexStoreSwift4() async throws { - try await checkCompilationDatabaseBuildSystem(""" + func testCompilationDatabaseBuildSystemIndexStoreSwift4() throws { + try checkCompilationDatabaseBuildSystem(""" [ { "file": "/a/a.swift", @@ -295,12 +295,12 @@ final class CompilationDatabaseTests: XCTestCase { } ] """) { buildSystem in - assertNil(await buildSystem.indexStorePath) + XCTAssertNil(buildSystem.indexStorePath) } } - func testCompilationDatabaseBuildSystemIndexStoreClang() async throws { - try await checkCompilationDatabaseBuildSystem(""" + func testCompilationDatabaseBuildSystemIndexStoreClang() throws { + try checkCompilationDatabaseBuildSystem(""" [ { "file": "/a/a.cpp", @@ -319,16 +319,16 @@ final class CompilationDatabaseTests: XCTestCase { } ] """) { buildSystem in - assertEqual(URL(fileURLWithPath: await buildSystem.indexStorePath?.pathString ?? "").path, "/b") - assertEqual(URL(fileURLWithPath: await buildSystem.indexDatabasePath?.pathString ?? "").path, "/IndexDatabase") + XCTAssertEqual(URL(fileURLWithPath: buildSystem.indexStorePath?.pathString ?? "").path, "/b") + XCTAssertEqual(URL(fileURLWithPath: buildSystem.indexDatabasePath?.pathString ?? "").path, "/IndexDatabase") } } } -private func checkCompilationDatabaseBuildSystem(_ compdb: ByteString, block: (CompilationDatabaseBuildSystem) async throws -> ()) async throws { +private func checkCompilationDatabaseBuildSystem(_ compdb: ByteString, block: (CompilationDatabaseBuildSystem) throws -> ()) throws { let fs = InMemoryFileSystem() try fs.createDirectory(AbsolutePath(validating: "/a")) try fs.writeFileContents(AbsolutePath(validating: "/a/compile_commands.json"), bytes: compdb) let buildSystem = CompilationDatabaseBuildSystem(projectRoot: try AbsolutePath(validating: "/a"), fileSystem: fs) - try await block(buildSystem) + try block(buildSystem) } diff --git a/Tests/SKCoreTests/FallbackBuildSystemTests.swift b/Tests/SKCoreTests/FallbackBuildSystemTests.swift index 6d103a3b..68b54ef1 100644 --- a/Tests/SKCoreTests/FallbackBuildSystemTests.swift +++ b/Tests/SKCoreTests/FallbackBuildSystemTests.swift @@ -29,7 +29,7 @@ final class FallbackBuildSystemTests: XCTestCase { XCTAssertNil(bs.indexStorePath) XCTAssertNil(bs.indexDatabasePath) - let settings = bs.buildSettings(for: source.asURI, language: .swift)! + let settings = bs.settings(for: source.asURI, .swift)! XCTAssertNil(settings.workingDirectory) let args = settings.compilerArguments @@ -41,7 +41,7 @@ final class FallbackBuildSystemTests: XCTestCase { bs.sdkpath = nil - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .swift)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .swift)?.compilerArguments, [ source.pathString, ]) } @@ -57,7 +57,7 @@ final class FallbackBuildSystemTests: XCTestCase { let bs = FallbackBuildSystem(buildSetup: buildSetup) bs.sdkpath = sdk - let args = bs.buildSettings(for: source.asURI, language: .swift)?.compilerArguments + let args = bs.settings(for: source.asURI, .swift)?.compilerArguments XCTAssertEqual(args, [ "-Xfrontend", "-debug-constraints", @@ -68,7 +68,7 @@ final class FallbackBuildSystemTests: XCTestCase { bs.sdkpath = nil - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .swift)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .swift)?.compilerArguments, [ "-Xfrontend", "-debug-constraints", source.pathString, @@ -88,7 +88,7 @@ final class FallbackBuildSystemTests: XCTestCase { let bs = FallbackBuildSystem(buildSetup: buildSetup) bs.sdkpath = sdk - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .swift)!.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .swift)!.compilerArguments, [ "-sdk", "/some/custom/sdk", "-Xfrontend", @@ -98,7 +98,7 @@ final class FallbackBuildSystemTests: XCTestCase { bs.sdkpath = nil - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .swift)!.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .swift)!.compilerArguments, [ "-sdk", "/some/custom/sdk", "-Xfrontend", @@ -114,7 +114,7 @@ final class FallbackBuildSystemTests: XCTestCase { let bs = FallbackBuildSystem(buildSetup: .default) bs.sdkpath = sdk - let settings = bs.buildSettings(for: source.asURI, language: .cpp)! + let settings = bs.settings(for: source.asURI, .cpp)! XCTAssertNil(settings.workingDirectory) let args = settings.compilerArguments @@ -126,7 +126,7 @@ final class FallbackBuildSystemTests: XCTestCase { bs.sdkpath = nil - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .cpp)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .cpp)?.compilerArguments, [ source.pathString, ]) } @@ -141,7 +141,7 @@ final class FallbackBuildSystemTests: XCTestCase { let bs = FallbackBuildSystem(buildSetup: buildSetup) bs.sdkpath = sdk - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .cpp)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .cpp)?.compilerArguments, [ "-v", "-isysroot", sdk.pathString, @@ -150,7 +150,7 @@ final class FallbackBuildSystemTests: XCTestCase { bs.sdkpath = nil - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .cpp)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .cpp)?.compilerArguments, [ "-v", source.pathString, ]) @@ -168,7 +168,7 @@ final class FallbackBuildSystemTests: XCTestCase { let bs = FallbackBuildSystem(buildSetup: buildSetup) bs.sdkpath = sdk - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .cpp)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .cpp)?.compilerArguments, [ "-isysroot", "/my/custom/sdk", "-v", @@ -177,7 +177,7 @@ final class FallbackBuildSystemTests: XCTestCase { bs.sdkpath = nil - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .cpp)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .cpp)?.compilerArguments, [ "-isysroot", "/my/custom/sdk", "-v", @@ -189,7 +189,7 @@ final class FallbackBuildSystemTests: XCTestCase { let source = try AbsolutePath(validating: "/my/source.c") let bs = FallbackBuildSystem(buildSetup: .default) bs.sdkpath = nil - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .c)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .c)?.compilerArguments, [ source.pathString, ]) } @@ -202,7 +202,7 @@ final class FallbackBuildSystemTests: XCTestCase { ])) let bs = FallbackBuildSystem(buildSetup: buildSetup) bs.sdkpath = nil - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .c)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .c)?.compilerArguments, [ "-v", source.pathString, ]) @@ -212,7 +212,7 @@ final class FallbackBuildSystemTests: XCTestCase { let source = try AbsolutePath(validating: "/my/source.m") let bs = FallbackBuildSystem(buildSetup: .default) bs.sdkpath = nil - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .objective_c)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .objective_c)?.compilerArguments, [ source.pathString, ]) } @@ -221,7 +221,7 @@ final class FallbackBuildSystemTests: XCTestCase { let source = try AbsolutePath(validating: "/my/source.mm") let bs = FallbackBuildSystem(buildSetup: .default) bs.sdkpath = nil - XCTAssertEqual(bs.buildSettings(for: source.asURI, language: .objective_cpp)?.compilerArguments, [ + XCTAssertEqual(bs.settings(for: source.asURI, .objective_cpp)?.compilerArguments, [ source.pathString, ]) } @@ -229,6 +229,6 @@ final class FallbackBuildSystemTests: XCTestCase { func testUnknown() throws { let source = try AbsolutePath(validating: "/my/source.mm") let bs = FallbackBuildSystem(buildSetup: .default) - XCTAssertNil(bs.buildSettings(for: source.asURI, language: Language(rawValue: "unknown"))) + XCTAssertNil(bs.settings(for: source.asURI, Language(rawValue: "unknown"))) } } diff --git a/Tests/SKSwiftPMWorkspaceTests/SwiftPMWorkspaceTests.swift b/Tests/SKSwiftPMWorkspaceTests/SwiftPMWorkspaceTests.swift index 06185973..a3e321d6 100644 --- a/Tests/SKSwiftPMWorkspaceTests/SwiftPMWorkspaceTests.swift +++ b/Tests/SKSwiftPMWorkspaceTests/SwiftPMWorkspaceTests.swift @@ -21,21 +21,20 @@ import SKSwiftPMWorkspace import SKTestSupport import TSCBasic import XCTest -import LSPTestSupport import struct PackageModel.BuildFlags final class SwiftPMWorkspaceTests: XCTestCase { - func testNoPackage() async throws { + func testNoPackage() throws { let fs = InMemoryFileSystem() - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", ]) let packageRoot = tempDir.appending(component: "pkg") let tr = ToolchainRegistry.shared - await assertThrowsError(try await SwiftPMWorkspace( + XCTAssertThrowsError(try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, @@ -43,9 +42,9 @@ final class SwiftPMWorkspaceTests: XCTestCase { } } - func testUnparsablePackage() async throws { + func testUnparsablePackage() throws { let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", "pkg/Package.swift": """ @@ -56,7 +55,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { ]) let packageRoot = tempDir.appending(component: "pkg") let tr = ToolchainRegistry.shared - await assertThrowsError(try await SwiftPMWorkspace( + XCTAssertThrowsError(try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, @@ -64,9 +63,9 @@ final class SwiftPMWorkspaceTests: XCTestCase { } } - func testNoToolchain() async throws { + func testNoToolchain() throws { let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", "pkg/Package.swift": """ @@ -77,7 +76,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { """ ]) let packageRoot = tempDir.appending(component: "pkg") - await assertThrowsError(try await SwiftPMWorkspace( + XCTAssertThrowsError(try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: ToolchainRegistry(), fileSystem: fs, @@ -85,10 +84,10 @@ final class SwiftPMWorkspaceTests: XCTestCase { } } - func testBasicSwiftArgs() async throws { + func testBasicSwiftArgs() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", "pkg/Package.swift": """ @@ -100,19 +99,19 @@ final class SwiftPMWorkspaceTests: XCTestCase { ]) let packageRoot = try resolveSymlinks(tempDir.appending(component: "pkg")) let tr = ToolchainRegistry.shared - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, buildSetup: TestSourceKitServer.serverOptions.buildSetup) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") - let hostTriple = await ws.buildParameters.targetTriple + let hostTriple = ws.buildParameters.targetTriple let build = buildPath(root: packageRoot, platform: hostTriple.platformBuildPathComponent) - assertEqual(await ws.buildPath, build) - assertNotNil(await ws.indexStorePath) - let arguments = try await ws._settings(for: aswift.asURI, .swift)!.compilerArguments + XCTAssertEqual(ws.buildPath, build) + XCTAssertNotNil(ws.indexStorePath) + let arguments = try ws._settings(for: aswift.asURI, .swift)!.compilerArguments check( "-module-name", "lib", "-incremental", "-emit-dependencies", @@ -135,10 +134,10 @@ final class SwiftPMWorkspaceTests: XCTestCase { } } - func testBuildSetup() async throws { + func testBuildSetup() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", "pkg/Package.swift": """ @@ -156,18 +155,18 @@ final class SwiftPMWorkspaceTests: XCTestCase { path: packageRoot.appending(component: "non_default_build_path"), flags: BuildFlags(cCompilerFlags: ["-m32"], swiftCompilerFlags: ["-typecheck"])) - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, buildSetup: config) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") - let hostTriple = await ws.buildParameters.targetTriple + let hostTriple = ws.buildParameters.targetTriple let build = buildPath(root: packageRoot, config: config, platform: hostTriple.platformBuildPathComponent) - assertEqual(await ws.buildPath, build) - let arguments = try await ws._settings(for: aswift.asURI, .swift)!.compilerArguments + XCTAssertEqual(ws.buildPath, build) + let arguments = try ws._settings(for: aswift.asURI, .swift)!.compilerArguments check("-typecheck", arguments: arguments) check("-Xcc", "-m32", arguments: arguments) @@ -175,10 +174,10 @@ final class SwiftPMWorkspaceTests: XCTestCase { } } - func testManifestArgs() async throws { + func testManifestArgs() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", "pkg/Package.swift": """ @@ -190,24 +189,24 @@ final class SwiftPMWorkspaceTests: XCTestCase { ]) let packageRoot = tempDir.appending(component: "pkg") let tr = ToolchainRegistry.shared - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, buildSetup: TestSourceKitServer.serverOptions.buildSetup) let source = try resolveSymlinks(packageRoot.appending(component: "Package.swift")) - let arguments = try await ws._settings(for: source.asURI, .swift)!.compilerArguments + let arguments = try ws._settings(for: source.asURI, .swift)!.compilerArguments check("-swift-version", "4.2", arguments: arguments) check(source.pathString, arguments: arguments) } } - func testMultiFileSwift() async throws { + func testMultiFileSwift() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", "pkg/Sources/lib/b.swift": "", @@ -220,7 +219,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { ]) let packageRoot = try resolveSymlinks(tempDir.appending(component: "pkg")) let tr = ToolchainRegistry.shared - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, @@ -229,19 +228,19 @@ final class SwiftPMWorkspaceTests: XCTestCase { let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") let bswift = packageRoot.appending(components: "Sources", "lib", "b.swift") - let argumentsA = try await ws._settings(for: aswift.asURI, .swift)!.compilerArguments + let argumentsA = try ws._settings(for: aswift.asURI, .swift)!.compilerArguments check(aswift.pathString, arguments: argumentsA) check(bswift.pathString, arguments: argumentsA) - let argumentsB = try await ws._settings(for: aswift.asURI, .swift)!.compilerArguments + let argumentsB = try ws._settings(for: aswift.asURI, .swift)!.compilerArguments check(aswift.pathString, arguments: argumentsB) check(bswift.pathString, arguments: argumentsB) } } - func testMultiTargetSwift() async throws { + func testMultiTargetSwift() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/libA/a.swift": "", "pkg/Sources/libB/b.swift": "", @@ -260,7 +259,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { ]) let packageRoot = try resolveSymlinks(tempDir.appending(component: "pkg")) let tr = ToolchainRegistry.shared - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, @@ -268,7 +267,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { let aswift = packageRoot.appending(components: "Sources", "libA", "a.swift") let bswift = packageRoot.appending(components: "Sources", "libB", "b.swift") - let arguments = try await ws._settings(for: aswift.asURI, .swift)!.compilerArguments + let arguments = try ws._settings(for: aswift.asURI, .swift)!.compilerArguments check(aswift.pathString, arguments: arguments) checkNot(bswift.pathString, arguments: arguments) // Temporary conditional to work around revlock between SourceKit-LSP and SwiftPM @@ -284,7 +283,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { arguments: arguments) } - let argumentsB = try await ws._settings(for: bswift.asURI, .swift)!.compilerArguments + let argumentsB = try ws._settings(for: bswift.asURI, .swift)!.compilerArguments check(bswift.pathString, arguments: argumentsB) checkNot(aswift.pathString, arguments: argumentsB) checkNot("-I", packageRoot.appending(components: "Sources", "libC", "include").pathString, @@ -292,10 +291,10 @@ final class SwiftPMWorkspaceTests: XCTestCase { } } - func testUnknownFile() async throws { + func testUnknownFile() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/libA/a.swift": "", "pkg/Sources/libB/b.swift": "", @@ -310,7 +309,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { ]) let packageRoot = tempDir.appending(component: "pkg") let tr = ToolchainRegistry.shared - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, @@ -318,16 +317,16 @@ final class SwiftPMWorkspaceTests: XCTestCase { let aswift = packageRoot.appending(components: "Sources", "libA", "a.swift") let bswift = packageRoot.appending(components: "Sources", "libB", "b.swift") - assertNotNil(try await ws._settings(for: aswift.asURI, .swift)) - assertNil(try await ws._settings(for: bswift.asURI, .swift)) - assertNil(try await ws._settings(for: DocumentURI(URL(string: "https://www.apple.com")!), .swift)) + XCTAssertNotNil(try ws._settings(for: aswift.asURI, .swift)) + XCTAssertNil(try ws._settings(for: bswift.asURI, .swift)) + XCTAssertNil(try ws._settings(for: DocumentURI(URL(string: "https://www.apple.com")!), .swift)) } } - func testBasicCXXArgs() async throws { + func testBasicCXXArgs() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/a.cpp": "", "pkg/Sources/lib/b.cpp": "", @@ -342,7 +341,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { ]) let packageRoot = try resolveSymlinks(tempDir.appending(component: "pkg")) let tr = ToolchainRegistry.shared - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, @@ -350,11 +349,11 @@ final class SwiftPMWorkspaceTests: XCTestCase { let acxx = packageRoot.appending(components: "Sources", "lib", "a.cpp") let bcxx = packageRoot.appending(components: "Sources", "lib", "b.cpp") - let hostTriple = await ws.buildParameters.targetTriple + let hostTriple = ws.buildParameters.targetTriple let build = buildPath(root: packageRoot, platform: hostTriple.platformBuildPathComponent) - assertEqual(await ws.buildPath, build) - assertNotNil(await ws.indexStorePath) + XCTAssertEqual(ws.buildPath, build) + XCTAssertNotNil(ws.indexStorePath) let checkArgsCommon = { (arguments: [String]) in check("-std=c++14", arguments: arguments) @@ -377,7 +376,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { checkNot(bcxx.pathString, arguments: arguments) } - let args = try await ws._settings(for: acxx.asURI, .cpp)!.compilerArguments + let args = try ws._settings(for: acxx.asURI, .cpp)!.compilerArguments checkArgsCommon(args) URL(fileURLWithPath: build.appending(components: "lib.build", "a.cpp.d").pathString) @@ -395,7 +394,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { } let header = packageRoot.appending(components: "Sources", "lib", "include", "a.h") - let headerArgs = try await ws._settings(for: header.asURI, .cpp)!.compilerArguments + let headerArgs = try ws._settings(for: header.asURI, .cpp)!.compilerArguments checkArgsCommon(headerArgs) check("-c", "-x", "c++-header", try AbsolutePath(validating: URL(fileURLWithPath: header.pathString).path).pathString, @@ -403,10 +402,10 @@ final class SwiftPMWorkspaceTests: XCTestCase { } } - func testDeploymentTargetSwift() async throws { + func testDeploymentTargetSwift() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", "pkg/Package.swift": """ @@ -419,16 +418,16 @@ final class SwiftPMWorkspaceTests: XCTestCase { """ ]) let packageRoot = tempDir.appending(component: "pkg") - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: ToolchainRegistry.shared, fileSystem: fs, buildSetup: TestSourceKitServer.serverOptions.buildSetup) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") - let arguments = try await ws._settings(for: aswift.asURI, .swift)!.compilerArguments + let arguments = try ws._settings(for: aswift.asURI, .swift)!.compilerArguments check("-target", arguments: arguments) // Only one! - let hostTriple = await ws.buildParameters.targetTriple + let hostTriple = ws.buildParameters.targetTriple #if os(macOS) check("-target", @@ -439,10 +438,10 @@ final class SwiftPMWorkspaceTests: XCTestCase { } } - func testSymlinkInWorkspaceSwift() async throws { + func testSymlinkInWorkspaceSwift() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg_real/Sources/lib/a.swift": "", "pkg_real/Package.swift": """ @@ -459,7 +458,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { withDestinationURL: URL(fileURLWithPath: tempDir.appending(component: "pkg_real").pathString)) let tr = ToolchainRegistry.shared - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, @@ -471,8 +470,8 @@ final class SwiftPMWorkspaceTests: XCTestCase { .appending(components: "Sources", "lib", "a.swift") let manifest = packageRoot.appending(components: "Package.swift") - let arguments1 = try await ws._settings(for: aswift1.asURI, .swift)?.compilerArguments - let arguments2 = try await ws._settings(for: aswift2.asURI, .swift)?.compilerArguments + let arguments1 = try ws._settings(for: aswift1.asURI, .swift)?.compilerArguments + let arguments2 = try ws._settings(for: aswift2.asURI, .swift)?.compilerArguments XCTAssertNotNil(arguments1) XCTAssertNotNil(arguments2) XCTAssertEqual(arguments1, arguments2) @@ -480,7 +479,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { checkNot(aswift1.pathString, arguments: arguments1 ?? []) check(try resolveSymlinks(aswift1).pathString, arguments: arguments1 ?? []) - let argsManifest = try await ws._settings(for: manifest.asURI, .swift)?.compilerArguments + let argsManifest = try ws._settings(for: manifest.asURI, .swift)?.compilerArguments XCTAssertNotNil(argsManifest) checkNot(manifest.pathString, arguments: argsManifest ?? []) @@ -488,10 +487,10 @@ final class SwiftPMWorkspaceTests: XCTestCase { } } - func testSymlinkInWorkspaceCXX() async throws { + func testSymlinkInWorkspaceCXX() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg_real/Sources/lib/a.cpp": "", "pkg_real/Sources/lib/b.cpp": "", @@ -512,7 +511,7 @@ final class SwiftPMWorkspaceTests: XCTestCase { withDestinationURL: URL(fileURLWithPath: tempDir.appending(component: "pkg_real").pathString)) let tr = ToolchainRegistry.shared - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, @@ -521,22 +520,22 @@ final class SwiftPMWorkspaceTests: XCTestCase { let acxx = packageRoot.appending(components: "Sources", "lib", "a.cpp") let ah = packageRoot.appending(components: "Sources", "lib", "include", "a.h") - let argsCxx = try await ws._settings(for: acxx.asURI, .cpp)?.compilerArguments + let argsCxx = try ws._settings(for: acxx.asURI, .cpp)?.compilerArguments XCTAssertNotNil(argsCxx) check(acxx.pathString, arguments: argsCxx ?? []) checkNot(try resolveSymlinks(acxx).pathString, arguments: argsCxx ?? []) - let argsH = try await ws._settings(for: ah.asURI, .cpp)?.compilerArguments + let argsH = try ws._settings(for: ah.asURI, .cpp)?.compilerArguments XCTAssertNotNil(argsH) checkNot(ah.pathString, arguments: argsH ?? []) check(try resolveSymlinks(ah).pathString, arguments: argsH ?? []) } } - func testSwiftDerivedSources() async throws { + func testSwiftDerivedSources() throws { // FIXME: should be possible to use InMemoryFileSystem. let fs = localFileSystem - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/a.swift": "", "pkg/Sources/lib/a.txt": "", @@ -553,14 +552,14 @@ final class SwiftPMWorkspaceTests: XCTestCase { ]) let packageRoot = try resolveSymlinks(tempDir.appending(component: "pkg")) let tr = ToolchainRegistry.shared - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, buildSetup: TestSourceKitServer.serverOptions.buildSetup) let aswift = packageRoot.appending(components: "Sources", "lib", "a.swift") - let arguments = try await ws._settings(for: aswift.asURI, .swift)!.compilerArguments + let arguments = try ws._settings(for: aswift.asURI, .swift)!.compilerArguments check(aswift.pathString, arguments: arguments) XCTAssertNotNil(arguments.firstIndex(where: { $0.hasSuffix(".swift") && $0.contains("DerivedSources") @@ -568,9 +567,9 @@ final class SwiftPMWorkspaceTests: XCTestCase { } } - func testNestedInvalidPackageSwift() async throws { + func testNestedInvalidPackageSwift() throws { let fs = InMemoryFileSystem() - try await withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in + try withTemporaryDirectory(removeTreeOnDeinit: true) { tempDir in try fs.createFiles(root: tempDir, files: [ "pkg/Sources/lib/Package.swift": "// not a valid package", "pkg/Package.swift": """ @@ -582,13 +581,13 @@ final class SwiftPMWorkspaceTests: XCTestCase { ]) let packageRoot = try resolveSymlinks(tempDir.appending(components: "pkg", "Sources", "lib")) let tr = ToolchainRegistry.shared - let ws = try await SwiftPMWorkspace( + let ws = try SwiftPMWorkspace( workspacePath: packageRoot, toolchainRegistry: tr, fileSystem: fs, buildSetup: TestSourceKitServer.serverOptions.buildSetup) - assertEqual(await ws._packageRoot, try resolveSymlinks(tempDir.appending(component: "pkg"))) + XCTAssertEqual(ws._packageRoot, try resolveSymlinks(tempDir.appending(component: "pkg"))) } } } diff --git a/Tests/SourceKitDTests/CrashRecoveryTests.swift b/Tests/SourceKitDTests/CrashRecoveryTests.swift index 062541ea..068fb84f 100644 --- a/Tests/SourceKitDTests/CrashRecoveryTests.swift +++ b/Tests/SourceKitDTests/CrashRecoveryTests.swift @@ -44,11 +44,11 @@ fileprivate extension HoverResponse { } final class CrashRecoveryTests: XCTestCase { - func testSourcekitdCrashRecovery() async throws { + func testSourcekitdCrashRecovery() throws { try XCTSkipUnless(Platform.current == .darwin, "Linux and Windows use in-process sourcekitd") try XCTSkipIf(longTestsDisabled) - let ws = try await staticSourceKitTibsWorkspace(name: "sourcekitdCrashRecovery")! + let ws = try staticSourceKitTibsWorkspace(name: "sourcekitdCrashRecovery")! let loc = ws.testLoc("loc") // Open the document. Wait for the semantic diagnostics to know it has been fully opened and we are not entering any data races about outstanding diagnostics when we crash sourcekitd. @@ -64,7 +64,7 @@ final class CrashRecoveryTests: XCTestCase { documentOpened.fulfill() }) try ws.openDocument(loc.url, language: .swift) - try await fulfillmentOfOrThrow([documentOpened]) + self.wait(for: [documentOpened], timeout: defaultTimeout) // Make a change to the file that's not saved to disk. This way we can check that we re-open the correct in-memory state. @@ -88,13 +88,13 @@ final class CrashRecoveryTests: XCTestCase { // Crash sourcekitd - let sourcekitdServer = await ws.testServer.server!._languageService(for: loc.docUri, .swift, in: ws.testServer.server!.workspaceForDocument(uri: loc.docUri)!) as! SwiftLanguageServer + let sourcekitdServer = ws.testServer.server!._languageService(for: loc.docUri, .swift, in: ws.testServer.server!.workspaceForDocumentOnQueue(uri: loc.docUri)!) as! SwiftLanguageServer let sourcekitdCrashed = expectation(description: "sourcekitd has crashed") let sourcekitdRestarted = expectation(description: "sourcekitd has been restarted (syntactic only)") let semanticFunctionalityRestored = expectation(description: "sourcekitd has restored semantic language functionality") - await sourcekitdServer.addStateChangeHandler { (oldState, newState) in + sourcekitdServer.addStateChangeHandler { (oldState, newState) in switch newState { case .connectionInterrupted: sourcekitdCrashed.fulfill() @@ -105,10 +105,10 @@ final class CrashRecoveryTests: XCTestCase { } } - await sourcekitdServer._crash() + sourcekitdServer._crash() - try await fulfillmentOfOrThrow([sourcekitdCrashed], timeout: 5) - try await fulfillmentOfOrThrow([sourcekitdRestarted], timeout: 30) + self.wait(for: [sourcekitdCrashed], timeout: 5) + self.wait(for: [sourcekitdRestarted], timeout: 30) // Check that we have syntactic functionality again @@ -118,7 +118,7 @@ final class CrashRecoveryTests: XCTestCase { // Send a hover request (which will fail) to trigger that timer. // Afterwards wait for semantic functionality to be restored. _ = try? ws.sk.sendSync(hoverRequest) - try await fulfillmentOfOrThrow([semanticFunctionalityRestored], timeout: 30) + self.wait(for: [semanticFunctionalityRestored], timeout: 30) // Check that we get the same hover response from the restored in-memory state @@ -132,13 +132,13 @@ final class CrashRecoveryTests: XCTestCase { /// - Parameters: /// - ws: The workspace for which the clangd server shall be crashed /// - document: The URI of a C/C++/... document in the workspace - private func crashClangd(for ws: SKTibsTestWorkspace, document docUri: DocumentURI) async throws { - let clangdServer = await ws.testServer.server!._languageService(for: docUri, .cpp, in: ws.testServer.server!.workspaceForDocument(uri: docUri)!)! - + private func crashClangd(for ws: SKTibsTestWorkspace, document docUri: DocumentURI) { + let clangdServer = ws.testServer.server!._languageService(for: docUri, .cpp, in: ws.testServer.server!.workspaceForDocumentOnQueue(uri: docUri)!)! + let clangdCrashed = self.expectation(description: "clangd crashed") let clangdRestarted = self.expectation(description: "clangd restarted") - await clangdServer.addStateChangeHandler { (oldState, newState) in + clangdServer.addStateChangeHandler { (oldState, newState) in switch newState { case .connectionInterrupted: clangdCrashed.fulfill() @@ -149,16 +149,16 @@ final class CrashRecoveryTests: XCTestCase { } } - await clangdServer._crash() + clangdServer._crash() - try await fulfillmentOfOrThrow([clangdCrashed]) - try await fulfillmentOfOrThrow([clangdRestarted]) + self.wait(for: [clangdCrashed], timeout: 5) + self.wait(for: [clangdRestarted], timeout: 30) } - func testClangdCrashRecovery() async throws { + func testClangdCrashRecovery() throws { try XCTSkipIf(longTestsDisabled) - let ws = try await staticSourceKitTibsWorkspace(name: "ClangCrashRecovery")! + let ws = try staticSourceKitTibsWorkspace(name: "ClangCrashRecovery")! let loc = ws.testLoc("loc") try ws.openDocument(loc.url, language: .cpp) @@ -182,7 +182,7 @@ final class CrashRecoveryTests: XCTestCase { // Crash clangd - try await crashClangd(for: ws, document: loc.docUri) + crashClangd(for: ws, document: loc.docUri) // Check that we have re-opened the document with the correct in-memory state @@ -192,10 +192,10 @@ final class CrashRecoveryTests: XCTestCase { } } - func testClangdCrashRecoveryReopensWithCorrectBuildSettings() async throws { + func testClangdCrashRecoveryReopensWithCorrectBuildSettings() throws { try XCTSkipIf(longTestsDisabled) - let ws = try await staticSourceKitTibsWorkspace(name: "ClangCrashRecoveryBuildSettings")! + let ws = try staticSourceKitTibsWorkspace(name: "ClangCrashRecoveryBuildSettings")! let loc = ws.testLoc("loc") try ws.openDocument(loc.url, language: .cpp) @@ -213,8 +213,8 @@ final class CrashRecoveryTests: XCTestCase { // Crash clangd - try await crashClangd(for: ws, document: loc.docUri) - + crashClangd(for: ws, document: loc.docUri) + // Check that we have re-opened the document with the correct build settings // If we did not recover the correct build settings, document highlight would // pick the definition of foo() in the #else branch. @@ -225,10 +225,10 @@ final class CrashRecoveryTests: XCTestCase { } } - func testPreventClangdCrashLoop() async throws { + func testPreventClangdCrashLoop() throws { try XCTSkipIf(longTestsDisabled) - let ws = try await staticSourceKitTibsWorkspace(name: "ClangCrashRecovery")! + let ws = try staticSourceKitTibsWorkspace(name: "ClangCrashRecovery")! let loc = ws.testLoc("loc") try ws.openDocument(loc.url, language: .cpp) @@ -240,8 +240,8 @@ final class CrashRecoveryTests: XCTestCase { // Keep track of clangd crashes - let clangdServer = await ws.testServer.server!._languageService(for: loc.docUri, .cpp, in: ws.testServer.server!.workspaceForDocument(uri: loc.docUri)!)! - + let clangdServer = ws.testServer.server!._languageService(for: loc.docUri, .cpp, in: ws.testServer.server!.workspaceForDocumentOnQueue(uri: loc.docUri)!)! + let clangdCrashed = self.expectation(description: "clangd crashed") clangdCrashed.assertForOverFulfill = false @@ -250,7 +250,7 @@ final class CrashRecoveryTests: XCTestCase { var clangdHasRestartedFirstTime = false - await clangdServer.addStateChangeHandler { (oldState, newState) in + clangdServer.addStateChangeHandler { (oldState, newState) in switch newState { case .connectionInterrupted: clangdCrashed.fulfill() @@ -266,17 +266,17 @@ final class CrashRecoveryTests: XCTestCase { } } - await clangdServer._crash() + clangdServer._crash() - try await fulfillmentOfOrThrow([clangdCrashed], timeout: 5) - try await fulfillmentOfOrThrow([clangdRestartedFirstTime], timeout: 30) + self.wait(for: [clangdCrashed], timeout: 5) + self.wait(for: [clangdRestartedFirstTime], timeout: 30) // Clangd has restarted. Note the date so we can check that the second restart doesn't happen too quickly. let firstRestartDate = Date() // Crash clangd again. This time, it should only restart after a delay. - await clangdServer._crash() + clangdServer._crash() - try await fulfillmentOfOrThrow([clangdRestartedSecondTime], timeout: 30) + self.wait(for: [clangdRestartedSecondTime], timeout: 30) XCTAssert(Date().timeIntervalSince(firstRestartDate) > 5, "Clangd restarted too quickly after crashing twice in a row. We are not preventing crash loops.") } } diff --git a/Tests/SourceKitLSPTests/BuildSystemTests.swift b/Tests/SourceKitLSPTests/BuildSystemTests.swift index 3d77efa6..1000373b 100644 --- a/Tests/SourceKitLSPTests/BuildSystemTests.swift +++ b/Tests/SourceKitLSPTests/BuildSystemTests.swift @@ -19,12 +19,6 @@ import SourceKitLSP import TSCBasic import XCTest -fileprivate extension SourceKitServer { - func setWorkspaces(_ workspaces: [Workspace]) { - self._workspaces = workspaces - } -} - // Workaround ambiguity with Foundation. typealias LSPNotification = LanguageServerProtocol.Notification @@ -37,20 +31,12 @@ final class TestBuildSystem: BuildSystem { weak var delegate: BuildSystemDelegate? - public func setDelegate(_ delegate: BuildSystemDelegate?) async { - self.delegate = delegate - } - /// Build settings by file. var buildSettingsByFile: [DocumentURI: FileBuildSettings] = [:] /// Files currently being watched by our delegate. var watchedFiles: Set = [] - func buildSettings(for document: DocumentURI, language: Language) async throws -> FileBuildSettings? { - return buildSettingsByFile[document] - } - func registerForChangeNotifications(for uri: DocumentURI, language: Language) { watchedFiles.insert(uri) @@ -60,8 +46,8 @@ final class TestBuildSystem: BuildSystem { return } - Task { - await delegate.fileBuildSettingsChanged([uri: .modified(settings)]) + DispatchQueue.global().async { + delegate.fileBuildSettingsChanged([uri: .modified(settings)]) } } @@ -88,6 +74,9 @@ final class BuildSystemTests: XCTestCase { /// The primary interface to make requests to the SourceKitServer. var sk: TestClient! = nil + /// The document manager of the server + var documentManager: DocumentManager! + /// The server's workspace data. Accessing this is unsafe if the server does so concurrently. var workspace: Workspace! = nil @@ -98,34 +87,28 @@ final class BuildSystemTests: XCTestCase { var haveClangd: Bool = false override func setUp() { - // XCTestCase.setUp cannot be async, so unfortunately we need to do some - // hackery to synchronously wait for a task to finish. This is very much an - // anti-pattern because it can easily lead to priority inversions and should - // thus not be copied to any non-test code. - let sema = DispatchSemaphore(value: 0) - Task { - haveClangd = ToolchainRegistry.shared.toolchains.contains { $0.clangd != nil } - testServer = TestSourceKitServer() - buildSystem = TestBuildSystem() + haveClangd = ToolchainRegistry.shared.toolchains.contains { $0.clangd != nil } + testServer = TestSourceKitServer() + buildSystem = TestBuildSystem() - let server = testServer.server! + let server = testServer.server! + documentManager = server._documentManager - self.workspace = await Workspace( - documentManager: DocumentManager(), - rootUri: nil, - capabilityRegistry: CapabilityRegistry(clientCapabilities: ClientCapabilities()), - toolchainRegistry: ToolchainRegistry.shared, - buildSetup: TestSourceKitServer.serverOptions.buildSetup, - underlyingBuildSystem: buildSystem, - index: nil, - indexDelegate: nil) + self.workspace = Workspace( + documentManager: DocumentManager(), + rootUri: nil, + capabilityRegistry: CapabilityRegistry(clientCapabilities: ClientCapabilities()), + toolchainRegistry: ToolchainRegistry.shared, + buildSetup: TestSourceKitServer.serverOptions.buildSetup, + underlyingBuildSystem: buildSystem, + index: nil, + indexDelegate: nil) + server._workspaces = [workspace] + workspace.buildSystemManager.delegate = server - await server.setWorkspaces([workspace]) - await workspace.buildSystemManager.setDelegate(server) - - sk = testServer.client - _ = try! sk.sendSync(InitializeRequest( + sk = testServer.client + _ = try! sk.sendSync(InitializeRequest( processId: nil, rootPath: nil, rootURI: nil, @@ -133,9 +116,6 @@ final class BuildSystemTests: XCTestCase { capabilities: ClientCapabilities(workspace: nil, textDocument: nil), trace: .off, workspaceFolders: nil)) - sema.signal() - } - sema.wait() } override func tearDown() { @@ -145,7 +125,7 @@ final class BuildSystemTests: XCTestCase { testServer = nil } - func testClangdDocumentUpdatedBuildSettings() async throws { + func testClangdDocumentUpdatedBuildSettings() throws { try XCTSkipIf(true, "rdar://115435598 - crashing on rebranch") guard haveClangd else { return } @@ -172,8 +152,6 @@ final class BuildSystemTests: XCTestCase { sk.allowUnexpectedNotification = false - let documentManager = await self.testServer.server!._documentManager - sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( uri: doc, language: .objective_c, @@ -181,7 +159,7 @@ final class BuildSystemTests: XCTestCase { text: text )), { (note: Notification) in XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) + XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text) }) // Modify the build settings and inform the delegate. @@ -192,19 +170,23 @@ final class BuildSystemTests: XCTestCase { let expectation = XCTestExpectation(description: "refresh") sk.handleNextNotification { (note: Notification) in XCTAssertEqual(note.params.diagnostics.count, 0) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) + XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text) expectation.fulfill() } - await buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)]) + buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)]) - try await fulfillmentOfOrThrow([expectation]) + let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout) + guard result == .completed else { + XCTFail("wait for diagnostics failed with \(result)") + return + } } - func testSwiftDocumentUpdatedBuildSettings() async throws { + func testSwiftDocumentUpdatedBuildSettings() { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let doc = DocumentURI(url) - let args = FallbackBuildSystem(buildSetup: .default).buildSettings(for: doc, language: .swift)!.compilerArguments + let args = FallbackBuildSystem(buildSetup: .default).settings(for: doc, .swift)!.compilerArguments buildSystem.buildSettingsByFile[doc] = FileBuildSettings(compilerArguments: args) @@ -218,8 +200,6 @@ final class BuildSystemTests: XCTestCase { sk.allowUnexpectedNotification = false - let documentManager = await self.testServer.server!._documentManager - sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( uri: doc, language: .swift, @@ -228,7 +208,7 @@ final class BuildSystemTests: XCTestCase { )), { (note: Notification) in // Syntactic analysis - no expected errors here. XCTAssertEqual(note.params.diagnostics.count, 0) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) + XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text) }, { (note: Notification) in // Semantic analysis - expect one error here. XCTAssertEqual(note.params.diagnostics.count, 1) @@ -251,13 +231,17 @@ final class BuildSystemTests: XCTestCase { XCTAssertEqual(note.params.diagnostics.count, 0) expectation.fulfill() } - await buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)]) + buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)]) - try await fulfillmentOfOrThrow([expectation]) + let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout) + guard result == .completed else { + XCTFail("wait for diagnostics failed with \(result)") + return + } } - func testClangdDocumentFallbackWithholdsDiagnostics() async throws { - try XCTSkipIf(!haveClangd) + func testClangdDocumentFallbackWithholdsDiagnostics() { + guard haveClangd else { return } #if os(Windows) let url = URL(fileURLWithPath: "C:/\(UUID())/file.m") @@ -279,8 +263,6 @@ final class BuildSystemTests: XCTestCase { sk.allowUnexpectedNotification = false - let documentManager = await self.testServer.server!._documentManager - sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( uri: doc, language: .objective_c, @@ -289,7 +271,7 @@ final class BuildSystemTests: XCTestCase { )), { (note: Notification) in // Expect diagnostics to be withheld. XCTAssertEqual(note.params.diagnostics.count, 0) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) + XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text) }) // Modify the build settings and inform the delegate. @@ -300,21 +282,25 @@ final class BuildSystemTests: XCTestCase { let expectation = XCTestExpectation(description: "refresh due to fallback --> primary") sk.handleNextNotification { (note: Notification) in XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) + XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text) expectation.fulfill() } - await buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)]) + buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)]) - try await fulfillmentOfOrThrow([expectation]) + let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout) + guard result == .completed else { + XCTFail("wait for diagnostics failed with \(result)") + return + } } - func testSwiftDocumentFallbackWithholdsSemanticDiagnostics() async throws { + func testSwiftDocumentFallbackWithholdsSemanticDiagnostics() { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let doc = DocumentURI(url) // Primary settings must be different than the fallback settings. - var primarySettings = FallbackBuildSystem(buildSetup: .default).buildSettings(for: doc, language: .swift)! + var primarySettings = FallbackBuildSystem(buildSetup: .default).settings(for: doc, .swift)! primarySettings.compilerArguments.append("-DPRIMARY") let text = """ @@ -328,8 +314,6 @@ final class BuildSystemTests: XCTestCase { sk.allowUnexpectedNotification = false - let documentManager = await self.testServer.server!._documentManager - sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( uri: doc, language: .swift, @@ -338,7 +322,7 @@ final class BuildSystemTests: XCTestCase { )), { (note: Notification) in // Syntactic analysis - one expected errors here (for `func`). XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual(text, documentManager.latestSnapshot(doc)!.text) + XCTAssertEqual(text, self.documentManager.latestSnapshot(doc)!.text) }, { (note: Notification) in // Should be the same syntactic analysis since we are using fallback arguments XCTAssertEqual(note.params.diagnostics.count, 1) @@ -358,19 +342,21 @@ final class BuildSystemTests: XCTestCase { XCTAssertEqual(note.params.diagnostics.count, 2) expectation.fulfill() } - await buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(primarySettings)]) + buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(primarySettings)]) - try await fulfillmentOfOrThrow([expectation]) + let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout) + guard result == .completed else { + XCTFail("wait for diagnostics failed with \(result)") + return + } } - func testSwiftDocumentBuildSettingsChangedFalseAlarm() async throws { + func testSwiftDocumentBuildSettingsChangedFalseAlarm() { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let doc = DocumentURI(url) sk.allowUnexpectedNotification = false - let documentManager = await self.testServer.server!._documentManager - sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( uri: doc, language: .swift, @@ -380,7 +366,7 @@ final class BuildSystemTests: XCTestCase { """ )), { (note: Notification) in XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual("func", documentManager.latestSnapshot(doc)!.text) + XCTAssertEqual("func", self.documentManager.latestSnapshot(doc)!.text) }, { (note: Notification) in // Using fallback system, so we will receive the same syntactic diagnostics from before. XCTAssertEqual(note.params.diagnostics.count, 1) @@ -388,23 +374,27 @@ final class BuildSystemTests: XCTestCase { // Modify the build settings and inform the SourceKitServer. // This shouldn't trigger new diagnostics since nothing actually changed (false alarm). - await buildSystem.delegate?.fileBuildSettingsChanged([doc: .removedOrUnavailable]) + buildSystem.delegate?.fileBuildSettingsChanged([doc: .removedOrUnavailable]) let expectation = XCTestExpectation(description: "refresh doesn't occur") expectation.isInverted = true sk.handleNextNotification { (note: Notification) in XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual("func", documentManager.latestSnapshot(doc)!.text) + XCTAssertEqual("func", self.documentManager.latestSnapshot(doc)!.text) expectation.fulfill() } - try await fulfillmentOfOrThrow([expectation], timeout: 1) + let result = XCTWaiter.wait(for: [expectation], timeout: 1) + guard result == .completed else { + XCTFail("wait for diagnostics failed with \(result)") + return + } } - func testMainFilesChanged() async throws { + func testMainFilesChanged() throws { try XCTSkipIf(true, "rdar://115176405 - failing on rebranch due to extra published diagnostic") - let ws = try await mutableSourceKitTibsTestWorkspace(name: "MainFiles")! + let ws = try mutableSourceKitTibsTestWorkspace(name: "MainFiles")! let unique_h = ws.testLoc("unique").docIdentifier.uri ws.testServer.client.allowUnexpectedNotification = false @@ -417,7 +407,7 @@ final class BuildSystemTests: XCTestCase { } try ws.openDocument(unique_h.fileURL!, language: .cpp) - try await fulfillmentOfOrThrow([expectation]) + wait(for: [expectation], timeout: defaultTimeout) let use_d = self.expectation(description: "update settings to d.cpp") ws.testServer.client.handleNextNotification { (note: Notification) in @@ -430,7 +420,7 @@ final class BuildSystemTests: XCTestCase { } try ws.buildAndIndex() - try await fulfillmentOfOrThrow([use_d]) + wait(for: [use_d], timeout: defaultTimeout) let use_c = self.expectation(description: "update settings to c.cpp") ws.testServer.client.handleNextNotification { (note: Notification) in @@ -451,7 +441,7 @@ final class BuildSystemTests: XCTestCase { """, to: ws.testLoc("c_func").url) } - try await fulfillmentOfOrThrow([use_c]) + wait(for: [use_c], timeout: defaultTimeout) } private func clangBuildSettings(for uri: DocumentURI) -> FileBuildSettings { diff --git a/Tests/SourceKitLSPTests/CallHierarchyTests.swift b/Tests/SourceKitLSPTests/CallHierarchyTests.swift index 42a20dd2..b15dff04 100644 --- a/Tests/SourceKitLSPTests/CallHierarchyTests.swift +++ b/Tests/SourceKitLSPTests/CallHierarchyTests.swift @@ -16,8 +16,8 @@ import TSCBasic import XCTest final class CallHierarchyTests: XCTestCase { - func testCallHierarchy() async throws { - let ws = try await staticSourceKitTibsWorkspace(name: "CallHierarchy")! + func testCallHierarchy() throws { + let ws = try staticSourceKitTibsWorkspace(name: "CallHierarchy")! try ws.buildAndIndex() try ws.openDocument(ws.testLoc("a.swift").url, language: .swift) diff --git a/Tests/SourceKitLSPTests/CodeActionTests.swift b/Tests/SourceKitLSPTests/CodeActionTests.swift index 7b8bf5a7..2045fb23 100644 --- a/Tests/SourceKitLSPTests/CodeActionTests.swift +++ b/Tests/SourceKitLSPTests/CodeActionTests.swift @@ -33,9 +33,9 @@ final class CodeActionTests: XCTestCase { return ClientCapabilities(workspace: nil, textDocument: documentCapabilities) } - private func refactorTibsWorkspace() async throws -> SKTibsTestWorkspace? { + private func refactorTibsWorkspace() throws -> SKTibsTestWorkspace? { let capabilities = clientCapabilitiesWithCodeActionSupport() - return try await staticSourceKitTibsWorkspace(name: "SemanticRefactor", clientCapabilities: capabilities) + return try staticSourceKitTibsWorkspace(name: "SemanticRefactor", clientCapabilities: capabilities) } func testCodeActionResponseLegacySupport() throws { @@ -176,8 +176,8 @@ final class CodeActionTests: XCTestCase { XCTAssertEqual(decoded, command) } - func testEmptyCodeActionResult() async throws { - guard let ws = try await refactorTibsWorkspace() else { return } + func testEmptyCodeActionResult() throws { + guard let ws = try refactorTibsWorkspace() else { return } let loc = ws.testLoc("sr:foo") try ws.openDocument(loc.url, language: .swift) @@ -190,8 +190,8 @@ final class CodeActionTests: XCTestCase { } } - func testSemanticRefactorLocalRenameResult() async throws { - guard let ws = try await refactorTibsWorkspace() else { return } + func testSemanticRefactorLocalRenameResult() throws { + guard let ws = try refactorTibsWorkspace() else { return } let loc = ws.testLoc("sr:local") try ws.openDocument(loc.url, language: .swift) @@ -203,8 +203,8 @@ final class CodeActionTests: XCTestCase { } } - func testSemanticRefactorLocationCodeActionResult() async throws { - guard let ws = try await refactorTibsWorkspace() else { return } + func testSemanticRefactorLocationCodeActionResult() throws { + guard let ws = try refactorTibsWorkspace() else { return } let loc = ws.testLoc("sr:string") try ws.openDocument(loc.url, language: .swift) @@ -225,8 +225,8 @@ final class CodeActionTests: XCTestCase { XCTAssertEqual(result, .codeActions([expectedCodeAction])) } - func testSemanticRefactorRangeCodeActionResult() async throws { - guard let ws = try await refactorTibsWorkspace() else { return } + func testSemanticRefactorRangeCodeActionResult() throws { + guard let ws = try refactorTibsWorkspace() else { return } let rangeStartLoc = ws.testLoc("sr:extractStart") let rangeEndLoc = ws.testLoc("sr:extractEnd") try ws.openDocument(rangeStartLoc.url, language: .swift) @@ -249,9 +249,9 @@ final class CodeActionTests: XCTestCase { XCTAssertEqual(result, .codeActions([expectedCodeAction])) } - func testCodeActionsRemovePlaceholders() async throws { + func testCodeActionsRemovePlaceholders() throws { let capabilities = clientCapabilitiesWithCodeActionSupport() - let ws = try await staticSourceKitTibsWorkspace(name: "Fixit", clientCapabilities: capabilities)! + let ws = try staticSourceKitTibsWorkspace(name: "Fixit", clientCapabilities: capabilities)! let def = ws.testLoc("MyStruct:def") @@ -276,7 +276,7 @@ final class CodeActionTests: XCTestCase { semanticDiagnosticsReceived.fulfill() } - try await fulfillmentOfOrThrow([syntacticDiagnosticsReceived, semanticDiagnosticsReceived]) + self.wait(for: [syntacticDiagnosticsReceived, semanticDiagnosticsReceived], timeout: defaultTimeout) let textDocument = TextDocumentIdentifier(def.url) let actionsRequest = CodeActionRequest(range: def.position.. (SKTibsTestWorkspace, DocumentURI)? { + func initializeWorkspace(withCapabilities capabilities: FoldingRangeCapabilities, testLoc: String) throws -> (SKTibsTestWorkspace, DocumentURI)? { var documentCapabilities = TextDocumentClientCapabilities() documentCapabilities.foldingRange = capabilities let capabilities = ClientCapabilities(workspace: nil, textDocument: documentCapabilities) - guard let ws = try await staticSourceKitTibsWorkspace(name: "FoldingRange", + guard let ws = try staticSourceKitTibsWorkspace(name: "FoldingRange", clientCapabilities: capabilities) else { return nil } let loc = ws.testLoc(testLoc) try ws.openDocument(loc.url, language: .swift) return (ws, DocumentURI(loc.url)) } - func testPartialLineFolding() async throws { + func testPartialLineFolding() throws { var capabilities = FoldingRangeCapabilities() capabilities.lineFoldingOnly = false - guard let (ws, uri) = try await initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:base") else { return } + guard let (ws, uri) = try initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:base") else { return } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(uri)) let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } @@ -56,11 +56,11 @@ final class FoldingRangeTests: XCTestCase { XCTAssertEqual(ranges, expected) } - func testLineFoldingOnly() async throws { + func testLineFoldingOnly() throws { var capabilities = FoldingRangeCapabilities() capabilities.lineFoldingOnly = true - guard let (ws, uri) = try await initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:base") else { return } + guard let (ws, uri) = try initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:base") else { return } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(uri)) let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } @@ -79,29 +79,29 @@ final class FoldingRangeTests: XCTestCase { XCTAssertEqual(ranges, expected) } - func testRangeLimit() async throws { + func testRangeLimit() throws { - func performTest(withRangeLimit limit: Int?, expecting expectedRanges: Int, line: Int = #line) async throws { + func performTest(withRangeLimit limit: Int?, expecting expectedRanges: Int, line: Int = #line) throws { var capabilities = FoldingRangeCapabilities() capabilities.lineFoldingOnly = false capabilities.rangeLimit = limit - guard let (ws, url) = try await initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:base") else { return } + guard let (ws, url) = try initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:base") else { return } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(url)) let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } XCTAssertEqual(ranges?.count, expectedRanges, "Failed rangeLimit test at line \(line)") } - try await performTest(withRangeLimit: -100, expecting: 0) - try await performTest(withRangeLimit: 0, expecting: 0) - try await performTest(withRangeLimit: 4, expecting: 4) - try await performTest(withRangeLimit: 5000, expecting: 12) - try await performTest(withRangeLimit: nil, expecting: 12) + try performTest(withRangeLimit: -100, expecting: 0) + try performTest(withRangeLimit: 0, expecting: 0) + try performTest(withRangeLimit: 4, expecting: 4) + try performTest(withRangeLimit: 5000, expecting: 12) + try performTest(withRangeLimit: nil, expecting: 12) } - func testNoRanges() async throws { + func testNoRanges() throws { let capabilities = FoldingRangeCapabilities() - guard let (ws, url) = try await initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:empty") else { return } + guard let (ws, url) = try initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:empty") else { return } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(url)) let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } @@ -109,12 +109,12 @@ final class FoldingRangeTests: XCTestCase { XCTAssertEqual(ranges?.count, 0) } - func testMultilineDocLineComment() async throws { + func testMultilineDocLineComment() throws { // In this file the range of the call to `print` and the range of the argument "/*fr:duplicateRanges*/" are the same. // Test that we only report the folding range once. let capabilities = FoldingRangeCapabilities() - guard let (ws, url) = try await initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:multilineDocLineComment") else { return } + guard let (ws, url) = try initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:multilineDocLineComment") else { return } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(url)) let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } @@ -131,12 +131,12 @@ final class FoldingRangeTests: XCTestCase { XCTAssertEqual(ranges, expected) } - func testDontReportDuplicateRangesRanges() async throws { + func testDontReportDuplicateRangesRanges() throws { // In this file the range of the call to `print` and the range of the argument "/*fr:duplicateRanges*/" are the same. // Test that we only report the folding range once. let capabilities = FoldingRangeCapabilities() - guard let (ws, url) = try await initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:duplicateRanges") else { return } + guard let (ws, url) = try initializeWorkspace(withCapabilities: capabilities, testLoc: "fr:duplicateRanges") else { return } let request = FoldingRangeRequest(textDocument: TextDocumentIdentifier(url)) let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) } diff --git a/Tests/SourceKitLSPTests/ImplementationTests.swift b/Tests/SourceKitLSPTests/ImplementationTests.swift index 060540a1..20e19307 100644 --- a/Tests/SourceKitLSPTests/ImplementationTests.swift +++ b/Tests/SourceKitLSPTests/ImplementationTests.swift @@ -16,8 +16,8 @@ import TSCBasic import XCTest final class ImplementationTests: XCTestCase { - func testImplementation() async throws { - let ws = try await staticSourceKitTibsWorkspace(name: "Implementation")! + func testImplementation() throws { + let ws = try staticSourceKitTibsWorkspace(name: "Implementation")! try ws.buildAndIndex() try ws.openDocument(ws.testLoc("a.swift").url, language: .swift) diff --git a/Tests/SourceKitLSPTests/LocalClangTests.swift b/Tests/SourceKitLSPTests/LocalClangTests.swift index 7e860ac9..73359f5c 100644 --- a/Tests/SourceKitLSPTests/LocalClangTests.swift +++ b/Tests/SourceKitLSPTests/LocalClangTests.swift @@ -191,8 +191,8 @@ final class LocalClangTests: XCTestCase { XCTAssertEqual(syms.first?.children?.first?.name, "foo") } - func testCodeAction() async throws { - guard let ws = try await staticSourceKitTibsWorkspace(name: "CodeActionCxx") else { return } + func testCodeAction() throws { + guard let ws = try staticSourceKitTibsWorkspace(name: "CodeActionCxx") else { return } if ToolchainRegistry.shared.default?.clangd == nil { return } let loc = ws.testLoc("SwitchColor") @@ -212,7 +212,10 @@ final class LocalClangTests: XCTestCase { try ws.openDocument(loc.url, language: .cpp) - try await fulfillmentOfOrThrow([expectation]) + let result = XCTWaiter.wait(for: [expectation], timeout: defaultTimeout) + if result != .completed { + fatalError("error \(result) waiting for diagnostics notification") + } let codeAction = CodeActionRequest( range: Position(loc)..) in log("Received diagnostics for open - semantic") XCTAssertEqual(note.params.version, 12) @@ -98,7 +93,7 @@ final class LocalSwiftTests: XCTestCase { // 0 = semantic update finished already XCTAssertEqual(note.params.version, 13) XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - XCTAssertEqual("func foo() {}\n", documentManager.latestSnapshot(uri)!.text) + XCTAssertEqual("func foo() {}\n", self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for edit 1 - semantic") XCTAssertEqual(note.params.version, 13) @@ -116,7 +111,7 @@ final class LocalSwiftTests: XCTestCase { XCTAssertEqual(""" func foo() {} bar() - """, documentManager.latestSnapshot(uri)!.text) + """, self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for edit 2 - semantic") XCTAssertEqual(note.params.version, 14) @@ -137,7 +132,7 @@ final class LocalSwiftTests: XCTestCase { XCTAssertEqual(""" func foo() {} foo() - """, documentManager.latestSnapshot(uri)!.text) + """, self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for edit 3 - semantic") XCTAssertEqual(note.params.version, 14) @@ -155,7 +150,7 @@ final class LocalSwiftTests: XCTestCase { XCTAssertEqual(""" func foo() {} fooTypo() - """, documentManager.latestSnapshot(uri)!.text) + """, self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for edit 4 - semantic") XCTAssertEqual(note.params.version, 15) @@ -178,7 +173,7 @@ final class LocalSwiftTests: XCTestCase { XCTAssertEqual(""" func bar() {} foo() - """, documentManager.latestSnapshot(uri)!.text) + """, self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for edit 5 - semantic") XCTAssertEqual(note.params.version, 16) @@ -189,13 +184,11 @@ final class LocalSwiftTests: XCTestCase { }) } - func testEditingNonURL() async { + func testEditingNonURL() { let uri = DocumentURI(string: "urn:uuid:A1B08909-E791-469E-BF0F-F5790977E051") sk.allowUnexpectedNotification = false - let documentManager = await connection.server!._documentManager - sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( uri: uri, language: .swift, @@ -207,7 +200,7 @@ final class LocalSwiftTests: XCTestCase { log("Received diagnostics for open - syntactic") XCTAssertEqual(note.params.version, 12) XCTAssertEqual(note.params.diagnostics.count, 1) - XCTAssertEqual("func", documentManager.latestSnapshot(uri)!.text) + XCTAssertEqual("func", self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for open - semantic") XCTAssertEqual(note.params.version, 12) @@ -225,7 +218,7 @@ final class LocalSwiftTests: XCTestCase { // 1 = remaining semantic error // 0 = semantic update finished already XCTAssertLessThanOrEqual(note.params.diagnostics.count, 1) - XCTAssertEqual("func foo() {}\n", documentManager.latestSnapshot(uri)!.text) + XCTAssertEqual("func foo() {}\n", self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for edit 1 - semantic") XCTAssertEqual(note.params.version, 13) @@ -243,7 +236,7 @@ final class LocalSwiftTests: XCTestCase { XCTAssertEqual(""" func foo() {} bar() - """, documentManager.latestSnapshot(uri)!.text) + """, self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for edit 2 - semantic") XCTAssertEqual(note.params.version, 14) @@ -264,7 +257,7 @@ final class LocalSwiftTests: XCTestCase { XCTAssertEqual(""" func foo() {} foo() - """, documentManager.latestSnapshot(uri)!.text) + """, self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for edit 3 - semantic") XCTAssertEqual(note.params.version, 14) @@ -282,7 +275,7 @@ final class LocalSwiftTests: XCTestCase { XCTAssertEqual(""" func foo() {} fooTypo() - """, documentManager.latestSnapshot(uri)!.text) + """, self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for edit 4 - semantic") XCTAssertEqual(note.params.version, 15) @@ -305,7 +298,7 @@ final class LocalSwiftTests: XCTestCase { XCTAssertEqual(""" func bar() {} foo() - """, documentManager.latestSnapshot(uri)!.text) + """, self.documentManager.latestSnapshot(uri)!.text) }, { (note: Notification) in log("Received diagnostics for edit 5 - semantic") XCTAssertEqual(note.params.version, 16) @@ -1485,13 +1478,13 @@ final class LocalSwiftTests: XCTestCase { XCTAssertNil(data) } - func testIncrementalParse() async throws { + func testIncrementalParse() throws { let url = URL(fileURLWithPath: "/\(UUID())/a.swift") let uri = DocumentURI(url) var reusedNodes: [Syntax] = [] - let swiftLanguageServer = await connection.server!._languageService(for: uri, .swift, in: connection.server!.workspaceForDocument(uri: uri)!) as! SwiftLanguageServer - await swiftLanguageServer.setReusedNodeCallback({ reusedNodes.append($0) }) + let swiftLanguageServer = connection.server!._languageService(for: uri, .swift, in: connection.server!.workspaceForDocumentOnQueue(uri: uri)!) as! SwiftLanguageServer + swiftLanguageServer.reusedNodeCallback = { reusedNodes.append($0) } sk.allowUnexpectedNotification = false sk.sendNoteSync(DidOpenTextDocumentNotification(textDocument: TextDocumentItem( diff --git a/Tests/SourceKitLSPTests/MainFilesProviderTests.swift b/Tests/SourceKitLSPTests/MainFilesProviderTests.swift index 130dfa90..f72ca02d 100644 --- a/Tests/SourceKitLSPTests/MainFilesProviderTests.swift +++ b/Tests/SourceKitLSPTests/MainFilesProviderTests.swift @@ -20,8 +20,8 @@ import XCTest final class MainFilesProviderTests: XCTestCase { - func testMainFilesChanged() async throws { - let ws = try await mutableSourceKitTibsTestWorkspace(name: "MainFiles")! + func testMainFilesChanged() throws { + let ws = try mutableSourceKitTibsTestWorkspace(name: "MainFiles")! let indexDelegate = SourceKitIndexDelegate() ws.tibsWorkspace.delegate = indexDelegate @@ -62,7 +62,7 @@ final class MainFilesProviderTests: XCTestCase { XCTAssertEqual(ws.index.mainFilesContainingFile(shared_h), [c, d]) XCTAssertEqual(ws.index.mainFilesContainingFile(bridging), [c]) - try await fulfillmentOfOrThrow([mainFilesDelegate.expectation]) + wait(for: [mainFilesDelegate.expectation], timeout: defaultTimeout) try ws.edit { changes, _ in changes.write(""" @@ -83,7 +83,7 @@ final class MainFilesProviderTests: XCTestCase { XCTAssertEqual(ws.index.mainFilesContainingFile(shared_h), []) XCTAssertEqual(ws.index.mainFilesContainingFile(bridging), [d]) - try await fulfillmentOfOrThrow([mainFilesDelegate.expectation]) + wait(for: [mainFilesDelegate.expectation], timeout: defaultTimeout) XCTAssertEqual(ws.index.mainFilesContainingFile(DocumentURI(string: "not:file")), []) } diff --git a/Tests/SourceKitLSPTests/SemanticTokensTests.swift b/Tests/SourceKitLSPTests/SemanticTokensTests.swift index ca960a8a..3c590db3 100644 --- a/Tests/SourceKitLSPTests/SemanticTokensTests.swift +++ b/Tests/SourceKitLSPTests/SemanticTokensTests.swift @@ -188,7 +188,7 @@ final class SemanticTokensTests: XCTestCase { XCTAssertEqual(decoded, tokens) } - func testRangeSplitting() async { + func testRangeSplitting() { let text = """ struct X { let x: Int @@ -199,7 +199,7 @@ final class SemanticTokensTests: XCTestCase { """ openDocument(text: text) - guard let snapshot = await connection.server?._documentManager.latestSnapshot(uri) else { + guard let snapshot = connection.server?._documentManager.latestSnapshot(uri) else { fatalError("Could not fetch document snapshot for \(#function)") } diff --git a/Tests/SourceKitLSPTests/SourceKitTests.swift b/Tests/SourceKitLSPTests/SourceKitTests.swift index 550c7809..4007c1d7 100644 --- a/Tests/SourceKitLSPTests/SourceKitTests.swift +++ b/Tests/SourceKitLSPTests/SourceKitTests.swift @@ -68,8 +68,8 @@ final class SKTests: XCTestCase { XCTAssertNotNil(initResult.capabilities.completionProvider) } - func testIndexSwiftModules() async throws { - guard let ws = try await staticSourceKitTibsWorkspace(name: "SwiftModules") else { return } + func testIndexSwiftModules() throws { + guard let ws = try staticSourceKitTibsWorkspace(name: "SwiftModules") else { return } try ws.buildAndIndex() defer { withExtendedLifetime(ws) {} } // Keep workspace alive for callbacks. @@ -108,7 +108,7 @@ final class SKTests: XCTestCase { ]) } - func testIndexShutdown() async throws { + func testIndexShutdown() throws { let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent("sk-test-data/\(testDirectoryName)", isDirectory: true) @@ -117,8 +117,8 @@ final class SKTests: XCTestCase { try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) } - func checkRunningIndex(build: Bool) async throws -> URL? { - guard let ws = try await staticSourceKitTibsWorkspace( + func checkRunningIndex(build: Bool) throws -> URL? { + guard let ws = try staticSourceKitTibsWorkspace( name: "SwiftModules", tmpDir: tmpDir, removeTmpDir: false) else { return nil @@ -158,19 +158,19 @@ final class SKTests: XCTestCase { return versionedPath } - guard let versionedPath = try await checkRunningIndex(build: true) else { return } + guard let versionedPath = try checkRunningIndex(build: true) else { return } let versionContentsAfter = try listdir(versionedPath) XCTAssertEqual(versionContentsAfter.count, 1) XCTAssertEqual(versionContentsAfter.first?.lastPathComponent, "saved") - _ = try await checkRunningIndex(build: true) + _ = try checkRunningIndex(build: true) try FileManager.default.removeItem(atPath: tmpDir.path) } - func testCodeCompleteSwiftTibs() async throws { - guard let ws = try await staticSourceKitTibsWorkspace(name: "CodeCompleteSingleModule") else { return } + func testCodeCompleteSwiftTibs() throws { + guard let ws = try staticSourceKitTibsWorkspace(name: "CodeCompleteSingleModule") else { return } let loc = ws.testLoc("cc:A") try ws.openDocument(loc.url, language: .swift) @@ -200,8 +200,8 @@ final class SKTests: XCTestCase { ]) } - func testDependenciesUpdatedSwiftTibs() async throws { - guard let ws = try await mutableSourceKitTibsTestWorkspace(name: "SwiftModules") else { return } + func testDependenciesUpdatedSwiftTibs() throws { + guard let ws = try mutableSourceKitTibsTestWorkspace(name: "SwiftModules") else { return } defer { withExtendedLifetime(ws) {} } // Keep workspace alive for callbacks. guard let server = ws.testServer.server else { XCTFail("Unable to fetch SourceKitServer to notify for build system events.") @@ -227,7 +227,10 @@ final class SKTests: XCTestCase { } try ws.openDocument(moduleRef.url, language: .swift) - try await fulfillmentOfOrThrow([startExpectation]) + let started = XCTWaiter.wait(for: [startExpectation], timeout: defaultTimeout) + if started != .completed { + fatalError("error \(started) waiting for initial diagnostics notification") + } try ws.buildAndIndex() @@ -245,11 +248,14 @@ final class SKTests: XCTestCase { } server.filesDependenciesUpdated([DocumentURI(moduleRef.url)]) - try await fulfillmentOfOrThrow([finishExpectation]) + let finished = XCTWaiter.wait(for: [finishExpectation], timeout: defaultTimeout) + if finished != .completed { + fatalError("error \(finished) waiting for post-build diagnostics notification") + } } - func testDependenciesUpdatedCXXTibs() async throws { - guard let ws = try await mutableSourceKitTibsTestWorkspace(name: "GeneratedHeader") else { return } + func testDependenciesUpdatedCXXTibs() throws { + guard let ws = try mutableSourceKitTibsTestWorkspace(name: "GeneratedHeader") else { return } defer { withExtendedLifetime(ws) {} } // Keep workspace alive for callbacks. guard let server = ws.testServer.server else { XCTFail("Unable to fetch SourceKitServer to notify for build system events.") @@ -272,7 +278,11 @@ final class SKTests: XCTestCase { // files without a recently upstreamed extension. try "".write(to: generatedHeaderURL, atomically: true, encoding: .utf8) try ws.openDocument(moduleRef.url, language: .c) - try await fulfillmentOfOrThrow([startExpectation]) + let started = XCTWaiter.wait(for: [startExpectation], timeout: defaultTimeout) + guard started == .completed else { + XCTFail("error \(started) waiting for initial diagnostics notification") + return + } // Update the header file to have the proper contents for our code to build. let contents = "int libX(int value);" @@ -288,11 +298,15 @@ final class SKTests: XCTestCase { } server.filesDependenciesUpdated([DocumentURI(moduleRef.url)]) - try await fulfillmentOfOrThrow([finishExpectation]) + let finished = XCTWaiter.wait(for: [finishExpectation], timeout: defaultTimeout) + guard finished == .completed else { + XCTFail("error \(finished) waiting for post-build diagnostics notification") + return + } } - func testClangdGoToInclude() async throws { - guard let ws = try await staticSourceKitTibsWorkspace(name: "BasicCXX") else { return } + func testClangdGoToInclude() throws { + guard let ws = try staticSourceKitTibsWorkspace(name: "BasicCXX") else { return } guard ToolchainRegistry.shared.default?.clangd != nil else { return } let mainLoc = ws.testLoc("Object:include:main") @@ -321,8 +335,8 @@ final class SKTests: XCTestCase { } } - func testClangdGoToDefinitionWithoutIndex() async throws { - guard let ws = try await staticSourceKitTibsWorkspace(name: "BasicCXX") else { return } + func testClangdGoToDefinitionWithoutIndex() throws { + guard let ws = try staticSourceKitTibsWorkspace(name: "BasicCXX") else { return } guard ToolchainRegistry.shared.default?.clangd != nil else { return } let refLoc = ws.testLoc("Object:ref:main") @@ -350,8 +364,8 @@ final class SKTests: XCTestCase { } } - func testClangdGoToDeclaration() async throws { - guard let ws = try await staticSourceKitTibsWorkspace(name: "BasicCXX") else { return } + func testClangdGoToDeclaration() throws { + guard let ws = try staticSourceKitTibsWorkspace(name: "BasicCXX") else { return } guard ToolchainRegistry.shared.default?.clangd != nil else { return } let mainLoc = ws.testLoc("Object:ref:newObject") diff --git a/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift index 27c62b9c..d92bab9c 100644 --- a/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift +++ b/Tests/SourceKitLSPTests/SwiftInterfaceTests.swift @@ -28,6 +28,10 @@ final class SwiftInterfaceTests: XCTestCase { /// The primary interface to make requests to the SourceKitServer. var sk: TestClient! = nil + var documentManager: DocumentManager! { + connection.server!._documentManager + } + override func setUp() { // This is the only test that references modules from the SDK (Foundation). // `testSystemModuleInterface` has been flaky for a long while and a @@ -86,8 +90,8 @@ final class SwiftInterfaceTests: XCTestCase { XCTAssert(fileContents.hasPrefix("import "), "Expected that the foundation swift interface starts with 'import ' but got '\(fileContents.prefix(100))'") } - func testOpenInterface() async throws { - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } + func testOpenInterface() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } try ws.buildAndIndex() let importedModule = ws.testLoc("lib:import") try ws.openDocument(importedModule.url, language: .swift) @@ -130,8 +134,8 @@ final class SwiftInterfaceTests: XCTestCase { ws.closeDocument(testLoc.url) } - func testDefinitionInSystemModuleInterface() async throws { - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SystemSwiftInterface") else { return } + func testDefinitionInSystemModuleInterface() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SystemSwiftInterface") else { return } try ws.buildAndIndex(withSystemSymbols: true) let stringRef = ws.testLoc("lib.string") let intRef = ws.testLoc("lib.integer") @@ -160,8 +164,8 @@ final class SwiftInterfaceTests: XCTestCase { ) } - func testSwiftInterfaceAcrossModules() async throws { - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } + func testSwiftInterfaceAcrossModules() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } try ws.buildAndIndex() let importedModule = ws.testLoc("lib:import") try ws.openDocument(importedModule.url, language: .swift) diff --git a/Tests/SourceKitLSPTests/SwiftPMIntegration.swift b/Tests/SourceKitLSPTests/SwiftPMIntegration.swift index 2a2529a1..d8f9ea68 100644 --- a/Tests/SourceKitLSPTests/SwiftPMIntegration.swift +++ b/Tests/SourceKitLSPTests/SwiftPMIntegration.swift @@ -16,8 +16,8 @@ import XCTest final class SwiftPMIntegrationTests: XCTestCase { - func testSwiftPMIntegration() async throws { - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } + func testSwiftPMIntegration() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } try ws.buildAndIndex() let call = ws.testLoc("Lib.foo:call") @@ -58,8 +58,8 @@ final class SwiftPMIntegrationTests: XCTestCase { ]) } - func testAddFile() async throws { - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } + func testAddFile() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } try ws.buildAndIndex() /// Add a new file to the project that wasn't built @@ -142,8 +142,8 @@ final class SwiftPMIntegrationTests: XCTestCase { ) } - func testModifyPackageManifest() async throws { - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } + func testModifyPackageManifest() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } try ws.buildAndIndex() let otherLib = ws.testLoc("OtherLib.topLevelFunction:libMember") @@ -211,7 +211,7 @@ final class SwiftPMIntegrationTests: XCTestCase { didReceiveCorrectCompletions = true break } - try await Task.sleep(nanoseconds: 1_000_000_000) + Thread.sleep(forTimeInterval: 1) } XCTAssert(didReceiveCorrectCompletions) diff --git a/Tests/SourceKitLSPTests/TypeHierarchyTests.swift b/Tests/SourceKitLSPTests/TypeHierarchyTests.swift index 4bf3941a..5313a1c8 100644 --- a/Tests/SourceKitLSPTests/TypeHierarchyTests.swift +++ b/Tests/SourceKitLSPTests/TypeHierarchyTests.swift @@ -16,8 +16,8 @@ import TSCBasic import XCTest final class TypeHierarchyTests: XCTestCase { - func testTypeHierarchy() async throws { - let ws = try await staticSourceKitTibsWorkspace(name: "TypeHierarchy")! + func testTypeHierarchy() throws { + let ws = try staticSourceKitTibsWorkspace(name: "TypeHierarchy")! try ws.buildAndIndex() try ws.openDocument(ws.testLoc("a.swift").url, language: .swift) diff --git a/Tests/SourceKitLSPTests/WorkspaceTests.swift b/Tests/SourceKitLSPTests/WorkspaceTests.swift index 5ab4055c..77066256 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTests.swift @@ -21,11 +21,11 @@ import XCTest final class WorkspaceTests: XCTestCase { - func testMultipleSwiftPMWorkspaces() async throws { - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } + func testMultipleSwiftPMWorkspaces() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage") else { return } try ws.buildAndIndex() - guard let otherWs = try await staticSourceKitSwiftPMWorkspace(name: "OtherSwiftPMPackage", server: ws.testServer) else { return } + guard let otherWs = try staticSourceKitSwiftPMWorkspace(name: "OtherSwiftPMPackage", server: ws.testServer) else { return } try otherWs.buildAndIndex() assert(ws.testServer === otherWs.testServer, "Sanity check: The two workspaces should be opened in the same server") @@ -96,8 +96,8 @@ final class WorkspaceTests: XCTestCase { ]) } - func testMultipleClangdWorkspaces() async throws { - guard let ws = try await staticSourceKitTibsWorkspace(name: "ClangModules") else { return } + func testMultipleClangdWorkspaces() throws { + guard let ws = try staticSourceKitTibsWorkspace(name: "ClangModules") else { return } let loc = ws.testLoc("main_file") @@ -110,9 +110,9 @@ final class WorkspaceTests: XCTestCase { try ws.openDocument(loc.url, language: .objective_c) - try await fulfillmentOfOrThrow([expectation]) + waitForExpectations(timeout: defaultTimeout) - let otherWs = try await staticSourceKitTibsWorkspace(name: "ClangCrashRecoveryBuildSettings", server: ws.testServer)! + let otherWs = try staticSourceKitTibsWorkspace(name: "ClangCrashRecoveryBuildSettings", server: ws.testServer)! assert(ws.testServer === otherWs.testServer, "Sanity check: The two workspaces should be opened in the same server") let otherLoc = otherWs.testLoc("loc") @@ -130,11 +130,11 @@ final class WorkspaceTests: XCTestCase { XCTAssertEqual(highlightResponse, expectedHighlightResponse) } - func testRecomputeFileWorkspaceMembershipOnPackageSwiftChange() async throws { - guard let otherWs = try await staticSourceKitSwiftPMWorkspace(name: "OtherSwiftPMPackage") else { return } + func testRecomputeFileWorkspaceMembershipOnPackageSwiftChange() throws { + guard let otherWs = try staticSourceKitSwiftPMWorkspace(name: "OtherSwiftPMPackage") else { return } try otherWs.buildAndIndex() - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage", server: otherWs.testServer) else { return } + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "SwiftPMPackage", server: otherWs.testServer) else { return } try ws.buildAndIndex() assert(ws.testServer === otherWs.testServer, "Sanity check: The two workspaces should be opened in the same server") @@ -148,7 +148,7 @@ final class WorkspaceTests: XCTestCase { // SwiftPMPackage that hasn't been added to Package.swift yet) will belong // to OtherSwiftPMPackage by default (because it provides fallback build // settings for it). - assertEqual(await ws.testServer.server!.workspaceForDocument(uri: otherLib.docUri)?.rootUri, DocumentURI(otherWs.sources.rootDirectory)) + XCTAssertEqual(ws.testServer.server!.workspaceForDocumentOnQueue(uri: otherLib.docUri)?.rootUri, DocumentURI(otherWs.sources.rootDirectory)) // Add the otherlib target to Package.swift _ = try ws.sources.edit { builder in @@ -179,18 +179,18 @@ final class WorkspaceTests: XCTestCase { // Updating the build settings takes a few seconds. Send code completion requests every second until we receive correct results. for _ in 0..<30 { - if await ws.testServer.server!.workspaceForDocument(uri: otherLib.docUri)?.rootUri == DocumentURI(ws.sources.rootDirectory) { + if ws.testServer.server!.workspaceForDocumentOnQueue(uri: otherLib.docUri)?.rootUri == DocumentURI(ws.sources.rootDirectory) { didReceiveCorrectWorkspaceMembership = true break } - try await Task.sleep(nanoseconds: 1_000_000_000) + Thread.sleep(forTimeInterval: 1) } XCTAssert(didReceiveCorrectWorkspaceMembership) } - func testMixedPackage() async throws { - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "MixedPackage") else { return } + func testMixedPackage() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "MixedPackage") else { return } try ws.buildAndIndex() let cLoc = ws.testLoc("clib_func:body") @@ -211,13 +211,13 @@ final class WorkspaceTests: XCTestCase { } } - try await fulfillmentOfOrThrow([receivedResponse]) + self.wait(for: [receivedResponse], timeout: defaultTimeout) } - func testChangeWorkspaceFolders() async throws { - guard let ws = try await staticSourceKitSwiftPMWorkspace(name: "ChangeWorkspaceFolders") else { return } + func testChangeWorkspaceFolders() throws { + guard let ws = try staticSourceKitSwiftPMWorkspace(name: "ChangeWorkspaceFolders") else { return } // Build the package. We can't use ws.buildAndIndex() because that doesn't put the build products in .build where SourceKit-LSP expects them. - try await TSCBasic.Process.checkNonZeroExit(arguments: [ + try TSCBasic.Process.checkNonZeroExit(arguments: [ ToolchainRegistry.shared.default!.swift!.pathString, "build", "--package-path", ws.sources.rootDirectory.path,