mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
280 lines
10 KiB
Swift
280 lines
10 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2024 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
package import BuildServerProtocol
|
|
import BuildSystemIntegration
|
|
package import Foundation
|
|
package import LanguageServerProtocol
|
|
import LanguageServerProtocolExtensions
|
|
import SKLogging
|
|
package import SKOptions
|
|
package import SourceKitLSP
|
|
import SwiftExtensions
|
|
import ToolchainRegistry
|
|
import XCTest
|
|
|
|
// MARK: - CustomBuildServer
|
|
|
|
package actor CustomBuildServerInProgressRequestTracker {
|
|
private var inProgressRequests: [RequestID: Task<Void, Never>] = [:]
|
|
private let queue = AsyncQueue<Serial>()
|
|
|
|
package init() {}
|
|
|
|
private func setInProgressRequestImpl(_ id: RequestID, task: Task<Void, Never>) {
|
|
guard inProgressRequests[id] == nil else {
|
|
logger.fault("Received duplicate request for id: \(id, privacy: .public)")
|
|
return
|
|
}
|
|
inProgressRequests[id] = task
|
|
}
|
|
|
|
fileprivate nonisolated func setInProgressRequest(_ id: RequestID, task: Task<Void, Never>) {
|
|
queue.async {
|
|
await self.setInProgressRequestImpl(id, task: task)
|
|
}
|
|
}
|
|
|
|
private func markTaskAsFinishedImpl(_ id: RequestID) {
|
|
guard inProgressRequests[id] != nil else {
|
|
logger.fault("Cannot mark request \(id, privacy: .public) as finished because it is not being tracked.")
|
|
return
|
|
}
|
|
inProgressRequests[id] = nil
|
|
}
|
|
|
|
fileprivate nonisolated func markTaskAsFinished(_ id: RequestID) {
|
|
queue.async {
|
|
await self.markTaskAsFinishedImpl(id)
|
|
}
|
|
}
|
|
|
|
private func cancelTaskImpl(_ id: RequestID) {
|
|
guard let task = inProgressRequests[id] else {
|
|
logger.fault("Cannot cancel task \(id, privacy: .public) because it isn't tracked")
|
|
return
|
|
}
|
|
task.cancel()
|
|
}
|
|
|
|
fileprivate nonisolated func cancelTask(_ id: RequestID) {
|
|
queue.async {
|
|
await self.cancelTaskImpl(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A build server that can be injected into `CustomBuildServerTestProject`.
|
|
package protocol CustomBuildServer: MessageHandler {
|
|
var inProgressRequestsTracker: CustomBuildServerInProgressRequestTracker { get }
|
|
|
|
init(projectRoot: URL, connectionToSourceKitLSP: any Connection)
|
|
|
|
func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse
|
|
func onBuildInitialized(_ notification: OnBuildInitializedNotification) throws
|
|
func buildShutdown(_ request: BuildShutdownRequest) async throws -> VoidResponse
|
|
func onBuildExit(_ notification: OnBuildExitNotification) throws
|
|
func workspaceBuildTargetsRequest(
|
|
_ request: WorkspaceBuildTargetsRequest
|
|
) async throws -> WorkspaceBuildTargetsResponse
|
|
func buildTargetSourcesRequest(_ request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse
|
|
func textDocumentSourceKitOptionsRequest(
|
|
_ request: TextDocumentSourceKitOptionsRequest
|
|
) async throws -> TextDocumentSourceKitOptionsResponse?
|
|
func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse
|
|
func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse
|
|
nonisolated func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws
|
|
func workspaceWaitForBuildSystemUpdatesRequest(
|
|
_ request: WorkspaceWaitForBuildSystemUpdatesRequest
|
|
) async throws -> VoidResponse
|
|
nonisolated func cancelRequest(_ notification: CancelRequestNotification) throws
|
|
}
|
|
|
|
extension CustomBuildServer {
|
|
package nonisolated func handle(_ notification: some NotificationType) {
|
|
do {
|
|
switch notification {
|
|
case let notification as CancelRequestNotification:
|
|
try self.cancelRequest(notification)
|
|
case let notification as OnBuildExitNotification:
|
|
try self.onBuildExit(notification)
|
|
case let notification as OnBuildInitializedNotification:
|
|
try self.onBuildInitialized(notification)
|
|
case let notification as OnWatchedFilesDidChangeNotification:
|
|
try self.onWatchedFilesDidChange(notification)
|
|
default:
|
|
throw ResponseError.methodNotFound(type(of: notification).method)
|
|
}
|
|
} catch {
|
|
logger.error("Error while handling BSP notification: \(error.forLogging)")
|
|
}
|
|
}
|
|
|
|
package nonisolated func handle<Request: RequestType>(
|
|
_ request: Request,
|
|
id: RequestID,
|
|
reply: @Sendable @escaping (LSPResult<Request.Response>) -> Void
|
|
) {
|
|
func handle<R: RequestType>(_ request: R, using handler: @Sendable @escaping (R) async throws -> R.Response) {
|
|
let task = Task {
|
|
defer { inProgressRequestsTracker.markTaskAsFinished(id) }
|
|
do {
|
|
reply(.success(try await handler(request) as! Request.Response))
|
|
} catch {
|
|
reply(.failure(ResponseError(error)))
|
|
}
|
|
}
|
|
inProgressRequestsTracker.setInProgressRequest(id, task: task)
|
|
}
|
|
|
|
switch request {
|
|
case let request as BuildShutdownRequest:
|
|
handle(request, using: self.buildShutdown(_:))
|
|
case let request as BuildTargetSourcesRequest:
|
|
handle(request, using: self.buildTargetSourcesRequest)
|
|
case let request as InitializeBuildRequest:
|
|
handle(request, using: self.initializeBuildRequest)
|
|
case let request as TextDocumentSourceKitOptionsRequest:
|
|
handle(request, using: self.textDocumentSourceKitOptionsRequest)
|
|
case let request as WorkspaceBuildTargetsRequest:
|
|
handle(request, using: self.workspaceBuildTargetsRequest)
|
|
case let request as WorkspaceWaitForBuildSystemUpdatesRequest:
|
|
handle(request, using: self.workspaceWaitForBuildSystemUpdatesRequest)
|
|
case let request as BuildTargetPrepareRequest:
|
|
handle(request, using: self.prepareTarget)
|
|
default:
|
|
reply(.failure(ResponseError.methodNotFound(type(of: request).method)))
|
|
}
|
|
}
|
|
}
|
|
|
|
package extension CustomBuildServer {
|
|
// MARK: Helper functions for the implementation of BSP methods
|
|
|
|
func initializationResponse(
|
|
initializeData: SourceKitInitializeBuildResponseData = .init(sourceKitOptionsProvider: true)
|
|
) -> InitializeBuildResponse {
|
|
InitializeBuildResponse(
|
|
displayName: "\(type(of: self))",
|
|
version: "",
|
|
bspVersion: "2.2.0",
|
|
capabilities: BuildServerCapabilities(),
|
|
dataKind: .sourceKit,
|
|
data: initializeData.encodeToLSPAny()
|
|
)
|
|
}
|
|
|
|
func initializationResponseSupportingBackgroundIndexing(projectRoot: URL) throws -> InitializeBuildResponse {
|
|
return initializationResponse(
|
|
initializeData: SourceKitInitializeBuildResponseData(
|
|
indexDatabasePath: try projectRoot.appendingPathComponent("index-db").filePath,
|
|
indexStorePath: try projectRoot.appendingPathComponent("index-store").filePath,
|
|
prepareProvider: true,
|
|
sourceKitOptionsProvider: true
|
|
)
|
|
)
|
|
}
|
|
|
|
func dummyTargetSourcesResponse(_ files: some Sequence<DocumentURI>) -> BuildTargetSourcesResponse {
|
|
return BuildTargetSourcesResponse(items: [
|
|
SourcesItem(target: .dummy, sources: files.map { SourceItem(uri: $0, kind: .file, generated: false) })
|
|
])
|
|
}
|
|
|
|
// MARK: Default implementation for all build server methods that usually don't need customization.
|
|
|
|
func initializeBuildRequest(_ request: InitializeBuildRequest) async throws -> InitializeBuildResponse {
|
|
return initializationResponse()
|
|
}
|
|
|
|
nonisolated func onBuildInitialized(_ notification: OnBuildInitializedNotification) throws {}
|
|
|
|
func buildShutdown(_ request: BuildShutdownRequest) async throws -> VoidResponse {
|
|
return VoidResponse()
|
|
}
|
|
|
|
nonisolated func onBuildExit(_ notification: OnBuildExitNotification) throws {}
|
|
|
|
func workspaceBuildTargetsRequest(
|
|
_ request: WorkspaceBuildTargetsRequest
|
|
) async throws -> WorkspaceBuildTargetsResponse {
|
|
return WorkspaceBuildTargetsResponse(targets: [
|
|
BuildTarget(
|
|
id: .dummy,
|
|
capabilities: BuildTargetCapabilities(),
|
|
languageIds: [],
|
|
dependencies: []
|
|
)
|
|
])
|
|
}
|
|
|
|
func prepareTarget(_ request: BuildTargetPrepareRequest) async throws -> VoidResponse {
|
|
return VoidResponse()
|
|
}
|
|
|
|
func waitForBuildSystemUpdates(request: WorkspaceWaitForBuildSystemUpdatesRequest) async -> VoidResponse {
|
|
return VoidResponse()
|
|
}
|
|
|
|
nonisolated func onWatchedFilesDidChange(_ notification: OnWatchedFilesDidChangeNotification) throws {}
|
|
|
|
func workspaceWaitForBuildSystemUpdatesRequest(
|
|
_ request: WorkspaceWaitForBuildSystemUpdatesRequest
|
|
) async throws -> VoidResponse {
|
|
return VoidResponse()
|
|
}
|
|
|
|
nonisolated func cancelRequest(_ notification: CancelRequestNotification) throws {
|
|
inProgressRequestsTracker.cancelTask(notification.id)
|
|
}
|
|
}
|
|
|
|
// MARK: - CustomBuildServerTestProject
|
|
|
|
/// A test project that launches a custom build server in-process.
|
|
///
|
|
/// In contrast to `ExternalBuildServerTestProject`, the custom build system runs in-process and is implemented in
|
|
/// Swift.
|
|
package final class CustomBuildServerTestProject<BuildServer: CustomBuildServer>: MultiFileTestProject {
|
|
private let buildServerBox = ThreadSafeBox<BuildServer?>(initialValue: nil)
|
|
|
|
package init(
|
|
files: [RelativeFileLocation: String],
|
|
buildServer buildServerType: BuildServer.Type,
|
|
options: SourceKitLSPOptions? = nil,
|
|
hooks: Hooks = Hooks(),
|
|
enableBackgroundIndexing: Bool = false,
|
|
testScratchDir: URL? = nil,
|
|
testName: String = #function
|
|
) async throws {
|
|
var hooks = hooks
|
|
XCTAssertNil(hooks.buildSystemHooks.injectBuildServer)
|
|
hooks.buildSystemHooks.injectBuildServer = { [buildServerBox] projectRoot, connectionToSourceKitLSP in
|
|
let buildServer = BuildServer(projectRoot: projectRoot, connectionToSourceKitLSP: connectionToSourceKitLSP)
|
|
buildServerBox.value = buildServer
|
|
return LocalConnection(receiverName: "TestBuildSystem", handler: buildServer)
|
|
}
|
|
try await super.init(
|
|
files: files,
|
|
options: options,
|
|
hooks: hooks,
|
|
enableBackgroundIndexing: enableBackgroundIndexing,
|
|
testScratchDir: testScratchDir,
|
|
testName: testName
|
|
)
|
|
}
|
|
|
|
package func buildServer(file: StaticString = #filePath, line: UInt = #line) throws -> BuildServer {
|
|
try XCTUnwrap(buildServerBox.value, "Accessing build server before it has been created", file: file, line: line)
|
|
}
|
|
}
|