Skip to content

Commit f6a29f4

Browse files
authored
Merge pull request #2017 from ahoppen/output-file-paths
Add BSP request to get the output file paths of a target
2 parents 0e22653 + 51a035d commit f6a29f4

18 files changed

+383
-46
lines changed

Contributor Documentation/BSP Extensions.md

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ Definition is the same as in LSP.
1414

1515
```ts
1616
export interface SourceKitInitializeBuildResponseData {
17+
/** The path at which SourceKit-LSP can store its index database, aggregating data from `indexStorePath`.
18+
* This should point to a directory that can be exclusively managed by SourceKit-LSP. Its exact location can be arbitrary. */
19+
indexDatabasePath?: string;
20+
1721
/** The directory to which the index store is written during compilation, ie. the path passed to `-index-store-path`
1822
* for `swiftc` or `clang` invocations **/
1923
indexStorePath?: string;
2024

21-
/** The path at which SourceKit-LSP can store its index database, aggregating data from `indexStorePath`.
22-
* This should point to a directory that can be exclusively managed by SourceKit-LSP. Its exact location can be arbitrary. */
23-
indexDatabasePath?: string;
25+
/** Whether the server implements the `buildTarget/outputPaths` request. */
26+
outputPathsProvider?: bool;
2427

2528
/** Whether the build server supports the `buildTarget/prepare` request */
2629
prepareProvider?: bool;
@@ -43,6 +46,37 @@ If `data` contains a string value for the `workDoneProgressTitle` key, then the
4346

4447
`changes` can be `null` to indicate that all targets have changed.
4548

49+
## `buildTarget/outputPaths`
50+
51+
For all the source files in this target, the output paths that are used during indexing, ie. the `-index-unit-output-path` for the file, if it is specified in the compiler arguments or the file that is passed as `-o`, if `-index-unit-output-path` is not specified.
52+
53+
This allows SourceKit-LSP to remove index entries for source files that are removed from a target but remain present on disk.
54+
55+
The server communicates during the initialize handshake whether this method is supported or not by setting `outputPathsProvider: true` in `SourceKitInitializeBuildResponseData`.
56+
57+
- method: `buildTarget/outputPaths`
58+
- params: `OutputPathsParams`
59+
- result: `OutputPathsResult`
60+
61+
```ts
62+
export interface BuildTargetOutputPathsParams {
63+
/** A list of build targets to get the output paths for. */
64+
targets: BuildTargetIdentifier[];
65+
}
66+
67+
export interface BuildTargetOutputPathsItem {
68+
/** The target these output file paths are for. */
69+
target: BuildTargetIdentifier;
70+
71+
/** The output paths for all source files in this target. */
72+
outputPaths: string[];
73+
}
74+
75+
export interface BuildTargetOutputPathsResult {
76+
items: BuildTargetOutputPathsItem[];
77+
}
78+
```
79+
4680
## `buildTarget/prepare`
4781

4882
The prepare build target request is sent from the client to the server to prepare the given list of build targets for editor functionality.
@@ -53,6 +87,7 @@ The server communicates during the initialize handshake whether this method is s
5387

5488
- method: `buildTarget/prepare`
5589
- params: `PrepareParams`
90+
- result: `void`
5691

5792
```ts
5893
export interface PrepareParams {

Sources/BuildServerProtocol/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ add_library(BuildServerProtocol STATIC
22
Messages.swift
33

44
Messages/BuildShutdownRequest.swift
5+
Messages/BuildTargetOutputPathsRequest.swift
56
Messages/BuildTargetPrepareRequest.swift
67
Messages/BuildTargetSourcesRequest.swift
78
Messages/InitializeBuildRequest.swift

Sources/BuildServerProtocol/Messages.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import LanguageServerProtocol
1818

1919
fileprivate let requestTypes: [_RequestType.Type] = [
2020
BuildShutdownRequest.self,
21+
BuildTargetOutputPathsRequest.self,
2122
BuildTargetPrepareRequest.self,
2223
BuildTargetSourcesRequest.self,
2324
CreateWorkDoneProgressRequest.self,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if compiler(>=6)
14+
public import LanguageServerProtocol
15+
#else
16+
import LanguageServerProtocol
17+
#endif
18+
19+
/// For all the source files in this target, the output paths that are used during indexing, ie. the
20+
/// `-index-unit-output-path` for the file, if it is specified in the compiler arguments or the file that is passed as
21+
/// `-o`, if `-index-unit-output-path` is not specified.
22+
public struct BuildTargetOutputPathsRequest: RequestType, Equatable, Hashable {
23+
public static let method: String = "buildTarget/outputPaths"
24+
25+
public typealias Response = BuildTargetOutputPathsResponse
26+
27+
/// A list of build targets to get the output paths for.
28+
public var targets: [BuildTargetIdentifier]
29+
30+
public init(targets: [BuildTargetIdentifier]) {
31+
self.targets = targets
32+
}
33+
}
34+
35+
public struct BuildTargetOutputPathsItem: Codable, Sendable {
36+
/// The target these output file paths are for.
37+
public var target: BuildTargetIdentifier
38+
39+
/// The output paths for all source files in this target.
40+
public var outputPaths: [String]
41+
42+
public init(target: BuildTargetIdentifier, outputPaths: [String]) {
43+
self.target = target
44+
self.outputPaths = outputPaths
45+
}
46+
}
47+
48+
public struct BuildTargetOutputPathsResponse: ResponseType {
49+
public var items: [BuildTargetOutputPathsItem]
50+
51+
public init(items: [BuildTargetOutputPathsItem]) {
52+
self.items = items
53+
}
54+
}

Sources/BuildServerProtocol/Messages/InitializeBuildRequest.swift

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -273,15 +273,19 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send
273273
/// The path at which SourceKit-LSP can store its index database, aggregating data from `indexStorePath`
274274
public var indexStorePath: String?
275275

276-
/// The files to watch for changes.
277-
public var watchers: [FileSystemWatcher]?
276+
/// Whether the server implements the `buildTarget/outputPaths` request.
277+
public var outputPathsProvider: Bool?
278278

279279
/// Whether the build server supports the `buildTarget/prepare` request.
280280
public var prepareProvider: Bool?
281281

282282
/// Whether the server implements the `textDocument/sourceKitOptions` request.
283283
public var sourceKitOptionsProvider: Bool?
284284

285+
/// The files to watch for changes.
286+
public var watchers: [FileSystemWatcher]?
287+
288+
@available(*, deprecated, message: "Use initializer with alphabetical order of parameters")
285289
public init(
286290
indexDatabasePath: String? = nil,
287291
indexStorePath: String? = nil,
@@ -296,22 +300,41 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send
296300
self.sourceKitOptionsProvider = sourceKitOptionsProvider
297301
}
298302

303+
public init(
304+
indexDatabasePath: String? = nil,
305+
indexStorePath: String? = nil,
306+
outputPathsProvider: Bool? = nil,
307+
prepareProvider: Bool? = nil,
308+
sourceKitOptionsProvider: Bool? = nil,
309+
watchers: [FileSystemWatcher]? = nil
310+
) {
311+
self.indexDatabasePath = indexDatabasePath
312+
self.indexStorePath = indexStorePath
313+
self.outputPathsProvider = outputPathsProvider
314+
self.prepareProvider = prepareProvider
315+
self.sourceKitOptionsProvider = sourceKitOptionsProvider
316+
self.watchers = watchers
317+
}
318+
299319
public init?(fromLSPDictionary dictionary: [String: LanguageServerProtocol.LSPAny]) {
300320
if case .string(let indexDatabasePath) = dictionary[CodingKeys.indexDatabasePath.stringValue] {
301321
self.indexDatabasePath = indexDatabasePath
302322
}
303323
if case .string(let indexStorePath) = dictionary[CodingKeys.indexStorePath.stringValue] {
304324
self.indexStorePath = indexStorePath
305325
}
306-
if let watchers = dictionary[CodingKeys.watchers.stringValue] {
307-
self.watchers = [FileSystemWatcher](fromLSPArray: watchers)
326+
if case .bool(let outputPathsProvider) = dictionary[CodingKeys.outputPathsProvider.stringValue] {
327+
self.outputPathsProvider = outputPathsProvider
308328
}
309329
if case .bool(let prepareProvider) = dictionary[CodingKeys.prepareProvider.stringValue] {
310330
self.prepareProvider = prepareProvider
311331
}
312332
if case .bool(let sourceKitOptionsProvider) = dictionary[CodingKeys.sourceKitOptionsProvider.stringValue] {
313333
self.sourceKitOptionsProvider = sourceKitOptionsProvider
314334
}
335+
if let watchers = dictionary[CodingKeys.watchers.stringValue] {
336+
self.watchers = [FileSystemWatcher](fromLSPArray: watchers)
337+
}
315338
}
316339

317340
public func encodeToLSPAny() -> LanguageServerProtocol.LSPAny {
@@ -322,15 +345,18 @@ public struct SourceKitInitializeBuildResponseData: LSPAnyCodable, Codable, Send
322345
if let indexStorePath {
323346
result[CodingKeys.indexStorePath.stringValue] = .string(indexStorePath)
324347
}
325-
if let watchers {
326-
result[CodingKeys.watchers.stringValue] = watchers.encodeToLSPAny()
348+
if let outputPathsProvider {
349+
result[CodingKeys.outputPathsProvider.stringValue] = .bool(outputPathsProvider)
327350
}
328351
if let prepareProvider {
329352
result[CodingKeys.prepareProvider.stringValue] = .bool(prepareProvider)
330353
}
331354
if let sourceKitOptionsProvider {
332355
result[CodingKeys.sourceKitOptionsProvider.stringValue] = .bool(sourceKitOptionsProvider)
333356
}
357+
if let watchers {
358+
result[CodingKeys.watchers.stringValue] = watchers.encodeToLSPAny()
359+
}
334360
return .dictionary(result)
335361
}
336362
}

Sources/BuildSystemIntegration/BuildSystemManager.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,8 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
379379

380380
private var cachedTargetSources = RequestCache<BuildTargetSourcesRequest>()
381381

382+
private var cachedTargetOutputPaths = RequestCache<BuildTargetOutputPathsRequest>()
383+
382384
/// `SourceFilesAndDirectories` is a global property that only gets reset when the build targets change and thus
383385
/// has no real key.
384386
private struct SourceFilesAndDirectoriesKey: Hashable {}
@@ -624,6 +626,13 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
624626
}
625627
return !updatedTargets.intersection(cacheKey.targets).isEmpty
626628
}
629+
self.cachedTargetOutputPaths.clear(isolation: self) { cacheKey in
630+
guard let updatedTargets else {
631+
// All targets might have changed
632+
return true
633+
}
634+
return !updatedTargets.isDisjoint(with: cacheKey.targets)
635+
}
627636
self.cachedSourceFilesAndDirectories.clearAll(isolation: self)
628637

629638
await delegate?.buildTargetsChanged(notification.changes)
@@ -1158,6 +1167,42 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
11581167
return response.items
11591168
}
11601169

1170+
/// Return the output paths for all source files known to the build server.
1171+
///
1172+
/// See `BuildTargetOutputPathsRequest` for details.
1173+
package func outputPathInAllTargets() async throws -> [String] {
1174+
return try await outputPaths(in: Set(buildTargets().map(\.key)))
1175+
}
1176+
1177+
/// For all source files in the given targets, return their output file paths.
1178+
///
1179+
/// See `BuildTargetOutputPathsRequest` for details.
1180+
package func outputPaths(in targets: Set<BuildTargetIdentifier>) async throws -> [String] {
1181+
guard let buildSystemAdapter = await buildSystemAdapterAfterInitialized, !targets.isEmpty else {
1182+
return []
1183+
}
1184+
1185+
let request = BuildTargetOutputPathsRequest(targets: targets.sorted { $0.uri.stringValue < $1.uri.stringValue })
1186+
1187+
// If we have a cached request for a superset of the targets, serve the result from that cache entry.
1188+
let fromSuperset = await orLog("Getting output paths from superset request") {
1189+
try await cachedTargetOutputPaths.getDerived(
1190+
isolation: self,
1191+
request,
1192+
canReuseKey: { targets.isSubset(of: $0.targets) },
1193+
transform: { BuildTargetOutputPathsResponse(items: $0.items.filter { targets.contains($0.target) }) }
1194+
)
1195+
}
1196+
if let fromSuperset {
1197+
return fromSuperset.items.flatMap(\.outputPaths)
1198+
}
1199+
1200+
let response = try await cachedTargetOutputPaths.get(request, isolation: self) { request in
1201+
try await buildSystemAdapter.send(request)
1202+
}
1203+
return response.items.flatMap(\.outputPaths)
1204+
}
1205+
11611206
/// Returns all source files in the project.
11621207
///
11631208
/// - SeeAlso: Comment in `sourceFilesAndDirectories` for a definition of what `buildable` means.

Sources/BuildSystemIntegration/BuildSystemMessageDependencyTracker.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ package enum BuildSystemMessageDependencyTracker: QueueBasedMessageHandlerDepend
9090
switch request {
9191
case is BuildShutdownRequest:
9292
self = .stateChange
93+
case is BuildTargetOutputPathsRequest:
94+
self = .stateRead
9395
case is BuildTargetPrepareRequest:
9496
self = .stateRead
9597
case is BuildTargetSourcesRequest:

Sources/BuildSystemIntegration/BuiltInBuildSystem.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,19 @@ package protocol BuiltInBuildSystem: AnyObject, Sendable {
4444
/// The path to put the index database, if any.
4545
var indexDatabasePath: URL? { get async }
4646

47-
/// Whether the build system is capable of preparing a target for indexing, ie. if the `prepare` methods has been
48-
/// implemented.
49-
var supportsPreparation: Bool { get }
47+
/// Whether the build system is capable of preparing a target for indexing and determining the output paths for the
48+
/// target, ie. whether the `prepare` and `buildTargetOutputPaths` methods have been implemented.
49+
var supportsPreparationAndOutputPaths: Bool { get }
5050

5151
/// Returns all targets in the build system
5252
func buildTargets(request: WorkspaceBuildTargetsRequest) async throws -> WorkspaceBuildTargetsResponse
5353

5454
/// Returns all the source files in the given targets
5555
func buildTargetSources(request: BuildTargetSourcesRequest) async throws -> BuildTargetSourcesResponse
5656

57+
/// Returns all the output paths for the source files in the given targets.
58+
func buildTargetOutputPaths(request: BuildTargetOutputPathsRequest) async throws -> BuildTargetOutputPathsResponse
59+
5760
/// Called when files in the project change.
5861
func didChangeWatchedFiles(notification: OnWatchedFilesDidChangeNotification) async
5962

Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ actor BuiltInBuildSystemAdapter: QueueBasedMessageHandler {
100100
indexStorePath: await orLog("getting index store file path") {
101101
try await underlyingBuildSystem.indexStorePath?.filePath
102102
},
103-
watchers: await underlyingBuildSystem.fileWatchers,
104-
prepareProvider: underlyingBuildSystem.supportsPreparation,
105-
sourceKitOptionsProvider: true
103+
outputPathsProvider: underlyingBuildSystem.supportsPreparationAndOutputPaths,
104+
prepareProvider: underlyingBuildSystem.supportsPreparationAndOutputPaths,
105+
sourceKitOptionsProvider: true,
106+
watchers: await underlyingBuildSystem.fileWatchers
106107
).encodeToLSPAny()
107108
)
108109
}
@@ -130,6 +131,8 @@ actor BuiltInBuildSystemAdapter: QueueBasedMessageHandler {
130131
switch request {
131132
case let request as RequestAndReply<BuildShutdownRequest>:
132133
await request.reply { VoidResponse() }
134+
case let request as RequestAndReply<BuildTargetOutputPathsRequest>:
135+
await request.reply { try await underlyingBuildSystem.buildTargetOutputPaths(request: request.params) }
133136
case let request as RequestAndReply<BuildTargetPrepareRequest>:
134137
await request.reply { try await underlyingBuildSystem.prepare(request: request.params) }
135138
case let request as RequestAndReply<BuildTargetSourcesRequest>:

Sources/BuildSystemIntegration/FixedCompilationDatabaseBuildSystem.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ package actor FixedCompilationDatabaseBuildSystem: BuiltInBuildSystem {
6666
indexStorePath?.deletingLastPathComponent().appendingPathComponent("IndexDatabase")
6767
}
6868

69-
package nonisolated var supportsPreparation: Bool { false }
69+
package nonisolated var supportsPreparationAndOutputPaths: Bool { false }
7070

7171
private static func parseCompileFlags(at configPath: URL) throws -> [String] {
7272
let fileContents: String = try String(contentsOf: configPath, encoding: .utf8)
@@ -121,7 +121,13 @@ package actor FixedCompilationDatabaseBuildSystem: BuiltInBuildSystem {
121121
}
122122

123123
package func prepare(request: BuildTargetPrepareRequest) async throws -> VoidResponse {
124-
throw PrepareNotSupportedError()
124+
throw ResponseError.methodNotFound(BuildTargetPrepareRequest.method)
125+
}
126+
127+
package func buildTargetOutputPaths(
128+
request: BuildTargetOutputPathsRequest
129+
) async throws -> BuildTargetOutputPathsResponse {
130+
throw ResponseError.methodNotFound(BuildTargetOutputPathsRequest.method)
125131
}
126132

127133
package func sourceKitOptions(

0 commit comments

Comments
 (0)