Skip to content

Commit e16f8c6

Browse files
authored
Merge pull request #1068 from ahoppen/ahoppen/cancel-publish-diags
Cancel the sourcekitd request to retrieve diagnostics if noone depends on it anymore
2 parents 6b0786e + 26bbe36 commit e16f8c6

File tree

3 files changed

+67
-12
lines changed

3 files changed

+67
-12
lines changed

Sources/SKSupport/AsyncUtils.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,55 @@
1212

1313
import Foundation
1414

15+
/// Wrapper around a task that allows multiple clients to depend on the task's value.
16+
///
17+
/// If all of the dependents are cancelled, the underlying task is cancelled as well.
18+
public actor RefCountedCancellableTask<Success> {
19+
public let task: Task<Success, Error>
20+
21+
/// The number of clients that depend on the task's result and that are not cancelled.
22+
private var refCount: Int = 0
23+
24+
/// Whether the task has been cancelled.
25+
public private(set) var isCancelled: Bool = false
26+
27+
public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) {
28+
self.task = Task(priority: priority, operation: operation)
29+
}
30+
31+
private func decrementRefCount() {
32+
refCount -= 1
33+
if refCount == 0 {
34+
self.cancel()
35+
}
36+
}
37+
38+
/// Get the task's value.
39+
///
40+
/// If all callers of `value` are cancelled, the underlying task gets cancelled as well.
41+
public var value: Success {
42+
get async throws {
43+
if isCancelled {
44+
throw CancellationError()
45+
}
46+
refCount += 1
47+
return try await withTaskCancellationHandler {
48+
return try await task.value
49+
} onCancel: {
50+
Task {
51+
await self.decrementRefCount()
52+
}
53+
}
54+
}
55+
}
56+
57+
/// Cancel the task and throw a `CancellationError` to all clients that are awaiting the value.
58+
public func cancel() {
59+
isCancelled = true
60+
task.cancel()
61+
}
62+
}
63+
1564
public extension Task {
1665
/// Awaits the value of the result.
1766
///

Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212

1313
import LSPLogging
1414
import LanguageServerProtocol
15+
import SKSupport
1516
import SourceKitD
1617
import SwiftParserDiagnostics
1718

1819
actor DiagnosticReportManager {
1920
/// A task to produce diagnostics, either from a diagnostics request to `sourcektid` or by using the built-in swift-syntax.
20-
private typealias ReportTask = Task<RelatedFullDocumentDiagnosticReport, Error>
21+
private typealias ReportTask = RefCountedCancellableTask<RelatedFullDocumentDiagnosticReport>
2122

2223
private let sourcekitd: SourceKitD
2324
private let syntaxTreeManager: SyntaxTreeManager
@@ -60,13 +61,13 @@ actor DiagnosticReportManager {
6061
for snapshot: DocumentSnapshot,
6162
buildSettings: SwiftCompileCommand?
6263
) async throws -> RelatedFullDocumentDiagnosticReport {
63-
if let reportTask = reportTask(for: snapshot.id, buildSettings: buildSettings) {
64+
if let reportTask = reportTask(for: snapshot.id, buildSettings: buildSettings), await !reportTask.isCancelled {
6465
return try await reportTask.value
6566
}
66-
let reportTask: Task<RelatedFullDocumentDiagnosticReport, Error>
67+
let reportTask: ReportTask
6768
if let buildSettings, !buildSettings.isFallback {
68-
reportTask = Task {
69-
return try await requestReport(with: snapshot, compilerArgs: buildSettings.compilerArgs)
69+
reportTask = ReportTask {
70+
return try await self.requestReport(with: snapshot, compilerArgs: buildSettings.compilerArgs)
7071
}
7172
} else {
7273
logger.log(
@@ -76,21 +77,20 @@ actor DiagnosticReportManager {
7677
// sourcekitd won't be able to give us accurate semantic diagnostics.
7778
// Fall back to providing syntactic diagnostics from the built-in
7879
// swift-syntax. That's the best we can do for now.
79-
reportTask = Task {
80-
return try await requestFallbackReport(with: snapshot)
80+
reportTask = ReportTask {
81+
return try await self.requestFallbackReport(with: snapshot)
8182
}
8283
}
8384
setReportTask(for: snapshot.id, buildSettings: buildSettings, reportTask: reportTask)
8485
return try await reportTask.value
8586
}
8687

8788
func removeItemsFromCache(with uri: DocumentURI) async {
88-
for item in reportTaskCache {
89-
if item.snapshotID.uri == uri {
90-
item.reportTask.cancel()
91-
}
92-
}
89+
let tasksToCancel = reportTaskCache.filter { $0.snapshotID.uri == uri }
9390
reportTaskCache.removeAll(where: { $0.snapshotID.uri == uri })
91+
for task in tasksToCancel {
92+
await task.reportTask.cancel()
93+
}
9494
}
9595

9696
private func requestReport(

Tests/SourceKitLSPTests/LifecycleTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,16 @@ final class LifecycleTests: XCTestCase {
9999
)
100100
)
101101

102+
let cursorInfoStartDate = Date()
102103
// Check that semantic functionality based on the AST is working again.
103104
let symbolInfo = try await testClient.send(
104105
SymbolInfoRequest(textDocument: TextDocumentIdentifier(uri), position: positions["3️⃣"])
105106
)
107+
XCTAssertLessThan(
108+
Date().timeIntervalSince(cursorInfoStartDate),
109+
2,
110+
"Cursor info request wasn't fast. sourcekitd still blocked?"
111+
)
106112
XCTAssertGreaterThan(symbolInfo.count, 0)
107113
}
108114
}

0 commit comments

Comments
 (0)