mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
If a source file is part of multiple targets, we should index it in the context of all of those targets because the different targets may produce different USRs since they might use different build settings to interpret the file.
281 lines
10 KiB
Swift
281 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,
|
|
outputPathsProvider: true,
|
|
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)
|
|
}
|
|
}
|