@@ -86,6 +86,17 @@ public final actor SemanticIndexManager {
8686 /// After the file is indexed, it is removed from this dictionary.
8787 private var inProgressIndexTasks : [ DocumentURI : InProgressIndexStore ] = [ : ]
8888
89+ /// The currently running task that prepares a document for editor functionality.
90+ ///
91+ /// This is used so we can cancel preparation tasks for documents that the user is no longer interacting with and
92+ /// avoid the following scenario: The user browses through documents from targets A, B, and C in quick succession. We
93+ /// don't want stack preparation of A, B, and C. Instead we want to only prepare target C - and also finish
94+ /// preparation of A if it has already started when the user opens C.
95+ ///
96+ /// `id` is a unique ID that identifies the preparation task and is used to set `inProgressPrepareForEditorTask` to
97+ /// `nil` when the current in progress task finishes.
98+ private var inProgressPrepareForEditorTask : ( id: UUID , document: DocumentURI , task: Task < Void , Never > ) ? = nil
99+
89100 /// The `TaskScheduler` that manages the scheduling of index tasks. This is shared among all `SemanticIndexManager`s
90101 /// in the process, to ensure that we don't schedule more index operations than processor cores from multiple
91102 /// workspaces.
@@ -287,20 +298,39 @@ public final actor SemanticIndexManager {
287298 /// Schedule preparation of the target that contains the given URI, building all modules that the file depends on.
288299 ///
289300 /// This is intended to be called when the user is interacting with the document at the given URI.
290- public func schedulePreparation( of uri: DocumentURI , priority: TaskPriority ? = nil ) {
291- Task ( priority: priority) {
301+ public func schedulePreparationForEditorFunctionality(
302+ of uri: DocumentURI ,
303+ priority: TaskPriority ? = nil
304+ ) {
305+ if inProgressPrepareForEditorTask? . document == uri {
306+ // We are already preparing this document, so nothing to do. This is necessary to avoid the following scenario:
307+ // Determining the canonical configured target for a document takes 1s and we get a new document request for the
308+ // document ever 0.5s, which would cancel the previous in-progress preparation task, cancelling the canonical
309+ // configured target configuration, never actually getting to the actual preparation.
310+ return
311+ }
312+ let id = UUID ( )
313+ let task = Task ( priority: priority) {
292314 await withLoggingScope ( " preparation " ) {
293315 guard let target = await buildSystemManager. canonicalConfiguredTarget ( for: uri) else {
294316 return
295317 }
318+ if Task . isCancelled {
319+ return
320+ }
296321 await self . prepare ( targets: [ target] , priority: priority)
322+ if inProgressPrepareForEditorTask? . id == id {
323+ inProgressPrepareForEditorTask = nil
324+ }
297325 }
298326 }
327+ inProgressPrepareForEditorTask? . task. cancel ( )
328+ inProgressPrepareForEditorTask = ( id, uri, task)
299329 }
300330
301331 // MARK: - Helper functions
302332
303- /// Prepare the given targets for indexing
333+ /// Prepare the given targets for indexing.
304334 private func prepare( targets: [ ConfiguredTarget ] , priority: TaskPriority ? ) async {
305335 // Perform a quick initial check whether the target is up-to-date, in which case we don't need to schedule a
306336 // preparation operation at all.
@@ -323,6 +353,9 @@ public final actor SemanticIndexManager {
323353 testHooks: testHooks
324354 )
325355 )
356+ if Task . isCancelled {
357+ return
358+ }
326359 let preparationTask = await indexTaskScheduler. schedule ( priority: priority, taskDescription) { task, newState in
327360 guard case . finished = newState else {
328361 return
@@ -337,7 +370,17 @@ public final actor SemanticIndexManager {
337370 for target in targetsToPrepare {
338371 inProgressPreparationTasks [ target] = OpaqueQueuedIndexTask ( preparationTask)
339372 }
340- return await preparationTask. waitToFinishPropagatingCancellation ( )
373+ await withTaskCancellationHandler {
374+ return await preparationTask. waitToFinish ( )
375+ } onCancel: {
376+ // Only cancel the preparation task if it hasn't started executing yet. This ensures that we always make progress
377+ // during preparation and can't get into the following scenario: The user has two target A and B that both take
378+ // 10s to prepare. The user is now switching between the files every 5 seconds, which would always cause
379+ // preparation for one target to get cancelled, never resulting in an up-to-date preparation status.
380+ if !preparationTask. isExecuting {
381+ preparationTask. cancel ( )
382+ }
383+ }
341384 }
342385
343386 /// Update the index store for the given files, assuming that their targets have already been prepared.
0 commit comments