Skip to content

Commit ebcdbb6

Browse files
committed
Make SwiftLanguageServer and ClangLanguageServerShim call directly into SourceKitServer instead of through a LocalConnection
`LocalConnection` with its dynamic registration of a message handler made the overall design unnecessarily complicated. If we just call `SourceKitServer` from `ClangLanguageServerShim` and `SwiftLanguageServer` directly, it’s a lot more obvious, what’s going on, IMO.
1 parent 607f040 commit ebcdbb6

File tree

4 files changed

+121
-128
lines changed

4 files changed

+121
-128
lines changed

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler {
4545
/// The queue on which clangd calls us back.
4646
public let clangdCommunicationQueue: DispatchQueue = DispatchQueue(label: "language-server-queue", qos: .userInitiated)
4747

48-
/// The connection to the client. In the case of `ClangLanguageServerShim`,
49-
/// the client is always a ``SourceKitServer``, which will forward the request
50-
/// to the editor.
51-
public let client: Connection
48+
/// The ``SourceKitServer`` instance that created this `ClangLanguageServerShim`.
49+
///
50+
/// Used to send requests and notifications to the editor.
51+
private weak var sourceKitServer: SourceKitServer?
5252

5353
/// The connection to the clangd LSP. `nil` until `startClangdProcesss` has been called.
5454
var clangd: Connection!
@@ -93,9 +93,6 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler {
9393
/// `clangd` doesn't have support for multi-root workspaces, so we need to start a separate `clangd` instance for every workspace root.
9494
private let workspace: WeakWorkspace
9595

96-
/// A callback with which `ClangLanguageServer` can request its owner to reopen all documents in case it has crashed.
97-
private let reopenDocuments: (ToolchainLanguageServer) async -> Void
98-
9996
/// The documents that have been opened and which language they have been
10097
/// opened with.
10198
private var openDocuments: [DocumentURI: Language] = [:]
@@ -110,12 +107,10 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler {
110107
/// Creates a language server for the given client referencing the clang binary specified in `toolchain`.
111108
/// Returns `nil` if `clangd` can't be found.
112109
public init?(
113-
client: LocalConnection,
110+
sourceKitServer: SourceKitServer,
114111
toolchain: Toolchain,
115112
options: SourceKitServer.Options,
116-
workspace: Workspace,
117-
reopenDocuments: @escaping (ToolchainLanguageServer) async -> Void,
118-
workspaceForDocument: @escaping (DocumentURI) async -> Workspace?
113+
workspace: Workspace
119114
) async throws {
120115
guard let clangdPath = toolchain.clangd else {
121116
return nil
@@ -124,9 +119,8 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler {
124119
self.clangdPath = clangdPath
125120
self.clangdOptions = options.clangdOptions
126121
self.workspace = WeakWorkspace(workspace)
127-
self.reopenDocuments = reopenDocuments
128122
self.state = .connected
129-
self.client = client
123+
self.sourceKitServer = sourceKitServer
130124
try startClangdProcesss()
131125
}
132126

@@ -245,7 +239,11 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler {
245239
// But since SourceKitServer more or less ignores them right now anyway, this should be fine for now.
246240
_ = try self.initializeSync(initializeRequest)
247241
self.clientInitialized(InitializedNotification())
248-
await self.reopenDocuments(self)
242+
if let sourceKitServer {
243+
await sourceKitServer.reopenDocuments(for: self)
244+
} else {
245+
log("Cannot reopen documents because SourceKitServer is no longer alive", level: .error)
246+
}
249247
self.state = .connected
250248
} catch {
251249
log("Failed to restart clangd after a crash.", level: .error)
@@ -256,12 +254,15 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler {
256254
/// Handler for notifications received **from** clangd, ie. **clangd** is
257255
/// sending a notification that's intended for the editor.
258256
///
259-
/// We should either handle it ourselves or forward it to the client.
257+
/// We should either handle it ourselves or forward it to the editor.
260258
func handle(_ params: some NotificationType, from clientID: ObjectIdentifier) async {
261-
if let publishDiags = params as? PublishDiagnosticsNotification {
259+
switch params {
260+
case let publishDiags as PublishDiagnosticsNotification:
262261
await self.publishDiagnostics(Notification(publishDiags, clientID: clientID))
263-
} else if clientID == ObjectIdentifier(self.clangd) {
264-
self.client.send(params)
262+
default:
263+
// We don't know how to handle any other notifications and ignore them.
264+
log("Ignoring unknown notification \(type(of: params))", level: .warning)
265+
break
265266
}
266267
}
267268

@@ -274,13 +275,19 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler {
274275
id: RequestID,
275276
from clientID: ObjectIdentifier,
276277
reply: @escaping (LSPResult<R.Response>) -> Void
277-
) {
278+
) async {
278279
let request = Request(params, id: id, clientID: clientID, cancellation: CancellationToken(), reply: { result in
279280
reply(result)
280281
})
282+
guard let sourceKitServer else {
283+
// `SourceKitServer` has been destructed. We are tearing down the language
284+
// server. Nothing left to do.
285+
request.reply(.failure(.unknown("Connection to the editor closed")))
286+
return
287+
}
281288

282289
if request.clientID == ObjectIdentifier(self.clangd) {
283-
self.forwardRequest(request, to: self.client)
290+
await sourceKitServer.sendRequestToClient(request.params, reply: request.reply)
284291
} else {
285292
request.reply(.failure(ResponseError.methodNotFound(R.method)))
286293
}
@@ -354,12 +361,21 @@ extension ClangLanguageServerShim {
354361
// non-fallback settings very shortly after, which will override the
355362
// incorrect result, making it very temporary.
356363
let buildSettings = await self.buildSettings(for: params.uri)
364+
guard let sourceKitServer else {
365+
log("Cannot publish diagnostics because SourceKitServer has been destroyed", level: .error)
366+
return
367+
}
357368
if buildSettings?.isFallback ?? true {
358369
// Fallback: send empty publish notification instead.
359-
client.send(PublishDiagnosticsNotification(
360-
uri: params.uri, version: params.version, diagnostics: []))
370+
await sourceKitServer.sendNotificationToClient(
371+
PublishDiagnosticsNotification(
372+
uri: params.uri,
373+
version: params.version,
374+
diagnostics: []
375+
)
376+
)
361377
} else {
362-
client.send(note.params)
378+
await sourceKitServer.sendNotificationToClient(note.params)
363379
}
364380
}
365381

@@ -388,9 +404,6 @@ extension ClangLanguageServerShim {
388404
guard let self else { return }
389405
Task {
390406
await self.clangd.send(ExitNotification())
391-
if let localConnection = self.client as? LocalConnection {
392-
localConnection.close()
393-
}
394407
continuation.resume()
395408
}
396409
}

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 20 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -292,12 +292,23 @@ public actor SourceKitServer {
292292
}
293293

294294
// Unknown requests from a language server are passed on to the client.
295-
let id = client.send(req.params, queue: clientCommunicationQueue) { result in
296-
req.reply(result)
297-
}
298-
req.cancellationToken.addCancellationHandler {
299-
self.client.send(CancelRequestNotification(id: id))
295+
sendRequestToClient(req.params, reply: req.reply)
296+
}
297+
298+
/// Send the given notification to the editor.
299+
public func sendNotificationToClient(_ notification: some NotificationType) {
300+
client.send(notification)
301+
}
302+
303+
/// Send the given request to the editor.
304+
///
305+
/// Return the result via the `reply` completion handler. `reply` is guaranteed
306+
/// to be called exactly once.
307+
public func sendRequestToClient<R: RequestType>(_ request: R, reply: @escaping (LSPResult<R.Response>) -> Void) {
308+
_ = client.send(request, queue: clientCommunicationQueue) { result in
309+
reply(result)
300310
}
311+
// FIXME: (async) Handle cancellation
301312
}
302313

303314
/// Handle an unknown notification.
@@ -388,19 +399,11 @@ public actor SourceKitServer {
388399

389400
// Start a new service.
390401
return await orLog("failed to start language service", level: .error) {
391-
let service = try await SourceKitLSP.languageService(
392-
for: toolchain,
393-
serverType,
402+
let service = try await serverType.serverType.init(
403+
sourceKitServer: self,
404+
toolchain: toolchain,
394405
options: options,
395-
client: self,
396-
in: workspace,
397-
reopenDocuments: { [weak self] toolchainLanguageServer in
398-
await self?.reopenDocuments(for: toolchainLanguageServer)
399-
},
400-
workspaceForDocument: { [weak self] document in
401-
guard let self else { return nil }
402-
return await self.workspaceForDocument(uri: document)
403-
}
406+
workspace: workspace
404407
)
405408

406409
guard let service else {
@@ -1989,34 +1992,6 @@ extension SourceKitServer {
19891992
}
19901993
}
19911994

1992-
/// Creates a new connection from `client` to a service for `language` if available, and launches
1993-
/// the service. Does *not* send the initialization request.
1994-
///
1995-
/// - returns: The connection, if a suitable language service is available; otherwise nil.
1996-
/// - throws: If there is a suitable service but it fails to launch, throws an error.
1997-
func languageService(
1998-
for toolchain: Toolchain,
1999-
_ languageServerType: LanguageServerType,
2000-
options: SourceKitServer.Options,
2001-
client: MessageHandler,
2002-
in workspace: Workspace,
2003-
reopenDocuments: @escaping (ToolchainLanguageServer) async -> Void,
2004-
workspaceForDocument: @escaping (DocumentURI) async -> Workspace?
2005-
) async throws -> ToolchainLanguageServer? {
2006-
let connectionToClient = LocalConnection()
2007-
2008-
let server = try await languageServerType.serverType.init(
2009-
client: connectionToClient,
2010-
toolchain: toolchain,
2011-
options: options,
2012-
workspace: workspace,
2013-
reopenDocuments: reopenDocuments,
2014-
workspaceForDocument: workspaceForDocument
2015-
)
2016-
connectionToClient.start(handler: client)
2017-
return server
2018-
}
2019-
20201995
private func languageClass(for language: Language) -> [Language] {
20211996
switch language {
20221997
case .c, .cpp, .objective_c, .objective_cpp:

0 commit comments

Comments
 (0)