Move syntactic index back to Workspace

This commit is contained in:
Adam Ward
2025-11-27 15:26:59 -05:00
parent 9602433d2a
commit 308375a135
11 changed files with 72 additions and 114 deletions

View File

@@ -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<BuildTargetIdentifier>?) 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<BuildTargetIdentifier>?) async
}
/// Methods with which the `BuildServerManager` can send messages to the client (aka. editor).

View File

@@ -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 []
}

View File

@@ -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 []
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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<BuildTargetIdentifier>?) 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<BuildTargetIdentifier>?) 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<BuildTargetIdentifier>?) async {
package func buildTargetsChanged(_ changedTargets: Set<BuildTargetIdentifier>?, _ 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<DocumentURI> = []
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

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,