diff --git a/Sources/BuildServerProtocol/CMakeLists.txt b/Sources/BuildServerProtocol/CMakeLists.txt index d02d8b87..c47337ea 100644 --- a/Sources/BuildServerProtocol/CMakeLists.txt +++ b/Sources/BuildServerProtocol/CMakeLists.txt @@ -11,7 +11,8 @@ add_library(BuildServerProtocol STATIC RegisterForChangeNotifications.swift ShutdownBuild.swift SourceKitOptionsRequest.swift - WaitForBuildSystemUpdates.swift) + WaitForBuildSystemUpdates.swift + WorkDoneProgress.swift) set_target_properties(BuildServerProtocol PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) target_link_libraries(BuildServerProtocol PRIVATE diff --git a/Sources/BuildServerProtocol/WorkDoneProgress.swift b/Sources/BuildServerProtocol/WorkDoneProgress.swift new file mode 100644 index 00000000..6b55b41c --- /dev/null +++ b/Sources/BuildServerProtocol/WorkDoneProgress.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +public typealias CreateWorkDoneProgressRequest = LanguageServerProtocol.CreateWorkDoneProgressRequest +public typealias WorkDoneProgress = LanguageServerProtocol.WorkDoneProgress diff --git a/Sources/BuildSystemIntegration/BuildServerBuildSystem.swift b/Sources/BuildSystemIntegration/BuildServerBuildSystem.swift index ac9b9e4d..4f89ba87 100644 --- a/Sources/BuildSystemIntegration/BuildServerBuildSystem.swift +++ b/Sources/BuildSystemIntegration/BuildServerBuildSystem.swift @@ -67,7 +67,7 @@ package actor BuildServerBuildSystem: MessageHandler { package private(set) var indexDatabasePath: AbsolutePath? package private(set) var indexStorePath: AbsolutePath? - package weak var messageHandler: BuiltInBuildSystemMessageHandler? + package let connectionToSourceKitLSP: any Connection /// The build settings that have been received from the build server. private var buildSettings: [DocumentURI: SourceKitOptionsResponse] = [:] @@ -76,7 +76,7 @@ package actor BuildServerBuildSystem: MessageHandler { package init( projectRoot: AbsolutePath, - messageHandler: BuiltInBuildSystemMessageHandler?, + connectionToSourceKitLSP: any Connection, fileSystem: FileSystem = localFileSystem ) async throws { let configPath = projectRoot.appending(component: "buildServer.json") @@ -96,18 +96,18 @@ package actor BuildServerBuildSystem: MessageHandler { #endif self.projectRoot = projectRoot self.serverConfig = config - self.messageHandler = messageHandler + self.connectionToSourceKitLSP = connectionToSourceKitLSP try await self.initializeBuildServer() } /// Creates a build system using the Build Server Protocol config. /// /// - Returns: nil if `projectRoot` has no config or there is an error parsing it. - package init?(projectRoot: AbsolutePath?, messageHandler: BuiltInBuildSystemMessageHandler?) async { + package init?(projectRoot: AbsolutePath?, connectionToSourceKitLSP: any Connection) async { guard let projectRoot else { return nil } do { - try await self.init(projectRoot: projectRoot, messageHandler: messageHandler) + try await self.init(projectRoot: projectRoot, connectionToSourceKitLSP: connectionToSourceKitLSP) } catch is FileSystemError { // config file was missing, no build server for this workspace return nil @@ -218,8 +218,8 @@ package actor BuildServerBuildSystem: MessageHandler { reply(.failure(ResponseError.methodNotFound(R.method))) } - func handleBuildTargetsChanged(_ notification: DidChangeBuildTargetNotification) async { - await self.messageHandler?.sendNotificationToSourceKitLSP(notification) + func handleBuildTargetsChanged(_ notification: DidChangeBuildTargetNotification) { + connectionToSourceKitLSP.send(notification) } func handleFileOptionsChanged(_ notification: FileOptionsChangedNotification) async { @@ -238,7 +238,7 @@ package actor BuildServerBuildSystem: MessageHandler { // FIXME: (BSP migration) When running in the legacy mode where teh BSP server pushes build settings to us, we could // consider having a separate target for each source file so that we can update individual targets instead of having // to send an update for all targets. - await self.messageHandler?.sendNotificationToSourceKitLSP(DidChangeBuildTargetNotification(changes: nil)) + connectionToSourceKitLSP.send(DidChangeBuildTargetNotification(changes: nil)) } } diff --git a/Sources/BuildSystemIntegration/BuildSystemDelegate.swift b/Sources/BuildSystemIntegration/BuildSystemDelegate.swift index fc947f33..ad849383 100644 --- a/Sources/BuildSystemIntegration/BuildSystemDelegate.swift +++ b/Sources/BuildSystemIntegration/BuildSystemDelegate.swift @@ -31,8 +31,20 @@ package protocol BuildSystemManagerDelegate: AnyObject, Sendable { func buildTargetsChanged(_ changes: [BuildTargetEvent]?) async /// Log the given message to the client's index log. + // FIXME: (BSP Migration) Use `sendNotificationToClient` func logMessageToIndexLog(taskID: IndexTaskID, message: String) + /// Whether the client can handle `WorkDoneProgress` requests. + var clientSupportsWorkDoneProgress: Bool { get async } + + func sendNotificationToClient(_ notification: some NotificationType) + + func sendRequestToClient(_ request: R) async throws -> R.Response + + /// Wait until the connection to the client has been initialized, ie. wait until `SourceKitLSPServer` has replied + /// to the `initialize` request. + func waitUntilInitialized() async + /// Notify the delegate that the list of source files in the build system might have changed. func sourceFilesDidChange() async } diff --git a/Sources/BuildSystemIntegration/BuildSystemManager.swift b/Sources/BuildSystemIntegration/BuildSystemManager.swift index 3f88eae0..2ad33ddd 100644 --- a/Sources/BuildSystemIntegration/BuildSystemManager.swift +++ b/Sources/BuildSystemIntegration/BuildSystemManager.swift @@ -12,6 +12,7 @@ import BuildServerProtocol import Dispatch +import Foundation import LanguageServerProtocol import SKLogging import SKOptions @@ -148,8 +149,7 @@ package actor BuildSystemManager: MessageHandler { buildSystemKind: BuildSystemKind?, toolchainRegistry: ToolchainRegistry, options: SourceKitLSPOptions, - buildSystemTestHooks: BuildSystemTestHooks, - reloadPackageStatusCallback: @Sendable @escaping (ReloadPackageStatus) async -> Void + buildSystemTestHooks: BuildSystemTestHooks ) async { self.fallbackBuildSystem = FallbackBuildSystem(options: options.fallbackBuildSystemOrDefault) self.toolchainRegistry = toolchainRegistry @@ -162,8 +162,7 @@ package actor BuildSystemManager: MessageHandler { toolchainRegistry: toolchainRegistry, options: options, buildSystemTestHooks: buildSystemTestHooks, - connectionToSourceKitLSP: connectionFromBuildSystemToSourceKitLSP, - reloadPackageStatusCallback: reloadPackageStatusCallback + connectionToSourceKitLSP: connectionFromBuildSystemToSourceKitLSP ) if let buildSystem { let connectionFromSourceKitLSPToBuildSystem = LocalConnection(receiverName: "\(type(of: buildSystem))") @@ -286,6 +285,8 @@ package actor BuildSystemManager: MessageHandler { await self.didChangeBuildTarget(notification: notification) case let notification as BuildServerProtocol.LogMessageNotification: await self.logMessage(notification: notification) + case let notification as BuildServerProtocol.WorkDoneProgress: + await self.workDoneProgress(notification: notification) default: logger.error("Ignoring unknown notification \(type(of: notification).method)") } @@ -294,12 +295,87 @@ package actor BuildSystemManager: MessageHandler { /// Implementation of `MessageHandler`, handling requests from the build system. /// /// - Important: Do not call directly. - nonisolated package func handle( + nonisolated package func handle( + _ params: R, + id: RequestID, + reply: @Sendable @escaping (LSPResult) -> Void + ) { + let signposter = Logger(subsystem: LoggingScope.subsystem, category: "build-system-message-handling") + .makeSignposter() + let signpostID = signposter.makeSignpostID() + let state = signposter.beginInterval("Request", id: signpostID, "\(R.self)") + + messageHandlingQueue.async { + signposter.emitEvent("Start handling", id: signpostID) + await withTaskCancellationHandler { + await self.handleImpl(params, id: id, reply: reply) + signposter.endInterval("Request", state, "Done") + } onCancel: { + signposter.emitEvent("Cancelled", id: signpostID) + } + } + } + + private func handleImpl( _ request: Request, id: RequestID, reply: @escaping @Sendable (LSPResult) -> Void - ) { - reply(.failure(ResponseError.methodNotFound(Request.method))) + ) async { + let startDate = Date() + + let request = RequestAndReply(request) { result in + reply(result) + let endDate = Date() + Task { + switch result { + case .success(let response): + logger.log( + """ + Succeeded (took \(endDate.timeIntervalSince(startDate) * 1000, privacy: .public)ms) + \(Request.method, privacy: .public) + \(response.forLogging) + """ + ) + case .failure(let error): + logger.log( + """ + Failed (took \(endDate.timeIntervalSince(startDate) * 1000, privacy: .public)ms) + \(Request.method, privacy: .public)(\(id, privacy: .public)) + \(error.forLogging, privacy: .private) + """ + ) + } + } + } + + switch request { + case let request as RequestAndReply: + await request.reply { try await self.createWorkDoneProgress(request: request.params) } + default: + await request.reply { throw ResponseError.methodNotFound(Request.method) } + } + } + + private func createWorkDoneProgress( + request: BuildServerProtocol.CreateWorkDoneProgressRequest + ) async throws -> BuildServerProtocol.CreateWorkDoneProgressRequest.Response { + guard let delegate else { + throw ResponseError.unknown("Connection to client closed") + } + guard await delegate.clientSupportsWorkDoneProgress else { + throw ResponseError.unknown("Client does not support work done progress") + } + await delegate.waitUntilInitialized() + return try await delegate.sendRequestToClient(request as LanguageServerProtocol.CreateWorkDoneProgressRequest) + } + + private func workDoneProgress(notification: BuildServerProtocol.WorkDoneProgress) async { + guard let delegate else { + logger.fault("Ignoring work done progress form build system because connection to client closed") + return + } + await delegate.waitUntilInitialized() + delegate.sendNotificationToClient(notification as LanguageServerProtocol.WorkDoneProgress) } /// - Note: Needed so we can set the delegate from a different isolation context. diff --git a/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift b/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift index e5c5298d..5251a5f6 100644 --- a/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift +++ b/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift @@ -22,13 +22,6 @@ import ToolchainRegistry import struct TSCBasic.AbsolutePath import struct TSCBasic.RelativePath -// FIXME: (BSP Migration) This should be a MessageHandler once we have migrated all build system queries to BSP and can use -// LocalConnection for the communication. -package protocol BuiltInBuildSystemMessageHandler: AnyObject, Sendable { - func sendNotificationToSourceKitLSP(_ notification: some NotificationType) async - func sendRequestToSourceKitLSP(_ request: R) async throws -> R.Response -} - package enum BuildSystemKind { case buildServer(projectRoot: AbsolutePath) case compilationDatabase(projectRoot: AbsolutePath) @@ -51,37 +44,35 @@ private func createBuildSystem( options: SourceKitLSPOptions, buildSystemTestHooks: BuildSystemTestHooks, toolchainRegistry: ToolchainRegistry, - messageHandler: BuiltInBuildSystemMessageHandler, - reloadPackageStatusCallback: @Sendable @escaping (ReloadPackageStatus) async -> Void + connectionToSourceKitLSP: any Connection ) async -> BuiltInBuildSystem? { switch buildSystemKind { case .buildServer(let projectRoot): - return await BuildServerBuildSystem(projectRoot: projectRoot, messageHandler: messageHandler) + return await BuildServerBuildSystem(projectRoot: projectRoot, connectionToSourceKitLSP: connectionToSourceKitLSP) case .compilationDatabase(let projectRoot): return CompilationDatabaseBuildSystem( projectRoot: projectRoot, searchPaths: (options.compilationDatabaseOrDefault.searchPaths ?? []).compactMap { try? RelativePath(validating: $0) }, - messageHandler: messageHandler + connectionToSourceKitLSP: connectionToSourceKitLSP ) case .swiftPM(let projectRoot): return await SwiftPMBuildSystem( projectRoot: projectRoot, toolchainRegistry: toolchainRegistry, options: options, - messageHandler: messageHandler, - reloadPackageStatusCallback: reloadPackageStatusCallback, + connectionToSourceKitLSP: connectionToSourceKitLSP, testHooks: buildSystemTestHooks.swiftPMTestHooks ) case .testBuildSystem(let projectRoot): - return TestBuildSystem(projectRoot: projectRoot, messageHandler: messageHandler) + return TestBuildSystem(projectRoot: projectRoot, connectionToSourceKitLSP: connectionToSourceKitLSP) } } /// A type that outwardly acts as a build server conforming to the Build System Integration Protocol and internally uses /// a `BuiltInBuildSystem` to satisfy the requests. -package actor BuiltInBuildSystemAdapter: BuiltInBuildSystemMessageHandler, MessageHandler { +package actor BuiltInBuildSystemAdapter: MessageHandler { /// The underlying build system // FIXME: (BSP Migration) This should be private, all messages should go through BSP. Only accessible from the outside for transition // purposes. @@ -96,8 +87,7 @@ package actor BuiltInBuildSystemAdapter: BuiltInBuildSystemMessageHandler, Messa toolchainRegistry: ToolchainRegistry, options: SourceKitLSPOptions, buildSystemTestHooks: BuildSystemTestHooks, - connectionToSourceKitLSP: LocalConnection, - reloadPackageStatusCallback: @Sendable @escaping (ReloadPackageStatus) async -> Void + connectionToSourceKitLSP: LocalConnection ) async { guard let buildSystemKind else { return nil @@ -109,8 +99,7 @@ package actor BuiltInBuildSystemAdapter: BuiltInBuildSystemMessageHandler, Messa options: options, buildSystemTestHooks: buildSystemTestHooks, toolchainRegistry: toolchainRegistry, - messageHandler: self, - reloadPackageStatusCallback: reloadPackageStatusCallback + connectionToSourceKitLSP: connectionToSourceKitLSP ) guard let buildSystem else { return nil @@ -160,6 +149,7 @@ package actor BuiltInBuildSystemAdapter: BuiltInBuildSystemMessageHandler, Messa id: RequestID, reply: @Sendable @escaping (LSPResult) -> Void ) { + // FIXME: Can we share this between the different message handler implementations? let signposter = Logger(subsystem: LoggingScope.subsystem, category: "build-system-message-handling") .makeSignposter() let signpostID = signposter.makeSignpostID() @@ -227,12 +217,4 @@ package actor BuiltInBuildSystemAdapter: BuiltInBuildSystemMessageHandler, Messa await request.reply { throw ResponseError.methodNotFound(Request.method) } } } - - package func sendNotificationToSourceKitLSP(_ notification: some LanguageServerProtocol.NotificationType) async { - connectionToSourceKitLSP.send(notification) - } - - package func sendRequestToSourceKitLSP(_ request: R) async throws -> R.Response { - return try await connectionToSourceKitLSP.send(request) - } } diff --git a/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift b/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift index 2647332d..73e7081a 100644 --- a/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift +++ b/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift @@ -38,10 +38,7 @@ package actor CompilationDatabaseBuildSystem { } } - /// Callbacks that should be called if the list of possible test files has changed. - package var testFilesDidChangeCallbacks: [() async -> Void] = [] - - package weak var messageHandler: BuiltInBuildSystemMessageHandler? + package let connectionToSourceKitLSP: any Connection package let projectRoot: AbsolutePath @@ -72,13 +69,13 @@ package actor CompilationDatabaseBuildSystem { package init?( projectRoot: AbsolutePath, searchPaths: [RelativePath], - messageHandler: (any BuiltInBuildSystemMessageHandler)?, + connectionToSourceKitLSP: any Connection, fileSystem: FileSystem = localFileSystem ) { self.fileSystem = fileSystem self.projectRoot = projectRoot self.searchPaths = searchPaths - self.messageHandler = messageHandler + self.connectionToSourceKitLSP = connectionToSourceKitLSP if let compdb = tryLoadCompilationDatabase(directory: projectRoot, additionalSearchPaths: searchPaths, fileSystem) { self.compdb = compdb } else { @@ -129,9 +126,9 @@ extension CompilationDatabaseBuildSystem: BuiltInBuildSystem { return BuildTargetSourcesResponse(items: [SourcesItem(target: .dummy, sources: sources)]) } - package func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) async { + package func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) { if notification.changes.contains(where: { self.fileEventShouldTriggerCompilationDatabaseReload(event: $0) }) { - await self.reloadCompilationDatabase() + self.reloadCompilationDatabase() } } @@ -194,16 +191,13 @@ extension CompilationDatabaseBuildSystem: BuiltInBuildSystem { /// The compilation database has been changed on disk. /// Reload it and notify the delegate about build setting changes. - private func reloadCompilationDatabase() async { + private func reloadCompilationDatabase() { self.compdb = tryLoadCompilationDatabase( directory: projectRoot, additionalSearchPaths: searchPaths, self.fileSystem ) - await messageHandler?.sendNotificationToSourceKitLSP(DidChangeBuildTargetNotification(changes: nil)) - for testFilesDidChangeCallback in testFilesDidChangeCallbacks { - await testFilesDidChangeCallback() - } + connectionToSourceKitLSP.send(DidChangeBuildTargetNotification(changes: nil)) } } diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 3cadf45c..2d3136b4 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -45,14 +45,6 @@ fileprivate typealias AbsolutePath = Basics.AbsolutePath @preconcurrency import SPMBuildCore #endif -/// Parameter of `reloadPackageStatusCallback` in ``SwiftPMWorkspace``. -/// -/// Informs the callback about whether `reloadPackage` started or finished executing. -package enum ReloadPackageStatus: Sendable { - case start - case end -} - /// A build target in SwiftPM package typealias SwiftBuildTarget = SourceKitLSPAPI.BuildTarget @@ -186,13 +178,7 @@ package actor SwiftPMBuildSystem { /// issues in SwiftPM. private let packageLoadingQueue = AsyncQueue() - package weak var messageHandler: BuiltInBuildSystemMessageHandler? - - /// This callback is informed when `reloadPackage` starts and ends executing. - private var reloadPackageStatusCallback: (ReloadPackageStatus) async -> Void - - /// Callbacks that should be called if the list of possible test files has changed. - private var testFilesDidChangeCallbacks: [() async -> Void] = [] + package let connectionToSourceKitLSP: any Connection /// Whether the `SwiftPMBuildSystem` is pointed at a `.index-build` directory that's independent of the /// user's build. @@ -258,15 +244,13 @@ package actor SwiftPMBuildSystem { /// - projectRoot: The directory containing `Package.swift` /// - toolchainRegistry: The toolchain registry to use to provide the Swift compiler used for /// manifest parsing and runtime support. - /// - reloadPackageStatusCallback: Will be informed when `reloadPackage` starts and ends executing. /// - Throws: If there is an error loading the package, or no manifest is found. package init( projectRoot: TSCAbsolutePath, toolchainRegistry: ToolchainRegistry, fileSystem: FileSystem = localFileSystem, options: SourceKitLSPOptions, - messageHandler: (any BuiltInBuildSystemMessageHandler)?, - reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void = { _ in }, + connectionToSourceKitLSP: any Connection, testHooks: SwiftPMTestHooks ) async throws { self.projectRoot = projectRoot @@ -281,7 +265,7 @@ package actor SwiftPMBuildSystem { self.toolchain = toolchain self.testHooks = testHooks - self.messageHandler = messageHandler + self.connectionToSourceKitLSP = connectionToSourceKitLSP guard let destinationToolchainBinDir = toolchain.swiftc?.parentDirectory else { throw Error.cannotDetermineHostToolchain @@ -373,8 +357,6 @@ package actor SwiftPMBuildSystem { flags: buildFlags ) - self.reloadPackageStatusCallback = reloadPackageStatusCallback - packageLoadingQueue.async { await orLog("Initial package loading") { // Schedule an initial generation of the build graph. Once the build graph is loaded, the build system will send @@ -387,15 +369,12 @@ package actor SwiftPMBuildSystem { /// Creates a build system using the Swift Package Manager, if this workspace is a package. /// - /// - 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. package init?( projectRoot: TSCBasic.AbsolutePath, toolchainRegistry: ToolchainRegistry, options: SourceKitLSPOptions, - messageHandler: any BuiltInBuildSystemMessageHandler, - reloadPackageStatusCallback: @escaping (ReloadPackageStatus) async -> Void, + connectionToSourceKitLSP: any Connection, testHooks: SwiftPMTestHooks ) async { do { @@ -404,8 +383,7 @@ package actor SwiftPMBuildSystem { toolchainRegistry: toolchainRegistry, fileSystem: localFileSystem, options: options, - messageHandler: messageHandler, - reloadPackageStatusCallback: reloadPackageStatusCallback, + connectionToSourceKitLSP: connectionToSourceKitLSP, testHooks: testHooks ) } catch Error.noManifest { @@ -423,12 +401,18 @@ extension SwiftPMBuildSystem { /// /// - Important: Must only be called on `packageLoadingQueue`. private func reloadPackageAssumingOnPackageLoadingQueue() async throws { - await reloadPackageStatusCallback(.start) + let progressManager = WorkDoneProgressManager( + connectionToClient: self.connectionToSourceKitLSP, + waitUntilClientInitialized: {}, + tokenPrefix: "package-reloading", + initialDebounce: options.workDoneProgressDebounceDurationOrDefault, + title: "SourceKit-LSP: Reloading Package" + ) await testHooks.reloadPackageDidStart?() defer { Task { + await progressManager?.end() await testHooks.reloadPackageDidFinish?() - await reloadPackageStatusCallback(.end) } } @@ -475,10 +459,7 @@ extension SwiftPMBuildSystem { swiftPMTargets[targetIdentifier] = buildTarget } - await messageHandler?.sendNotificationToSourceKitLSP(DidChangeBuildTargetNotification(changes: nil)) - for testFilesDidChangeCallback in testFilesDidChangeCallbacks { - await testFilesDidChangeCallback() - } + connectionToSourceKitLSP.send(DidChangeBuildTargetNotification(changes: nil)) } } @@ -651,16 +632,13 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem { } private nonisolated func logMessageToIndexLog(_ taskID: IndexTaskID, _ message: String) { - // FIXME: When `messageHandler` is a Connection, we don't need to go via Task anymore - Task { - await self.messageHandler?.sendNotificationToSourceKitLSP( - BuildServerProtocol.LogMessageNotification( - type: .info, - task: TaskId(id: taskID.rawValue), - message: message - ) + connectionToSourceKitLSP.send( + BuildServerProtocol.LogMessageNotification( + type: .info, + task: TaskId(id: taskID.rawValue), + message: message ) - } + ) } private func prepare(singleTarget target: BuildTargetIdentifier) async throws { diff --git a/Sources/BuildSystemIntegration/TestBuildSystem.swift b/Sources/BuildSystemIntegration/TestBuildSystem.swift index d8f48d0a..c0a3bd7f 100644 --- a/Sources/BuildSystemIntegration/TestBuildSystem.swift +++ b/Sources/BuildSystemIntegration/TestBuildSystem.swift @@ -28,24 +28,24 @@ package actor TestBuildSystem: BuiltInBuildSystem { package let indexStorePath: AbsolutePath? = nil package let indexDatabasePath: AbsolutePath? = nil - private weak var messageHandler: BuiltInBuildSystemMessageHandler? + private let connectionToSourceKitLSP: any Connection /// Build settings by file. private var buildSettingsByFile: [DocumentURI: SourceKitOptionsResponse] = [:] - package func setBuildSettings(for uri: DocumentURI, to buildSettings: SourceKitOptionsResponse?) async { + package func setBuildSettings(for uri: DocumentURI, to buildSettings: SourceKitOptionsResponse?) { buildSettingsByFile[uri] = buildSettings - await self.messageHandler?.sendNotificationToSourceKitLSP(DidChangeBuildTargetNotification(changes: nil)) + connectionToSourceKitLSP.send(DidChangeBuildTargetNotification(changes: nil)) } package nonisolated var supportsPreparation: Bool { false } package init( projectRoot: AbsolutePath, - messageHandler: any BuiltInBuildSystemMessageHandler + connectionToSourceKitLSP: any Connection ) { self.projectRoot = projectRoot - self.messageHandler = messageHandler + self.connectionToSourceKitLSP = connectionToSourceKitLSP } package func buildTargets(request: BuildTargetsRequest) async throws -> BuildTargetsResponse { diff --git a/Sources/SKSupport/CMakeLists.txt b/Sources/SKSupport/CMakeLists.txt index 03725654..448d76ce 100644 --- a/Sources/SKSupport/CMakeLists.txt +++ b/Sources/SKSupport/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(SKSupport STATIC RequestAndReply.swift ResponseError+Init.swift SwitchableProcessResultExitStatus.swift + WorkDoneProgressManager.swift ) set_target_properties(SKSupport PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/SourceKitLSP/WorkDoneProgressManager.swift b/Sources/SKSupport/WorkDoneProgressManager.swift similarity index 60% rename from Sources/SourceKitLSP/WorkDoneProgressManager.swift rename to Sources/SKSupport/WorkDoneProgressManager.swift index c1316f0f..39b0c3d0 100644 --- a/Sources/SourceKitLSP/WorkDoneProgressManager.swift +++ b/Sources/SKSupport/WorkDoneProgressManager.swift @@ -13,14 +13,13 @@ import Foundation import LanguageServerProtocol import SKLogging -import SKSupport import SwiftExtensions /// Represents a single `WorkDoneProgress` task that gets communicated with the client. /// /// The work done progress is started when the object is created and ended when the object is destroyed. /// In between, updates can be sent to the client. -actor WorkDoneProgressManager { +package actor WorkDoneProgressManager { private enum Status: Equatable { case inProgress(message: String?, percentage: Int?) case done @@ -34,7 +33,11 @@ actor WorkDoneProgressManager { /// The queue on which progress updates are sent to the client. private let progressUpdateQueue = AsyncQueue() - private weak var server: SourceKitLSPServer? + private let connectionToClient: any Connection + + /// Closure that wait until the connection between SourceKit-LSP and the editor has been initialized. Other than that, + /// the closure should not perform any work. + private let waitUntilClientInitialized: () async -> Void /// A string with which the `token` of the generated `WorkDoneProgress` sent to the client starts. /// @@ -60,42 +63,18 @@ actor WorkDoneProgressManager { /// The last status that was sent to the client. Used so we don't send no-op updates to the client. private var lastStatus: Status? = nil - init?( - server: SourceKitLSPServer, - tokenPrefix: String, - initialDebounce: Duration? = nil, - title: String, - message: String? = nil, - percentage: Int? = nil - ) async { - guard let capabilityRegistry = await server.capabilityRegistry else { - return nil - } - self.init( - server: server, - capabilityRegistry: capabilityRegistry, - tokenPrefix: tokenPrefix, - initialDebounce: initialDebounce, - title: title, - message: message, - percentage: percentage - ) - } - - init?( - server: SourceKitLSPServer, - capabilityRegistry: CapabilityRegistry, + package init?( + connectionToClient: any Connection, + waitUntilClientInitialized: @escaping () async -> Void, tokenPrefix: String, initialDebounce: Duration? = nil, title: String, message: String? = nil, percentage: Int? = nil ) { - guard capabilityRegistry.clientCapabilities.window?.workDoneProgress ?? false else { - return nil - } self.tokenPrefix = tokenPrefix - self.server = server + self.connectionToClient = connectionToClient + self.waitUntilClientInitialized = waitUntilClientInitialized self.title = title self.pendingStatus = .inProgress(message: message, percentage: percentage) progressUpdateQueue.async { @@ -114,15 +93,11 @@ actor WorkDoneProgressManager { guard statusToSend != lastStatus else { return } - guard let server else { - // SourceKitLSPServer has been destroyed, we don't have a way to send notifications to the client anymore. - return - } - await server.waitUntilInitialized() + await waitUntilClientInitialized() switch statusToSend { case .inProgress(message: let message, percentage: let percentage): if let token { - server.sendNotificationToClient( + connectionToClient.send( WorkDoneProgress( token: token, value: .report(WorkDoneProgressReport(cancellable: false, message: message, percentage: percentage)) @@ -131,11 +106,11 @@ actor WorkDoneProgressManager { } else { let token = ProgressToken.string("\(tokenPrefix).\(UUID().uuidString)") do { - _ = try await server.client.send(CreateWorkDoneProgressRequest(token: token)) + _ = try await connectionToClient.send(CreateWorkDoneProgressRequest(token: token)) } catch { return } - server.sendNotificationToClient( + connectionToClient.send( WorkDoneProgress( token: token, value: .begin(WorkDoneProgressBegin(title: title, message: message, percentage: percentage)) @@ -145,14 +120,14 @@ actor WorkDoneProgressManager { } case .done: if let token { - server.sendNotificationToClient(WorkDoneProgress(token: token, value: .end(WorkDoneProgressEnd()))) + connectionToClient.send(WorkDoneProgress(token: token, value: .end(WorkDoneProgressEnd()))) self.token = nil } } lastStatus = statusToSend } - func update(message: String? = nil, percentage: Int? = nil) { + package func update(message: String? = nil, percentage: Int? = nil) { pendingStatus = .inProgress(message: message, percentage: percentage) progressUpdateQueue.async { await self.sendProgressUpdateAssumingOnProgressUpdateQueue() @@ -162,7 +137,7 @@ actor WorkDoneProgressManager { /// Ends the work done progress. Any further update calls are no-ops. /// /// `end` must be should be called before the `WorkDoneProgressManager` is deallocated. - func end() { + package func end() { pendingStatus = .done progressUpdateQueue.async { await self.sendProgressUpdateAssumingOnProgressUpdateQueue() @@ -180,73 +155,8 @@ actor WorkDoneProgressManager { // in `progressUpdateQueue`, which keep the `WorkDoneProgressManager` alive and thus prevent the work done // progress to be implicitly ended by the deinitializer. if let token { - server?.sendNotificationToClient(WorkDoneProgress(token: token, value: .end(WorkDoneProgressEnd()))) + connectionToClient.send(WorkDoneProgress(token: token, value: .end(WorkDoneProgressEnd()))) } } } } - -/// A `WorkDoneProgressManager` that essentially has two states. If any operation tracked by this type is currently -/// running, it displays a work done progress in the client. If multiple operations are running at the same time, it -/// doesn't show multiple work done progress in the client. For example, we only want to show one progress indicator -/// when sourcekitd has crashed, not one per `SwiftLanguageService`. -actor SharedWorkDoneProgressManager { - private weak var sourceKitLSPServer: SourceKitLSPServer? - - /// The number of in-progress operations. When greater than 0 `workDoneProgress` non-nil and a work done progress is - /// displayed to the user. - private var inProgressOperations = 0 - private var workDoneProgress: WorkDoneProgressManager? - - private let tokenPrefix: String - private let title: String - private let message: String? - - package init( - sourceKitLSPServer: SourceKitLSPServer, - tokenPrefix: String, - title: String, - message: String? = nil - ) { - self.sourceKitLSPServer = sourceKitLSPServer - self.tokenPrefix = tokenPrefix - self.title = title - self.message = message - } - - func start() async { - guard let sourceKitLSPServer else { - return - } - // Do all asynchronous operations up-front so that incrementing `inProgressOperations` and setting `workDoneProgress` - // cannot be interrupted by an `await` call - let initialDebounceDuration = await sourceKitLSPServer.options.workDoneProgressDebounceDurationOrDefault - let capabilityRegistry = await sourceKitLSPServer.capabilityRegistry - - inProgressOperations += 1 - if let capabilityRegistry, workDoneProgress == nil { - workDoneProgress = WorkDoneProgressManager( - server: sourceKitLSPServer, - capabilityRegistry: capabilityRegistry, - tokenPrefix: tokenPrefix, - initialDebounce: initialDebounceDuration, - title: title, - message: message - ) - } - } - - func end() async { - if inProgressOperations > 0 { - inProgressOperations -= 1 - } else { - logger.fault( - "Unbalanced calls to SharedWorkDoneProgressManager.start and end for \(self.tokenPrefix, privacy: .public)" - ) - } - if inProgressOperations == 0, let workDoneProgress { - self.workDoneProgress = nil - await workDoneProgress.end() - } - } -} diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt index 34c2e919..b3f3cb85 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(SourceKitLSP STATIC MessageHandlingDependencyTracker.swift Rename.swift SemanticTokensLegend+SourceKitLSPLegend.swift + SharedWorkDoneProgressManager.swift SourceKitIndexDelegate.swift SourceKitLSPCommandMetadata.swift SourceKitLSPServer.swift @@ -18,7 +19,6 @@ add_library(SourceKitLSP STATIC TestDiscovery.swift TestHooks.swift TextEdit+IsNoop.swift - WorkDoneProgressManager.swift Workspace.swift ) target_sources(SourceKitLSP PRIVATE diff --git a/Sources/SourceKitLSP/IndexProgressManager.swift b/Sources/SourceKitLSP/IndexProgressManager.swift index 74ce8f48..a83af864 100644 --- a/Sources/SourceKitLSP/IndexProgressManager.swift +++ b/Sources/SourceKitLSP/IndexProgressManager.swift @@ -126,6 +126,7 @@ actor IndexProgressManager { } else { workDoneProgress = await WorkDoneProgressManager( server: sourceKitLSPServer, + capabilityRegistry: await sourceKitLSPServer.capabilityRegistry, tokenPrefix: "indexing", initialDebounce: sourceKitLSPServer.options.workDoneProgressDebounceDurationOrDefault, title: "Indexing", diff --git a/Sources/SourceKitLSP/SharedWorkDoneProgressManager.swift b/Sources/SourceKitLSP/SharedWorkDoneProgressManager.swift new file mode 100644 index 00000000..a35aae3a --- /dev/null +++ b/Sources/SourceKitLSP/SharedWorkDoneProgressManager.swift @@ -0,0 +1,107 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import LanguageServerProtocol +import SKLogging +import SKSupport +import SwiftExtensions + +extension WorkDoneProgressManager { + init?( + server: SourceKitLSPServer, + capabilityRegistry: CapabilityRegistry?, + tokenPrefix: String, + initialDebounce: Duration? = nil, + title: String, + message: String? = nil, + percentage: Int? = nil + ) { + guard let capabilityRegistry, capabilityRegistry.clientCapabilities.window?.workDoneProgress ?? false else { + return nil + } + self.init( + connectionToClient: server.client, + waitUntilClientInitialized: { [weak server] in await server?.waitUntilInitialized() }, + tokenPrefix: tokenPrefix, + initialDebounce: initialDebounce, + title: title, + message: message, + percentage: percentage + ) + } +} + +/// A `WorkDoneProgressManager` that essentially has two states. If any operation tracked by this type is currently +/// running, it displays a work done progress in the client. If multiple operations are running at the same time, it +/// doesn't show multiple work done progress in the client. For example, we only want to show one progress indicator +/// when sourcekitd has crashed, not one per `SwiftLanguageService`. +actor SharedWorkDoneProgressManager { + private weak var sourceKitLSPServer: SourceKitLSPServer? + + /// The number of in-progress operations. When greater than 0 `workDoneProgress` non-nil and a work done progress is + /// displayed to the user. + private var inProgressOperations = 0 + private var workDoneProgress: WorkDoneProgressManager? + + private let tokenPrefix: String + private let title: String + private let message: String? + + package init( + sourceKitLSPServer: SourceKitLSPServer, + tokenPrefix: String, + title: String, + message: String? = nil + ) { + self.sourceKitLSPServer = sourceKitLSPServer + self.tokenPrefix = tokenPrefix + self.title = title + self.message = message + } + + func start() async { + guard let sourceKitLSPServer else { + return + } + // Do all asynchronous operations up-front so that incrementing `inProgressOperations` and setting `workDoneProgress` + // cannot be interrupted by an `await` call + let initialDebounceDuration = await sourceKitLSPServer.options.workDoneProgressDebounceDurationOrDefault + let capabilityRegistry = await sourceKitLSPServer.capabilityRegistry + + inProgressOperations += 1 + if let capabilityRegistry, workDoneProgress == nil { + workDoneProgress = WorkDoneProgressManager( + server: sourceKitLSPServer, + capabilityRegistry: capabilityRegistry, + tokenPrefix: tokenPrefix, + initialDebounce: initialDebounceDuration, + title: title, + message: message + ) + } + } + + func end() async { + if inProgressOperations > 0 { + inProgressOperations -= 1 + } else { + logger.fault( + "Unbalanced calls to SharedWorkDoneProgressManager.start and end for \(self.tokenPrefix, privacy: .public)" + ) + } + if inProgressOperations == 0, let workDoneProgress { + self.workDoneProgress = nil + await workDoneProgress.end() + } + } +} diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index fd22ce16..22cf01ed 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -98,13 +98,7 @@ package actor SourceKitLSPServer { /// `SourceKitLSPServer`. /// `nonisolated(unsafe)` because `indexProgressManager` will not be modified after it is assigned from the /// initializer. - private nonisolated(unsafe) var indexProgressManager: IndexProgressManager! - - /// Implicitly unwrapped optional so we can create an `SharedWorkDoneProgressManager` that has a weak reference to - /// `SourceKitLSPServer`. - /// `nonisolated(unsafe)` because `packageLoadingWorkDoneProgress` will not be modified after it is assigned from the - /// initializer. - private nonisolated(unsafe) var packageLoadingWorkDoneProgress: SharedWorkDoneProgressManager! + private(set) nonisolated(unsafe) var indexProgressManager: IndexProgressManager! /// Implicitly unwrapped optional so we can create an `SharedWorkDoneProgressManager` that has a weak reference to /// `SourceKitLSPServer`. @@ -201,11 +195,6 @@ package actor SourceKitLSPServer { ]) self.indexProgressManager = nil self.indexProgressManager = IndexProgressManager(sourceKitLSPServer: self) - self.packageLoadingWorkDoneProgress = SharedWorkDoneProgressManager( - sourceKitLSPServer: self, - tokenPrefix: "package-reloading", - title: "SourceKit-LSP: Reloading Package" - ) self.sourcekitdCrashedWorkDoneProgress = SharedWorkDoneProgressManager( sourceKitLSPServer: self, tokenPrefix: "sourcekitd-crashed", @@ -215,7 +204,7 @@ package actor SourceKitLSPServer { } /// Await until the server has send the reply to the initialize request. - func waitUntilInitialized() async { + package func waitUntilInitialized() async { // The polling of `initialized` is not perfect but it should be OK, because // - In almost all cases the server should already be initialized. // - If it's not initialized, we expect initialization to finish fairly quickly. Even if initialization takes 5s @@ -826,6 +815,11 @@ extension SourceKitLSPServer { ) ) } + + func fileHandlingCapabilityChanged() { + logger.log("Updating URI to workspace because file handling capability of a workspace changed") + self.scheduleUpdateOfUriToWorkspace() + } } // MARK: - Request and notification handling @@ -834,15 +828,6 @@ extension SourceKitLSPServer { // MARK: - General - private func reloadPackageStatusCallback(_ status: ReloadPackageStatus) async { - switch status { - case .start: - await packageLoadingWorkDoneProgress.start() - case .end: - await packageLoadingWorkDoneProgress.end() - } - } - /// Creates a workspace at the given `uri`. /// /// If the build system that was determined for the workspace does not satisfy `condition`, `nil` is returned. @@ -867,6 +852,7 @@ extension SourceKitLSPServer { logger.log("Creating workspace at \(workspaceFolder.forLogging) with options: \(options.forLogging)") let workspace = await Workspace( + sourceKitLSPServer: self, documentManager: self.documentManager, rootUri: workspaceFolder, capabilityRegistry: capabilityRegistry, @@ -874,23 +860,7 @@ extension SourceKitLSPServer { toolchainRegistry: self.toolchainRegistry, options: options, testHooks: testHooks, - indexTaskScheduler: indexTaskScheduler, - logMessageToIndexLog: { [weak self] taskID, message in - self?.logMessageToIndexLog(taskID: taskID, message: message) - }, - indexTasksWereScheduled: { [weak self] count in - self?.indexProgressManager.indexTasksWereScheduled(count: count) - }, - indexProgressStatusDidChange: { [weak self] in - self?.indexProgressManager.indexProgressStatusDidChange() - }, - reloadPackageStatusCallback: { [weak self] status in - await self?.reloadPackageStatusCallback(status) - }, - fileHandlingCapabilityChanged: { [weak self] in - logger.log("Updating URI to workspace because file handling capability of a workspace changed") - await self?.scheduleUpdateOfUriToWorkspace() - } + indexTaskScheduler: indexTaskScheduler ) if options.backgroundIndexingOrDefault, workspace.semanticIndexManager == nil, !self.didSendBackgroundIndexingNotSupportedNotification @@ -1002,6 +972,7 @@ extension SourceKitLSPServer { let options = self.options let workspace = await Workspace( + sourceKitLSPServer: self, documentManager: self.documentManager, rootUri: req.rootURI, capabilityRegistry: self.capabilityRegistry!, @@ -1009,23 +980,7 @@ extension SourceKitLSPServer { toolchainRegistry: self.toolchainRegistry, options: options, testHooks: testHooks, - indexTaskScheduler: self.indexTaskScheduler, - logMessageToIndexLog: { [weak self] taskID, message in - self?.logMessageToIndexLog(taskID: taskID, message: message) - }, - indexTasksWereScheduled: { [weak self] count in - self?.indexProgressManager.indexTasksWereScheduled(count: count) - }, - indexProgressStatusDidChange: { [weak self] in - self?.indexProgressManager.indexProgressStatusDidChange() - }, - reloadPackageStatusCallback: { [weak self] status in - await self?.reloadPackageStatusCallback(status) - }, - fileHandlingCapabilityChanged: { [weak self] in - logger.log("Updating URI to workspace because file handling capability of a workspace changed") - await self?.scheduleUpdateOfUriToWorkspace() - } + indexTaskScheduler: self.indexTaskScheduler ) self.workspacesAndIsImplicit.append((workspace: workspace, isImplicit: false)) diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift index dec56ad7..4f7b0aa0 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift @@ -96,7 +96,7 @@ package struct SwiftCompileCommand: Sendable, Equatable { package actor SwiftLanguageService: LanguageService, Sendable { /// The ``SourceKitLSPServer`` instance that created this `SwiftLanguageService`. - weak var sourceKitLSPServer: SourceKitLSPServer? + private(set) weak var sourceKitLSPServer: SourceKitLSPServer? let sourcekitd: SourceKitD @@ -152,7 +152,7 @@ package actor SwiftLanguageService: LanguageService, Sendable { var documentManager: DocumentManager { get throws { - guard let sourceKitLSPServer = self.sourceKitLSPServer else { + guard let sourceKitLSPServer else { throw ResponseError.unknown("Connection to the editor closed") } return sourceKitLSPServer.documentManager diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 2b892985..3cad730f 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -50,6 +50,12 @@ fileprivate func firstNonNil( /// /// Typically a workspace is contained in a root directory. package final class Workspace: Sendable, BuildSystemManagerDelegate { + /// The ``SourceKitLSPServer`` instance that created this `Workspace`. + private(set) weak nonisolated(unsafe) var sourceKitLSPServer: SourceKitLSPServer? { + didSet { + preconditionFailure("sourceKitLSPServer must not be modified. It is only a var because it is weak") + } + } /// The root directory of the workspace. package let rootUri: DocumentURI? @@ -83,14 +89,8 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { /// `nil` if background indexing is not enabled. let semanticIndexManager: SemanticIndexManager? - /// A callback that should be called when the build system wants to log a message to the index log. - private let logMessageToIndexLogCallback: @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void - - /// A callback that should be called when the file handling capability (ie. the presence of a target for a source - /// files) of this workspace changes. - private let fileHandlingCapabilityChangedCallback: @Sendable () async -> Void - private init( + sourceKitLSPServer: SourceKitLSPServer?, rootUri: DocumentURI?, capabilityRegistry: CapabilityRegistry, options: SourceKitLSPOptions, @@ -98,19 +98,14 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { buildSystemManager: BuildSystemManager, index uncheckedIndex: UncheckedIndex?, indexDelegate: SourceKitIndexDelegate?, - indexTaskScheduler: TaskScheduler, - logMessageToIndexLog: @escaping @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void, - indexTasksWereScheduled: @escaping @Sendable (Int) -> Void, - indexProgressStatusDidChange: @escaping @Sendable () -> Void, - fileHandlingCapabilityChanged: @escaping @Sendable () async -> Void + indexTaskScheduler: TaskScheduler ) async { + self.sourceKitLSPServer = sourceKitLSPServer self.rootUri = rootUri self.capabilityRegistry = capabilityRegistry self.options = options self._uncheckedIndex = ThreadSafeBox(initialValue: uncheckedIndex) self.buildSystemManager = buildSystemManager - self.logMessageToIndexLogCallback = logMessageToIndexLog - self.fileHandlingCapabilityChangedCallback = fileHandlingCapabilityChanged if options.backgroundIndexingOrDefault, let uncheckedIndex, await buildSystemManager.initializationData?.supportsPreparation ?? false { @@ -120,9 +115,15 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { updateIndexStoreTimeout: options.indexOrDefault.updateIndexStoreTimeoutOrDefault, testHooks: testHooks.indexTestHooks, indexTaskScheduler: indexTaskScheduler, - logMessageToIndexLog: logMessageToIndexLog, - indexTasksWereScheduled: indexTasksWereScheduled, - indexProgressStatusDidChange: indexProgressStatusDidChange + logMessageToIndexLog: { [weak sourceKitLSPServer] in + sourceKitLSPServer?.logMessageToIndexLog(taskID: $0, message: $1) + }, + indexTasksWereScheduled: { [weak sourceKitLSPServer] in + sourceKitLSPServer?.indexProgressManager.indexTasksWereScheduled(count: $0) + }, + indexProgressStatusDidChange: { [weak sourceKitLSPServer] in + sourceKitLSPServer?.indexProgressManager.indexProgressStatusDidChange() + } ) } else { self.semanticIndexManager = nil @@ -149,6 +150,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { /// - clientCapabilities: The client capabilities provided during server initialization. /// - toolchainRegistry: The toolchain registry. convenience init( + sourceKitLSPServer: SourceKitLSPServer, documentManager: DocumentManager, rootUri: DocumentURI?, capabilityRegistry: CapabilityRegistry, @@ -156,19 +158,13 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { toolchainRegistry: ToolchainRegistry, options: SourceKitLSPOptions, testHooks: TestHooks, - indexTaskScheduler: TaskScheduler, - logMessageToIndexLog: @escaping @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void, - indexTasksWereScheduled: @Sendable @escaping (Int) -> Void, - indexProgressStatusDidChange: @Sendable @escaping () -> Void, - reloadPackageStatusCallback: @Sendable @escaping (ReloadPackageStatus) async -> Void, - fileHandlingCapabilityChanged: @Sendable @escaping () async -> Void + indexTaskScheduler: TaskScheduler ) async { let buildSystemManager = await BuildSystemManager( buildSystemKind: buildSystemKind, toolchainRegistry: toolchainRegistry, options: options, - buildSystemTestHooks: testHooks.buildSystemTestHooks, - reloadPackageStatusCallback: reloadPackageStatusCallback + buildSystemTestHooks: testHooks.buildSystemTestHooks ) let buildSystem = await buildSystemManager.buildSystem?.underlyingBuildSystem @@ -216,6 +212,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { await buildSystemManager.setMainFilesProvider(UncheckedIndex(index)) await self.init( + sourceKitLSPServer: sourceKitLSPServer, rootUri: rootUri, capabilityRegistry: capabilityRegistry, options: options, @@ -223,11 +220,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { buildSystemManager: buildSystemManager, index: UncheckedIndex(index), indexDelegate: indexDelegate, - indexTaskScheduler: indexTaskScheduler, - logMessageToIndexLog: logMessageToIndexLog, - indexTasksWereScheduled: indexTasksWereScheduled, - indexProgressStatusDidChange: indexProgressStatusDidChange, - fileHandlingCapabilityChanged: fileHandlingCapabilityChanged + indexTaskScheduler: indexTaskScheduler ) await buildSystemManager.setDelegate(self) } @@ -239,6 +232,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { indexTaskScheduler: TaskScheduler ) async -> Workspace { return await Workspace( + sourceKitLSPServer: nil, rootUri: nil, capabilityRegistry: CapabilityRegistry(clientCapabilities: ClientCapabilities()), options: options, @@ -246,11 +240,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { buildSystemManager: buildSystemManager, index: nil, indexDelegate: nil, - indexTaskScheduler: indexTaskScheduler, - logMessageToIndexLog: { _, _ in }, - indexTasksWereScheduled: { _ in }, - indexProgressStatusDidChange: {}, - fileHandlingCapabilityChanged: {} + indexTaskScheduler: indexTaskScheduler ) } @@ -316,11 +306,39 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { } package func buildTargetsChanged(_ changes: [BuildTargetEvent]?) async { - await self.fileHandlingCapabilityChangedCallback() + await sourceKitLSPServer?.fileHandlingCapabilityChanged() } package func logMessageToIndexLog(taskID: IndexTaskID, message: String) { - self.logMessageToIndexLogCallback(taskID, message) + sourceKitLSPServer?.logMessageToIndexLog(taskID: taskID, message: message) + } + + package var clientSupportsWorkDoneProgress: Bool { + get async { + await sourceKitLSPServer?.capabilityRegistry?.clientCapabilities.window?.workDoneProgress ?? false + } + } + + package func sendNotificationToClient(_ notification: some NotificationType) { + guard let sourceKitLSPServer else { + logger.error( + "Not sending \(type(of: notification), privacy: .public) to the client because sourceKitLSPServer has been deallocated" + ) + return + } + sourceKitLSPServer.sendNotificationToClient(notification) + + } + + package func sendRequestToClient(_ request: R) async throws -> R.Response { + guard let sourceKitLSPServer else { + throw ResponseError.unknown("Connection to the editor closed") + } + return try await sourceKitLSPServer.sendRequestToClient(request) + } + + package func waitUntilInitialized() async { + await sourceKitLSPServer?.waitUntilInitialized() } package func sourceFilesDidChange() async { diff --git a/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift index c0171e8c..ef01681c 100644 --- a/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift @@ -15,6 +15,7 @@ import BuildSystemIntegration import Foundation import ISDBTestSupport import LanguageServerProtocol +import SKSupport import SKTestSupport import TSCBasic import XCTest @@ -53,7 +54,10 @@ final class BuildServerBuildSystemTests: XCTestCase { let buildFolder = try! AbsolutePath(validating: NSTemporaryDirectory()) func testServerInitialize() async throws { - let buildSystem = try await BuildServerBuildSystem(projectRoot: root, messageHandler: nil) + let buildSystem = try await BuildServerBuildSystem( + projectRoot: root, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP") + ) assertEqual( await buildSystem.indexDatabasePath, @@ -68,14 +72,13 @@ final class BuildServerBuildSystemTests: XCTestCase { func testFileRegistration() async throws { let uri = DocumentURI(filePath: "/some/file/path", isDirectory: false) let expectation = self.expectation(description: "\(uri) settings updated") - let buildSystemDelegate = TestDelegate(targetExpectations: [ + let testMessageHandler = TestMessageHandler(targetExpectations: [ (DidChangeBuildTargetNotification(changes: nil), expectation) ]) - defer { - // BuildSystemManager has a weak reference to delegate. Keep it alive. - _fixLifetime(buildSystemDelegate) - } - let buildSystem = try await BuildServerBuildSystem(projectRoot: root, messageHandler: buildSystemDelegate) + let buildSystem = try await BuildServerBuildSystem( + projectRoot: root, + connectionToSourceKitLSP: testMessageHandler.connection + ) _ = try await buildSystem.sourceKitOptions( request: SourceKitOptionsRequest( textDocument: TextDocumentIdentifier(uri: uri), @@ -93,7 +96,7 @@ final class BuildServerBuildSystemTests: XCTestCase { func testBuildTargetsChanged() async throws { let uri = DocumentURI(filePath: "/some/file/path", isDirectory: false) let expectation = XCTestExpectation(description: "target changed") - let buildSystemDelegate = TestDelegate(targetExpectations: [ + let testMessageHandler = TestMessageHandler(targetExpectations: [ ( DidChangeBuildTargetNotification(changes: [ BuildTargetEvent( @@ -107,9 +110,12 @@ final class BuildServerBuildSystemTests: XCTestCase { ]) defer { // BuildSystemManager has a weak reference to delegate. Keep it alive. - _fixLifetime(buildSystemDelegate) + _fixLifetime(testMessageHandler) } - let buildSystem = try await BuildServerBuildSystem(projectRoot: root, messageHandler: buildSystemDelegate) + let buildSystem = try await BuildServerBuildSystem( + projectRoot: root, + connectionToSourceKitLSP: testMessageHandler.connection + ) _ = try await buildSystem.sourceKitOptions( request: SourceKitOptionsRequest( textDocument: TextDocumentIdentifier(uri: uri), @@ -125,9 +131,15 @@ final class BuildServerBuildSystemTests: XCTestCase { } } -final class TestDelegate: BuiltInBuildSystemMessageHandler { +fileprivate final class TestMessageHandler: MessageHandler { let targetExpectations: [(DidChangeBuildTargetNotification, XCTestExpectation)] + var connection: LocalConnection { + let connection = LocalConnection(receiverName: "Test message handler") + connection.start(handler: self) + return connection + } + package init(targetExpectations: [(DidChangeBuildTargetNotification, XCTestExpectation)] = []) { self.targetExpectations = targetExpectations } @@ -140,11 +152,15 @@ final class TestDelegate: BuiltInBuildSystemMessageHandler { } } - func sendRequestToSourceKitLSP(_ request: R) async throws -> R.Response { - throw ResponseError.methodNotFound(R.method) + func handle( + _ request: Request, + id: RequestID, + reply: @escaping @Sendable (LSPResult) -> Void + ) { + reply(.failure(.methodNotFound(Request.method))) } - func sendNotificationToSourceKitLSP(_ notification: some NotificationType) async { + func handle(_ notification: some NotificationType) { switch notification { case let notification as DidChangeBuildTargetNotification: didChangeBuildTarget(notification: notification) diff --git a/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift b/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift index a4828be0..f3d6c797 100644 --- a/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift +++ b/Tests/BuildSystemIntegrationTests/BuildSystemManagerTests.swift @@ -46,8 +46,7 @@ final class BuildSystemManagerTests: XCTestCase { buildSystemKind: nil, toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), - buildSystemTestHooks: BuildSystemTestHooks(), - reloadPackageStatusCallback: { _ in } + buildSystemTestHooks: BuildSystemTestHooks() ) await bsm.setMainFilesProvider(mainFiles) defer { withExtendedLifetime(bsm) {} } // Keep BSM alive for callbacks. @@ -105,8 +104,7 @@ final class BuildSystemManagerTests: XCTestCase { buildSystemKind: .testBuildSystem(projectRoot: try AbsolutePath(validating: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), - buildSystemTestHooks: BuildSystemTestHooks(), - reloadPackageStatusCallback: { _ in } + buildSystemTestHooks: BuildSystemTestHooks() ) await bsm.setMainFilesProvider(mainFiles) let bs = try await unwrap(bsm.buildSystem?.underlyingBuildSystem as? TestBuildSystem) @@ -134,8 +132,7 @@ final class BuildSystemManagerTests: XCTestCase { buildSystemKind: .testBuildSystem(projectRoot: try AbsolutePath(validating: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), - buildSystemTestHooks: BuildSystemTestHooks(), - reloadPackageStatusCallback: { _ in } + buildSystemTestHooks: BuildSystemTestHooks() ) await bsm.setMainFilesProvider(mainFiles) let bs = try await unwrap(bsm.buildSystem?.underlyingBuildSystem as? TestBuildSystem) @@ -156,8 +153,7 @@ final class BuildSystemManagerTests: XCTestCase { buildSystemKind: .testBuildSystem(projectRoot: try AbsolutePath(validating: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), - buildSystemTestHooks: BuildSystemTestHooks(), - reloadPackageStatusCallback: { _ in } + buildSystemTestHooks: BuildSystemTestHooks() ) await bsm.setMainFilesProvider(mainFiles) let bs = try await unwrap(bsm.buildSystem?.underlyingBuildSystem as? TestBuildSystem) @@ -194,8 +190,7 @@ final class BuildSystemManagerTests: XCTestCase { buildSystemKind: .testBuildSystem(projectRoot: try AbsolutePath(validating: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), - buildSystemTestHooks: BuildSystemTestHooks(), - reloadPackageStatusCallback: { _ in } + buildSystemTestHooks: BuildSystemTestHooks() ) await bsm.setMainFilesProvider(mainFiles) let bs = try await unwrap(bsm.buildSystem?.underlyingBuildSystem as? TestBuildSystem) @@ -255,8 +250,7 @@ final class BuildSystemManagerTests: XCTestCase { buildSystemKind: .testBuildSystem(projectRoot: try AbsolutePath(validating: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), - buildSystemTestHooks: BuildSystemTestHooks(), - reloadPackageStatusCallback: { _ in } + buildSystemTestHooks: BuildSystemTestHooks() ) await bsm.setMainFilesProvider(mainFiles) let bs = try await unwrap(bsm.buildSystem?.underlyingBuildSystem as? TestBuildSystem) @@ -299,8 +293,7 @@ final class BuildSystemManagerTests: XCTestCase { buildSystemKind: .testBuildSystem(projectRoot: try AbsolutePath(validating: "/")), toolchainRegistry: ToolchainRegistry.forTesting, options: SourceKitLSPOptions(), - buildSystemTestHooks: BuildSystemTestHooks(), - reloadPackageStatusCallback: { _ in } + buildSystemTestHooks: BuildSystemTestHooks() ) await bsm.setMainFilesProvider(mainFiles) let bs = try await unwrap(bsm.buildSystem?.underlyingBuildSystem as? TestBuildSystem) @@ -417,4 +410,14 @@ private actor BSMDelegate: BuildSystemManagerDelegate { nonisolated func logMessageToIndexLog(taskID: BuildSystemIntegration.IndexTaskID, message: String) {} func sourceFilesDidChange() async {} + + var clientSupportsWorkDoneProgress: Bool { false } + + nonisolated func sendNotificationToClient(_ notification: some NotificationType) {} + + func sendRequestToClient(_ request: R) async throws -> R.Response { + throw ResponseError.methodNotFound(R.method) + } + + func waitUntilInitialized() async {} } diff --git a/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift b/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift index b126db1d..ab324ae1 100644 --- a/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift +++ b/Tests/BuildSystemIntegrationTests/CompilationDatabaseTests.swift @@ -13,6 +13,7 @@ import BuildServerProtocol import BuildSystemIntegration import LanguageServerProtocol +import SKSupport import SKTestSupport import TSCBasic import XCTest @@ -423,7 +424,7 @@ private func checkCompilationDatabaseBuildSystem( let buildSystem = CompilationDatabaseBuildSystem( projectRoot: try AbsolutePath(validating: "/a"), searchPaths: try [RelativePath(validating: ".")], - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), fileSystem: fs ) try await block(XCTUnwrap(buildSystem)) diff --git a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift index 75352924..9585bac5 100644 --- a/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift +++ b/Tests/BuildSystemIntegrationTests/SwiftPMBuildSystemTests.swift @@ -16,6 +16,7 @@ import BuildServerProtocol import LanguageServerProtocol import PackageModel import SKOptions +import SKSupport import SKTestSupport import SourceKitLSP import TSCBasic @@ -82,7 +83,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: ToolchainRegistry(toolchains: []), fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) ) @@ -114,7 +115,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -180,7 +181,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: localFileSystem, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -243,7 +244,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(swiftPM: options), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -291,7 +292,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(swiftPM: options), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy"), testHooks: SwiftPMTestHooks() ) let path = await swiftpmBuildSystem.destinationBuildParameters.toolchain.sdkRootPath @@ -327,7 +328,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -365,7 +366,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -415,7 +416,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -471,7 +472,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -511,7 +512,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -593,7 +594,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: ToolchainRegistry.forTesting, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -646,7 +647,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -715,7 +716,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: ToolchainRegistry.forTesting, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -756,7 +757,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() @@ -825,7 +826,7 @@ final class SwiftPMBuildSystemTests: XCTestCase { toolchainRegistry: tr, fileSystem: fs, options: SourceKitLSPOptions(), - messageHandler: nil, + connectionToSourceKitLSP: LocalConnection(receiverName: "Dummy SourceKit-LSP"), testHooks: SwiftPMTestHooks() ) await swiftpmBuildSystem.waitForUpToDateBuildGraph() diff --git a/Tests/SourceKitLSPTests/BuildSystemTests.swift b/Tests/SourceKitLSPTests/BuildSystemTests.swift index e690d042..eece3253 100644 --- a/Tests/SourceKitLSPTests/BuildSystemTests.swift +++ b/Tests/SourceKitLSPTests/BuildSystemTests.swift @@ -52,8 +52,7 @@ final class BuildSystemTests: XCTestCase { buildSystemKind: .testBuildSystem(projectRoot: try AbsolutePath(validating: "/")), toolchainRegistry: .forTesting, options: .testDefault(), - buildSystemTestHooks: BuildSystemTestHooks(), - reloadPackageStatusCallback: { _ in } + buildSystemTestHooks: BuildSystemTestHooks() ) buildSystem = try await unwrap(buildSystemManager.buildSystem?.underlyingBuildSystem as? TestBuildSystem)