mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
It’s perfectly fine for `mainFilesProvider` to be `nil` if we don’t have an index. We shouldn’t log a fault for that.
2094 lines
87 KiB
Swift
2094 lines
87 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
@_spi(SourceKitLSP) package import BuildServerProtocol
|
|
import Dispatch
|
|
package import Foundation
|
|
@_spi(SourceKitLSP) package import LanguageServerProtocol
|
|
@_spi(SourceKitLSP) import LanguageServerProtocolExtensions
|
|
@_spi(SourceKitLSP) package import LanguageServerProtocolTransport
|
|
@_spi(SourceKitLSP) import SKLogging
|
|
package import SKOptions
|
|
import SKUtilities
|
|
import SwiftExtensions
|
|
import TSCExtensions
|
|
package import ToolchainRegistry
|
|
@_spi(SourceKitLSP) package import ToolsProtocolsSwiftExtensions
|
|
|
|
import struct TSCBasic.RelativePath
|
|
|
|
private typealias RequestCache<Request: RequestType & Hashable> = Cache<Request, Request.Response>
|
|
|
|
/// An output path returned from the build server in the `SourceItem.data.outputPath` field.
|
|
package enum OutputPath: Hashable, Comparable, CustomLogStringConvertible {
|
|
/// An output path returned from the build server.
|
|
case path(String)
|
|
|
|
/// The build server does not support output paths.
|
|
case notSupported
|
|
|
|
package var description: String {
|
|
switch self {
|
|
case .notSupported: return "<output path not supported>"
|
|
case .path(let path): return path
|
|
}
|
|
}
|
|
|
|
package var redactedDescription: String {
|
|
switch self {
|
|
case .notSupported: return "<output path not supported>"
|
|
case .path(let path): return path.hashForLogging
|
|
}
|
|
}
|
|
}
|
|
|
|
package struct SourceFileInfo: Sendable {
|
|
/// Maps the targets that this source file is a member of to the output path the file has within that target.
|
|
///
|
|
/// The value in the dictionary can be:
|
|
/// - `.path` if the build server supports output paths and produced a result
|
|
/// - `.notSupported` if the build server does not support output paths.
|
|
/// - `nil` if the build server supports output paths but did not return an output path for this file in this target.
|
|
package var targetsToOutputPath: [BuildTargetIdentifier: OutputPath?]
|
|
|
|
/// The targets that this source file is a member of
|
|
package var targets: some Collection<BuildTargetIdentifier> & Sendable { targetsToOutputPath.keys }
|
|
|
|
/// `true` if this file belongs to the root project that the user is working on. It is false, if the file belongs
|
|
/// to a dependency of the project.
|
|
package var isPartOfRootProject: Bool
|
|
|
|
/// Whether the file might contain test cases. This property is an over-approximation. It might be true for files
|
|
/// from non-test targets or files that don't actually contain any tests.
|
|
package var mayContainTests: Bool
|
|
|
|
/// Source files returned here fall into two categories:
|
|
/// - Buildable source files are files that can be built by the build server and that make sense to background index
|
|
/// - Non-buildable source files include eg. the SwiftPM package manifest or header files. We have sufficient
|
|
/// compiler arguments for these files to provide semantic editor functionality but we can't build them.
|
|
package var isBuildable: Bool
|
|
|
|
/// If this source item gets copied to a different destination during preparation, the destinations it will be copied
|
|
/// to.
|
|
package var copyDestinations: Set<DocumentURI>
|
|
|
|
fileprivate func merging(_ other: SourceFileInfo?) -> SourceFileInfo {
|
|
guard let other else {
|
|
return self
|
|
}
|
|
let mergedTargetsToOutputPaths = targetsToOutputPath.merging(
|
|
other.targetsToOutputPath,
|
|
uniquingKeysWith: { lhs, rhs in
|
|
if lhs == rhs {
|
|
return lhs
|
|
}
|
|
logger.error("Received mismatching output files: \(lhs?.forLogging) vs \(rhs?.forLogging)")
|
|
// Deterministically pick an output file if they mismatch. But really, this shouldn't happen.
|
|
switch (lhs, rhs) {
|
|
case (let lhs?, nil): return lhs
|
|
case (nil, let rhs?): return rhs
|
|
case (nil, nil): return nil // Should be handled above already
|
|
case (let lhs?, let rhs?): return min(lhs, rhs)
|
|
}
|
|
}
|
|
)
|
|
return SourceFileInfo(
|
|
targetsToOutputPath: mergedTargetsToOutputPaths,
|
|
isPartOfRootProject: other.isPartOfRootProject || isPartOfRootProject,
|
|
mayContainTests: other.mayContainTests || mayContainTests,
|
|
isBuildable: other.isBuildable || isBuildable,
|
|
copyDestinations: copyDestinations.union(other.copyDestinations)
|
|
)
|
|
}
|
|
}
|
|
|
|
private struct BuildTargetInfo {
|
|
/// The build target itself.
|
|
var target: BuildTarget
|
|
|
|
/// The maximum depth at which this target occurs at the build graph, ie. the number of edges on the longest path
|
|
/// from this target to a root target (eg. an executable)
|
|
var depth: Int
|
|
|
|
/// The targets that depend on this target, ie. the inverse of `BuildTarget.dependencies`.
|
|
var dependents: Set<BuildTargetIdentifier>
|
|
}
|
|
|
|
fileprivate extension BuildTarget {
|
|
var sourceKitData: SourceKitBuildTarget? {
|
|
guard dataKind == .sourceKit else {
|
|
return nil
|
|
}
|
|
return SourceKitBuildTarget(fromLSPAny: data)
|
|
}
|
|
}
|
|
|
|
fileprivate extension InitializeBuildResponse {
|
|
var sourceKitData: SourceKitInitializeBuildResponseData? {
|
|
guard dataKind == nil || dataKind == .sourceKit else {
|
|
return nil
|
|
}
|
|
return SourceKitInitializeBuildResponseData(fromLSPAny: data)
|
|
}
|
|
}
|
|
|
|
/// A build server adapter is responsible for receiving messages from the `BuildServerManager` and forwarding them to
|
|
/// the build server. For built-in build servers, this means that we need to translate the BSP messages to methods in
|
|
/// the `BuiltInBuildServer` protocol. For external (aka. out-of-process, aka. BSP servers) build servers, this means
|
|
/// that we need to manage the external build server's lifetime.
|
|
private enum BuildServerAdapter {
|
|
case builtIn(BuiltInBuildServerAdapter, connectionToBuildServer: any Connection)
|
|
case external(ExternalBuildServerAdapter)
|
|
/// A message handler that was created by `injectBuildServer` and will handle all BSP messages.
|
|
case injected(any Connection)
|
|
|
|
/// Send a notification to the build server.
|
|
func send(_ notification: some NotificationType) async {
|
|
switch self {
|
|
case .builtIn(_, let connectionToBuildServer):
|
|
connectionToBuildServer.send(notification)
|
|
case .external(let external):
|
|
await external.send(notification)
|
|
case .injected(let connection):
|
|
connection.send(notification)
|
|
}
|
|
}
|
|
|
|
/// Send a request to the build server.
|
|
func send<Request: RequestType>(_ request: Request) async throws -> Request.Response {
|
|
switch self {
|
|
case .builtIn(_, let connectionToBuildServer):
|
|
return try await connectionToBuildServer.send(request)
|
|
case .external(let external):
|
|
return try await external.send(request)
|
|
case .injected(let messageHandler):
|
|
// After we sent the request, the ID of the request.
|
|
// When we send a `CancelRequestNotification` this is reset to `nil` so that we don't send another cancellation
|
|
// notification.
|
|
let requestID = ThreadSafeBox<RequestID?>(initialValue: nil)
|
|
|
|
return try await withTaskCancellationHandler {
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
if Task.isCancelled {
|
|
return continuation.resume(throwing: CancellationError())
|
|
}
|
|
requestID.value = messageHandler.send(request) { response in
|
|
continuation.resume(with: response)
|
|
}
|
|
if Task.isCancelled {
|
|
// The task might have been cancelled after we checked `Task.isCancelled` above but before `requestID.value`
|
|
// is set, we won't send a `CancelRequestNotification` from the `onCancel` handler. Send it from here.
|
|
if let requestID = requestID.takeValue() {
|
|
messageHandler.send(CancelRequestNotification(id: requestID))
|
|
}
|
|
}
|
|
}
|
|
} onCancel: {
|
|
if let requestID = requestID.takeValue() {
|
|
messageHandler.send(CancelRequestNotification(id: requestID))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension BuildServerSpec {
|
|
private func createBuiltInBuildServerAdapter(
|
|
messagesToSourceKitLSPHandler: any MessageHandler,
|
|
buildServerHooks: BuildServerHooks,
|
|
_ createBuildServer:
|
|
@Sendable (
|
|
_ connectionToSourceKitLSP: any Connection
|
|
) async throws -> (any BuiltInBuildServer)?
|
|
) async -> BuildServerAdapter? {
|
|
let connectionToSourceKitLSP = LocalConnection(
|
|
receiverName: "BuildServerManager for \(projectRoot.lastPathComponent)",
|
|
handler: messagesToSourceKitLSPHandler
|
|
)
|
|
|
|
let buildServer = await orLog("Creating build server") {
|
|
try await createBuildServer(connectionToSourceKitLSP)
|
|
}
|
|
guard let buildServer else {
|
|
logger.log("Failed to create build server at \(projectRoot)")
|
|
return nil
|
|
}
|
|
logger.log("Created \(type(of: buildServer), privacy: .public) at \(projectRoot)")
|
|
let buildServerAdapter = BuiltInBuildServerAdapter(
|
|
underlyingBuildServer: buildServer,
|
|
connectionToSourceKitLSP: connectionToSourceKitLSP,
|
|
buildServerHooks: buildServerHooks
|
|
)
|
|
let connectionToBuildServer = LocalConnection(
|
|
receiverName: "\(type(of: buildServer)) for \(projectRoot.lastPathComponent)",
|
|
handler: buildServerAdapter
|
|
)
|
|
return .builtIn(buildServerAdapter, connectionToBuildServer: connectionToBuildServer)
|
|
}
|
|
|
|
/// Create a `BuildServerAdapter` that manages a build server of this kind and return a connection that can be used
|
|
/// to send messages to the build server.
|
|
func createBuildServerAdapter(
|
|
toolchainRegistry: ToolchainRegistry,
|
|
options: SourceKitLSPOptions,
|
|
buildServerHooks: BuildServerHooks,
|
|
messagesToSourceKitLSPHandler: any MessageHandler
|
|
) async -> BuildServerAdapter? {
|
|
switch self.kind {
|
|
case .externalBuildServer:
|
|
let buildServer = await orLog("Creating external build server") {
|
|
try await ExternalBuildServerAdapter(
|
|
projectRoot: projectRoot,
|
|
configPath: configPath,
|
|
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
|
|
)
|
|
}
|
|
guard let buildServer else {
|
|
logger.log("Failed to create external build server at \(projectRoot)")
|
|
return nil
|
|
}
|
|
logger.log("Created external build server at \(projectRoot)")
|
|
return .external(buildServer)
|
|
case .jsonCompilationDatabase:
|
|
return await createBuiltInBuildServerAdapter(
|
|
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
|
|
buildServerHooks: buildServerHooks
|
|
) { connectionToSourceKitLSP in
|
|
try JSONCompilationDatabaseBuildServer(
|
|
configPath: configPath,
|
|
toolchainRegistry: toolchainRegistry,
|
|
connectionToSourceKitLSP: connectionToSourceKitLSP
|
|
)
|
|
}
|
|
case .fixedCompilationDatabase:
|
|
return await createBuiltInBuildServerAdapter(
|
|
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
|
|
buildServerHooks: buildServerHooks
|
|
) { connectionToSourceKitLSP in
|
|
try FixedCompilationDatabaseBuildServer(
|
|
configPath: configPath,
|
|
connectionToSourceKitLSP: connectionToSourceKitLSP
|
|
)
|
|
}
|
|
case .swiftPM:
|
|
switch options.swiftPMOrDefault.buildSystem {
|
|
case .swiftbuild:
|
|
let buildServer = await orLog("Creating external SwiftPM build server") {
|
|
try await ExternalBuildServerAdapter(
|
|
projectRoot: projectRoot,
|
|
config: BuildServerConfig.forSwiftPMBuildServer(
|
|
projectRoot: projectRoot,
|
|
swiftPMOptions: options.swiftPMOrDefault,
|
|
toolchainRegistry: toolchainRegistry
|
|
),
|
|
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
|
|
)
|
|
}
|
|
guard let buildServer else {
|
|
logger.log("Failed to create external SwiftPM build server at \(projectRoot)")
|
|
return nil
|
|
}
|
|
logger.log("Created external SwiftPM build server at \(projectRoot)")
|
|
return .external(buildServer)
|
|
case .native, nil:
|
|
#if !NO_SWIFTPM_DEPENDENCY
|
|
return await createBuiltInBuildServerAdapter(
|
|
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
|
|
buildServerHooks: buildServerHooks
|
|
) { connectionToSourceKitLSP in
|
|
try await SwiftPMBuildServer(
|
|
projectRoot: projectRoot,
|
|
toolchainRegistry: toolchainRegistry,
|
|
options: options,
|
|
connectionToSourceKitLSP: connectionToSourceKitLSP,
|
|
testHooks: buildServerHooks.swiftPMTestHooks
|
|
)
|
|
}
|
|
#else
|
|
return nil
|
|
#endif
|
|
}
|
|
case .injected(let injector):
|
|
let connectionToSourceKitLSP = LocalConnection(
|
|
receiverName: "BuildServerManager for \(projectRoot.lastPathComponent)",
|
|
handler: messagesToSourceKitLSPHandler
|
|
)
|
|
return .injected(
|
|
await injector(projectRoot, connectionToSourceKitLSP)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Entry point for all build server queries.
|
|
package actor BuildServerManager: QueueBasedMessageHandler {
|
|
package let messageHandlingHelper = QueueBasedMessageHandlerHelper(
|
|
signpostLoggingCategory: "build-server-manager-message-handling",
|
|
createLoggingScope: false
|
|
)
|
|
|
|
package let messageHandlingQueue = AsyncQueue<BuildServerMessageDependencyTracker>()
|
|
|
|
/// The path to the main configuration file (or directory) that this build server manages.
|
|
///
|
|
/// Some examples:
|
|
/// - The path to `Package.swift` for SwiftPM packages
|
|
/// - The path to `compile_commands.json` for a JSON compilation database
|
|
///
|
|
/// `nil` if the `BuildServerManager` does not have an underlying build server.
|
|
package let configPath: URL?
|
|
|
|
/// The files for which the delegate has requested change notifications, ie. the files for which the delegate wants to
|
|
/// get `fileBuildSettingsChanged` and `filesDependenciesUpdated` callbacks.
|
|
private var watchedFiles: [DocumentURI: (mainFile: DocumentURI, language: Language)] = [:]
|
|
|
|
private var connectionToClient: any BuildServerManagerConnectionToClient
|
|
|
|
/// The build serer adapter that is used to answer build server queries.
|
|
private var buildServerAdapter: BuildServerAdapter?
|
|
|
|
/// The build server adapter after initialization finishes. When sending messages to the BSP server, this should be
|
|
/// preferred over `buildServerAdapter` because no messages must be sent to the build server before initialization
|
|
/// finishes.
|
|
private var buildServerAdapterAfterInitialized: BuildServerAdapter? {
|
|
get async throws {
|
|
guard await initializeResult.value != nil else {
|
|
throw ResponseError.unknown("Build server failed to initialize")
|
|
}
|
|
return buildServerAdapter
|
|
}
|
|
}
|
|
|
|
/// Provider of file to main file mappings.
|
|
///
|
|
/// Force-unwrapped optional because initializing it requires access to `self`.
|
|
private var mainFilesProvider: Task<(any MainFilesProvider)?, Never>! {
|
|
didSet {
|
|
// Must only be set once
|
|
precondition(oldValue == nil)
|
|
precondition(mainFilesProvider != nil)
|
|
}
|
|
}
|
|
|
|
package func mainFilesProvider<T: MainFilesProvider>(as: T.Type) async -> T? {
|
|
guard let mainFilesProvider = await mainFilesProvider.value else {
|
|
return nil
|
|
}
|
|
guard let index = mainFilesProvider as? T else {
|
|
logger.fault("Expected the main files provider of the build server manager to be a '\(T.self)'")
|
|
return nil
|
|
}
|
|
return index
|
|
}
|
|
|
|
/// Build server delegate that will receive notifications about setting changes, etc.
|
|
private weak var delegate: (any BuildServerManagerDelegate)?
|
|
|
|
private let buildSettingsLogger = BuildSettingsLogger()
|
|
|
|
/// The list of toolchains that are available.
|
|
///
|
|
/// Used to determine which toolchain to use for a given document.
|
|
private let toolchainRegistry: ToolchainRegistry
|
|
|
|
private let options: SourceKitLSPOptions
|
|
|
|
/// A task that stores the result of the `build/initialize` request once it is received.
|
|
///
|
|
/// Force-unwrapped optional because initializing it requires access to `self`.
|
|
private var initializeResult: Task<InitializeBuildResponse?, Never>! {
|
|
didSet {
|
|
// Must only be set once
|
|
precondition(oldValue == nil)
|
|
precondition(initializeResult != nil)
|
|
}
|
|
}
|
|
|
|
/// For tasks from the build server that should create a work done progress in the client, a mapping from the `TaskId`
|
|
/// in the build server to a `WorkDoneProgressManager` that manages that work done progress in the client.
|
|
private var workDoneProgressManagers: [TaskIdentifier: WorkDoneProgressManager] = [:]
|
|
|
|
/// Debounces calls to `delegate.filesDependenciesUpdated`.
|
|
///
|
|
/// This is to ensure we don't call `filesDependenciesUpdated` for the same file multiple time if the client does not
|
|
/// debounce `workspace/didChangeWatchedFiles` and sends a separate notification eg. for every file within a target as
|
|
/// it's being updated by a git checkout, which would cause other files within that target to receive a
|
|
/// `fileDependenciesUpdated` call once for every updated file within the target.
|
|
///
|
|
/// Force-unwrapped optional because initializing it requires access to `self`.
|
|
private var filesDependenciesUpdatedDebouncer: Debouncer<Set<DocumentURI>>! = nil {
|
|
didSet {
|
|
// Must only be set once
|
|
precondition(oldValue == nil)
|
|
precondition(filesDependenciesUpdatedDebouncer != nil)
|
|
}
|
|
}
|
|
|
|
/// Debounces calls to `delegate.fileBuildSettingsChanged`.
|
|
///
|
|
/// This helps in the following situation: A build server takes 5s to return build settings for a file and we have 10
|
|
/// requests for those build settings coming in that time period. Once we get build settings, we get 10 calls to
|
|
/// `resultReceivedAfterTimeout` in `buildSettings(for:in:language:fallbackAfterTimeout:)`, all for the same document.
|
|
/// But calling `fileBuildSettingsChanged` once is totally sufficient.
|
|
///
|
|
/// Force-unwrapped optional because initializing it requires access to `self`.
|
|
private var filesBuildSettingsChangedDebouncer: Debouncer<Set<DocumentURI>>! = nil {
|
|
didSet {
|
|
// Must only be set once
|
|
precondition(oldValue == nil)
|
|
precondition(filesBuildSettingsChangedDebouncer != nil)
|
|
}
|
|
}
|
|
|
|
private var cachedAdjustedSourceKitOptions = RequestCache<TextDocumentSourceKitOptionsRequest>()
|
|
|
|
private var cachedBuildTargets = Cache<WorkspaceBuildTargetsRequest, [BuildTargetIdentifier: BuildTargetInfo]>()
|
|
|
|
private var cachedTargetSources = RequestCache<BuildTargetSourcesRequest>()
|
|
|
|
/// `SourceFilesAndDirectories` is a global property that only gets reset when the build targets change and thus
|
|
/// has no real key.
|
|
private struct SourceFilesAndDirectoriesKey: Hashable {}
|
|
|
|
private struct SourceFilesAndDirectories {
|
|
/// The source files in the workspace, ie. all `SourceItem`s that have `kind == .file`.
|
|
let files: [DocumentURI: SourceFileInfo]
|
|
|
|
/// The source directories in the workspace, ie. all `SourceItem`s that have `kind == .directory`.
|
|
///
|
|
/// `pathComponents` is the result of `key.fileURL?.pathComponents`. We frequently need these path components to
|
|
/// determine if a file is descendent of the directory and computing them from the `DocumentURI` is expensive.
|
|
let directories: [DocumentURI: (pathComponents: [String]?, info: SourceFileInfo)]
|
|
|
|
/// Same as `Set(files.filter(\.value.isBuildable).keys)`. Pre-computed because we need this pretty frequently in
|
|
/// `SemanticIndexManager.filesToIndex`.
|
|
let buildableSourceFiles: Set<DocumentURI>
|
|
|
|
internal init(
|
|
files: [DocumentURI: SourceFileInfo],
|
|
directories: [DocumentURI: (pathComponents: [String]?, info: SourceFileInfo)]
|
|
) {
|
|
self.files = files
|
|
self.directories = directories
|
|
self.buildableSourceFiles = Set(files.filter(\.value.isBuildable).keys)
|
|
}
|
|
|
|
}
|
|
|
|
private let cachedSourceFilesAndDirectories = Cache<SourceFilesAndDirectoriesKey, SourceFilesAndDirectories>()
|
|
|
|
/// The latest map of copied file URIs to their original source locations.
|
|
///
|
|
/// We don't use a `Cache` for this because we can provide reasonable functionality even without or with an
|
|
/// out-of-date copied file map - in the worst case we jump to a file in the build directory instead of the source
|
|
/// directory.
|
|
/// We don't want to block requests like definition on receiving up-to-date index information from the build server.
|
|
private var cachedCopiedFileMap: [DocumentURI: DocumentURI] = [:]
|
|
|
|
/// The latest task to update the `cachedCopiedFileMap`. This allows us to cancel previous tasks to update the copied
|
|
/// file map when a new update is requested.
|
|
private var copiedFileMapUpdateTask: Task<Void, Never>?
|
|
|
|
/// The `SourceKitInitializeBuildResponseData` received from the `build/initialize` request, if any.
|
|
package var initializationData: SourceKitInitializeBuildResponseData? {
|
|
get async {
|
|
return await initializeResult.value?.sourceKitData
|
|
}
|
|
}
|
|
|
|
package init(
|
|
buildServerSpec: BuildServerSpec?,
|
|
toolchainRegistry: ToolchainRegistry,
|
|
options: SourceKitLSPOptions,
|
|
connectionToClient: any BuildServerManagerConnectionToClient,
|
|
buildServerHooks: BuildServerHooks,
|
|
createMainFilesProvider:
|
|
@escaping @Sendable (
|
|
SourceKitInitializeBuildResponseData?, _ mainFilesChangedCallback: @escaping @Sendable () async -> Void
|
|
) async -> (any MainFilesProvider)?
|
|
) async {
|
|
self.toolchainRegistry = toolchainRegistry
|
|
self.options = options
|
|
self.connectionToClient = connectionToClient
|
|
self.configPath = buildServerSpec?.configPath
|
|
self.buildServerAdapter = await buildServerSpec?.createBuildServerAdapter(
|
|
toolchainRegistry: toolchainRegistry,
|
|
options: options,
|
|
buildServerHooks: buildServerHooks,
|
|
messagesToSourceKitLSPHandler: WeakMessageHandler(self)
|
|
)
|
|
|
|
// The debounce duration of 500ms was chosen arbitrarily without any measurements.
|
|
self.filesDependenciesUpdatedDebouncer = Debouncer(
|
|
debounceDuration: .milliseconds(500),
|
|
combineResults: { $0.union($1) },
|
|
makeCall: { [weak self] (filesWithUpdatedDependencies) in
|
|
guard let self, let delegate = await self.delegate else {
|
|
logger.fault("Not calling filesDependenciesUpdated because no delegate exists in SwiftPMBuildServer")
|
|
return
|
|
}
|
|
let changedWatchedFiles = await self.watchedFilesReferencing(mainFiles: filesWithUpdatedDependencies)
|
|
if !changedWatchedFiles.isEmpty {
|
|
await delegate.filesDependenciesUpdated(changedWatchedFiles)
|
|
}
|
|
}
|
|
)
|
|
|
|
// We don't need a large debounce duration here. It just needs to be big enough to accumulate
|
|
// `resultReceivedAfterTimeout` calls for the same document (see comment on `filesBuildSettingsChangedDebouncer`).
|
|
// Since they should all come in at the same time, a couple of milliseconds should be sufficient here, an 20ms be
|
|
// plenty while still not causing a noticeable delay to the user.
|
|
self.filesBuildSettingsChangedDebouncer = Debouncer(
|
|
debounceDuration: .milliseconds(20),
|
|
combineResults: { $0.union($1) },
|
|
makeCall: { [weak self] (filesWithChangedBuildSettings) in
|
|
guard let self, let delegate = await self.delegate else {
|
|
logger.fault("Not calling fileBuildSettingsChanged because no delegate exists in SwiftPMBuildServer")
|
|
return
|
|
}
|
|
if !filesWithChangedBuildSettings.isEmpty {
|
|
await delegate.fileBuildSettingsChanged(filesWithChangedBuildSettings)
|
|
}
|
|
}
|
|
)
|
|
|
|
// TODO: Forward file watch patterns from this initialize request to the client
|
|
// (https://github.com/swiftlang/sourcekit-lsp/issues/1671)
|
|
initializeResult = Task { () -> InitializeBuildResponse? in
|
|
guard let buildServerAdapter else {
|
|
return nil
|
|
}
|
|
guard let buildServerSpec else {
|
|
logger.fault("If we have a connectionToBuildServer, we must have had a buildServerSpec")
|
|
return nil
|
|
}
|
|
let initializeResponse: InitializeBuildResponse?
|
|
do {
|
|
initializeResponse = try await buildServerAdapter.send(
|
|
InitializeBuildRequest(
|
|
displayName: "SourceKit-LSP",
|
|
version: "",
|
|
bspVersion: "2.2.0",
|
|
rootUri: URI(buildServerSpec.projectRoot),
|
|
capabilities: BuildClientCapabilities(languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift])
|
|
)
|
|
)
|
|
} catch {
|
|
initializeResponse = nil
|
|
let errorMessage: String
|
|
if let error = error as? ResponseError {
|
|
errorMessage = error.message
|
|
} else {
|
|
errorMessage = "\(error)"
|
|
}
|
|
connectionToClient.send(
|
|
ShowMessageNotification(type: .error, message: "Failed to initialize build server: \(errorMessage)")
|
|
)
|
|
}
|
|
|
|
if let initializeResponse, !(initializeResponse.sourceKitData?.sourceKitOptionsProvider ?? false),
|
|
case .external(let externalBuildServerAdapter) = buildServerAdapter
|
|
{
|
|
// The BSP server does not support the pull-based settings model. Inject a `LegacyBuildServerBuildServer` that
|
|
// offers the pull-based model to `BuildServerManager` and uses the push-based model to get build settings from
|
|
// the build server.
|
|
logger.log("Launched a legacy BSP server. Using push-based build settings model.")
|
|
let legacyBuildServer = await LegacyBuildServer(
|
|
projectRoot: buildServerSpec.projectRoot,
|
|
configPath: buildServerSpec.configPath,
|
|
initializationData: initializeResponse,
|
|
externalBuildServerAdapter
|
|
)
|
|
let adapter = BuiltInBuildServerAdapter(
|
|
underlyingBuildServer: legacyBuildServer,
|
|
connectionToSourceKitLSP: legacyBuildServer.connectionToSourceKitLSP,
|
|
buildServerHooks: buildServerHooks
|
|
)
|
|
let connectionToBuildSerer = LocalConnection(receiverName: "Legacy BSP server", handler: adapter)
|
|
self.buildServerAdapter = .builtIn(adapter, connectionToBuildServer: connectionToBuildSerer)
|
|
}
|
|
Task {
|
|
var filesToWatch = initializeResponse?.sourceKitData?.watchers ?? []
|
|
filesToWatch.append(FileSystemWatcher(globPattern: "**/*.swift", kind: [.change]))
|
|
if !options.backgroundIndexingOrDefault {
|
|
filesToWatch.append(FileSystemWatcher(globPattern: "**/*.swiftmodule", kind: [.create, .change, .delete]))
|
|
}
|
|
await connectionToClient.watchFiles(filesToWatch)
|
|
}
|
|
await buildServerAdapter.send(OnBuildInitializedNotification())
|
|
return initializeResponse
|
|
}
|
|
self.mainFilesProvider = Task {
|
|
await createMainFilesProvider(initializationData) { [weak self] in
|
|
await self?.mainFilesChanged()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Explicitly shut down the build server.
|
|
///
|
|
/// The build server is automatically shut down using a background task when `BuildServerManager` is deallocated.
|
|
/// This, however, leads to possible race conditions where the shutdown task might not finish before the test is done,
|
|
/// which could result in the connection being reported as a leak. To avoid this problem, we want to explicitly shut
|
|
/// down the build server when the `SourceKitLSPServer` gets shut down.
|
|
package func shutdown() async {
|
|
// Clear any pending work done progresses from the build server.
|
|
self.workDoneProgressManagers.removeAll()
|
|
guard let buildServerAdapter = try? await self.buildServerAdapterAfterInitialized else {
|
|
return
|
|
}
|
|
await orLog("Sending shutdown request to build server") {
|
|
// Give the build server 2 seconds to shut down by itself. If it doesn't shut down within that time, terminate it.
|
|
try await withTimeout(.seconds(2)) {
|
|
_ = try await buildServerAdapter.send(BuildShutdownRequest())
|
|
await buildServerAdapter.send(OnBuildExitNotification())
|
|
}
|
|
}
|
|
if case .external(let externalBuildServerAdapter) = buildServerAdapter {
|
|
await orLog("Terminating external build server") {
|
|
// Give the build server 1 second to exit after receiving the `build/exit` notification. If it doesn't exit
|
|
// within that time, terminate it.
|
|
try await externalBuildServerAdapter.terminateIfRunning(after: .seconds(1))
|
|
}
|
|
}
|
|
self.buildServerAdapter = nil
|
|
}
|
|
|
|
deinit {
|
|
// Shut down the build server before closing the connection to it
|
|
Task { [buildServerAdapter, initializeResult] in
|
|
guard let buildServerAdapter else {
|
|
return
|
|
}
|
|
// We are accessing the raw connection to the build server, so we need to ensure that it has been initialized here
|
|
_ = await initializeResult?.value
|
|
await orLog("Sending shutdown request to build server") {
|
|
_ = try await buildServerAdapter.send(BuildShutdownRequest())
|
|
await buildServerAdapter.send(OnBuildExitNotification())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// - Note: Needed because `BuildSererManager` is created before `Workspace` is initialized and `Workspace` needs to
|
|
/// create the `BuildServerManager`, then initialize itself and then set itself as the delegate.
|
|
package func setDelegate(_ delegate: (any BuildServerManagerDelegate)?) {
|
|
self.delegate = delegate
|
|
}
|
|
|
|
// MARK: Handling messages from the build server
|
|
|
|
package func handle(notification: some NotificationType) async {
|
|
switch notification {
|
|
case let notification as OnBuildTargetDidChangeNotification:
|
|
await self.didChangeBuildTarget(notification: notification)
|
|
case let notification as OnBuildLogMessageNotification:
|
|
await self.logMessage(notification: notification)
|
|
case let notification as TaskFinishNotification:
|
|
await self.taskFinish(notification: notification)
|
|
case let notification as TaskProgressNotification:
|
|
await self.taskProgress(notification: notification)
|
|
case let notification as TaskStartNotification:
|
|
await self.taskStart(notification: notification)
|
|
default:
|
|
logger.error("Ignoring unknown notification \(type(of: notification).method)")
|
|
}
|
|
}
|
|
|
|
package func handle<Request: RequestType>(
|
|
request: Request,
|
|
id: RequestID,
|
|
reply: @Sendable @escaping (LSPResult<Request.Response>) -> Void
|
|
) async {
|
|
let request = RequestAndReply(request, reply: reply)
|
|
switch request {
|
|
default:
|
|
await request.reply { throw ResponseError.methodNotFound(Request.method) }
|
|
}
|
|
}
|
|
|
|
private func didChangeBuildTarget(notification: OnBuildTargetDidChangeNotification) async {
|
|
let changedTargets: Set<BuildTargetIdentifier>? =
|
|
if let changes = notification.changes {
|
|
Set(changes.map(\.target))
|
|
} else {
|
|
nil
|
|
}
|
|
await self.buildTargetsDidChange(.didChangeBuildTargets(changedTargets: changedTargets))
|
|
}
|
|
|
|
private enum BuildTargetsChange {
|
|
case didChangeBuildTargets(changedTargets: Set<BuildTargetIdentifier>?)
|
|
case buildTargetsReceivedResultAfterTimeout(
|
|
request: WorkspaceBuildTargetsRequest,
|
|
newResult: [BuildTargetIdentifier: BuildTargetInfo]
|
|
)
|
|
case sourceFilesReceivedResultAfterTimeout(
|
|
request: BuildTargetSourcesRequest,
|
|
newResult: BuildTargetSourcesResponse
|
|
)
|
|
}
|
|
|
|
/// Update the cached state in `BuildServerManager` because new data was received from the BSP server.
|
|
///
|
|
/// This handles a few seemingly unrelated reasons to ensure that we think about which caches to invalidate in the
|
|
/// other scenarios as well, when making changes in here.
|
|
private func buildTargetsDidChange(_ stateChange: BuildTargetsChange) async {
|
|
let changedTargets: Set<BuildTargetIdentifier>?
|
|
|
|
switch stateChange {
|
|
case .didChangeBuildTargets(let changedTargetsValue):
|
|
changedTargets = changedTargetsValue
|
|
self.cachedAdjustedSourceKitOptions.clear(isolation: self) { cacheKey in
|
|
guard let changedTargets else {
|
|
// All targets might have changed
|
|
return true
|
|
}
|
|
return changedTargets.contains(cacheKey.target)
|
|
}
|
|
self.cachedBuildTargets.clearAll(isolation: self)
|
|
self.cachedTargetSources.clear(isolation: self) { cacheKey in
|
|
guard let changedTargets else {
|
|
// All targets might have changed
|
|
return true
|
|
}
|
|
return !changedTargets.intersection(cacheKey.targets).isEmpty
|
|
}
|
|
case .buildTargetsReceivedResultAfterTimeout(let request, let newResult):
|
|
changedTargets = nil
|
|
|
|
// Caches not invalidated:
|
|
// - cachedAdjustedSourceKitOptions: We would not have requested SourceKit options for targets that we didn't
|
|
// know about. Even if we did, the build server now telling us about the target should not change the options of
|
|
// the file within the target
|
|
// - cachedTargetSources: Similar to cachedAdjustedSourceKitOptions, we would not have requested sources for
|
|
// targets that we didn't know about and if we did, they wouldn't be affected
|
|
self.cachedBuildTargets.set(request, to: newResult)
|
|
case .sourceFilesReceivedResultAfterTimeout(let request, let newResult):
|
|
changedTargets = Set(request.targets)
|
|
|
|
// Caches not invalidated:
|
|
// - cachedAdjustedSourceKitOptions: Same as for buildTargetsReceivedResultAfterTimeout.
|
|
// - cachedBuildTargets: Getting a result for the source files in a target doesn't change anything about the
|
|
// target's existence.
|
|
self.cachedTargetSources.set(request, to: newResult)
|
|
}
|
|
// Clear caches that capture global state and are affected by all changes
|
|
self.cachedSourceFilesAndDirectories.clearAll(isolation: self)
|
|
self.scheduleRecomputeCopyFileMap()
|
|
|
|
await delegate?.buildTargetsChanged(changedTargets)
|
|
await filesBuildSettingsChangedDebouncer.scheduleCall(Set(watchedFiles.keys))
|
|
}
|
|
|
|
private func logMessage(notification: OnBuildLogMessageNotification) async {
|
|
await connectionToClient.waitUntilInitialized()
|
|
let type: WindowMessageType =
|
|
switch notification.type {
|
|
case .error: .error
|
|
case .warning: .warning
|
|
case .info: .info
|
|
case .log: .log
|
|
}
|
|
connectionToClient.logMessageToIndexLog(
|
|
message: notification.message,
|
|
type: type,
|
|
structure: notification.lspStructure
|
|
)
|
|
}
|
|
|
|
private func taskStart(notification: TaskStartNotification) async {
|
|
guard let workDoneProgressTitle = WorkDoneProgressTask(fromLSPAny: notification.data)?.title,
|
|
await connectionToClient.clientSupportsWorkDoneProgress
|
|
else {
|
|
return
|
|
}
|
|
|
|
guard workDoneProgressManagers[notification.taskId.id] == nil else {
|
|
logger.error("Client is already tracking a work done progress for task \(notification.taskId.id)")
|
|
return
|
|
}
|
|
workDoneProgressManagers[notification.taskId.id] = WorkDoneProgressManager(
|
|
connectionToClient: connectionToClient,
|
|
waitUntilClientInitialized: connectionToClient.waitUntilInitialized,
|
|
tokenPrefix: notification.taskId.id,
|
|
initialDebounce: options.workDoneProgressDebounceDurationOrDefault,
|
|
title: workDoneProgressTitle
|
|
)
|
|
}
|
|
|
|
private func taskProgress(notification: TaskProgressNotification) async {
|
|
guard let progressManager = workDoneProgressManagers[notification.taskId.id] else {
|
|
return
|
|
}
|
|
let percentage: Int? =
|
|
if let progress = notification.progress, let total = notification.total {
|
|
Int((Double(progress) / Double(total) * 100).rounded())
|
|
} else {
|
|
nil
|
|
}
|
|
await progressManager.update(message: notification.message, percentage: percentage)
|
|
}
|
|
|
|
private func taskFinish(notification: TaskFinishNotification) async {
|
|
guard let progressManager = workDoneProgressManagers[notification.taskId.id] else {
|
|
return
|
|
}
|
|
await progressManager.end()
|
|
workDoneProgressManagers[notification.taskId.id] = nil
|
|
}
|
|
|
|
// MARK: Build server queries
|
|
|
|
/// Returns the toolchain that should be used to process the given target.
|
|
///
|
|
/// If `target` is `nil` or the build server does not explicitly specify a toolchain for this target, the preferred
|
|
/// toolchain for the given language is returned.
|
|
package func toolchain(
|
|
for target: BuildTargetIdentifier?,
|
|
language: Language
|
|
) async -> Toolchain? {
|
|
let toolchainPath = await orLog("Getting toolchain from build targets") { () -> URL? in
|
|
guard let target else {
|
|
return nil
|
|
}
|
|
let targets = try await self.buildTargets()
|
|
guard let target = targets[target]?.target else {
|
|
logger.error("Failed to find target \(target.forLogging) to determine toolchain")
|
|
return nil
|
|
}
|
|
guard let toolchain = target.sourceKitData?.toolchain else {
|
|
return nil
|
|
}
|
|
guard let toolchainUrl = toolchain.fileURL else {
|
|
logger.error("Toolchain is not a file URL")
|
|
return nil
|
|
}
|
|
return toolchainUrl
|
|
}
|
|
if let toolchainPath {
|
|
if let toolchain = await self.toolchainRegistry.toolchain(withPath: toolchainPath) {
|
|
return toolchain
|
|
}
|
|
logger.error("Toolchain at \(toolchainPath) not registered in toolchain registry.")
|
|
}
|
|
|
|
switch language {
|
|
case .swift, .markdown, .tutorial:
|
|
return await toolchainRegistry.preferredToolchain(containing: [\.sourcekitd, \.swift, \.swiftc])
|
|
case .c, .cpp, .objective_c, .objective_cpp:
|
|
return await toolchainRegistry.preferredToolchain(containing: [\.clang, \.clangd])
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Ask the build server if it explicitly specifies a language for this document. Return `nil` if it does not.
|
|
private func languageInferredFromBuildServer(
|
|
for document: DocumentURI,
|
|
in target: BuildTargetIdentifier
|
|
) async throws -> Language? {
|
|
let sourcesItems = try await self.sourceFiles(in: [target])
|
|
let sourceFiles = sourcesItems.flatMap(\.sources)
|
|
var result: Language? = nil
|
|
for sourceFile in sourceFiles where sourceFile.uri == document {
|
|
guard let language = sourceFile.sourceKitData?.language else {
|
|
continue
|
|
}
|
|
if result != nil && result != language {
|
|
logger.error("Conflicting languages for \(document.forLogging) in \(target)")
|
|
return nil
|
|
}
|
|
result = language
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// Returns the language that a document should be interpreted in for background tasks where the editor doesn't
|
|
/// specify the document's language.
|
|
package func defaultLanguage(for document: DocumentURI, in target: BuildTargetIdentifier) async -> Language? {
|
|
let languageFromBuildServer = await orLog("Getting source files to determine default language") {
|
|
try await languageInferredFromBuildServer(for: document, in: target)
|
|
}
|
|
return languageFromBuildServer ?? Language(inferredFromFileExtension: document)
|
|
}
|
|
|
|
/// Returns the language that a document should be interpreted in for background tasks where the editor doesn't
|
|
/// specify the document's language.
|
|
///
|
|
/// If the language could not be determined, this method throws an error.
|
|
package func defaultLanguageInCanonicalTarget(for document: DocumentURI) async throws -> Language {
|
|
struct UnableToInferLanguage: Error, CustomStringConvertible {
|
|
let document: DocumentURI
|
|
var description: String { "Unable to infer language for \(document)" }
|
|
}
|
|
|
|
guard let canonicalTarget = await self.canonicalTarget(for: document) else {
|
|
guard let language = Language(inferredFromFileExtension: document) else {
|
|
throw UnableToInferLanguage(document: document)
|
|
}
|
|
return language
|
|
}
|
|
guard let language = await defaultLanguage(for: document, in: canonicalTarget) else {
|
|
throw UnableToInferLanguage(document: document)
|
|
}
|
|
return language
|
|
}
|
|
|
|
/// Retrieve information about the given source file within the build server.
|
|
package func sourceFileInfo(for document: DocumentURI) async -> SourceFileInfo? {
|
|
return await orLog("Getting targets for source file") {
|
|
var result: SourceFileInfo? = nil
|
|
let filesAndDirectories = try await sourceFilesAndDirectories()
|
|
if let info = filesAndDirectories.files[document] {
|
|
result = result?.merging(info) ?? info
|
|
}
|
|
if !filesAndDirectories.directories.isEmpty, let documentPathComponents = document.fileURL?.pathComponents {
|
|
for (_, (directoryPathComponents, info)) in filesAndDirectories.directories {
|
|
guard let directoryPathComponents else {
|
|
continue
|
|
}
|
|
if isDescendant(documentPathComponents, of: directoryPathComponents) {
|
|
result = result?.merging(info) ?? info
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
/// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to
|
|
/// the original source file.
|
|
package func locationAdjustedForCopiedFiles(_ location: Location) -> Location {
|
|
guard let originalUri = cachedCopiedFileMap[location.uri] else {
|
|
return location
|
|
}
|
|
// If we regularly get issues that the copied file is out-of-sync with its original, we can check that the contents
|
|
// of the lines touched by the location match and only return the original URI if they do. For now, we avoid this
|
|
// check due to its performance cost of reading files from disk.
|
|
return Location(uri: originalUri, range: location.range)
|
|
}
|
|
|
|
/// Check if the URI referenced by `location` has been copied during the preparation phase. If so, adjust the URI to
|
|
/// the original source file.
|
|
package func locationsAdjustedForCopiedFiles(_ locations: [Location]) -> [Location] {
|
|
return locations.map { locationAdjustedForCopiedFiles($0) }
|
|
}
|
|
|
|
private func uriAdjustedForCopiedFiles(_ uri: DocumentURI) -> DocumentURI {
|
|
guard let originalUri = cachedCopiedFileMap[uri] else {
|
|
return uri
|
|
}
|
|
return originalUri
|
|
}
|
|
|
|
package func workspaceEditAdjustedForCopiedFiles(_ workspaceEdit: WorkspaceEdit?) -> WorkspaceEdit? {
|
|
guard var edit = workspaceEdit else {
|
|
return nil
|
|
}
|
|
if let changes = edit.changes {
|
|
var newChanges: [DocumentURI: [TextEdit]] = [:]
|
|
for (uri, edits) in changes {
|
|
let newUri = self.uriAdjustedForCopiedFiles(uri)
|
|
newChanges[newUri, default: []] += edits
|
|
}
|
|
edit.changes = newChanges
|
|
}
|
|
if let documentChanges = edit.documentChanges {
|
|
edit.documentChanges = documentChanges.map { change in
|
|
switch change {
|
|
case .textDocumentEdit(var textEdit):
|
|
textEdit.textDocument.uri = self.uriAdjustedForCopiedFiles(textEdit.textDocument.uri)
|
|
return .textDocumentEdit(textEdit)
|
|
case .createFile(var create):
|
|
create.uri = self.uriAdjustedForCopiedFiles(create.uri)
|
|
return .createFile(create)
|
|
case .renameFile(var rename):
|
|
rename.oldUri = self.uriAdjustedForCopiedFiles(rename.oldUri)
|
|
rename.newUri = self.uriAdjustedForCopiedFiles(rename.newUri)
|
|
return .renameFile(rename)
|
|
case .deleteFile(var delete):
|
|
delete.uri = self.uriAdjustedForCopiedFiles(delete.uri)
|
|
return .deleteFile(delete)
|
|
}
|
|
}
|
|
}
|
|
return edit
|
|
}
|
|
|
|
package func locationsOrLocationLinksAdjustedForCopiedFiles(
|
|
_ response: LocationsOrLocationLinksResponse?
|
|
) -> LocationsOrLocationLinksResponse? {
|
|
guard let response = response else {
|
|
return nil
|
|
}
|
|
switch response {
|
|
case .locations(let locations):
|
|
let remappedLocations = self.locationsAdjustedForCopiedFiles(locations)
|
|
return .locations(remappedLocations)
|
|
case .locationLinks(let locationLinks):
|
|
let remappedLinks = locationLinks.map { link -> LocationLink in
|
|
let adjustedTargetLocation = self.locationAdjustedForCopiedFiles(
|
|
Location(uri: link.targetUri, range: link.targetRange)
|
|
)
|
|
let adjustedTargetSelectionLocation = self.locationAdjustedForCopiedFiles(
|
|
Location(uri: link.targetUri, range: link.targetSelectionRange)
|
|
)
|
|
return LocationLink(
|
|
originSelectionRange: link.originSelectionRange,
|
|
targetUri: adjustedTargetLocation.uri,
|
|
targetRange: adjustedTargetLocation.range,
|
|
targetSelectionRange: adjustedTargetSelectionLocation.range
|
|
)
|
|
}
|
|
return .locationLinks(remappedLinks)
|
|
}
|
|
}
|
|
|
|
package func typeHierarchyItemAdjustedForCopiedFiles(_ item: TypeHierarchyItem) -> TypeHierarchyItem {
|
|
let adjustedLocation = self.locationAdjustedForCopiedFiles(Location(uri: item.uri, range: item.range))
|
|
let adjustedSelectionLocation = self.locationAdjustedForCopiedFiles(
|
|
Location(uri: item.uri, range: item.selectionRange)
|
|
)
|
|
return TypeHierarchyItem(
|
|
name: item.name,
|
|
kind: item.kind,
|
|
tags: item.tags,
|
|
detail: item.detail,
|
|
uri: adjustedLocation.uri,
|
|
range: adjustedLocation.range,
|
|
selectionRange: adjustedSelectionLocation.range,
|
|
data: item.data
|
|
)
|
|
}
|
|
|
|
package func callHierarchyItemAdjustedForCopiedFiles(_ item: CallHierarchyItem) -> CallHierarchyItem {
|
|
let adjustedLocation = self.locationAdjustedForCopiedFiles(Location(uri: item.uri, range: item.range))
|
|
let adjustedSelectionLocation = self.locationAdjustedForCopiedFiles(
|
|
Location(uri: item.uri, range: item.selectionRange)
|
|
)
|
|
return CallHierarchyItem(
|
|
name: item.name,
|
|
kind: item.kind,
|
|
tags: item.tags,
|
|
detail: item.detail,
|
|
uri: adjustedLocation.uri,
|
|
range: adjustedLocation.range,
|
|
selectionRange: adjustedSelectionLocation.range,
|
|
data: .dictionary([
|
|
"usr": item.data.flatMap { data in
|
|
if case let .dictionary(dict) = data {
|
|
return dict["usr"]
|
|
}
|
|
return nil
|
|
} ?? .null,
|
|
"uri": .string(adjustedLocation.uri.stringValue),
|
|
])
|
|
)
|
|
}
|
|
|
|
@discardableResult
|
|
package func scheduleRecomputeCopyFileMap() -> Task<Void, Never> {
|
|
let task = Task { [previousUpdateTask = copiedFileMapUpdateTask] in
|
|
previousUpdateTask?.cancel()
|
|
await orLog("Re-computing copy file map") {
|
|
let sourceFilesAndDirectories = try await self.sourceFilesAndDirectories()
|
|
try Task.checkCancellation()
|
|
var copiedFileMap: [DocumentURI: DocumentURI] = [:]
|
|
for (file, fileInfo) in sourceFilesAndDirectories.files {
|
|
for copyDestination in fileInfo.copyDestinations {
|
|
copiedFileMap[copyDestination] = file
|
|
}
|
|
}
|
|
self.cachedCopiedFileMap = copiedFileMap
|
|
}
|
|
}
|
|
copiedFileMapUpdateTask = task
|
|
return task
|
|
}
|
|
|
|
/// Returns all the targets that the document is part of.
|
|
package func targets(for document: DocumentURI) async -> [BuildTargetIdentifier] {
|
|
guard let targets = await sourceFileInfo(for: document)?.targets else {
|
|
return []
|
|
}
|
|
return Array(targets)
|
|
}
|
|
|
|
/// Returns the `BuildTargetIdentifier` that should be used for semantic functionality of the given document.
|
|
package func canonicalTarget(for document: DocumentURI) async -> BuildTargetIdentifier? {
|
|
// Sort the targets to deterministically pick the same `BuildTargetIdentifier` every time.
|
|
// We could allow the user to specify a preference of one target over another.
|
|
return await targets(for: document)
|
|
.sorted { $0.uri.stringValue < $1.uri.stringValue }
|
|
.first
|
|
}
|
|
|
|
/// Returns the target's module name as parsed from the `BuildTargetIdentifier`'s compiler arguments.
|
|
package func moduleName(for document: DocumentURI, in target: BuildTargetIdentifier) async -> String? {
|
|
guard let language = await self.defaultLanguage(for: document, in: target),
|
|
let buildSettings = await buildSettings(
|
|
for: document,
|
|
in: target,
|
|
language: language,
|
|
fallbackAfterTimeout: false
|
|
)
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
switch language {
|
|
case .swift:
|
|
// Module name is specified in the form -module-name MyLibrary
|
|
guard let moduleNameFlagIndex = buildSettings.compilerArguments.lastIndex(of: "-module-name") else {
|
|
return nil
|
|
}
|
|
return buildSettings.compilerArguments[safe: moduleNameFlagIndex + 1]
|
|
case .objective_c:
|
|
// Specified in the form -fmodule-name=MyLibrary
|
|
guard
|
|
let moduleNameArgument = buildSettings.compilerArguments.last(where: { $0.starts(with: "-fmodule-name=") }),
|
|
let moduleName = moduleNameArgument.split(separator: "=").last
|
|
else {
|
|
return nil
|
|
}
|
|
return String(moduleName)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Returns the build settings for `document` from `buildServer`.
|
|
///
|
|
/// Implementation detail of `buildSettings(for:language:)`.
|
|
private func buildSettingsFromBuildServer(
|
|
for document: DocumentURI,
|
|
in target: BuildTargetIdentifier,
|
|
language: Language
|
|
) async throws -> FileBuildSettings? {
|
|
guard let buildServerAdapter = try await buildServerAdapterAfterInitialized else {
|
|
return nil
|
|
}
|
|
let request = TextDocumentSourceKitOptionsRequest(
|
|
textDocument: TextDocumentIdentifier(document),
|
|
target: target,
|
|
language: language
|
|
)
|
|
let response = try await cachedAdjustedSourceKitOptions.get(request, isolation: self) { request in
|
|
let options = try await buildServerAdapter.send(request)
|
|
switch language.semanticKind {
|
|
case .swift:
|
|
return options?.adjustArgsForSemanticSwiftFunctionality(fileToIndex: document)
|
|
case .clang:
|
|
return options?.adjustingArgsForSemanticClangFunctionality()
|
|
default:
|
|
return options
|
|
}
|
|
}
|
|
|
|
guard let response else {
|
|
return nil
|
|
}
|
|
|
|
return FileBuildSettings(
|
|
compilerArguments: response.compilerArguments,
|
|
workingDirectory: response.workingDirectory,
|
|
language: language,
|
|
data: response.data,
|
|
isFallback: false
|
|
)
|
|
}
|
|
|
|
/// Returns the build settings for the given file in the given target.
|
|
///
|
|
/// Only call this method if it is known that `document` is a main file. Prefer `buildSettingsInferredFromMainFile`
|
|
/// otherwise. If `document` is a header file, this will most likely return fallback settings because header files
|
|
/// don't have build settings by themselves.
|
|
///
|
|
/// If `fallbackAfterTimeout` is true fallback build settings will be returned if no build settings can be found in
|
|
/// `SourceKitLSPOptions.buildSettingsTimeoutOrDefault`.
|
|
package func buildSettings(
|
|
for document: DocumentURI,
|
|
in target: BuildTargetIdentifier,
|
|
language: Language,
|
|
fallbackAfterTimeout: Bool
|
|
) async -> FileBuildSettings? {
|
|
let buildSettingsFromBuildServer = await orLog("Getting build settings") {
|
|
if fallbackAfterTimeout {
|
|
try await withTimeout(options.buildSettingsTimeoutOrDefault) {
|
|
return try await self.buildSettingsFromBuildServer(for: document, in: target, language: language)
|
|
} resultReceivedAfterTimeout: { _ in
|
|
await self.filesBuildSettingsChangedDebouncer.scheduleCall([document])
|
|
}
|
|
} else {
|
|
try await self.buildSettingsFromBuildServer(for: document, in: target, language: language)
|
|
}
|
|
}
|
|
guard let buildSettingsFromBuildServer else {
|
|
return fallbackBuildSettings(
|
|
for: document,
|
|
language: language,
|
|
options: options.fallbackBuildSystemOrDefault
|
|
)
|
|
}
|
|
return buildSettingsFromBuildServer
|
|
|
|
}
|
|
|
|
/// Try finding a source file with the same language as `document` in the same directory as `document` and patch its
|
|
/// build settings to provide more accurate fallback settings than the generic fallback settings.
|
|
private func fallbackBuildSettingsInferredFromSiblingFile(
|
|
of document: DocumentURI,
|
|
target explicitlyRequestedTarget: BuildTargetIdentifier?,
|
|
language: Language?,
|
|
fallbackAfterTimeout: Bool
|
|
) async throws -> FileBuildSettings? {
|
|
guard let documentFileURL = document.fileURL else {
|
|
return nil
|
|
}
|
|
let directory = documentFileURL.deletingLastPathComponent()
|
|
guard let language = language ?? Language(inferredFromFileExtension: document) else {
|
|
return nil
|
|
}
|
|
let siblingFile = try await self.sourceFilesAndDirectories().files.compactMap { (uri, info) -> DocumentURI? in
|
|
guard info.isBuildable, uri.fileURL?.deletingLastPathComponent() == directory else {
|
|
return nil
|
|
}
|
|
if let explicitlyRequestedTarget, !info.targets.contains(explicitlyRequestedTarget) {
|
|
return nil
|
|
}
|
|
// Only consider build settings from sibling files that appear to have the same language. In theory, we might skip
|
|
// valid sibling files because of this since non-standard file extension might be mapped to `language` by the
|
|
// build server, but this is a good first check to avoid requesting build settings for too many documents. And
|
|
// since all of this is fallback-logic, skipping over possibly valid files is not a correctness issue.
|
|
guard let siblingLanguage = Language(inferredFromFileExtension: uri), siblingLanguage == language else {
|
|
return nil
|
|
}
|
|
return uri
|
|
}.sorted(by: { $0.pseudoPath < $1.pseudoPath }).first
|
|
|
|
guard let siblingFile else {
|
|
return nil
|
|
}
|
|
|
|
let siblingSettings = await self.buildSettingsInferredFromMainFile(
|
|
for: siblingFile,
|
|
target: explicitlyRequestedTarget,
|
|
language: language,
|
|
fallbackAfterTimeout: fallbackAfterTimeout,
|
|
allowInferenceFromSiblingFile: false
|
|
)
|
|
guard var siblingSettings, !siblingSettings.isFallback else {
|
|
return nil
|
|
}
|
|
siblingSettings.isFallback = true
|
|
switch language.semanticKind {
|
|
case .swift:
|
|
siblingSettings.compilerArguments += [try documentFileURL.filePath]
|
|
case .clang:
|
|
siblingSettings = siblingSettings.patching(newFile: document, originalFile: siblingFile)
|
|
case nil:
|
|
return nil
|
|
}
|
|
return siblingSettings
|
|
}
|
|
|
|
/// Returns the build settings for the given document.
|
|
///
|
|
/// If the document doesn't have builds settings by itself, eg. because it is a C header file, the build settings will
|
|
/// be inferred from the primary main file of the document. In practice this means that we will compute the build
|
|
/// settings of a C file that includes the header and replace any file references to that C file in the build settings
|
|
/// by the header file.
|
|
///
|
|
/// When a target is passed in, the build settings for the document, interpreted as part of that target, are returned,
|
|
/// otherwise a canonical target is inferred for the source file.
|
|
///
|
|
/// If no language is passed, this method tries to infer the language of the document from the build server. If that
|
|
/// fails, it returns `nil`.
|
|
package func buildSettingsInferredFromMainFile(
|
|
for document: DocumentURI,
|
|
target explicitlyRequestedTarget: BuildTargetIdentifier? = nil,
|
|
language: Language?,
|
|
fallbackAfterTimeout: Bool,
|
|
allowInferenceFromSiblingFile: Bool = true
|
|
) async -> FileBuildSettings? {
|
|
if buildServerAdapter == nil {
|
|
guard let language = language ?? Language(inferredFromFileExtension: document) else {
|
|
return nil
|
|
}
|
|
guard
|
|
var settings = fallbackBuildSettings(
|
|
for: document,
|
|
language: language,
|
|
options: options.fallbackBuildSystemOrDefault
|
|
)
|
|
else {
|
|
return nil
|
|
}
|
|
// If there is no build server and we only have the fallback build server, we will never get real build settings.
|
|
// Consider the build settings non-fallback.
|
|
settings.isFallback = false
|
|
return settings
|
|
}
|
|
|
|
func mainFileAndSettings(
|
|
basedOn document: DocumentURI
|
|
) async -> (mainFile: DocumentURI, settings: FileBuildSettings)? {
|
|
let mainFile = await self.mainFile(for: document, language: language)
|
|
let settings: FileBuildSettings? = await orLog("Getting build settings") { () -> FileBuildSettings? in
|
|
let target: WithTimeoutResult<BuildTargetIdentifier?> =
|
|
if let explicitlyRequestedTarget {
|
|
.result(explicitlyRequestedTarget)
|
|
} else {
|
|
try await withTimeoutResult(options.buildSettingsTimeoutOrDefault) {
|
|
return await self.canonicalTarget(for: mainFile)
|
|
} resultReceivedAfterTimeout: { _ in
|
|
await self.filesBuildSettingsChangedDebouncer.scheduleCall([document])
|
|
}
|
|
}
|
|
var languageForFile: Language
|
|
if let language {
|
|
languageForFile = language
|
|
} else if case let .result(target?) = target,
|
|
let language = await self.defaultLanguage(for: mainFile, in: target)
|
|
{
|
|
languageForFile = language
|
|
} else if let language = Language(inferredFromFileExtension: mainFile) {
|
|
languageForFile = language
|
|
} else {
|
|
// We don't know the language as which to interpret the document, so we can't ask the build server for its
|
|
// settings.
|
|
return nil
|
|
}
|
|
switch target {
|
|
case .result(let target?):
|
|
return await self.buildSettings(
|
|
for: mainFile,
|
|
in: target,
|
|
language: languageForFile,
|
|
fallbackAfterTimeout: fallbackAfterTimeout
|
|
)
|
|
case .result(nil):
|
|
if allowInferenceFromSiblingFile {
|
|
let settingsFromSibling = await orLog("Inferring build settings from sibling file") {
|
|
try await self.fallbackBuildSettingsInferredFromSiblingFile(
|
|
of: document,
|
|
target: explicitlyRequestedTarget,
|
|
language: language,
|
|
fallbackAfterTimeout: fallbackAfterTimeout
|
|
)
|
|
}
|
|
if let settingsFromSibling {
|
|
return settingsFromSibling
|
|
}
|
|
}
|
|
fallthrough
|
|
case .timedOut:
|
|
// If we timed out, we don't want to try inferring the build settings from a sibling since that would kick off
|
|
// new requests to the build server, which will likely also time out.
|
|
return fallbackBuildSettings(
|
|
for: document,
|
|
language: languageForFile,
|
|
options: options.fallbackBuildSystemOrDefault
|
|
)
|
|
}
|
|
}
|
|
guard let settings else {
|
|
return nil
|
|
}
|
|
return (mainFile, settings)
|
|
}
|
|
|
|
var settings: FileBuildSettings?
|
|
var mainFile: DocumentURI?
|
|
if let mainFileAndSettings = await mainFileAndSettings(basedOn: document) {
|
|
(mainFile, settings) = mainFileAndSettings
|
|
}
|
|
if settings?.isFallback ?? true, let symlinkTarget = document.symlinkTarget,
|
|
let mainFileAndSettings = await mainFileAndSettings(basedOn: symlinkTarget)
|
|
{
|
|
(mainFile, settings) = mainFileAndSettings
|
|
}
|
|
guard var settings, let mainFile else {
|
|
return nil
|
|
}
|
|
|
|
if mainFile != document {
|
|
// If the main file isn't the file itself, we need to patch the build settings
|
|
// to reference `document` instead of `mainFile`.
|
|
settings = settings.patching(newFile: document, originalFile: mainFile)
|
|
}
|
|
|
|
await buildSettingsLogger.log(settings: settings, for: document)
|
|
return settings
|
|
}
|
|
|
|
package func waitForUpToDateBuildGraph() async {
|
|
await orLog("Waiting for build server updates") {
|
|
let _: VoidResponse? = try await buildServerAdapterAfterInitialized?.send(
|
|
WorkspaceWaitForBuildSystemUpdatesRequest()
|
|
)
|
|
}
|
|
// Handle any messages the build server might have sent us while updating.
|
|
await messageHandlingQueue.async(metadata: .stateChange) {}.valuePropagatingCancellation
|
|
|
|
// Ensure that we send out all delegate calls so that everybody is informed about the changes.
|
|
await filesBuildSettingsChangedDebouncer.flush()
|
|
await filesDependenciesUpdatedDebouncer.flush()
|
|
}
|
|
|
|
/// The root targets of the project have depth of 0 and all target dependencies have a greater depth than the target
|
|
/// itself.
|
|
private func targetDepthsAndDependents(
|
|
for buildTargets: [BuildTarget]
|
|
) -> (depths: [BuildTargetIdentifier: Int], dependents: [BuildTargetIdentifier: Set<BuildTargetIdentifier>]) {
|
|
var nonRoots: Set<BuildTargetIdentifier> = []
|
|
for buildTarget in buildTargets {
|
|
nonRoots.formUnion(buildTarget.dependencies)
|
|
}
|
|
let targetsById = Dictionary(elements: buildTargets, keyedBy: \.id)
|
|
var dependents: [BuildTargetIdentifier: Set<BuildTargetIdentifier>] = [:]
|
|
var depths: [BuildTargetIdentifier: Int] = [:]
|
|
let rootTargets = buildTargets.filter { !nonRoots.contains($0.id) }
|
|
var worksList: [(target: BuildTargetIdentifier, depth: Int)] = rootTargets.map { ($0.id, 0) }
|
|
while let (target, depth) = worksList.popLast() {
|
|
depths[target] = max(depths[target, default: 0], depth)
|
|
for dependency in targetsById[target]?.dependencies ?? [] {
|
|
dependents[dependency, default: []].insert(target)
|
|
// Check if we have already recorded this target with a greater depth, in which case visiting it again will
|
|
// not increase its depth or any of its children.
|
|
if depths[dependency, default: 0] < depth + 1 {
|
|
worksList.append((dependency, depth + 1))
|
|
}
|
|
}
|
|
}
|
|
return (depths, dependents)
|
|
}
|
|
|
|
/// Sort the targets so that low-level targets occur before high-level targets.
|
|
///
|
|
/// This sorting is best effort but allows the indexer to prepare and index low-level targets first, which allows
|
|
/// index data to be available earlier.
|
|
package func topologicalSort(of targets: [BuildTargetIdentifier]) async throws -> [BuildTargetIdentifier] {
|
|
guard let buildTargets = await orLog("Getting build targets for topological sort", { try await buildTargets() })
|
|
else {
|
|
return targets.sorted { $0.uri.stringValue < $1.uri.stringValue }
|
|
}
|
|
|
|
return targets.sorted { (lhs: BuildTargetIdentifier, rhs: BuildTargetIdentifier) -> Bool in
|
|
let lhsDepth = buildTargets[lhs]?.depth ?? 0
|
|
let rhsDepth = buildTargets[rhs]?.depth ?? 0
|
|
if lhsDepth != rhsDepth {
|
|
return lhsDepth > rhsDepth
|
|
}
|
|
return lhs.uri.stringValue < rhs.uri.stringValue
|
|
}
|
|
}
|
|
|
|
/// Returns the list of targets that might depend on the given target and that need to be re-prepared when a file in
|
|
/// `target` is modified.
|
|
package func targets(dependingOn targetIds: some Collection<BuildTargetIdentifier>) async -> [BuildTargetIdentifier] {
|
|
guard
|
|
let buildTargets = await orLog("Getting build targets for dependents", { try await self.buildTargets() })
|
|
else {
|
|
return []
|
|
}
|
|
|
|
return transitiveClosure(of: targetIds, successors: { buildTargets[$0]?.dependents ?? [] })
|
|
.sorted { $0.uri.stringValue < $1.uri.stringValue }
|
|
}
|
|
|
|
package func prepare(targets: Set<BuildTargetIdentifier>) async throws {
|
|
let _: VoidResponse? = try await buildServerAdapterAfterInitialized?.send(
|
|
BuildTargetPrepareRequest(targets: targets.sorted { $0.uri.stringValue < $1.uri.stringValue })
|
|
)
|
|
await orLog("Calling fileDependenciesUpdated") {
|
|
let filesInPreparedTargets = try await self.sourceFiles(in: targets).flatMap(\.sources).map(\.uri)
|
|
await filesDependenciesUpdatedDebouncer.scheduleCall(Set(filesInPreparedTargets))
|
|
}
|
|
}
|
|
|
|
package func registerForChangeNotifications(for uri: DocumentURI, language: Language) async {
|
|
let mainFile = await mainFile(for: uri, language: language)
|
|
self.watchedFiles[uri] = (mainFile, language)
|
|
}
|
|
|
|
package func unregisterForChangeNotifications(for uri: DocumentURI) async {
|
|
self.watchedFiles[uri] = nil
|
|
}
|
|
|
|
private func buildTargets() async throws -> [BuildTargetIdentifier: BuildTargetInfo] {
|
|
let request = WorkspaceBuildTargetsRequest()
|
|
let result = try await cachedBuildTargets.get(request, isolation: self) { request in
|
|
let result = try await withTimeout(self.options.buildServerWorkspaceRequestsTimeoutOrDefault) {
|
|
guard let buildServerAdapter = try await self.buildServerAdapterAfterInitialized else {
|
|
return [:]
|
|
}
|
|
let buildTargets = try await buildServerAdapter.send(request).targets
|
|
let (depths, dependents) = await self.targetDepthsAndDependents(for: buildTargets)
|
|
var result: [BuildTargetIdentifier: BuildTargetInfo] = [:]
|
|
result.reserveCapacity(buildTargets.count)
|
|
for buildTarget in buildTargets {
|
|
guard result[buildTarget.id] == nil else {
|
|
logger.error("Found two targets with the same ID \(buildTarget.id)")
|
|
continue
|
|
}
|
|
let depth: Int
|
|
if let d = depths[buildTarget.id] {
|
|
depth = d
|
|
} else {
|
|
logger.fault("Did not compute depth for target \(buildTarget.id)")
|
|
depth = 0
|
|
}
|
|
result[buildTarget.id] = BuildTargetInfo(
|
|
target: buildTarget,
|
|
depth: depth,
|
|
dependents: dependents[buildTarget.id] ?? []
|
|
)
|
|
}
|
|
return result
|
|
} resultReceivedAfterTimeout: { newResult in
|
|
await self.buildTargetsDidChange(
|
|
.buildTargetsReceivedResultAfterTimeout(request: request, newResult: newResult)
|
|
)
|
|
}
|
|
guard let result else {
|
|
logger.error("Failed to get targets of workspace within timeout")
|
|
return [:]
|
|
}
|
|
return result
|
|
}
|
|
return result
|
|
}
|
|
|
|
package func buildTarget(named identifier: BuildTargetIdentifier) async -> BuildTarget? {
|
|
return await orLog("Getting built target with ID") {
|
|
try await buildTargets()[identifier]?.target
|
|
}
|
|
}
|
|
|
|
package func sourceFiles(in targets: Set<BuildTargetIdentifier>) async throws -> [SourcesItem] {
|
|
guard !targets.isEmpty else {
|
|
return []
|
|
}
|
|
|
|
let request = BuildTargetSourcesRequest(targets: targets.sorted { $0.uri.stringValue < $1.uri.stringValue })
|
|
|
|
// If we have a cached request for a superset of the targets, serve the result from that cache entry.
|
|
let fromSuperset = await orLog("Getting source files from superset request") {
|
|
try await cachedTargetSources.getDerived(
|
|
isolation: self,
|
|
request,
|
|
canReuseKey: { targets.isSubset(of: $0.targets) },
|
|
transform: { BuildTargetSourcesResponse(items: $0.items.filter { targets.contains($0.target) }) }
|
|
)
|
|
}
|
|
if let fromSuperset {
|
|
return fromSuperset.items
|
|
}
|
|
|
|
let response = try await cachedTargetSources.get(request, isolation: self) { request in
|
|
try await withTimeout(self.options.buildServerWorkspaceRequestsTimeoutOrDefault) {
|
|
guard let buildServerAdapter = try await self.buildServerAdapterAfterInitialized else {
|
|
return BuildTargetSourcesResponse(items: [])
|
|
}
|
|
return try await buildServerAdapter.send(request)
|
|
} resultReceivedAfterTimeout: { newResult in
|
|
await self.buildTargetsDidChange(.sourceFilesReceivedResultAfterTimeout(request: request, newResult: newResult))
|
|
} ?? BuildTargetSourcesResponse(items: [])
|
|
}
|
|
return response.items
|
|
}
|
|
|
|
/// Return the output paths for all source files known to the build server.
|
|
///
|
|
/// See `SourceKitSourceItemData.outputFilePath` for details.
|
|
package func outputPathsInAllTargets() async throws -> [String] {
|
|
return try await outputPaths(in: Set(buildTargets().map(\.key)))
|
|
}
|
|
|
|
/// For all source files in the given targets, return their output file paths.
|
|
///
|
|
/// See `BuildTargetOutputPathsRequest` for details.
|
|
package func outputPaths(in targets: Set<BuildTargetIdentifier>) async throws -> [String] {
|
|
return try await sourceFiles(in: targets).flatMap(\.sources).compactMap(\.sourceKitData?.outputPath)
|
|
}
|
|
|
|
/// Returns all source files in the project.
|
|
///
|
|
/// - SeeAlso: Comment in `sourceFilesAndDirectories` for a definition of what `buildable` means.
|
|
package func sourceFiles(includeNonBuildableFiles: Bool) async throws -> [DocumentURI: SourceFileInfo] {
|
|
let files = try await sourceFilesAndDirectories().files
|
|
if includeNonBuildableFiles {
|
|
return files
|
|
} else {
|
|
return files.filter(\.value.isBuildable)
|
|
}
|
|
}
|
|
|
|
/// Returns all source files in the project that are considered buildable.
|
|
///
|
|
/// - SeeAlso: Comment in `sourceFilesAndDirectories` for a definition of what `buildable` means.
|
|
package func buildableSourceFiles() async throws -> Set<DocumentURI> {
|
|
return try await sourceFilesAndDirectories().buildableSourceFiles
|
|
}
|
|
|
|
/// Get all files and directories that are known to the build server, ie. that are returned by a `buildTarget/sources`
|
|
/// request for any target in the project.
|
|
///
|
|
/// - Important: This method returns both buildable and non-buildable source files. Callers need to check
|
|
/// `SourceFileInfo.isBuildable` if they are only interested in buildable source files.
|
|
private func sourceFilesAndDirectories() async throws -> SourceFilesAndDirectories {
|
|
|
|
return try await cachedSourceFilesAndDirectories.get(
|
|
SourceFilesAndDirectoriesKey(),
|
|
isolation: self
|
|
) { key in
|
|
let targets = try await self.buildTargets()
|
|
let sourcesItems = try await self.sourceFiles(in: Set(targets.keys))
|
|
|
|
var files: [DocumentURI: SourceFileInfo] = [:]
|
|
var directories: [DocumentURI: (pathComponents: [String]?, info: SourceFileInfo)] = [:]
|
|
for sourcesItem in sourcesItems {
|
|
let target = targets[sourcesItem.target]?.target
|
|
let isPartOfRootProject = !(target?.tags.contains(.dependency) ?? false)
|
|
let mayContainTests = target?.tags.contains(.test) ?? true
|
|
for sourceItem in sourcesItem.sources {
|
|
let sourceKitData = sourceItem.sourceKitData
|
|
let outputPath: OutputPath? =
|
|
if !(await self.initializationData?.outputPathsProvider ?? false) {
|
|
.notSupported
|
|
} else if let outputPath = sourceKitData?.outputPath {
|
|
.path(outputPath)
|
|
} else {
|
|
nil
|
|
}
|
|
let info = SourceFileInfo(
|
|
targetsToOutputPath: [sourcesItem.target: outputPath],
|
|
isPartOfRootProject: isPartOfRootProject,
|
|
mayContainTests: mayContainTests,
|
|
isBuildable: !(target?.tags.contains(.notBuildable) ?? false)
|
|
&& (sourceKitData?.kind ?? .source) == .source,
|
|
copyDestinations: Set(sourceKitData?.copyDestinations ?? [])
|
|
)
|
|
switch sourceItem.kind {
|
|
case .file:
|
|
files[sourceItem.uri] = info.merging(files[sourceItem.uri])
|
|
case .directory:
|
|
directories[sourceItem.uri] = (
|
|
sourceItem.uri.fileURL?.pathComponents, info.merging(directories[sourceItem.uri]?.info)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
return SourceFilesAndDirectories(files: files, directories: directories)
|
|
}
|
|
}
|
|
|
|
package func projectTestFiles() async throws -> [DocumentURI] {
|
|
return try await sourceFiles(includeNonBuildableFiles: false).compactMap { (uri, info) -> DocumentURI? in
|
|
guard info.isPartOfRootProject, info.mayContainTests else {
|
|
return nil
|
|
}
|
|
return uri
|
|
}
|
|
}
|
|
|
|
/// Differs from `sourceFiles(in targets: Set<BuildTargetIdentifier>)` making sure it only includes source files that
|
|
/// are part of the root project for cases where we don't care about dependency source files
|
|
///
|
|
/// - Parameter include: If `nil` will include all targets, otherwise only return files who are part of at least one matching target
|
|
/// - Returns: List of filtered source files in root project
|
|
package func projectSourceFiles(
|
|
in include: Set<BuildTargetIdentifier>? = nil
|
|
) async throws -> [DocumentURI: SourceFileInfo] {
|
|
return try await sourceFiles(includeNonBuildableFiles: false).filter { (uri, info) -> Bool in
|
|
var includeTarget = true
|
|
if let include {
|
|
includeTarget = info.targets.contains(anyIn: include)
|
|
}
|
|
guard info.isPartOfRootProject, includeTarget else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private func watchedFilesReferencing(mainFiles: Set<DocumentURI>) -> Set<DocumentURI> {
|
|
return Set(
|
|
watchedFiles.compactMap { (watchedFile, mainFileAndLanguage) in
|
|
if mainFiles.contains(mainFileAndLanguage.mainFile) {
|
|
return watchedFile
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
/// Return the main file that should be used to get build settings for `uri`.
|
|
///
|
|
/// For Swift or normal C files, this will be the file itself. For header files, we pick a main file that includes the
|
|
/// header since header files don't have build settings by themselves.
|
|
///
|
|
/// `language` is a hint of the document's language to speed up the `main` file lookup. Passing `nil` if the language
|
|
/// is unknown should always be safe.
|
|
package func mainFile(for uri: DocumentURI, language: Language?, useCache: Bool = true) async -> DocumentURI {
|
|
if language == .swift {
|
|
// Swift doesn't have main files. Skip the main file provider query.
|
|
return uri
|
|
}
|
|
if useCache, let mainFile = self.watchedFiles[uri]?.mainFile {
|
|
// Performance optimization: We did already compute the main file and have
|
|
// it cached. We can just return it.
|
|
return mainFile
|
|
}
|
|
|
|
let mainFiles = await mainFiles(containing: uri)
|
|
if mainFiles.contains(uri) {
|
|
// If the main files contain the file itself, prefer to use that one
|
|
return uri
|
|
} else if let mainFile = mainFiles.min(by: { $0.pseudoPath < $1.pseudoPath }) {
|
|
// Pick the lexicographically first main file if it exists.
|
|
// This makes sure that picking a main file is deterministic.
|
|
return mainFile
|
|
} else {
|
|
return uri
|
|
}
|
|
}
|
|
|
|
/// Returns all main files that include the given document.
|
|
///
|
|
/// On Darwin platforms, this also performs the following normalization: indexstore-db by itself returns realpaths
|
|
/// but the build server might be using standardized Darwin paths (eg. realpath is `/private/tmp` but the standardized
|
|
/// path is `/tmp`). If the realpath that indexstore-db returns could not be found in the build server's source files
|
|
/// but the standardized path is part of the source files, return the standardized path instead.
|
|
package func mainFiles(containing uri: DocumentURI) async -> [DocumentURI] {
|
|
guard let mainFilesProvider = await mainFilesProvider.value else {
|
|
return [uri]
|
|
}
|
|
let mainFiles = Array(await mainFilesProvider.mainFiles(containing: uri, crossLanguage: false))
|
|
if Platform.current == .darwin {
|
|
if let buildableSourceFiles = try? await self.buildableSourceFiles() {
|
|
return mainFiles.map { mainFile in
|
|
if mainFile == uri {
|
|
// Do not apply the standardized file normalization to the source file itself. Otherwise we would get the
|
|
// following behavior:
|
|
// - We have a build server that uses standardized file paths and index a file as /tmp/test.c
|
|
// - We are asking for the main files of /private/tmp/test.c
|
|
// - Since indexstore-db uses realpath for everything, we find the unit for /tmp/test.c as a unit containg
|
|
// /private/tmp/test.c, which has /private/tmp/test.c as the main file.
|
|
// - If we applied the path normalization, we would normalize /private/tmp/test.c to /tmp/test.c, thus
|
|
// reporting that /tmp/test.c is a main file containing /private/tmp/test.c,
|
|
// But that doesn't make sense (it would, in fact cause us to treat /private/tmp/test.c as a header file that
|
|
// we should index using /tmp/test.c as a main file.
|
|
return mainFile
|
|
}
|
|
if buildableSourceFiles.contains(mainFile) {
|
|
return mainFile
|
|
}
|
|
guard let fileURL = mainFile.fileURL else {
|
|
return mainFile
|
|
}
|
|
let standardized = DocumentURI(fileURL.standardizedFileURL)
|
|
if buildableSourceFiles.contains(standardized) {
|
|
return standardized
|
|
}
|
|
return mainFile
|
|
}
|
|
}
|
|
}
|
|
return mainFiles
|
|
}
|
|
|
|
/// Returns the main file used for `uri`, if this is a registered file.
|
|
///
|
|
/// For testing purposes only.
|
|
package func cachedMainFile(for uri: DocumentURI) -> DocumentURI? {
|
|
return self.watchedFiles[uri]?.mainFile
|
|
}
|
|
|
|
// MARK: Informing BuildSererManager about changes
|
|
|
|
package func filesDidChange(_ events: [FileEvent]) async {
|
|
if let buildServerAdapter = try? await buildServerAdapterAfterInitialized {
|
|
await buildServerAdapter.send(OnWatchedFilesDidChangeNotification(changes: events))
|
|
}
|
|
|
|
var targetsWithUpdatedDependencies: Set<BuildTargetIdentifier> = []
|
|
// If a Swift file within a target is updated, reload all the other files within the target since they might be
|
|
// referring to a function in the updated file.
|
|
let targetsWithChangedSwiftFiles =
|
|
await events
|
|
.filter { Language(inferredFromFileExtension: $0.uri) == .swift }
|
|
.asyncFlatMap { await self.targets(for: $0.uri) }
|
|
targetsWithUpdatedDependencies.formUnion(targetsWithChangedSwiftFiles)
|
|
|
|
// If a `.swiftmodule` file is updated, this means that we have performed a build / are
|
|
// performing a build and files that depend on this module have updated dependencies.
|
|
// We don't have access to the build graph from the SwiftPM API offered to SourceKit-LSP to figure out which files
|
|
// depend on the updated module, so assume that all files have updated dependencies.
|
|
// The file watching here is somewhat fragile as well because it assumes that the `.swiftmodule` files are being
|
|
// written to a directory within the project root. This is not necessarily true if the user specifies a build
|
|
// directory outside the source tree.
|
|
// If we have background indexing enabled, this is not necessary because we call `fileDependenciesUpdated` when
|
|
// preparation of a target finishes.
|
|
if !options.backgroundIndexingOrDefault,
|
|
events.contains(where: { $0.uri.fileURL?.pathExtension == "swiftmodule" })
|
|
{
|
|
await orLog("Getting build targets") {
|
|
targetsWithUpdatedDependencies.formUnion(try await self.buildTargets().keys)
|
|
}
|
|
}
|
|
|
|
var filesWithUpdatedDependencies: Set<DocumentURI> = []
|
|
|
|
await orLog("Getting source files in targets") {
|
|
let sourceFiles = try await self.sourceFiles(in: Set(targetsWithUpdatedDependencies))
|
|
filesWithUpdatedDependencies.formUnion(sourceFiles.flatMap(\.sources).map(\.uri))
|
|
}
|
|
|
|
var mainFiles = await Set(events.asyncFlatMap { await self.mainFiles(containing: $0.uri) })
|
|
mainFiles.subtract(events.map(\.uri))
|
|
filesWithUpdatedDependencies.formUnion(mainFiles)
|
|
|
|
await self.filesDependenciesUpdatedDebouncer.scheduleCall(filesWithUpdatedDependencies)
|
|
}
|
|
|
|
/// Checks if there are any files in `mainFileAssociations` where the main file
|
|
/// that we have stored has changed.
|
|
///
|
|
/// For all of these files, re-associate the file with the new main file and
|
|
/// inform the delegate that the build settings for it might have changed.
|
|
package func mainFilesChanged() async {
|
|
var changedMainFileAssociations: Set<DocumentURI> = []
|
|
for (file, (oldMainFile, language)) in self.watchedFiles {
|
|
let newMainFile = await self.mainFile(for: file, language: language, useCache: false)
|
|
if newMainFile != oldMainFile {
|
|
self.watchedFiles[file] = (newMainFile, language)
|
|
changedMainFileAssociations.insert(file)
|
|
}
|
|
}
|
|
|
|
for file in changedMainFileAssociations {
|
|
guard let language = watchedFiles[file]?.language else {
|
|
continue
|
|
}
|
|
// Re-register for notifications of this file within the build server.
|
|
// This is the easiest way to make sure we are watching for build setting
|
|
// changes of the new main file and stop watching for build setting
|
|
// changes in the old main file if no other watched file depends on it.
|
|
await self.unregisterForChangeNotifications(for: file)
|
|
await self.registerForChangeNotifications(for: file, language: language)
|
|
}
|
|
|
|
if let delegate, !changedMainFileAssociations.isEmpty {
|
|
await delegate.fileBuildSettingsChanged(changedMainFileAssociations)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the path components `selfPathComponents`, retrieved from `URL.pathComponents` are a descendent
|
|
/// of the other path components.
|
|
///
|
|
/// This operates directly on path components instead of `URL`s because computing the path components of a URL is
|
|
/// expensive and this allows us to cache the path components.
|
|
private func isDescendant(_ selfPathComponents: [String], of otherPathComponents: [String]) -> Bool {
|
|
return selfPathComponents.dropLast().starts(with: otherPathComponents)
|
|
}
|
|
|
|
fileprivate extension TextDocumentSourceKitOptionsResponse {
|
|
/// Adjust compiler arguments that were created for building to compiler arguments that should be used for indexing
|
|
/// or background AST builds.
|
|
///
|
|
/// This removes compiler arguments that produce output files and adds arguments to eg. allow errors and index the
|
|
/// file.
|
|
func adjustArgsForSemanticSwiftFunctionality(fileToIndex: DocumentURI) -> TextDocumentSourceKitOptionsResponse {
|
|
// Technically, `-o` and the output file don't need to be separated by a space. Eg. `swiftc -oa file.swift` is
|
|
// valid and will write to an output file named `a`.
|
|
// We can't support that because the only way to know that `-output-file-map` is a different flag and not an option
|
|
// to write to an output file named `utput-file-map` is to know all compiler arguments of `swiftc`, which we don't.
|
|
let outputPathOption = CompilerCommandLineOption.option("o", [.singleDash], [.separatedBySpace])
|
|
|
|
let indexUnitOutputPathOption =
|
|
CompilerCommandLineOption.option("index-unit-output-path", [.singleDash], [.separatedBySpace])
|
|
|
|
let optionsToRemove: [CompilerCommandLineOption] = [
|
|
.flag("c", [.singleDash]),
|
|
.flag("disable-cmo", [.singleDash]),
|
|
.flag("emit-dependencies", [.singleDash]),
|
|
.flag("emit-module-interface", [.singleDash]),
|
|
.flag("emit-module", [.singleDash]),
|
|
.flag("emit-objc-header", [.singleDash]),
|
|
.flag("incremental", [.singleDash]),
|
|
.flag("no-color-diagnostics", [.singleDash]),
|
|
.flag("parseable-output", [.singleDash]),
|
|
.flag("save-temps", [.singleDash]),
|
|
.flag("serialize-diagnostics", [.singleDash]),
|
|
.flag("use-frontend-parseable-output", [.singleDash]),
|
|
.flag("validate-clang-modules-once", [.singleDash]),
|
|
.flag("whole-module-optimization", [.singleDash]),
|
|
.flag("experimental-skip-all-function-bodies", frontendName: "Xfrontend", [.singleDash]),
|
|
.flag("experimental-skip-non-inlinable-function-bodies", frontendName: "Xfrontend", [.singleDash]),
|
|
.flag("experimental-skip-non-exportable-decls", frontendName: "Xfrontend", [.singleDash]),
|
|
.flag("experimental-lazy-typecheck", frontendName: "Xfrontend", [.singleDash]),
|
|
|
|
.option("clang-build-session-file", [.singleDash], [.separatedBySpace]),
|
|
.option("emit-module-interface-path", [.singleDash], [.separatedBySpace]),
|
|
.option("emit-module-path", [.singleDash], [.separatedBySpace]),
|
|
.option("emit-objc-header-path", [.singleDash], [.separatedBySpace]),
|
|
.option("emit-package-module-interface-path", [.singleDash], [.separatedBySpace]),
|
|
.option("emit-private-module-interface-path", [.singleDash], [.separatedBySpace]),
|
|
.option("num-threads", [.singleDash], [.separatedBySpace]),
|
|
outputPathOption,
|
|
.option("output-file-map", [.singleDash], [.separatedBySpace, .separatedByEqualSign]),
|
|
]
|
|
|
|
var result: [String] = []
|
|
result.reserveCapacity(compilerArguments.count)
|
|
var iterator = compilerArguments.makeIterator()
|
|
while let argument = iterator.next() {
|
|
switch optionsToRemove.firstMatch(for: argument) {
|
|
case .removeOption:
|
|
continue
|
|
case .removeOptionAndNextArgument:
|
|
_ = iterator.next()
|
|
continue
|
|
case .removeOptionAndPreviousArgument(let name):
|
|
if let previousArg = result.last, previousArg.hasSuffix("-\(name)") {
|
|
_ = result.popLast()
|
|
}
|
|
continue
|
|
case nil:
|
|
break
|
|
}
|
|
result.append(argument)
|
|
}
|
|
|
|
result += [
|
|
// Avoid emitting the ABI descriptor, we don't need it
|
|
"-Xfrontend", "-empty-abi-descriptor",
|
|
]
|
|
|
|
result += supplementalClangIndexingArgs.flatMap { ["-Xcc", $0] }
|
|
|
|
if let outputPathIndex = compilerArguments.lastIndex(where: { outputPathOption.matches(argument: $0) != nil }),
|
|
compilerArguments.allSatisfy({ indexUnitOutputPathOption.matches(argument: $0) == nil }),
|
|
outputPathIndex + 1 < compilerArguments.count
|
|
{
|
|
// The original compiler arguments contained `-o` to specify the output file but we have stripped that away.
|
|
// Re-introduce the output path as `-index-unit-output-path` so that we have an output path for the unit file.
|
|
result += ["-index-unit-output-path", compilerArguments[outputPathIndex + 1]]
|
|
}
|
|
|
|
var adjusted = self
|
|
adjusted.compilerArguments = result
|
|
return adjusted
|
|
}
|
|
|
|
/// Adjust compiler arguments that were created for building to compiler arguments that should be used for indexing
|
|
/// or background AST builds.
|
|
///
|
|
/// This removes compiler arguments that produce output files and adds arguments to eg. typecheck only.
|
|
func adjustingArgsForSemanticClangFunctionality() -> TextDocumentSourceKitOptionsResponse {
|
|
let optionsToRemove: [CompilerCommandLineOption] = [
|
|
// Disable writing of a depfile
|
|
.flag("M", [.singleDash]),
|
|
.flag("MD", [.singleDash]),
|
|
.flag("MMD", [.singleDash]),
|
|
.flag("MG", [.singleDash]),
|
|
.flag("MM", [.singleDash]),
|
|
.flag("MV", [.singleDash]),
|
|
// Don't create phony targets
|
|
.flag("MP", [.singleDash]),
|
|
// Don't write out compilation databases
|
|
.flag("MJ", [.singleDash]),
|
|
// Don't compile
|
|
.flag("c", [.singleDash]),
|
|
|
|
.flag("fmodules-validate-once-per-build-session", [.singleDash]),
|
|
|
|
// Disable writing of a depfile
|
|
.option("MT", [.singleDash], [.noSpace, .separatedBySpace]),
|
|
.option("MF", [.singleDash], [.noSpace, .separatedBySpace]),
|
|
.option("MQ", [.singleDash], [.noSpace, .separatedBySpace]),
|
|
|
|
// Don't write serialized diagnostic files
|
|
.option("serialize-diagnostics", [.singleDash, .doubleDash], [.separatedBySpace]),
|
|
|
|
.option("fbuild-session-file", [.singleDash], [.separatedByEqualSign]),
|
|
]
|
|
|
|
var result: [String] = []
|
|
result.reserveCapacity(compilerArguments.count)
|
|
var iterator = compilerArguments.makeIterator()
|
|
while let argument = iterator.next() {
|
|
switch optionsToRemove.firstMatch(for: argument) {
|
|
case .removeOption:
|
|
continue
|
|
case .removeOptionAndNextArgument:
|
|
_ = iterator.next()
|
|
continue
|
|
case .removeOptionAndPreviousArgument(let name):
|
|
if let previousArg = result.last, previousArg.hasSuffix("-\(name)") {
|
|
_ = result.popLast()
|
|
}
|
|
continue
|
|
case nil:
|
|
break
|
|
}
|
|
result.append(argument)
|
|
}
|
|
result += supplementalClangIndexingArgs
|
|
result.append(
|
|
"-fsyntax-only"
|
|
)
|
|
|
|
var adjusted = self
|
|
adjusted.compilerArguments = result
|
|
return adjusted
|
|
}
|
|
}
|
|
|
|
private let supplementalClangIndexingArgs: [String] = [
|
|
// Retain extra information for indexing
|
|
"-fretain-comments-from-system-headers",
|
|
// Pick up macro definitions during indexing
|
|
"-Xclang", "-detailed-preprocessing-record",
|
|
|
|
// libclang uses 'raw' module-format. Match it so we can reuse the module cache and PCHs that libclang uses.
|
|
"-Xclang", "-fmodule-format=raw",
|
|
|
|
// Be less strict - we want to continue and typecheck/index as much as possible
|
|
"-Xclang", "-fallow-pch-with-compiler-errors",
|
|
"-Xclang", "-fallow-pcm-with-compiler-errors",
|
|
"-Wno-non-modular-include-in-framework-module",
|
|
"-Wno-incomplete-umbrella",
|
|
]
|
|
|
|
private extension OnBuildLogMessageNotification {
|
|
var lspStructure: LanguageServerProtocol.StructuredLogKind? {
|
|
guard let taskId = self.task?.id else {
|
|
return nil
|
|
}
|
|
switch structure {
|
|
case .begin(let info):
|
|
return .begin(StructuredLogBegin(title: info.title, taskID: taskId))
|
|
case .report:
|
|
return .report(StructuredLogReport(taskID: taskId))
|
|
case .end:
|
|
return .end(StructuredLogEnd(taskID: taskId))
|
|
case nil:
|
|
return nil
|
|
}
|
|
}
|
|
}
|