Skip to content

Commit fccbc39

Browse files
committed
Migrate SwiftPMWorkspace to be an actor
1 parent e663bbc commit fccbc39

File tree

7 files changed

+181
-156
lines changed

7 files changed

+181
-156
lines changed

Sources/LSPTestSupport/Assertions.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@ public func assertNoThrow<T>(
2222
XCTAssertNoThrow(try expression(), message(), file: file, line: line)
2323
}
2424

25+
/// Same as `XCTAssertThrows` but executes the trailing closure.
26+
public func assertThrowsError<T>(
27+
_ expression: @autoclosure () async throws -> T,
28+
_ message: @autoclosure () -> String = "",
29+
file: StaticString = #filePath,
30+
line: UInt = #line,
31+
_ errorHandler: (_ error: Error) -> Void = { _ in }
32+
) async {
33+
let didThrow: Bool
34+
do {
35+
_ = try await expression()
36+
didThrow = false
37+
} catch {
38+
errorHandler(error)
39+
didThrow = true
40+
}
41+
if !didThrow {
42+
XCTFail("Expression was expected to throw but did not throw", file: file, line: line)
43+
}
44+
}
45+
2546
/// Same as `XCTAssertEqual` but doesn't take autoclosures and thus `expression1`
2647
/// and `expression2` can contain `await`.
2748
public func assertEqual<T: Equatable>(
@@ -34,6 +55,28 @@ public func assertEqual<T: Equatable>(
3455
XCTAssertEqual(expression1, expression2, message(), file: file, line: line)
3556
}
3657

58+
/// Same as `XCTAssertNil` but doesn't take autoclosures and thus `expression`
59+
/// can contain `await`.
60+
public func assertNil<T: Equatable>(
61+
_ expression: T?,
62+
_ message: @autoclosure () -> String = "",
63+
file: StaticString = #filePath,
64+
line: UInt = #line
65+
) {
66+
XCTAssertNil(expression, message(), file: file, line: line)
67+
}
68+
69+
/// Same as `XCTAssertNotNil` but doesn't take autoclosures and thus `expression`
70+
/// can contain `await`.
71+
public func assertNotNil<T: Equatable>(
72+
_ expression: T?,
73+
_ message: @autoclosure () -> String = "",
74+
file: StaticString = #filePath,
75+
line: UInt = #line
76+
) {
77+
XCTAssertNotNil(expression, message(), file: file, line: line)
78+
}
79+
3780
extension XCTestCase {
3881
private struct ExpectationNotFulfilledError: Error, CustomStringConvertible {
3982
var expecatations: [XCTestExpectation]

Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift

Lines changed: 31 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public enum ReloadPackageStatus {
4848
/// This class implements the `BuildSystem` interface to provide the build settings for a Swift
4949
/// Package Manager (SwiftPM) package. The settings are determined by loading the Package.swift
5050
/// manifest using `libSwiftPM` and constructing a build plan using the default (debug) parameters.
51-
public final class SwiftPMWorkspace {
51+
public actor SwiftPMWorkspace {
5252

5353
public enum Error: Swift.Error {
5454

@@ -82,14 +82,6 @@ public final class SwiftPMWorkspace {
8282
/// mapped to the language the delegate specified when registering for change notifications.
8383
var watchedFiles: [DocumentURI: Language] = [:]
8484

85-
/// Queue guarding the following properties:
86-
/// - `delegate`
87-
/// - `watchedFiles`
88-
/// - `packageGraph`
89-
/// - `fileToTarget`
90-
/// - `sourceDirToTarget`
91-
let queue: DispatchQueue = .init(label: "SwiftPMWorkspace.queue", qos: .utility)
92-
9385
/// This callback is informed when `reloadPackage` starts and ends executing.
9486
var reloadPackageStatusCallback: (ReloadPackageStatus) -> Void
9587

@@ -108,8 +100,7 @@ public final class SwiftPMWorkspace {
108100
fileSystem: FileSystem = localFileSystem,
109101
buildSetup: BuildSetup,
110102
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) -> Void = { _ in }
111-
) throws
112-
{
103+
) async throws {
113104
self.workspacePath = workspacePath
114105
self.fileSystem = fileSystem
115106

@@ -169,15 +160,14 @@ public final class SwiftPMWorkspace {
169160
/// - Parameters:
170161
/// - reloadPackageStatusCallback: Will be informed when `reloadPackage` starts and ends executing.
171162
/// - Returns: nil if `workspacePath` is not part of a package or there is an error.
172-
public convenience init?(
163+
public init?(
173164
url: URL,
174165
toolchainRegistry: ToolchainRegistry,
175166
buildSetup: BuildSetup,
176167
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) -> Void
177-
)
178-
{
168+
) async {
179169
do {
180-
try self.init(
170+
try await self.init(
181171
workspacePath: try TSCAbsolutePath(validating: url.path),
182172
toolchainRegistry: toolchainRegistry,
183173
fileSystem: localFileSystem,
@@ -198,7 +188,6 @@ extension SwiftPMWorkspace {
198188

199189
/// (Re-)load the package settings by parsing the manifest and resolving all the targets and
200190
/// dependencies.
201-
/// Must only be called on `queue` or from the initializer.
202191
func reloadPackage() throws {
203192
reloadPackageStatusCallback(.start)
204193
defer {
@@ -256,7 +245,7 @@ extension SwiftPMWorkspace {
256245
var changedFiles: [DocumentURI: FileBuildSettingsChange] = [:]
257246
for (uri, language) in self.watchedFiles {
258247
orLog {
259-
if let settings = try self.settings(for: uri, language) {
248+
if let settings = try self.buildSettings(for: uri, language: language) {
260249
changedFiles[uri] = FileBuildSettingsChange(settings)
261250
} else {
262251
changedFiles[uri] = .removedOrUnavailable
@@ -289,29 +278,10 @@ extension SwiftPMWorkspace: SKCore.BuildSystem {
289278
for uri: DocumentURI,
290279
_ language: Language) throws -> FileBuildSettings?
291280
{
292-
return try queue.sync {
293-
try self.settings(for: uri, language)
294-
}
281+
try self.buildSettings(for: uri, language: language)
295282
}
296283

297-
public func buildSettings(for document: DocumentURI, language: Language) async throws -> FileBuildSettings? {
298-
return try await withCheckedThrowingContinuation { continuation in
299-
queue.async {
300-
do {
301-
continuation.resume(returning: try self.settings(for: document, language))
302-
} catch {
303-
continuation.resume(throwing: error)
304-
}
305-
}
306-
}
307-
}
308-
309-
/// Must only be called on `queue`.
310-
private func settings(
311-
for uri: DocumentURI,
312-
_ language: Language) throws -> FileBuildSettings?
313-
{
314-
dispatchPrecondition(condition: .onQueue(queue))
284+
public func buildSettings(for uri: DocumentURI, language: Language) throws -> FileBuildSettings? {
315285
guard let url = uri.fileURL else {
316286
// We can't determine build settings for non-file URIs.
317287
return nil
@@ -336,37 +306,31 @@ extension SwiftPMWorkspace: SKCore.BuildSystem {
336306
}
337307

338308
public func registerForChangeNotifications(for uri: DocumentURI, language: Language) {
339-
queue.async {
340-
assert(self.watchedFiles[uri] == nil, "Registered twice for change notifications of the same URI")
341-
guard let delegate = self.delegate else { return }
342-
self.watchedFiles[uri] = language
343-
344-
var settings: FileBuildSettings? = nil
345-
do {
346-
settings = try self.settings(for: uri, language)
347-
} catch {
348-
log("error computing settings: \(error)")
349-
}
350-
if let settings = settings {
351-
delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
352-
} else {
353-
delegate.fileBuildSettingsChanged([uri: .removedOrUnavailable])
354-
}
309+
assert(self.watchedFiles[uri] == nil, "Registered twice for change notifications of the same URI")
310+
guard let delegate = self.delegate else { return }
311+
self.watchedFiles[uri] = language
312+
313+
var settings: FileBuildSettings? = nil
314+
do {
315+
settings = try self.buildSettings(for: uri, language: language)
316+
} catch {
317+
log("error computing settings: \(error)")
318+
}
319+
if let settings = settings {
320+
delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)])
321+
} else {
322+
delegate.fileBuildSettingsChanged([uri: .removedOrUnavailable])
355323
}
356324
}
357325

358326
/// Unregister the given file for build-system level change notifications, such as command
359327
/// line flag changes, dependency changes, etc.
360328
public func unregisterForChangeNotifications(for uri: DocumentURI) {
361-
queue.async {
362-
self.watchedFiles[uri] = nil
363-
}
329+
self.watchedFiles[uri] = nil
364330
}
365331

366332
/// Returns the resolved target description for the given file, if one is known.
367-
/// Must only be called on `queue`.
368333
private func targetDescription(for file: AbsolutePath) throws -> TargetBuildDescription? {
369-
dispatchPrecondition(condition: .onQueue(queue))
370334
if let td = fileToTarget[file] {
371335
return td
372336
}
@@ -403,12 +367,10 @@ extension SwiftPMWorkspace: SKCore.BuildSystem {
403367
}
404368

405369
public func filesDidChange(_ events: [FileEvent]) {
406-
queue.async {
407-
if events.contains(where: { self.fileEventShouldTriggerPackageReload(event: $0) }) {
408-
orLog {
409-
// TODO: It should not be necessary to reload the entire package just to get build settings for one file.
410-
try self.reloadPackage()
411-
}
370+
if events.contains(where: { self.fileEventShouldTriggerPackageReload(event: $0) }) {
371+
orLog {
372+
// TODO: It should not be necessary to reload the entire package just to get build settings for one file.
373+
try self.reloadPackage()
412374
}
413375
}
414376
}
@@ -417,12 +379,10 @@ extension SwiftPMWorkspace: SKCore.BuildSystem {
417379
guard let fileUrl = uri.fileURL else {
418380
return .unhandled
419381
}
420-
return self.queue.sync {
421-
if (try? targetDescription(for: AbsolutePath(validating: fileUrl.path))) != nil {
422-
return .handled
423-
} else {
424-
return .unhandled
425-
}
382+
if (try? targetDescription(for: AbsolutePath(validating: fileUrl.path))) != nil {
383+
return .handled
384+
} else {
385+
return .unhandled
426386
}
427387
}
428388
}
@@ -450,9 +410,7 @@ extension SwiftPMWorkspace {
450410
}
451411

452412
/// Retrieve settings for a package manifest (Package.swift).
453-
/// Must only be called on `queue`.
454413
private func settings(forPackageManifest path: AbsolutePath) throws -> FileBuildSettings? {
455-
dispatchPrecondition(condition: .onQueue(queue))
456414
func impl(_ path: AbsolutePath) -> FileBuildSettings? {
457415
for package in packageGraph.packages where path == package.manifest.path {
458416
let compilerArgs = workspace.interpreterFlags(for: package.path) + [path.pathString]
@@ -470,9 +428,7 @@ extension SwiftPMWorkspace {
470428
}
471429

472430
/// Retrieve settings for a given header file.
473-
/// Must only be called on `queue`.
474431
private func settings(forHeader path: AbsolutePath, _ language: Language) throws -> FileBuildSettings? {
475-
dispatchPrecondition(condition: .onQueue(queue))
476432
func impl(_ path: AbsolutePath) throws -> FileBuildSettings? {
477433
var dir = path.parentDirectory
478434
while !dir.isRoot {

Sources/SKTestSupport/SKSwiftPMTestWorkspace.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,19 @@ public final class SKSwiftPMTestWorkspace {
8585
let buildPath = try AbsolutePath(validating: buildDir.path)
8686
let buildSetup = BuildSetup(configuration: .debug, path: buildPath, flags: BuildFlags())
8787

88-
let swiftpm = try SwiftPMWorkspace(
88+
let swiftpm = try await SwiftPMWorkspace(
8989
workspacePath: sourcePath,
9090
toolchainRegistry: ToolchainRegistry.shared,
9191
buildSetup: buildSetup)
9292

9393
let libIndexStore = try IndexStoreLibrary(dylibPath: toolchain.libIndexStore!.pathString)
9494

95-
try fm.createDirectory(atPath: swiftpm.indexStorePath!.pathString, withIntermediateDirectories: true)
95+
try fm.createDirectory(atPath: await swiftpm.indexStorePath!.pathString, withIntermediateDirectories: true)
9696

9797
let indexDelegate = SourceKitIndexDelegate()
9898

9999
self.index = try IndexStoreDB(
100-
storePath: swiftpm.indexStorePath!.pathString,
100+
storePath: await swiftpm.indexStorePath!.pathString,
101101
databasePath: tmpDir.path,
102102
library: libIndexStore,
103103
delegate: indexDelegate,

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -987,14 +987,28 @@ extension SwiftLanguageServer {
987987
_ = handle
988988
}
989989

990-
public func foldingRange(_ req: Request<FoldingRangeRequest>) {
990+
public func foldingRange(_ req: Request<FoldingRangeRequest>) async {
991991
let foldingRangeCapabilities = capabilityRegistry.clientCapabilities.textDocument?.foldingRange
992-
guard let snapshot = self.documentManager.latestSnapshot(req.params.textDocument.uri) else {
992+
let uri = req.params.textDocument.uri
993+
guard var snapshot = self.documentManager.latestSnapshot(uri) else {
993994
log("failed to find snapshot for url \(req.params.textDocument.uri)")
994995
req.reply(nil)
995996
return
996997
}
997998

999+
// FIXME: (async) We might not have computed the syntax tree yet. Wait until we have a syntax tree.
1000+
// Really, getting the syntax tree should be an async operation.
1001+
while snapshot.tokens.syntaxTree == nil {
1002+
try? await Task.sleep(nanoseconds: 1_000_000)
1003+
if let newSnapshot = documentManager.latestSnapshot(uri) {
1004+
snapshot = newSnapshot
1005+
} else {
1006+
log("failed to find snapshot for url \(req.params.textDocument.uri)")
1007+
req.reply(nil)
1008+
return
1009+
}
1010+
}
1011+
9981012
guard let sourceFile = snapshot.tokens.syntaxTree else {
9991013
log("no lexical structure available for url \(req.params.textDocument.uri)")
10001014
req.reply(nil)

Sources/SourceKitLSP/Swift/VariableTypeInfo.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,21 @@ extension SwiftLanguageServer {
9494
_ range: Range<Position>? = nil,
9595
_ completion: @escaping (Swift.Result<[VariableTypeInfo], VariableTypeInfoError>) -> Void
9696
) async {
97-
guard let snapshot = documentManager.latestSnapshot(uri) else {
97+
guard var snapshot = documentManager.latestSnapshot(uri) else {
9898
return completion(.failure(.unknownDocument(uri)))
9999
}
100100

101+
// FIXME: (async) We might not have computed the syntax tree yet. Wait until we have a syntax tree.
102+
// Really, getting the syntax tree should be an async operation.
103+
while snapshot.tokens.syntaxTree == nil {
104+
try? await Task.sleep(nanoseconds: 1_000_000)
105+
if let newSnapshot = documentManager.latestSnapshot(uri) {
106+
snapshot = newSnapshot
107+
} else {
108+
return completion(.failure(.unknownDocument(uri)))
109+
}
110+
}
111+
101112
let keys = self.keys
102113

103114
let skreq = SKDRequestDictionary(sourcekitd: sourcekitd)

Sources/SourceKitLSP/Workspace.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public final class Workspace {
9999
if let rootUrl = rootUri.fileURL, let rootPath = try? AbsolutePath(validating: rootUrl.path) {
100100
if let buildServer = BuildServerBuildSystem(projectRoot: rootPath, buildSetup: buildSetup) {
101101
buildSystem = buildServer
102-
} else if let swiftpm = SwiftPMWorkspace(
102+
} else if let swiftpm = await SwiftPMWorkspace(
103103
url: rootUrl,
104104
toolchainRegistry: toolchainRegistry,
105105
buildSetup: buildSetup,

0 commit comments

Comments
 (0)