From c5ba9671f035cd7e30835cb8315ceeb20e004266 Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Wed, 11 Sep 2024 12:54:50 -0700 Subject: [PATCH] Migrate getting the list of all source files to BSP --- .../BuildTargetSourcesRequest.swift | 101 ++++++++++++++ ...argets.swift => BuildTargetsRequest.swift} | 123 +++++++++--------- Sources/BuildServerProtocol/CMakeLists.txt | 3 +- Sources/BuildServerProtocol/Messages.swift | 4 +- .../BuildServerBuildSystem.swift | 49 ++++--- .../BuildSystemManager.swift | 59 ++++++++- .../BuiltInBuildSystem.swift | 31 ++--- .../BuiltInBuildSystemAdapter.swift | 4 + .../CompilationDatabaseBuildSystem.swift | 65 +++++---- .../SwiftPMBuildSystem.swift | 35 +++++ .../TestBuildSystem.swift | 32 +++-- Sources/SKSupport/CMakeLists.txt | 1 + .../Dictionary+InitWithElementsKeyedBy.swift | 32 +++++ .../SemanticIndex/SemanticIndexManager.swift | 12 +- Sources/SourceKitLSP/Workspace.swift | 9 +- 15 files changed, 423 insertions(+), 137 deletions(-) create mode 100644 Sources/BuildServerProtocol/BuildTargetSourcesRequest.swift rename Sources/BuildServerProtocol/{BuildTargets.swift => BuildTargetsRequest.swift} (71%) create mode 100644 Sources/SKSupport/Dictionary+InitWithElementsKeyedBy.swift diff --git a/Sources/BuildServerProtocol/BuildTargetSourcesRequest.swift b/Sources/BuildServerProtocol/BuildTargetSourcesRequest.swift new file mode 100644 index 00000000..dda38bc4 --- /dev/null +++ b/Sources/BuildServerProtocol/BuildTargetSourcesRequest.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import LanguageServerProtocol + +/// The build target sources request is sent from the client to the server to +/// query for the list of text documents and directories that are belong to a +/// build target. The sources response must not include sources that are +/// external to the workspace. +public struct BuildTargetSourcesRequest: RequestType, Hashable { + public static let method: String = "buildTarget/sources" + public typealias Response = BuildTargetSourcesResponse + + public var targets: [BuildTargetIdentifier] + + public init(targets: [BuildTargetIdentifier]) { + self.targets = targets + } +} + +public struct BuildTargetSourcesResponse: ResponseType, Hashable { + public var items: [SourcesItem] + + public init(items: [SourcesItem]) { + self.items = items + } +} + +public struct SourcesItem: Codable, Hashable, Sendable { + public var target: BuildTargetIdentifier + + /// The text documents and directories that belong to this build target. + public var sources: [SourceItem] + + public init(target: BuildTargetIdentifier, sources: [SourceItem]) { + self.target = target + self.sources = sources + } +} + +public struct SourceItem: Codable, Hashable, Sendable { + /// Either a text document or a directory. A directory entry must end with a + /// forward slash "/" and a directory entry implies that every nested text + /// document within the directory belongs to this source item. + public var uri: URI + + /// Type of file of the source item, such as whether it is file or directory. + public var kind: SourceItemKind + + /// Indicates if this source is automatically generated by the build and is + /// not intended to be manually edited by the user. + public var generated: Bool + + /// Kind of data to expect in the `data` field. If this field is not set, the kind of data is not specified. + public var dataKind: SourceItemDataKind? + + /// Language-specific metadata about this source item. + public var data: LSPAny? + + public init( + uri: URI, + kind: SourceItemKind, + generated: Bool, + dataKind: SourceItemDataKind? = nil, + data: LSPAny? = nil + ) { + self.uri = uri + self.kind = kind + self.generated = generated + self.dataKind = dataKind + self.data = data + } +} + +public enum SourceItemKind: Int, Codable, Hashable, Sendable { + /// The source item references a normal file. + case file = 1 + + /// The source item references a directory. + case directory = 2 +} + +public struct SourceItemDataKind: RawRepresentable, Codable, Hashable, Sendable { + public var rawValue: String + + public init(rawValue: String) { + self.rawValue = rawValue + } + + /// `data` field must contain a JvmSourceItemData object. + public static let jvm = "jvm" +} diff --git a/Sources/BuildServerProtocol/BuildTargets.swift b/Sources/BuildServerProtocol/BuildTargetsRequest.swift similarity index 71% rename from Sources/BuildServerProtocol/BuildTargets.swift rename to Sources/BuildServerProtocol/BuildTargetsRequest.swift index dec87cf2..827c542d 100644 --- a/Sources/BuildServerProtocol/BuildTargets.swift +++ b/Sources/BuildServerProtocol/BuildTargetsRequest.swift @@ -9,21 +9,26 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// + import LanguageServerProtocol public typealias URI = DocumentURI /// The workspace build targets request is sent from the client to the server to /// ask for the list of all available build targets in the workspace. -public struct BuildTargets: RequestType, Hashable { +public struct BuildTargetsRequest: RequestType, Hashable { public static let method: String = "workspace/buildTargets" - public typealias Response = BuildTargetsResult + public typealias Response = BuildTargetsResponse public init() {} } -public struct BuildTargetsResult: ResponseType, Hashable { +public struct BuildTargetsResponse: ResponseType, Hashable { public var targets: [BuildTarget] + + public init(targets: [BuildTarget]) { + self.targets = targets + } } public struct BuildTarget: Codable, Hashable, Sendable { @@ -52,9 +57,6 @@ public struct BuildTarget: Codable, Hashable, Sendable { /// are free to define new tags for custom purposes. public var tags: [BuildTargetTag] - /// The capabilities of this build target. - public var capabilities: BuildTargetCapabilities - /// The set of languages that this target contains. /// The ID string for each language is defined in the LSP. public var languageIds: [Language] @@ -62,6 +64,16 @@ public struct BuildTarget: Codable, Hashable, Sendable { /// The direct upstream build target dependencies of this build target public var dependencies: [BuildTargetIdentifier] + /// The capabilities of this build target. + public var capabilities: BuildTargetCapabilities + + /// Kind of data to expect in the `data` field. If this field is not set, the kind of data is not specified. + public var dataKind: BuildTargetDataKind? + + /// Language-specific metadata about this target. + /// See ScalaBuildTarget as an example. + public var data: LSPAny? + public init( id: BuildTargetIdentifier, displayName: String?, @@ -96,17 +108,13 @@ public struct BuildTargetTag: Codable, Hashable, RawRepresentable, Sendable { self.rawValue = rawValue } - /// Target contains re-usable functionality for downstream targets. May have - /// any combination of capabilities. - public static let library: Self = Self(rawValue: "library") - /// Target contains source code for producing any kind of application, may /// have but does not require the `canRun` capability. public static let application: Self = Self(rawValue: "application") - /// Target contains source code for testing purposes, may have but does not - /// require the `canTest` capability. - public static let test: Self = Self(rawValue: "test") + /// Target contains source code to measure performance of a program, may have + /// but does not require the `canRun` build target capability. + public static let benchmark: Self = Self(rawValue: "benchmark") /// Target contains source code for integration testing purposes, may have /// but does not require the `canTest` capability. The difference between @@ -115,77 +123,76 @@ public struct BuildTargetTag: Codable, Hashable, RawRepresentable, Sendable { /// execute. public static let integrationTest: Self = Self(rawValue: "integration-test") - /// Target contains source code to measure performance of a program, may have - /// but does not require the `canRun` build target capability. - public static let benchmark: Self = Self(rawValue: "benchmark") + /// Target contains re-usable functionality for downstream targets. May have + /// any combination of capabilities. + public static let library: Self = Self(rawValue: "library") + + /// Actions on the target such as build and test should only be invoked manually + /// and explicitly. For example, triggering a build on all targets in the workspace + /// should by default not include this target. + /// The original motivation to add the "manual" tag comes from a similar functionality + /// that exists in Bazel, where targets with this tag have to be specified explicitly + /// on the command line. + public static let manual: Self = Self(rawValue: "manual") /// Target should be ignored by IDEs. public static let noIDE: Self = Self(rawValue: "no-ide") + + /// Target contains source code for testing purposes, may have but does not + /// require the `canTest` capability. + public static let test: Self = Self(rawValue: "test") + + /// This is a target of a dependency from the project the user opened, eg. a target that builds a SwiftPM dependency. + /// + /// **(BSP Extension)** + public static let dependency: Self = Self(rawValue: "dependency") } public struct BuildTargetCapabilities: Codable, Hashable, Sendable { /// This target can be compiled by the BSP server. - public var canCompile: Bool + public var canCompile: Bool? /// This target can be tested by the BSP server. - public var canTest: Bool + public var canTest: Bool? /// This target can be run by the BSP server. - public var canRun: Bool + public var canRun: Bool? - public init(canCompile: Bool, canTest: Bool, canRun: Bool) { + /// This target can be debugged by the BSP server. + public var canDebug: Bool? + + public init(canCompile: Bool? = nil, canTest: Bool? = nil, canRun: Bool? = nil, canDebug: Bool? = nil) { self.canCompile = canCompile self.canTest = canTest self.canRun = canRun + self.canDebug = canDebug } } -/// The build target sources request is sent from the client to the server to -/// query for the list of text documents and directories that are belong to a -/// build target. The sources response must not include sources that are -/// external to the workspace. -public struct BuildTargetSources: RequestType, Hashable { - public static let method: String = "buildTarget/sources" - public typealias Response = BuildTargetSourcesResult +public struct BuildTargetDataKind: RawRepresentable, Codable, Hashable, Sendable { + public var rawValue: String - public var targets: [BuildTargetIdentifier] - - public init(targets: [BuildTargetIdentifier]) { - self.targets = targets + public init(rawValue: String) { + self.rawValue = rawValue } -} -public struct BuildTargetSourcesResult: ResponseType, Hashable { - public var items: [SourcesItem] -} + /// `data` field must contain a CargoBuildTarget object. + public static let cargo = "cargo" -public struct SourcesItem: Codable, Hashable, Sendable { - public var target: BuildTargetIdentifier + /// `data` field must contain a CppBuildTarget object. + public static let cpp = "cpp" - /// The text documents and directories that belong to this build target. - public var sources: [SourceItem] -} + /// `data` field must contain a JvmBuildTarget object. + public static let jvm = "jvm" -public struct SourceItem: Codable, Hashable, Sendable { - /// Either a text document or a directory. A directory entry must end with a - /// forward slash "/" and a directory entry implies that every nested text - /// document within the directory belongs to this source item. - public var uri: URI + /// `data` field must contain a PythonBuildTarget object. + public static let python = "python" - /// Type of file of the source item, such as whether it is file or directory. - public var kind: SourceItemKind + /// `data` field must contain a SbtBuildTarget object. + public static let sbt = "sbt" - /// Indicates if this source is automatically generated by the build and is - /// not intended to be manually edited by the user. - public var generated: Bool -} - -public enum SourceItemKind: Int, Codable, Hashable, Sendable { - /// The source item references a normal file. - case file = 1 - - /// The source item references a directory. - case directory = 2 + /// `data` field must contain a ScalaBuildTarget object. + public static let scala = "scala" } /// The build target output paths request is sent from the client to the server diff --git a/Sources/BuildServerProtocol/CMakeLists.txt b/Sources/BuildServerProtocol/CMakeLists.txt index adbdb832..8a6d9cc7 100644 --- a/Sources/BuildServerProtocol/CMakeLists.txt +++ b/Sources/BuildServerProtocol/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(BuildServerProtocol STATIC - BuildTargets.swift + BuildTargetSourcesRequest.swift + BuildTargetsRequest.swift DidChangeBuildTargetNotification.swift DidChangeWatchedFilesNotification.swift InitializeBuild.swift diff --git a/Sources/BuildServerProtocol/Messages.swift b/Sources/BuildServerProtocol/Messages.swift index 06c567d7..7a3d1c64 100644 --- a/Sources/BuildServerProtocol/Messages.swift +++ b/Sources/BuildServerProtocol/Messages.swift @@ -13,8 +13,8 @@ import LanguageServerProtocol fileprivate let requestTypes: [_RequestType.Type] = [ BuildTargetOutputPaths.self, - BuildTargets.self, - BuildTargetSources.self, + BuildTargetsRequest.self, + BuildTargetSourcesRequest.self, InitializeBuild.self, InverseSourcesRequest.self, RegisterForChanges.self, diff --git a/Sources/BuildSystemIntegration/BuildServerBuildSystem.swift b/Sources/BuildSystemIntegration/BuildServerBuildSystem.swift index 6b00d82c..887170b7 100644 --- a/Sources/BuildSystemIntegration/BuildServerBuildSystem.swift +++ b/Sources/BuildSystemIntegration/BuildServerBuildSystem.swift @@ -270,6 +270,39 @@ extension BuildServerBuildSystem: BuiltInBuildSystem { package nonisolated var supportsPreparation: Bool { false } + package func buildTargets(request: BuildTargetsRequest) async throws -> BuildTargetsResponse { + // TODO: (BSP migration) Forward this request to the BSP server + return BuildTargetsResponse(targets: [ + BuildTarget( + id: .dummy, + displayName: "Compilation database", + baseDirectory: nil, + tags: [.test], + capabilities: BuildTargetCapabilities(), + // Be conservative with the languages that might be used in the target. SourceKit-LSP doesn't use this property. + languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift], + dependencies: [] + ) + ]) + } + + package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { + // BuildServerBuildSystem does not support syntactic test discovery or background indexing. + // (https://github.com/swiftlang/sourcekit-lsp/issues/1173). + // TODO: (BSP migration) Forward this request to the BSP server + return BuildTargetSourcesResponse(items: []) + } + + package func inverseSources(request: InverseSourcesRequest) -> InverseSourcesResponse { + return InverseSourcesResponse(targets: [BuildTargetIdentifier.dummy]) + } + + package func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) {} + + package func prepare(request: PrepareTargetsRequest) async throws -> VoidResponse { + throw PrepareNotSupportedError() + } + package func sourceKitOptions(request: SourceKitOptionsRequest) async throws -> SourceKitOptionsResponse? { // FIXME: (BSP Migration) If the BSP server supports it, send the `SourceKitOptions` request to it. Only do the // `RegisterForChanges` dance if we are in the legacy mode. @@ -313,10 +346,6 @@ extension BuildServerBuildSystem: BuiltInBuildSystem { return nil } - package func inverseSources(request: InverseSourcesRequest) -> InverseSourcesResponse { - return InverseSourcesResponse(targets: [BuildTargetIdentifier.dummy]) - } - package func scheduleBuildGraphGeneration() {} package func waitForUpToDateBuildGraph() async {} @@ -329,18 +358,6 @@ extension BuildServerBuildSystem: BuiltInBuildSystem { return nil } - package func prepare(request: PrepareTargetsRequest) async throws -> VoidResponse { - throw PrepareNotSupportedError() - } - - package func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) {} - - package func sourceFiles() async -> [SourceFileInfo] { - // BuildServerBuildSystem does not support syntactic test discovery or background indexing. - // (https://github.com/swiftlang/sourcekit-lsp/issues/1173). - return [] - } - package func addSourceFilesDidChangeCallback(_ callback: @escaping () async -> Void) { // BuildServerBuildSystem does not support syntactic test discovery or background indexing. // (https://github.com/swiftlang/sourcekit-lsp/issues/1173). diff --git a/Sources/BuildSystemIntegration/BuildSystemManager.swift b/Sources/BuildSystemIntegration/BuildSystemManager.swift index 5827e664..1218133e 100644 --- a/Sources/BuildSystemIntegration/BuildSystemManager.swift +++ b/Sources/BuildSystemIntegration/BuildSystemManager.swift @@ -15,6 +15,7 @@ import Dispatch import LanguageServerProtocol import SKLogging import SKOptions +import SKSupport import SwiftExtensions import ToolchainRegistry @@ -98,6 +99,10 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate { private var cachedSourceKitOptions = RequestCache() + private var cachedBuildTargets = RequestCache() + + private var cachedTargetSources = RequestCache() + /// The root of the project that this build system manages. For example, for SwiftPM packages, this is the folder /// containing Package.swift. For compilation databases it is the root folder based on which the compilation database /// was found. @@ -384,12 +389,50 @@ package actor BuildSystemManager: BuiltInBuildSystemAdapterDelegate { self.watchedFiles[uri] = nil } - package func sourceFiles() async -> [SourceFileInfo] { - return await buildSystem?.underlyingBuildSystem.sourceFiles() ?? [] + package func buildTargets() async throws -> [BuildTarget] { + guard let buildSystem else { + return [] + } + + let request = BuildTargetsRequest() + let response = try await cachedBuildTargets.get(request) { request in + try await buildSystem.send(request) + } + return response.targets } - package func testFiles() async -> [DocumentURI] { - return await sourceFiles().compactMap { (info: SourceFileInfo) -> DocumentURI? in + package func sourceFiles(in targets: [BuildTargetIdentifier]) async throws -> [SourcesItem] { + guard let buildSystem else { + return [] + } + + let request = BuildTargetSourcesRequest.init(targets: targets) + let response = try await cachedTargetSources.get(request) { request in + try await buildSystem.send(request) + } + return response.items + } + + package func sourceFiles() async throws -> [SourceFileInfo] { + // FIXME: (BSP Migration): Consider removing this method and letting callers get all targets first and then + // retrieving the source files for those targets. + let targets = try await self.buildTargets() + let targetsById = Dictionary(elements: targets, keyedBy: \.id) + let sourceFiles = try await self.sourceFiles(in: targets.map(\.id)).flatMap { sourcesItem in + let target = targetsById[sourcesItem.target] + return sourcesItem.sources.map { sourceItem in + SourceFileInfo( + uri: sourceItem.uri, + isPartOfRootProject: !(target?.tags.contains(.dependency) ?? false), + mayContainTests: target?.tags.contains(.test) ?? true + ) + } + } + return sourceFiles + } + + package func testFiles() async throws -> [DocumentURI] { + return try await sourceFiles().compactMap { (info: SourceFileInfo) -> DocumentURI? in guard info.isPartOfRootProject, info.mayContainTests else { return nil } @@ -445,6 +488,14 @@ extension BuildSystemManager: BuildSystemDelegate { } return updatedTargets.contains(cacheKey.target) } + self.cachedBuildTargets.clearAll() + self.cachedTargetSources.clear { cacheKey in + guard let updatedTargets else { + // All targets might have changed + return true + } + return !updatedTargets.intersection(cacheKey.targets).isEmpty + } await delegate?.buildTargetsChanged(notification.changes) // FIXME: (BSP Migration) Communicate that the build target has changed to the `BuildSystemManagerDelegate` and make diff --git a/Sources/BuildSystemIntegration/BuiltInBuildSystem.swift b/Sources/BuildSystemIntegration/BuiltInBuildSystem.swift index 05f98345..3e01b7b7 100644 --- a/Sources/BuildSystemIntegration/BuiltInBuildSystem.swift +++ b/Sources/BuildSystemIntegration/BuiltInBuildSystem.swift @@ -91,6 +91,22 @@ package protocol BuiltInBuildSystem: AnyObject, Sendable { /// implemented. var supportsPreparation: Bool { get } + /// Returns all targets in the build system + func buildTargets(request: BuildTargetsRequest) async throws -> BuildTargetsResponse + + /// Returns all the source files in the given targets + func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse + + /// Called when files in the project change. + func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) async + + /// Return the list of targets that the given document can be built for. + func inverseSources(request: InverseSourcesRequest) async throws -> InverseSourcesResponse + + /// Prepare the given targets for indexing and semantic functionality. This should build all swift modules of target + /// dependencies. + func prepare(request: PrepareTargetsRequest) async throws -> VoidResponse + /// Retrieve build settings for the given document with the given source /// language. /// @@ -98,9 +114,6 @@ package protocol BuiltInBuildSystem: AnyObject, Sendable { /// file or if it hasn't computed build settings for the file yet. func sourceKitOptions(request: SourceKitOptionsRequest) async throws -> SourceKitOptionsResponse? - /// Return the list of targets that the given document can be built for. - func inverseSources(request: InverseSourcesRequest) async throws -> InverseSourcesResponse - /// Schedule a task that re-generates the build graph. The function may return before the build graph has finished /// being generated. If clients need to wait for an up-to-date build graph, they should call /// `waitForUpToDateBuildGraph` afterwards. @@ -126,10 +139,6 @@ package protocol BuiltInBuildSystem: AnyObject, Sendable { /// Returning `nil` indicates that all targets should be considered depending on the given target. func targets(dependingOn targets: [BuildTargetIdentifier]) async -> [BuildTargetIdentifier]? - /// Prepare the given targets for indexing and semantic functionality. This should build all swift modules of target - /// dependencies. - func prepare(request: PrepareTargetsRequest) async throws -> VoidResponse - /// If the build system has knowledge about the language that this document should be compiled in, return it. /// /// This is used to determine the language in which a source file should be background indexed. @@ -142,14 +151,6 @@ package protocol BuiltInBuildSystem: AnyObject, Sendable { /// If `nil` is returned, then the default toolchain for the given language is used. func toolchain(for uri: DocumentURI, _ language: Language) async -> Toolchain? - /// Called when files in the project change. - func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) async - - /// Returns the list of source files in the project. - /// - /// Header files should not be considered as source files because they cannot be compiled. - func sourceFiles() async -> [SourceFileInfo] - /// Adds a callback that should be called when the value returned by `sourceFiles()` changes. /// /// The callback might also be called without an actual change to `sourceFiles`. diff --git a/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift b/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift index 006f471d..bcdc99c6 100644 --- a/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift +++ b/Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift @@ -138,6 +138,10 @@ package actor BuiltInBuildSystemAdapter: BuiltInBuildSystemMessageHandler { } switch request { + case let request as BuildTargetsRequest: + return try await handle(request, underlyingBuildSystem.buildTargets) + case let request as BuildTargetSourcesRequest: + return try await handle(request, underlyingBuildSystem.buildTargetSources) case let request as InverseSourcesRequest: return try await handle(request, underlyingBuildSystem.inverseSources) case let request as PrepareTargetsRequest: diff --git a/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift b/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift index 55375ef6..2f7b147b 100644 --- a/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift +++ b/Sources/BuildSystemIntegration/CompilationDatabaseBuildSystem.swift @@ -108,6 +108,48 @@ extension CompilationDatabaseBuildSystem: BuiltInBuildSystem { indexStorePath?.parentDirectory.appending(component: "IndexDatabase") } + package func buildTargets(request: BuildTargetsRequest) async throws -> BuildTargetsResponse { + return BuildTargetsResponse(targets: [ + BuildTarget( + id: .dummy, + displayName: nil, + baseDirectory: nil, + tags: [.test], + capabilities: BuildTargetCapabilities(), + // Be conservative with the languages that might be used in the target. SourceKit-LSP doesn't use this property. + languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift], + dependencies: [] + ) + ]) + } + + package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { + guard request.targets.contains(.dummy) else { + return BuildTargetSourcesResponse(items: []) + } + guard let compdb else { + return BuildTargetSourcesResponse(items: []) + } + let sources = compdb.allCommands.map { + SourceItem(uri: $0.uri, kind: .file, generated: false) + } + return BuildTargetSourcesResponse(items: [SourcesItem(target: .dummy, sources: sources)]) + } + + package func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) async { + if notification.changes.contains(where: { self.fileEventShouldTriggerCompilationDatabaseReload(event: $0) }) { + await self.reloadCompilationDatabase() + } + } + + package func inverseSources(request: InverseSourcesRequest) -> InverseSourcesResponse { + return InverseSourcesResponse(targets: [BuildTargetIdentifier.dummy]) + } + + package func prepare(request: PrepareTargetsRequest) async throws -> VoidResponse { + throw PrepareNotSupportedError() + } + package func sourceKitOptions(request: SourceKitOptionsRequest) async throws -> SourceKitOptionsResponse? { guard let db = database(for: request.textDocument.uri), let cmd = db[request.textDocument.uri].first else { return nil @@ -126,14 +168,6 @@ extension CompilationDatabaseBuildSystem: BuiltInBuildSystem { return nil } - package func inverseSources(request: InverseSourcesRequest) -> InverseSourcesResponse { - return InverseSourcesResponse(targets: [BuildTargetIdentifier.dummy]) - } - - package func prepare(request: PrepareTargetsRequest) async throws -> VoidResponse { - throw PrepareNotSupportedError() - } - package func scheduleBuildGraphGeneration() {} package func waitForUpToDateBuildGraph() async {} @@ -196,21 +230,6 @@ extension CompilationDatabaseBuildSystem: BuiltInBuildSystem { } } - package func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) async { - if notification.changes.contains(where: { self.fileEventShouldTriggerCompilationDatabaseReload(event: $0) }) { - await self.reloadCompilationDatabase() - } - } - - package func sourceFiles() async -> [SourceFileInfo] { - guard let compdb else { - return [] - } - return compdb.allCommands.map { - SourceFileInfo(uri: $0.uri, isPartOfRootProject: true, mayContainTests: true) - } - } - package func addSourceFilesDidChangeCallback(_ callback: @escaping () async -> Void) async { testFilesDidChangeCallbacks.append(callback) } diff --git a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift index 9ba4515c..9a209ba4 100644 --- a/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift +++ b/Sources/BuildSystemIntegration/SwiftPMBuildSystem.swift @@ -540,6 +540,41 @@ extension SwiftPMBuildSystem: BuildSystemIntegration.BuiltInBuildSystem { } } + package func buildTargets(request: BuildTargetsRequest) async throws -> BuildTargetsResponse { + let targets = self.targets.map { (targetId, target) in + var tags: [BuildTargetTag] = [.test] + if target.depth != 1 { + tags.append(.dependency) + } + return BuildTarget( + id: targetId, + displayName: nil, + baseDirectory: nil, + tags: tags, + capabilities: BuildTargetCapabilities(), + // Be conservative with the languages that might be used in the target. SourceKit-LSP doesn't use this property. + languageIds: [.c, .cpp, .objective_c, .objective_cpp, .swift], + // FIXME: (BSP migration) List the target's dependencies + dependencies: [] + ) + } + return BuildTargetsResponse(targets: targets) + } + + package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { + var result: [SourcesItem] = [] + for target in request.targets { + guard let swiftPMTarget = self.targets[target] else { + continue + } + let sources = swiftPMTarget.buildTarget.sources.map { + SourceItem(uri: DocumentURI($0), kind: .file, generated: false) + } + result.append(SourcesItem(target: target, sources: sources)) + } + return BuildTargetSourcesResponse(items: result) + } + package func sourceKitOptions(request: SourceKitOptionsRequest) async throws -> SourceKitOptionsResponse? { guard let url = request.textDocument.uri.fileURL, let path = try? AbsolutePath(validating: url.path) else { // We can't determine build settings for non-file URIs. diff --git a/Sources/BuildSystemIntegration/TestBuildSystem.swift b/Sources/BuildSystemIntegration/TestBuildSystem.swift index bfc9afba..b507ba4f 100644 --- a/Sources/BuildSystemIntegration/TestBuildSystem.swift +++ b/Sources/BuildSystemIntegration/TestBuildSystem.swift @@ -54,6 +54,24 @@ package actor TestBuildSystem: BuiltInBuildSystem { self.messageHandler = messageHandler } + package func buildTargets(request: BuildTargetsRequest) async throws -> BuildTargetsResponse { + return BuildTargetsResponse(targets: []) + } + + package func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse { + return BuildTargetSourcesResponse(items: []) + } + + package func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) async {} + + package func inverseSources(request: InverseSourcesRequest) -> InverseSourcesResponse { + return InverseSourcesResponse(targets: [BuildTargetIdentifier.dummy]) + } + + package func prepare(request: PrepareTargetsRequest) async throws -> VoidResponse { + throw PrepareNotSupportedError() + } + package func sourceKitOptions(request: SourceKitOptionsRequest) async throws -> SourceKitOptionsResponse? { return buildSettingsByFile[request.textDocument.uri] } @@ -66,14 +84,6 @@ package actor TestBuildSystem: BuiltInBuildSystem { return nil } - package func inverseSources(request: InverseSourcesRequest) -> InverseSourcesResponse { - return InverseSourcesResponse(targets: [BuildTargetIdentifier.dummy]) - } - - package func prepare(request: PrepareTargetsRequest) async throws -> VoidResponse { - throw PrepareNotSupportedError() - } - package func scheduleBuildGraphGeneration() {} package func waitForUpToDateBuildGraph() async {} @@ -86,11 +96,5 @@ package actor TestBuildSystem: BuiltInBuildSystem { return nil } - package func didChangeWatchedFiles(notification: BuildServerProtocol.DidChangeWatchedFilesNotification) async {} - - package func sourceFiles() async -> [SourceFileInfo] { - return [] - } - package func addSourceFilesDidChangeCallback(_ callback: @escaping () async -> Void) async {} } diff --git a/Sources/SKSupport/CMakeLists.txt b/Sources/SKSupport/CMakeLists.txt index 63ccd5da..e6540223 100644 --- a/Sources/SKSupport/CMakeLists.txt +++ b/Sources/SKSupport/CMakeLists.txt @@ -5,6 +5,7 @@ add_library(SKSupport STATIC ByteString.swift Connection+Send.swift Debouncer.swift + Dictionary+InitWithElementsKeyedBy.swift DocumentURI+CustomLogStringConvertible.swift ExperimentalFeatures.swift FileSystem.swift diff --git a/Sources/SKSupport/Dictionary+InitWithElementsKeyedBy.swift b/Sources/SKSupport/Dictionary+InitWithElementsKeyedBy.swift new file mode 100644 index 00000000..223b106f --- /dev/null +++ b/Sources/SKSupport/Dictionary+InitWithElementsKeyedBy.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import SKLogging + +extension Dictionary { + /// Create a new dictionary from the given elements, assuming that they all have a unique value identified by `keyedBy`. + /// If two elements have the same key, log an error and choose the first value with that key. + public init(elements: some Sequence, keyedBy: KeyPath) { + self = [:] + self.reserveCapacity(elements.underestimatedCount) + for element in elements { + let key = element[keyPath: keyedBy] + if let existingElement = self[key] { + logger.error( + "Found duplicate key \(String(describing: key)): \(String(describing: existingElement)) vs. \(String(describing: element))" + ) + continue + } + self[key] = element + } + } +} diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index 5656730b..78e0384e 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -274,7 +274,10 @@ package final actor SemanticIndexManager { await testHooks.buildGraphGenerationDidFinish?() // TODO: Ideally this would be a type like any Collection & Sendable but that doesn't work due to // https://github.com/swiftlang/swift/issues/75602 - var filesToIndex: [DocumentURI] = await self.buildSystemManager.sourceFiles().lazy.map(\.uri) + var filesToIndex: [DocumentURI] = + await orLog("Getting files to index") { + try await self.buildSystemManager.sourceFiles().lazy.map(\.uri) + } ?? [] if !indexFilesWithUpToDateUnit { let index = index.checked(for: .modifiedFiles) filesToIndex = filesToIndex.filter { !index.hasUpToDateUnit(for: $0) } @@ -375,7 +378,12 @@ package final actor SemanticIndexManager { private func filesToIndex( toCover files: some Collection & Sendable ) async -> [FileToIndex] { - let sourceFiles = Set(await buildSystemManager.sourceFiles().map(\.uri)) + let sourceFiles = await orLog("Getting source files in project") { + Set(try await buildSystemManager.sourceFiles().map(\.uri)) + } + guard let sourceFiles else { + return [] + } let filesToReIndex = await files.asyncCompactMap { (uri) -> FileToIndex? in if sourceFiles.contains(uri) { // If this is a source file, just index it. diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 296517bf..176e0a41 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -132,10 +132,15 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate { guard let self else { return } - await self.syntacticTestIndex.listOfTestFilesDidChange(self.buildSystemManager.testFiles()) + guard let testFiles = await orLog("Getting test files", { try await self.buildSystemManager.testFiles() }) else { + return + } + await self.syntacticTestIndex.listOfTestFilesDidChange(testFiles) } // Trigger an initial population of `syntacticTestIndex`. - await syntacticTestIndex.listOfTestFilesDidChange(buildSystemManager.testFiles()) + if let testFiles = await orLog("Getting initial test files", { try await self.buildSystemManager.testFiles() }) { + await syntacticTestIndex.listOfTestFilesDidChange(testFiles) + } if let semanticIndexManager { await semanticIndexManager.scheduleBuildGraphGenerationAndBackgroundIndexAllFiles() }