Skip to content

Commit 24317f1

Browse files
committed
Show a progress indicator in the editor if SourceKit-LSP is reloading packages
I noticed that the initial package loading can take ~5s. It’s good behavior to inform the client that sourcekit-lsp is busy reloading the package, showing the user that semantic functionality might not be ready yet. swiftlang#620 rdar://111917300
1 parent 644214a commit 24317f1

File tree

4 files changed

+141
-11
lines changed

4 files changed

+141
-11
lines changed

Sources/LSPTestSupport/TestJSONRPCConnection.swift

+7
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ public final class TestClient: MessageHandler {
137137
}
138138

139139
public func handle<R: RequestType>(_ params: R, id: RequestID, from clientID: ObjectIdentifier, reply: @escaping (LSPResult<R.Response>) -> Void) {
140+
if R.self == CreateWorkDoneProgressRequest.self {
141+
// We don’t want to require tests to specify request handlers for work done progress.
142+
// Simply ignore requests to create WorkDoneProgress for now.
143+
reply(.failure(.unknown("WorkDone not supported in TestClient")))
144+
return
145+
}
146+
140147
let cancellationToken = CancellationToken()
141148

142149
let request = Request(params, id: id, clientID: clientID, cancellation: cancellationToken, reply: reply)

Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift

+34-6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ import func TSCBasic.resolveSymlinks
3535
import protocol TSCBasic.FileSystem
3636
import var TSCBasic.localFileSystem
3737

38+
/// Parameter of `reloadPackageStatusCallback` in ``SwiftPMWorkspace``.
39+
///
40+
/// Informs the callback about whether `reloadPackage` started or finished executing.
41+
public enum ReloadPackageStatus {
42+
case start
43+
case end
44+
}
45+
3846
/// Swift Package Manager build system and workspace support.
3947
///
4048
/// This class implements the `BuildSystem` interface to provide the build settings for a Swift
@@ -78,18 +86,25 @@ public final class SwiftPMWorkspace {
7886
/// - `sourceDirToTarget`
7987
let queue: DispatchQueue = .init(label: "SwiftPMWorkspace.queue", qos: .utility)
8088

89+
/// This callback is informed when `reloadPackage` starts and ends executing.
90+
var reloadPackageStatusCallback: (ReloadPackageStatus) -> Void
91+
92+
8193
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
8294
///
8395
/// - Parameters:
8496
/// - workspace: The workspace root path.
8597
/// - toolchainRegistry: The toolchain registry to use to provide the Swift compiler used for
8698
/// manifest parsing and runtime support.
99+
/// - reloadPackageStatusCallback: Will be informed when `reloadPackage` starts and ends executing.
87100
/// - Throws: If there is an error loading the package, or no manifest is found.
88101
public init(
89102
workspacePath: TSCAbsolutePath,
90103
toolchainRegistry: ToolchainRegistry,
91104
fileSystem: FileSystem = localFileSystem,
92-
buildSetup: BuildSetup) throws
105+
buildSetup: BuildSetup,
106+
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) -> Void = { _ in }
107+
) throws
93108
{
94109
self.workspacePath = workspacePath
95110
self.fileSystem = fileSystem
@@ -142,23 +157,31 @@ public final class SwiftPMWorkspace {
142157
)
143158

144159
self.packageGraph = try PackageGraph(rootPackages: [], dependencies: [], binaryArtifacts: [:])
160+
self.reloadPackageStatusCallback = reloadPackageStatusCallback
145161

146162
try reloadPackage()
147163
}
148164

149165
/// Creates a build system using the Swift Package Manager, if this workspace is a package.
150-
///
166+
///
167+
/// - Parameters:
168+
/// - reloadPackageStatusCallback: Will be informed when `reloadPackage` starts and ends executing.
151169
/// - Returns: nil if `workspacePath` is not part of a package or there is an error.
152-
public convenience init?(url: URL,
153-
toolchainRegistry: ToolchainRegistry,
154-
buildSetup: BuildSetup)
170+
public convenience init?(
171+
url: URL,
172+
toolchainRegistry: ToolchainRegistry,
173+
buildSetup: BuildSetup,
174+
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) -> Void
175+
)
155176
{
156177
do {
157178
try self.init(
158179
workspacePath: try TSCAbsolutePath(validating: url.path),
159180
toolchainRegistry: toolchainRegistry,
160181
fileSystem: localFileSystem,
161-
buildSetup: buildSetup)
182+
buildSetup: buildSetup,
183+
reloadPackageStatusCallback: reloadPackageStatusCallback
184+
)
162185
} catch Error.noManifest(let path) {
163186
log("could not find manifest, or not a SwiftPM package: \(path)", level: .warning)
164187
return nil
@@ -175,6 +198,11 @@ extension SwiftPMWorkspace {
175198
/// dependencies.
176199
/// Must only be called on `queue` or from the initializer.
177200
func reloadPackage() throws {
201+
reloadPackageStatusCallback(.start)
202+
defer {
203+
reloadPackageStatusCallback(.end)
204+
}
205+
178206

179207
let observabilitySystem = ObservabilitySystem({ scope, diagnostic in
180208
log(diagnostic.description, level: diagnostic.severity.asLogLevel)

Sources/SourceKitLSP/SourceKitServer.swift

+92-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,84 @@ enum LanguageServerType: Hashable {
5858
}
5959
}
6060

61+
/// Keeps track of the state to send work done progress updates to the client
62+
final class WorkDoneProgressState {
63+
private enum State {
64+
/// No `WorkDoneProgress` has been created.
65+
case noProgress
66+
/// We have sent the request to create a `WorkDoneProgress` but haven’t received a respose yet.
67+
case creating
68+
/// A `WorkDoneProgress` has been created.
69+
case created
70+
/// The creation of a `WorkDoneProgress has failed`.
71+
///
72+
/// This causes us to just give up creating any more `WorkDoneProgress` in
73+
/// the future as those will most likely also fail.
74+
case progressCreationFailed
75+
}
76+
77+
/// How many active tasks are running.
78+
///
79+
/// A work done progress should be displayed if activeTasks > 0
80+
private var activeTasks: Int = 0
81+
private var state: State = .noProgress
82+
83+
/// The token by which we track the `WorkDoneProgress`.
84+
private let token: ProgressToken
85+
86+
/// The title that should be displayed to the user in the UI.
87+
private let title: String
88+
89+
init(_ token: String, title: String) {
90+
self.token = ProgressToken.string(token)
91+
self.title = title
92+
}
93+
94+
/// Start a new task, creating a new `WorkDoneProgress` if none is running right now.
95+
///
96+
/// - Parameter server: The server that is used to create the `WorkDoneProgress` on the client
97+
///
98+
/// - Important: Must be called on `server.queue`.
99+
func startProgress(server: SourceKitServer) {
100+
dispatchPrecondition(condition: .onQueue(server.queue))
101+
activeTasks += 1
102+
if state == .noProgress {
103+
state = .creating
104+
// Discard the handle. We don't support cancellation of the creation of a work done progress.
105+
_ = server.client.send(CreateWorkDoneProgressRequest(token: token), queue: server.queue) { result in
106+
if result.success != nil {
107+
if self.activeTasks == 0 {
108+
// ActiveTasks might have been decreased while we created the `WorkDoneProgress`
109+
self.state = .noProgress
110+
server.client.send(WorkDoneProgress(token: self.token, value: .end(WorkDoneProgressEnd())))
111+
} else {
112+
self.state = .created
113+
server.client.send(WorkDoneProgress(token: self.token, value: .begin(WorkDoneProgressBegin(title: self.title))))
114+
}
115+
} else {
116+
self.state = .progressCreationFailed
117+
}
118+
}
119+
}
120+
}
121+
122+
/// End a new task stated using `startProgress`.
123+
///
124+
/// If this drops the active task count to 0, the work done progress is ended on the client.
125+
///
126+
/// - Parameter server: The server that is used to send and update of the `WorkDoneProgress` to the client
127+
///
128+
/// - Important: Must be called on `server.queue`.
129+
func endProgress(server: SourceKitServer) {
130+
dispatchPrecondition(condition: .onQueue(server.queue))
131+
assert(activeTasks > 0, "Unbalanced startProgress/endProgress calls")
132+
activeTasks -= 1
133+
if state == .created && activeTasks == 0 {
134+
server.client.send(WorkDoneProgress(token: token, value: .end(WorkDoneProgressEnd())))
135+
}
136+
}
137+
}
138+
61139
/// The SourceKit language server.
62140
///
63141
/// This is the client-facing language server implementation, providing indexing, multiple-toolchain
@@ -80,6 +158,8 @@ public final class SourceKitServer: LanguageServer {
80158

81159
private let documentManager = DocumentManager()
82160

161+
private var packageLoadingWorkDoneProgress = WorkDoneProgressState("SourceKitLSP.SoruceKitServer.reloadPackage", title: "Reloading Package")
162+
83163
/// **Public for testing**
84164
public var _documentManager: DocumentManager {
85165
return documentManager
@@ -559,7 +639,18 @@ extension SourceKitServer {
559639
capabilityRegistry: capabilityRegistry,
560640
toolchainRegistry: self.toolchainRegistry,
561641
buildSetup: self.options.buildSetup,
562-
indexOptions: self.options.indexOptions)
642+
indexOptions: self.options.indexOptions,
643+
reloadPackageStatusCallback: { status in
644+
self.queue.async {
645+
switch status {
646+
case .start:
647+
self.packageLoadingWorkDoneProgress.startProgress(server: self)
648+
case .end:
649+
self.packageLoadingWorkDoneProgress.endProgress(server: self)
650+
}
651+
}
652+
}
653+
)
563654
}
564655

565656
func initialize(_ req: Request<InitializeRequest>) {

Sources/SourceKitLSP/Workspace.swift

+8-4
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,19 @@ public final class Workspace {
8484
capabilityRegistry: CapabilityRegistry,
8585
toolchainRegistry: ToolchainRegistry,
8686
buildSetup: BuildSetup,
87-
indexOptions: IndexOptions = IndexOptions()
87+
indexOptions: IndexOptions = IndexOptions(),
88+
reloadPackageStatusCallback: @escaping (ReloadPackageStatus) -> Void
8889
) throws {
8990
var buildSystem: BuildSystem? = nil
9091
if let rootUrl = rootUri.fileURL, let rootPath = try? AbsolutePath(validating: rootUrl.path) {
9192
if let buildServer = BuildServerBuildSystem(projectRoot: rootPath, buildSetup: buildSetup) {
9293
buildSystem = buildServer
93-
} else if let swiftpm = SwiftPMWorkspace(url: rootUrl,
94-
toolchainRegistry: toolchainRegistry,
95-
buildSetup: buildSetup) {
94+
} else if let swiftpm = SwiftPMWorkspace(
95+
url: rootUrl,
96+
toolchainRegistry: toolchainRegistry,
97+
buildSetup: buildSetup,
98+
reloadPackageStatusCallback: reloadPackageStatusCallback
99+
) {
96100
buildSystem = swiftpm
97101
} else {
98102
buildSystem = CompilationDatabaseBuildSystem(projectRoot: rootPath)

0 commit comments

Comments
 (0)