-
Notifications
You must be signed in to change notification settings - Fork 378
Expand file tree
/
Copy pathBuildServerManager.swift
More file actions
2097 lines (1922 loc) · 87.1 KB
/
Copy pathBuildServerManager.swift
File metadata and controls
2097 lines (1922 loc) · 87.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//===----------------------------------------------------------------------===//
//
// 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 {
// Close the index store before shutting down the build server so it is
// released deterministically rather than waiting for deallocation.
await self.mainFilesProvider?.value?.close()
// 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
}