Files
sourcekit-lsp/Sources/SourceKitLSP/IndexProgressManager.swift
Alex Hoppen d10c868497 Support indexing a file in the context of multiple targets
If a source file is part of multiple targets, we should index it in the context of all of those targets because the different targets may produce different USRs since they might use different build settings to interpret the file.
2025-03-14 15:49:59 -07:00

141 lines
5.9 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 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
//
//===----------------------------------------------------------------------===//
import LanguageServerProtocol
import LanguageServerProtocolExtensions
import SKLogging
import SKOptions
import SemanticIndex
import SwiftExtensions
/// Listens for index status updates from `SemanticIndexManagers`. From that information, it manages a
/// `WorkDoneProgress` that communicates the index progress to the editor.
actor IndexProgressManager {
/// A queue on which `indexTaskWasQueued` and `indexProgressStatusDidChange` are handled.
///
/// This allows the two functions two be `nonisolated` (and eg. the caller of `indexProgressStatusDidChange` doesn't have to
/// wait for the work done progress to be updated) while still guaranteeing that there is only one
/// `indexProgressStatusDidChangeImpl` running at a time, preventing race conditions that would cause two
/// `WorkDoneProgressManager`s to be created.
private let queue = AsyncQueue<Serial>()
/// The `SourceKitLSPServer` for which this manages the index progress. It gathers all `SemanticIndexManagers` from
/// the workspaces in the `SourceKitLSPServer`.
private weak var sourceKitLSPServer: SourceKitLSPServer?
/// This is the target number of index tasks (eg. the `3` in `1/3 done`).
///
/// Every time a new index task is scheduled, this number gets incremented, so that it only ever increases.
/// When indexing of one session is done (ie. when there are no more `scheduled` or `executing` tasks in any
/// `SemanticIndexManager`), `queuedIndexTasks` gets reset to 0 and the work done progress gets ended.
/// This way, when the next work done progress is started, it starts at zero again.
///
/// The number of outstanding tasks is determined from the `scheduled` and `executing` tasks in all the
/// `SemanticIndexManager`s.
///
/// Note that the `queuedIndexTasks` might exceed the number of files in the project, eg. in the following scenario:
/// - Schedule indexing of A.swift and B.swift -> 0 / 2
/// - Indexing of A.swift finishes -> 1 / 2
/// - A.swift is modified and should be indexed again -> 1 / 3
/// - B.swift finishes indexing -> 2 / 3
/// - A.swift finishes indexing for the second time -> 3 / 3 -> Status disappears
private var queuedIndexTasks = 0
/// While there are ongoing index tasks, a `WorkDoneProgressManager` that displays the work done progress.
private var workDoneProgress: WorkDoneProgressManager?
init(sourceKitLSPServer: SourceKitLSPServer) {
self.sourceKitLSPServer = sourceKitLSPServer
}
/// Called when a new file is scheduled to be indexed. Increments the target index count, eg. the 3 in `1/3`.
nonisolated func indexTasksWereScheduled(count: Int) {
queue.async {
await self.indexTasksWereScheduledImpl(count: count)
}
}
private func indexTasksWereScheduledImpl(count: Int) async {
queuedIndexTasks += count
await indexProgressStatusDidChangeImpl()
}
/// Called when a `SemanticIndexManager` finishes indexing a file. Adjusts the done index count, eg. the 1 in `1/3`.
nonisolated func indexProgressStatusDidChange() {
queue.async {
await self.indexProgressStatusDidChangeImpl()
}
}
private func indexProgressStatusDidChangeImpl() async {
guard let sourceKitLSPServer else {
await workDoneProgress?.end()
workDoneProgress = nil
return
}
var status = IndexProgressStatus.upToDate
for indexManager in await sourceKitLSPServer.workspaces.compactMap({ $0.semanticIndexManager }) {
status = status.merging(with: await indexManager.progressStatus)
}
var message: String
let percentage: Int
switch status {
case .preparingFileForEditorFunctionality:
message = "Preparing current file"
percentage = 0
case .schedulingIndexing:
message = "Scheduling tasks"
percentage = 0
case .indexing(preparationTasks: let preparationTasks, indexTasks: let indexTasks):
// We can get into a situation where queuedIndexTasks < indexTasks.count if we haven't processed all
// `indexTasksWereScheduled` calls yet but the semantic index managers already track them in their in-progress tasks.
// Clip the finished tasks to 0 because showing a negative number there looks stupid.
let finishedTasks = max(queuedIndexTasks - indexTasks.count, 0)
if indexTasks.isEmpty {
message = "Preparing targets"
if preparationTasks.isEmpty {
logger.fault("Indexer status is 'indexing' but there is no update indexstore or preparation task")
}
} else {
message = "\(finishedTasks) / \(queuedIndexTasks)"
}
logger.debug("In-progress index tasks: \(indexTasks.map(\.key.file.sourceFile))")
if queuedIndexTasks != 0 {
percentage = Int(Double(finishedTasks) / Double(queuedIndexTasks) * 100)
} else {
percentage = 0
}
case .upToDate:
// Nothing left to index. Reset the target count and dismiss the work done progress.
queuedIndexTasks = 0
await workDoneProgress?.end()
workDoneProgress = nil
return
}
if let workDoneProgress {
await workDoneProgress.update(message: message, percentage: percentage)
} else {
workDoneProgress = WorkDoneProgressManager(
server: sourceKitLSPServer,
capabilityRegistry: await sourceKitLSPServer.capabilityRegistry,
tokenPrefix: "indexing",
initialDebounce: sourceKitLSPServer.options.workDoneProgressDebounceDurationOrDefault,
title: "Indexing",
message: message,
percentage: percentage
)
}
}
}