@@ -111,6 +111,9 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
111111 }
112112 }
113113 var state : ServerState = . waitingForInitializeRequest
114+
115+ private var headersByTargetGUID : [ String : Set < Basics . AbsolutePath > ] = [ : ]
116+
114117 /// Allows customization of server exit behavior.
115118 var exitHandler : ( Int ) -> Void
116119
@@ -213,6 +216,26 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
213216 await logToClient ( . warning, " SwiftPM build server processed target sources request for unexpected target ' \( target) ' " )
214217 }
215218 }
219+
220+ // Add entries for the target's headers, which are not currently represented in the PIF.
221+ for index in sourcesResponse. items. indices {
222+ guard let targetGUID = sourcesResponse. items [ index] . target. targetGUID else {
223+ logToClient ( . warning, " Unable to determine target GUID for \( sourcesResponse. items [ index] . target) when looking up headers " )
224+ continue
225+ }
226+ let headers = self . headersByTargetGUID [ targetGUID] ?? [ ]
227+ for header in headers {
228+ sourcesResponse. items [ index] . sources. append (
229+ SourceItem (
230+ uri: DocumentURI ( header. asURL) ,
231+ kind: . file,
232+ generated: false ,
233+ dataKind: . sourceKit,
234+ data: SourceKitSourceItemData ( kind: . header) . encodeToLSPAny ( )
235+ )
236+ )
237+ }
238+ }
216239 return sourcesResponse
217240 }
218241 case let request as RequestAndReply < InitializeBuildRequest > :
@@ -221,9 +244,15 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
221244 await request. reply {
222245 if request. params. target. isSwiftPMBuildServerTargetID {
223246 return try await manifestSourceKitOptions ( request: request. params)
224- } else {
225- return try await connectionToUnderlyingBuildServer. send ( request. params)
226247 }
248+ if let targetGUID = request. params. target. targetGUID,
249+ let headers = headersByTargetGUID [ targetGUID] ,
250+ let requestPath = try ? request. params. textDocument. uri. fileURL? . filePath,
251+ headers. contains ( requestPath) ,
252+ let response = try await self . headerSourceKitOptions ( request: request. params) {
253+ return response
254+ }
255+ return try await connectionToUnderlyingBuildServer. send ( request. params)
227256 }
228257 case let request as RequestAndReply < WorkspaceBuildTargetsRequest > :
229258 await request. reply {
@@ -314,6 +343,79 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
314343 return TextDocumentSourceKitOptionsResponse ( compilerArguments: compilerArgs)
315344 }
316345
346+ /// If the requested file is a known header, returns compiler arguments derived from a substitute source file
347+ private func headerSourceKitOptions(
348+ request: TextDocumentSourceKitOptionsRequest
349+ ) async throws -> TextDocumentSourceKitOptionsResponse ? {
350+ guard let fileURL = request. textDocument. uri. fileURL,
351+ let filePath = try ? fileURL. filePath else {
352+ return nil
353+ }
354+
355+ var substituteSourceFile : URI ? = nil
356+ let sourcesResponse = try await connectionToUnderlyingBuildServer. send ( BuildTargetSourcesRequest ( targets: [ request. target] ) )
357+ for sourcesItem in sourcesResponse. items {
358+ for sourceFile in sourcesItem. sources {
359+ let language = SourceKitSourceItemData ( fromLSPAny: sourceFile. data) ? . language
360+ switch language {
361+ case . c, . cpp, . objective_c, . objective_cpp, nil :
362+ // SourceKit-LSP historically chose the first source file of a C-family target as the substitute.
363+ // Here, we specifically look for the first C/C++/ObjC/ObjC++ file so this is futureproof against
364+ // mixed Swift/C-family targets. However, we may also want to consider if e.g. a .hpp header should
365+ // use a C++ source file over a C source file if a target has both.
366+ substituteSourceFile = sourceFile. uri
367+ default :
368+ break
369+ }
370+ }
371+ }
372+
373+ guard let substituteSourceFile, let substituteSourceFilePath = try ? substituteSourceFile. fileURL? . filePath else {
374+ logToClient ( . info, " Unable to find a substitute source file for ' \( filePath) ' " )
375+ return nil
376+ }
377+ logToClient ( . info, " Getting compiler arguments for ' \( filePath) ' using substitute file ' \( substituteSourceFilePath) ' " )
378+
379+ let substituteRequest = TextDocumentSourceKitOptionsRequest (
380+ textDocument: TextDocumentIdentifier ( substituteSourceFile) ,
381+ target: request. target,
382+ language: request. language
383+ )
384+ guard let substituteResponse = try await connectionToUnderlyingBuildServer. send ( substituteRequest) else {
385+ return nil
386+ }
387+
388+ // Replace the substitute file path with the header path
389+ // It's possible the arguments use relative paths while the `originalFile` given
390+ // is an absolute/real path value. We guess based on suffixes instead of hitting
391+ // the file system. Copied from SourceKit-LSP
392+ var arguments = substituteResponse. compilerArguments
393+ let substituteBasename = substituteSourceFilePath. basename
394+ if let index = arguments. lastIndex ( where: {
395+ $0. hasSuffix ( substituteBasename) && substituteSourceFilePath. pathString. hasSuffix ( $0)
396+ } ) {
397+ arguments [ index] = filePath. pathString
398+ }
399+
400+ return TextDocumentSourceKitOptionsResponse (
401+ compilerArguments: arguments,
402+ workingDirectory: substituteResponse. workingDirectory,
403+ data: substituteResponse. data
404+ )
405+ }
406+
407+ private func rebuildHeaderMapping( pifAccompanyingMetadata: [ PackagePIFBuilder . ModuleOrProduct ] ) async {
408+ var headers : [ String : Set < Basics . AbsolutePath > ] = [ : ]
409+ for moduleOrProduct in pifAccompanyingMetadata {
410+ guard let pifTarget = moduleOrProduct. pifTarget else { continue }
411+ let guid = pifTarget. id. value
412+ if !moduleOrProduct. headerFiles. isEmpty {
413+ headers [ guid] = moduleOrProduct. headerFiles
414+ }
415+ }
416+ self . headersByTargetGUID = headers
417+ }
418+
317419 private func shutdown( ) -> VoidResponse {
318420 state = . shutdown
319421 return VoidResponse ( )
@@ -345,7 +447,9 @@ public actor SwiftPMBuildServer: QueueBasedMessageHandler {
345447 public func scheduleRegeneratingBuildDescription( ) {
346448 packageLoadingQueue. async { [ buildSystem] in
347449 do {
348- try await buildSystem. writePIF ( buildParameters: buildSystem. buildParameters)
450+ let result = try await buildSystem. generatePIFAndAccompanyingMetadata ( preserveStructure: false )
451+ try localFileSystem. writeIfChanged ( path: buildSystem. buildParameters. pifManifest, string: result. pif)
452+ await self . rebuildHeaderMapping ( pifAccompanyingMetadata: result. accompanyingMetadata)
349453 self . connectionToUnderlyingBuildServer. send ( OnWatchedFilesDidChangeNotification ( changes: [
350454 . init( uri: . init( buildSystem. buildParameters. pifManifest. asURL) , type: . changed)
351455 ] ) )
@@ -365,5 +469,13 @@ extension BuildTargetIdentifier {
365469 var isSwiftPMBuildServerTargetID : Bool {
366470 uri. scheme == Self . swiftPMBuildServerTargetScheme
367471 }
472+
473+ var targetGUID : String ? {
474+ guard let components = URLComponents ( url: uri. arbitrarySchemeURL, resolvingAgainstBaseURL: false ) ,
475+ let value = components. queryItems? . last ( where: { $0. name == " targetGUID " } ) ? . value else {
476+ return nil
477+ }
478+ return value
479+ }
368480}
369481#endif
0 commit comments