diff --git a/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift b/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift index 9fabb4b7..69145c3e 100644 --- a/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift +++ b/Sources/BuildServerIntegration/BuildServerManagerDelegate.swift @@ -30,16 +30,6 @@ package protocol BuildServerManagerDelegate: AnyObject, Sendable { /// Notify the delegate that some information about the given build targets has changed and that it should recompute /// any information based on top of it. func buildTargetsChanged(_ changedTargets: Set?) async - - func addBuiltTargetListener(_ listener: any BuildTargetListener) - - func removeBuiltTargetListener(_ listener: any BuildTargetListener) -} - -package protocol BuildTargetListener: AnyObject, Sendable { - /// Notify the listener that some information about the given build targets has changed and that it should recompute - /// any information based on top of it. - func buildTargetsChanged(_ changedTargets: Set?) async } /// Methods with which the `BuildServerManager` can send messages to the client (aka. editor). diff --git a/Sources/ClangLanguageService/ClangLanguageService.swift b/Sources/ClangLanguageService/ClangLanguageService.swift index 5595cc11..dd819993 100644 --- a/Sources/ClangLanguageService/ClangLanguageService.swift +++ b/Sources/ClangLanguageService/ClangLanguageService.swift @@ -644,11 +644,14 @@ extension ClangLanguageService { return nil } - package func syntacticTests(in workspace: Workspace) async -> [AnnotatedTestItem] { + package func syntacticTestItems(for snapshot: DocumentSnapshot) async -> [AnnotatedTestItem] { return [] } - package func syntacticPlaygrounds(in workspace: Workspace) async -> [Playground] { + package func syntacticPlaygrounds( + for snapshot: DocumentSnapshot, + in workspace: Workspace + ) async -> [TextDocumentPlayground] { return [] } diff --git a/Sources/DocumentationLanguageService/DocumentationLanguageService.swift b/Sources/DocumentationLanguageService/DocumentationLanguageService.swift index 282d0b4a..4386e6bd 100644 --- a/Sources/DocumentationLanguageService/DocumentationLanguageService.swift +++ b/Sources/DocumentationLanguageService/DocumentationLanguageService.swift @@ -88,11 +88,14 @@ package actor DocumentationLanguageService: LanguageService, Sendable { // The DocumentationLanguageService does not do anything with document events } - package func syntacticTests(in workspace: Workspace) async -> [AnnotatedTestItem] { + package func syntacticTestItems(for snapshot: DocumentSnapshot) async -> [AnnotatedTestItem] { return [] } - package func syntacticPlaygrounds(in workspace: Workspace) async -> [Playground] { + package func syntacticPlaygrounds( + for snapshot: DocumentSnapshot, + in workspace: Workspace + ) async -> [TextDocumentPlayground] { return [] } diff --git a/Sources/SourceKitLSP/LanguageService.swift b/Sources/SourceKitLSP/LanguageService.swift index 5ec6a59f..2294fdec 100644 --- a/Sources/SourceKitLSP/LanguageService.swift +++ b/Sources/SourceKitLSP/LanguageService.swift @@ -313,15 +313,20 @@ package protocol LanguageService: AnyObject, Sendable { /// A return value of `nil` indicates that this language service does not support syntactic test discovery. func syntacticDocumentTests(for uri: DocumentURI, in workspace: Workspace) async throws -> [AnnotatedTestItem]? - /// Returns the syntactically scanned tests declared within the workspace. + /// Syntactically scans the file at the given URL for tests declared within it. + /// + /// Does not write the results to the index. /// /// The order of the returned tests is not defined. The results should be sorted before being returned to the editor. - func syntacticTests(in workspace: Workspace) async -> [AnnotatedTestItem] + func syntacticTestItems(for snapshot: DocumentSnapshot) async -> [AnnotatedTestItem] /// Returns the syntactically scanned playgrounds declared within the workspace. /// /// The order of the returned playgrounds is not defined. The results should be sorted before being returned to the editor. - func syntacticPlaygrounds(in workspace: Workspace) async -> [Playground] + func syntacticPlaygrounds( + for snapshot: DocumentSnapshot, + in workspace: Workspace + ) async -> [TextDocumentPlayground] /// A position that is canonical for all positions within a declaration. For example, if we have the following /// declaration, then all `|` markers should return the same canonical position. diff --git a/Sources/SourceKitLSP/PlaygroundDiscovery.swift b/Sources/SourceKitLSP/PlaygroundDiscovery.swift index 2f9621b5..a5005242 100644 --- a/Sources/SourceKitLSP/PlaygroundDiscovery.swift +++ b/Sources/SourceKitLSP/PlaygroundDiscovery.swift @@ -28,9 +28,7 @@ extension SourceKitLSPServer { // playgrounds. await workspace.buildServerManager.waitForUpToDateBuildGraph() - let playgroundsFromSyntacticIndex = await languageServices.values.asyncFlatMap { - await $0.asyncFlatMap { await $0.syntacticPlaygrounds(in: workspace) } - } + let playgroundsFromSyntacticIndex = await workspace.syntacticIndex.playgrounds() // We don't need to sort the playgrounds here because they will get sorted by `workspacePlaygrounds` request handler return playgroundsFromSyntacticIndex diff --git a/Sources/SourceKitLSP/SwiftSyntacticIndex.swift b/Sources/SourceKitLSP/SwiftSyntacticIndex.swift index 13cdfaf9..134f6a1a 100644 --- a/Sources/SourceKitLSP/SwiftSyntacticIndex.swift +++ b/Sources/SourceKitLSP/SwiftSyntacticIndex.swift @@ -83,7 +83,7 @@ private struct IndexedSourceFile { /// /// The index does not get persisted to disk but instead gets rebuilt every time a workspace is opened (ie. usually when /// sourcekit-lsp is launched). Building it takes only a few seconds, even for large projects. -package actor SwiftSyntacticIndex: BuildTargetListener, Sendable { +package actor SwiftSyntacticIndex: Sendable { /// The tests discovered by the index. private var indexedSources: [DocumentURI: IndexedSourceFile] = [:] @@ -104,20 +104,23 @@ package actor SwiftSyntacticIndex: BuildTargetListener, Sendable { /// Fetch the list of source files to scan for a given set of build targets private let determineFilesToScan: @Sendable (Set?) async -> [DocumentURI] - // Syntactically parse tests from the given snapshot - private let syntacticTests: @Sendable (DocumentSnapshot) async -> [AnnotatedTestItem] + /// Syntactically parse tests from the given snapshot + private let syntacticTests: @Sendable (DocumentSnapshot, Workspace) async -> [AnnotatedTestItem] - // Syntactically parse playgrounds from the given snapshot - private let syntacticPlaygrounds: @Sendable (DocumentSnapshot) async -> [TextDocumentPlayground] + /// Syntactically parse playgrounds from the given snapshot + private let syntacticPlaygrounds: @Sendable (DocumentSnapshot, Workspace) async -> [TextDocumentPlayground] package init( determineFilesToScan: @Sendable @escaping (Set?) async -> [DocumentURI], - syntacticTests: @Sendable @escaping (DocumentSnapshot) async -> [AnnotatedTestItem], - syntacticPlaygrounds: @Sendable @escaping (DocumentSnapshot) async -> [TextDocumentPlayground] + syntacticTests: @Sendable @escaping (DocumentSnapshot, Workspace) async -> [AnnotatedTestItem], + syntacticPlaygrounds: @Sendable @escaping (DocumentSnapshot, Workspace) async -> [TextDocumentPlayground] ) { self.determineFilesToScan = determineFilesToScan self.syntacticTests = syntacticTests self.syntacticPlaygrounds = syntacticPlaygrounds + } + + func scan(workspace: Workspace) { indexingQueue.async(priority: .low, metadata: .initialPopulation) { let filesToScan = await self.determineFilesToScan(nil) // Divide the files into multiple batches. This is more efficient than spawning a new task for every file, mostly @@ -128,7 +131,7 @@ package actor SwiftSyntacticIndex: BuildTargetListener, Sendable { let batches = filesToScan.partition(intoNumberOfBatches: ProcessInfo.processInfo.activeProcessorCount * 4) await batches.concurrentForEach { filesInBatch in for uri in filesInBatch { - await self.rescanFileAssumingOnQueue(uri) + await self.rescanFileAssumingOnQueue(uri, workspace) } } } @@ -144,15 +147,15 @@ package actor SwiftSyntacticIndex: BuildTargetListener, Sendable { /// Called when the list of targets is updated. /// /// All files that are not in the new list of buildable files will be removed from the index. - package func buildTargetsChanged(_ changedTargets: Set?) async { + package func buildTargetsChanged(_ changedTargets: Set?, _ workspace: Workspace) async { let changedFiles = await determineFilesToScan(changedTargets) let removedFiles = Set(self.indexedSources.keys).subtracting(changedFiles) removeFilesFromIndex(removedFiles) - rescanFiles(changedFiles) + rescanFiles(changedFiles, workspace) } - package func filesDidChange(_ events: [FileEvent]) { + package func filesDidChange(_ events: [FileEvent], _ workspace: Workspace) { var removedFiles: Set = [] var filesToRescan: [DocumentURI] = [] for fileEvent in events { @@ -168,11 +171,11 @@ package actor SwiftSyntacticIndex: BuildTargetListener, Sendable { } } removeFilesFromIndex(removedFiles) - rescanFiles(filesToRescan) + rescanFiles(filesToRescan, workspace) } /// Called when a list of files was updated. Re-scans those files - private func rescanFiles(_ uris: [DocumentURI]) { + private func rescanFiles(_ uris: [DocumentURI], _ workspace: Workspace) { // If we scan a file again, it might have been added after being removed before. Remove it from the list of removed // files. removedFiles.subtract(uris) @@ -212,7 +215,7 @@ package actor SwiftSyntacticIndex: BuildTargetListener, Sendable { for batch in batches { self.indexingQueue.async(priority: .low, metadata: .index(Set(batch))) { for uri in batch { - await self.rescanFileAssumingOnQueue(uri) + await self.rescanFileAssumingOnQueue(uri, workspace) } } } @@ -221,7 +224,7 @@ package actor SwiftSyntacticIndex: BuildTargetListener, Sendable { /// Re-scans a single file. /// /// - Important: This method must be called in a task that is executing on `indexingQueue`. - private func rescanFileAssumingOnQueue(_ uri: DocumentURI) async { + private func rescanFileAssumingOnQueue(_ uri: DocumentURI, _ workspace: Workspace) async { guard let url = uri.fileURL else { logger.log("Not indexing \(uri.forLogging) because it is not a file URL") return @@ -254,10 +257,6 @@ package actor SwiftSyntacticIndex: BuildTargetListener, Sendable { return } - guard let url = uri.fileURL else { - logger.log("Not indexing \(uri.forLogging) because it is not a file URL") - return - } let snapshot: DocumentSnapshot? = orLog("Getting document snapshot for syntactic Swift scanning") { try DocumentSnapshot(withContentsFromDisk: url, language: .swift) } @@ -265,7 +264,9 @@ package actor SwiftSyntacticIndex: BuildTargetListener, Sendable { return } - let (testItems, playgrounds) = await (syntacticTests(snapshot), syntacticPlaygrounds(snapshot)) + let (testItems, playgrounds) = await ( + syntacticTests(snapshot, workspace), syntacticPlaygrounds(snapshot, workspace) + ) guard !removedFiles.contains(uri) else { // Check whether the file got removed while we were scanning it for tests. If so, don't add it back to diff --git a/Sources/SourceKitLSP/TestDiscovery.swift b/Sources/SourceKitLSP/TestDiscovery.swift index 90f3a90f..cf630153 100644 --- a/Sources/SourceKitLSP/TestDiscovery.swift +++ b/Sources/SourceKitLSP/TestDiscovery.swift @@ -245,9 +245,7 @@ extension SourceKitLSPServer { let semanticTestSymbolOccurrences = index?.unitTests().filter { return $0.canBeTestDefinition } ?? [] - let testsFromSyntacticIndex = await languageServices.values.asyncFlatMap { - await $0.asyncFlatMap { await $0.syntacticTests(in: workspace) } - } + let testsFromSyntacticIndex = await workspace.syntacticIndex.tests() let testsFromSemanticIndex = testItems( for: semanticTestSymbolOccurrences, index: index, diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index a4a17bad..0ebcfa69 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -184,13 +184,12 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { } } + /// The index that syntactically scans the workspace for Swift symbols. + let syntacticIndex: SwiftSyntacticIndex + /// Language service for an open document, if available. private let languageServices: ThreadSafeBox<[DocumentURI: [any LanguageService]]> = ThreadSafeBox(initialValue: [:]) - /// Build target listeners - private let buildTargetListeners: ThreadSafeBox<[ObjectIdentifier: BuildTargetListener]> = ThreadSafeBox( - initialValue: [:]) - /// The task that constructs the `SemanticIndexManager`, which keeps track of whose file's index is up-to-date in the /// workspace and schedules indexing and preparation tasks for files with out-of-date index. /// @@ -261,6 +260,25 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { return nil } } + // Trigger an initial population of `syntacticIndex`. + self.syntacticIndex = SwiftSyntacticIndex( + determineFilesToScan: { targets in + await orLog("Getting list of files for syntactic index population") { + try await buildServerManager.projectSourceFiles(in: targets) + } ?? [] + }, + syntacticTests: { (snapshot, workspace) in + await sourceKitLSPServer.languageServices(for: snapshot.uri, snapshot.language, in: workspace).asyncFlatMap { + await $0.syntacticTestItems(for: snapshot) + } + }, + syntacticPlaygrounds: { (snapshot, workspace) in + await sourceKitLSPServer.languageServices(for: snapshot.uri, snapshot.language, in: workspace).asyncFlatMap { + await $0.syntacticPlaygrounds(for: snapshot, in: workspace) + } + } + ) + await syntacticIndex.scan(workspace: self) } /// Creates a workspace for a given root `DocumentURI`, inferring the `ExternalWorkspace` if possible. @@ -399,9 +417,9 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { // Notify all clients about the reported and inferred edits. await buildServerManager.filesDidChange(events) - async let updateServer: Void? = await sourceKitLSPServer?.filesDidChange(events) + async let updateSyntacticIndex: Void = await syntacticIndex.filesDidChange(events, self) async let updateSemanticIndex: Void? = await semanticIndexManager?.filesDidChange(events) - _ = await (updateServer, updateSemanticIndex) + _ = await (updateSyntacticIndex, updateSemanticIndex) } /// The language services that can handle the given document. Callers should try to merge the results from the @@ -464,26 +482,12 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { await sourceKitLSPServer?.fileHandlingCapabilityChanged() await semanticIndexManager?.buildTargetsChanged(changedTargets) await orLog("Scheduling syntactic file re-indexing") { - _ = await buildTargetListeners.value.values.asyncMap { - await $0.buildTargetsChanged(changedTargets) - } + await syntacticIndex.buildTargetsChanged(changedTargets, self) } await scheduleUpdateOfUnitOutputPathsInIndexIfNecessary() } - package func addBuiltTargetListener(_ listener: any BuildTargetListener) { - buildTargetListeners.withLock { - $0[ObjectIdentifier(listener)] = listener - } - } - - package func removeBuiltTargetListener(_ listener: any BuildTargetListener) { - buildTargetListeners.withLock { - $0[ObjectIdentifier(listener)] = nil - } - } - private func scheduleUpdateOfUnitOutputPathsInIndexIfNecessary() async { indexUnitOutputPathsUpdateQueue.async { guard await self.uncheckedIndex?.usesExplicitOutputPaths ?? false else { diff --git a/Sources/SwiftLanguageService/PlaygroundDiscovery.swift b/Sources/SwiftLanguageService/PlaygroundDiscovery.swift index 4d445a9f..8329b3a1 100644 --- a/Sources/SwiftLanguageService/PlaygroundDiscovery.swift +++ b/Sources/SwiftLanguageService/PlaygroundDiscovery.swift @@ -13,24 +13,18 @@ import BuildServerIntegration @_spi(SourceKitLSP) import BuildServerProtocol import Foundation -@_spi(SourceKitLSP) import LanguageServerProtocol +@_spi(SourceKitLSP) package import LanguageServerProtocol @_spi(SourceKitLSP) import SKLogging import SemanticIndex -import SourceKitLSP +package import SourceKitLSP import SwiftExtensions -import ToolchainRegistry extension SwiftLanguageService { - static func syntacticPlaygrounds( + package func syntacticPlaygrounds( for snapshot: DocumentSnapshot, - in workspace: Workspace, - using syntaxTreeManager: SyntaxTreeManager, - toolchain: Toolchain + in workspace: Workspace ) async -> [TextDocumentPlayground] { - guard toolchain.swiftPlay != nil else { - return [] - } - return await SwiftPlaygroundsScanner.findDocumentPlaygrounds( + await SwiftPlaygroundsScanner.findDocumentPlaygrounds( for: snapshot, workspace: workspace, syntaxTreeManager: syntaxTreeManager diff --git a/Sources/SwiftLanguageService/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift index d8011022..a82a5ffd 100644 --- a/Sources/SwiftLanguageService/SwiftLanguageService.swift +++ b/Sources/SwiftLanguageService/SwiftLanguageService.swift @@ -139,9 +139,6 @@ package actor SwiftLanguageService: LanguageService, Sendable { /// Shared syntax tree manager to share syntax trees when syntactically parsing different types let syntaxTreeManager: SyntaxTreeManager - /// The index that syntactically scans the workspace. - let syntacticIndex: SwiftSyntacticIndex - /// Workspace this language service was created for let workspace: Workspace @@ -279,27 +276,6 @@ package actor SwiftLanguageService: LanguageService, Sendable { clientHasDiagnosticsCodeDescriptionSupport: await capabilityRegistry.clientHasDiagnosticsCodeDescriptionSupport ) - // Trigger an initial population of `syntacticIndex`. - self.syntacticIndex = SwiftSyntacticIndex( - determineFilesToScan: { targets in - await orLog("Getting list of files for syntactic index population") { - try await workspace.buildServerManager.projectSourceFiles(in: targets) - } ?? [] - }, - syntacticTests: { - await SwiftLanguageService.syntacticTestItems(for: $0, using: syntaxTreeManager) - }, - syntacticPlaygrounds: { - await SwiftLanguageService.syntacticPlaygrounds( - for: $0, - in: workspace, - using: syntaxTreeManager, - toolchain: toolchain - ) - } - ) - workspace.addBuiltTargetListener(syntacticIndex) - self.macroExpansionManager = MacroExpansionManager(swiftLanguageService: self) self.generatedInterfaceManager = GeneratedInterfaceManager(swiftLanguageService: self) @@ -397,10 +373,6 @@ package actor SwiftLanguageService: LanguageService, Sendable { ) { self.stateChangeHandlers.append(handler) } - - package func filesDidChange(_ events: [FileEvent]) async { - await syntacticIndex.filesDidChange(events) - } } extension SwiftLanguageService { @@ -458,7 +430,6 @@ extension SwiftLanguageService { } package func shutdown() async { - self.workspace.removeBuiltTargetListener(syntacticIndex) await self.sourcekitd.removeNotificationHandler(self) } @@ -1154,14 +1125,6 @@ extension SwiftLanguageService { ) } } - - package func syntacticTests(in workspace: Workspace) async -> [AnnotatedTestItem] { - await syntacticIndex.tests() - } - - package func syntacticPlaygrounds(in workspace: Workspace) async -> [Playground] { - await syntacticIndex.playgrounds() - } } extension SwiftLanguageService: SKDNotificationHandler { diff --git a/Sources/SwiftLanguageService/TestDiscovery.swift b/Sources/SwiftLanguageService/TestDiscovery.swift index b18333cc..f645349b 100644 --- a/Sources/SwiftLanguageService/TestDiscovery.swift +++ b/Sources/SwiftLanguageService/TestDiscovery.swift @@ -52,9 +52,8 @@ extension SwiftLanguageService { /// Does not write the results to the index. /// /// The order of the returned tests is not defined. The results should be sorted before being returned to the editor. - static package func syntacticTestItems( + package func syntacticTestItems( for snapshot: DocumentSnapshot, - using syntaxTreeManager: SyntaxTreeManager ) async -> [AnnotatedTestItem] { async let swiftTestingTests = SyntacticSwiftTestingTestScanner.findTestSymbols( in: snapshot,