diff --git a/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift b/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift index d8f6232f2..a87a5a9a0 100644 --- a/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift +++ b/Sources/DocumentationLanguageService/DoccDocumentationHandler.swift @@ -63,15 +63,14 @@ extension DocumentationLanguageService { let symbolOccurrence = try await index.primaryDefinitionOrDeclarationOccurrence( ofDocCSymbolLink: symbolLink, fetchSymbolGraph: { location in - guard let symbolWorkspace = try await workspaceForDocument(uri: location.documentUri), - let languageService = await sourceKitLSPServer.languageService( - for: location.documentUri, - .swift, - in: symbolWorkspace - ) - else { + guard let symbolWorkspace = try await workspaceForDocument(uri: location.documentUri) else { throw ResponseError.internalError("Unable to find language service for \(location.documentUri)") } + let languageService = try await sourceKitLSPServer.primaryLanguageService( + for: location.documentUri, + .swift, + in: symbolWorkspace + ) return try await languageService.symbolGraph(forOnDiskContentsOf: location.documentUri, at: location) } ) @@ -79,16 +78,14 @@ extension DocumentationLanguageService { throw ResponseError.requestFailed(doccDocumentationError: .symbolNotFound(symbolName)) } let symbolDocumentUri = symbolOccurrence.location.documentUri - guard - let symbolWorkspace = try await workspaceForDocument(uri: symbolDocumentUri), - let languageService = await sourceKitLSPServer.languageService( - for: symbolDocumentUri, - .swift, - in: symbolWorkspace - ) - else { + guard let symbolWorkspace = try await workspaceForDocument(uri: symbolDocumentUri) else { throw ResponseError.internalError("Unable to find language service for \(symbolDocumentUri)") } + let languageService = try await sourceKitLSPServer.primaryLanguageService( + for: symbolDocumentUri, + .swift, + in: symbolWorkspace + ) let symbolGraph = try await languageService.symbolGraph( forOnDiskContentsOf: symbolDocumentUri, at: symbolOccurrence.location diff --git a/Sources/DocumentationLanguageService/DocumentationLanguageService.swift b/Sources/DocumentationLanguageService/DocumentationLanguageService.swift index f10fd911d..f5acc05a3 100644 --- a/Sources/DocumentationLanguageService/DocumentationLanguageService.swift +++ b/Sources/DocumentationLanguageService/DocumentationLanguageService.swift @@ -51,17 +51,6 @@ package actor DocumentationLanguageService: LanguageService, Sendable { return await sourceKitLSPServer.workspaceForDocument(uri: uri) } - func languageService( - for uri: DocumentURI, - _ language: LanguageServerProtocol.Language, - in workspace: Workspace - ) async throws -> LanguageService? { - guard let sourceKitLSPServer else { - throw ResponseError.unknown("Connection to the editor closed") - } - return await sourceKitLSPServer.languageService(for: uri, language, in: workspace) - } - package nonisolated func canHandle(workspace: Workspace, toolchain: Toolchain) -> Bool { return true } diff --git a/Sources/LanguageServerProtocol/Error.swift b/Sources/LanguageServerProtocol/Error.swift index 5a4bbcc55..8a7d026fd 100644 --- a/Sources/LanguageServerProtocol/Error.swift +++ b/Sources/LanguageServerProtocol/Error.swift @@ -85,6 +85,8 @@ public struct ErrorCode: RawRepresentable, Codable, Hashable, Sendable { public static let workspaceNotOpen: ErrorCode = ErrorCode(rawValue: -32003) /// The method is not implemented in this `LanguageService`. + /// + /// This informs `SourceKitLSPServer` that it should query secondary language services for the results. public static let requestNotImplemented: ErrorCode = ErrorCode(rawValue: -32004) } diff --git a/Sources/SourceKitLSP/LanguageServiceRegistry.swift b/Sources/SourceKitLSP/LanguageServiceRegistry.swift index 0d7c0aca4..b591d3ae9 100644 --- a/Sources/SourceKitLSP/LanguageServiceRegistry.swift +++ b/Sources/SourceKitLSP/LanguageServiceRegistry.swift @@ -34,27 +34,35 @@ struct LanguageServiceType: Hashable { /// Registry in which conformers to `LanguageService` can be registered to server semantic functionality for a set of /// languages. package struct LanguageServiceRegistry { - private var byLanguage: [Language: LanguageService.Type] = [:] + private var byLanguage: [Language: [LanguageServiceType]] = [:] package init() {} package mutating func register(_ languageService: LanguageService.Type, for languages: [Language]) { for language in languages { - if let existingLanguageService = byLanguage[language] { - logger.fault( - "Cannot register \(languageService) for \(language, privacy: .public) because \(existingLanguageService) is already registered" - ) + let services = byLanguage[language] ?? [] + if services.contains(LanguageServiceType(languageService)) { + logger.fault("\(languageService) already registered for \(language, privacy: .public)") continue } - byLanguage[language] = languageService + byLanguage[language, default: []].append(LanguageServiceType(languageService)) } } - func languageService(for language: Language) -> LanguageService.Type? { - return byLanguage[language] + /// The language services that can handle a document of the given language. + /// + /// Multiple language services may be able to handle a document. Depending on the use case, callers need to combine + /// the results of the language services. + /// If it is possible to merge the results of the language service (eg. combining code actions from multiple language + /// services), that's the preferred choice. + /// Otherwise the language services occurring early in the array should be given precedence and the results of the + /// first language service that produces some should be returned. + func languageServices(for language: Language) -> [LanguageService.Type] { + return byLanguage[language]?.map(\.type) ?? [] } + /// All language services that are registered in the registry. var languageServices: Set { - return Set(byLanguage.values.map { LanguageServiceType($0) }) + return Set(byLanguage.values.flatMap { $0 }) } } diff --git a/Sources/SourceKitLSP/Rename.swift b/Sources/SourceKitLSP/Rename.swift index 8faf5e233..2ecce54ef 100644 --- a/Sources/SourceKitLSP/Rename.swift +++ b/Sources/SourceKitLSP/Rename.swift @@ -124,7 +124,9 @@ extension SourceKitLSPServer { guard let snapshot = self.documentManager.latestSnapshotOrDisk(uri, language: .swift) else { return nil } - let swiftLanguageService = await self.languageService(for: uri, .swift, in: workspace) as? NameTranslatorService + let swiftLanguageService = await orLog("Getting NameTranslatorService") { + try await self.primaryLanguageService(for: uri, .swift, in: workspace) as? NameTranslatorService + } guard let swiftLanguageService else { return nil } @@ -210,7 +212,7 @@ extension SourceKitLSPServer { return CrossLanguageName(clangName: definitionName, swiftName: swiftName, definitionLanguage: definitionLanguage) case .swift: guard - let swiftLanguageService = await self.languageService( + let swiftLanguageService = try await self.primaryLanguageService( for: definitionDocumentUri, definitionLanguage, in: workspace @@ -278,9 +280,7 @@ extension SourceKitLSPServer { guard let workspace = await workspaceForDocument(uri: uri) else { throw ResponseError.workspaceNotOpen(uri) } - guard let primaryFileLanguageService = workspace.documentService(for: uri) else { - return nil - } + let primaryFileLanguageService = try await primaryLanguageService(for: uri, snapshot.language, in: workspace) // Determine the local edits and the USR to rename let renameResult = try await primaryFileLanguageService.rename(request) @@ -395,7 +395,10 @@ extension SourceKitLSPServer { logger.error("Failed to get document snapshot for \(uri.forLogging)") return nil } - guard let languageService = await self.languageService(for: uri, language, in: workspace) else { + let languageService = await orLog("Getting language service to compute edits in file") { + try await self.primaryLanguageService(for: uri, language, in: workspace) + } + guard let languageService else { return nil } diff --git a/Sources/SourceKitLSP/SourceKitLSPServer.swift b/Sources/SourceKitLSP/SourceKitLSPServer.swift index 272f9e2d8..7f27c9346 100644 --- a/Sources/SourceKitLSP/SourceKitLSPServer.swift +++ b/Sources/SourceKitLSP/SourceKitLSPServer.swift @@ -274,16 +274,6 @@ package actor SourceKitLSPServer { }.valuePropagatingCancellation } - private func documentService(for uri: DocumentURI) async throws -> LanguageService { - guard let workspace = await self.workspaceForDocument(uri: uri) else { - throw ResponseError.workspaceNotOpen(uri) - } - guard let languageService = workspace.documentService(for: uri) else { - throw ResponseError.unknown("No language service for '\(uri)' found") - } - return languageService - } - /// This method must be executed on `workspaceQueue` to ensure that the file handling capabilities of the /// workspaces don't change during the computation. Otherwise, we could run into a race condition like the following: /// 1. We don't have an entry for file `a.swift` in `workspaceForUri` and start the computation @@ -385,11 +375,9 @@ package actor SourceKitLSPServer { // This should be created as soon as we receive an open call, even if the document // isn't yet ready. - guard let languageService = workspace.documentService(for: doc) else { - return + for languageService in workspace.languageServices(for: doc) { + await notificationHandler(notification, languageService) } - - await notificationHandler(notification, languageService) } private func handleRequest( @@ -406,10 +394,19 @@ package actor SourceKitLSPServer { guard let workspace = await self.workspaceForDocument(uri: request.textDocument.uri) else { throw ResponseError.workspaceNotOpen(request.textDocument.uri) } - guard let languageService = workspace.documentService(for: doc) else { + let languageServices = workspace.languageServices(for: doc) + if languageServices.isEmpty { throw ResponseError.unknown("No language service for '\(request.textDocument.uri)' found") } - return try await requestHandler(request, workspace, languageService) + // Return the results from the first language service that doesn't throw a `requestNotImplemented` error. + for languageService in languageServices { + do { + return try await requestHandler(request, workspace, languageService) + } catch let error as ResponseError where error.code == .requestNotImplemented { + continue + } + } + throw ResponseError.unknown("No language service implements request") } } @@ -429,7 +426,7 @@ package actor SourceKitLSPServer { guard let workspace = await self.workspaceForDocument(uri: documentUri) else { continue } - guard workspace.documentService(for: documentUri) === languageService else { + guard workspace.languageServices(for: documentUri).contains(where: { $0 === languageService }) else { continue } guard let snapshot = try? self.documentManager.latestSnapshot(documentUri) else { @@ -437,7 +434,10 @@ package actor SourceKitLSPServer { continue } - // Close the document properly in the document manager and build server manager to start with a clean sheet when re-opening it. + // Close the document properly in the document manager and build server manager to start with a clean sheet when + // re-opening it. + // This closes and re-opens the document in all of its language services, not just the crashed language service + // but since crashing language services should be rare, this is acceptable. let closeNotification = DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(documentUri)) await self.closeDocument(closeNotification, workspace: workspace) @@ -466,94 +466,113 @@ package actor SourceKitLSPServer { return nil } - func languageService( + /// Get the language services that can handle the given languages in the given workspace using the given toolchain. + /// + /// If we have language services that can handle this combination but that haven't been started yet, start them. + func languageServices( for toolchain: Toolchain, _ language: Language, in workspace: Workspace - ) async -> LanguageService? { - guard let serverType = languageServiceRegistry.languageService(for: language) else { - logger.error("Unable to infer language server type for language '\(language)'") - return nil - } - // Pick the first language service that can handle this workspace. - if let languageService = existingLanguageService(serverType, toolchain: toolchain, workspace: workspace) { - return languageService - } + ) async -> [LanguageService] { + var result: [any LanguageService] = [] + for serverType in languageServiceRegistry.languageServices(for: language) { + if let languageService = existingLanguageService(serverType, toolchain: toolchain, workspace: workspace) { + result.append(languageService) + continue + } - // Start a new service. - return await orLog("failed to start language service", level: .error) { [options = workspace.options, hooks] in - let service = try await serverType.init( - sourceKitLSPServer: self, - toolchain: toolchain, - options: options, - hooks: hooks, - workspace: workspace - ) + // Start a new service. + let languageService: (any LanguageService)? = await orLog("failed to start language service") { + [options = workspace.options, hooks] in + let service = try await serverType.init( + sourceKitLSPServer: self, + toolchain: toolchain, + options: options, + hooks: hooks, + workspace: workspace + ) - guard let service else { - return nil - } + guard let service else { + return nil + } - let pid = Int(ProcessInfo.processInfo.processIdentifier) - let resp = try await service.initialize( - InitializeRequest( - processId: pid, - rootPath: nil, - rootURI: workspace.rootUri, - initializationOptions: nil, - capabilities: workspace.capabilityRegistry.clientCapabilities, - trace: .off, - workspaceFolders: nil + let pid = Int(ProcessInfo.processInfo.processIdentifier) + let resp = try await service.initialize( + InitializeRequest( + processId: pid, + rootPath: nil, + rootURI: workspace.rootUri, + initializationOptions: nil, + capabilities: workspace.capabilityRegistry.clientCapabilities, + trace: .off, + workspaceFolders: nil + ) + ) + let languages = languageClass(for: language) + await self.registerCapabilities( + for: resp.capabilities, + languages: languages, + registry: workspace.capabilityRegistry ) - ) - let languages = languageClass(for: language) - await self.registerCapabilities( - for: resp.capabilities, - languages: languages, - registry: workspace.capabilityRegistry - ) - var syncKind: TextDocumentSyncKind - switch resp.capabilities.textDocumentSync { - case .options(let options): - syncKind = options.change ?? .incremental - case .kind(let kind): - syncKind = kind - default: - syncKind = .incremental - } - guard syncKind == .incremental else { - throw ResponseError.internalError("non-incremental update not implemented") - } + var syncKind: TextDocumentSyncKind + switch resp.capabilities.textDocumentSync { + case .options(let options): + syncKind = options.change ?? .incremental + case .kind(let kind): + syncKind = kind + default: + syncKind = .incremental + } + guard syncKind == .incremental else { + throw ResponseError.internalError("non-incremental update not implemented") + } - await service.clientInitialized(InitializedNotification()) + await service.clientInitialized(InitializedNotification()) + + if let concurrentlyInitializedService = existingLanguageService( + serverType, + toolchain: toolchain, + workspace: workspace + ) { + // Since we 'await' above, another call to languageService might have + // happened concurrently, passed the `existingLanguageService` check at + // the top and started initializing another language service. + // If this race happened, just shut down our server and return the + // other one. + await service.shutdown() + return concurrentlyInitializedService + } - if let concurrentlyInitializedService = existingLanguageService( - serverType, - toolchain: toolchain, - workspace: workspace - ) { - // Since we 'await' above, another call to languageService might have - // happened concurrently, passed the `existingLanguageService` check at - // the top and started initializing another language service. - // If this race happened, just shut down our server and return the - // other one. - await service.shutdown() - return concurrentlyInitializedService + languageServices[LanguageServiceType(serverType), default: []].append(service) + return service } - - languageServices[LanguageServiceType(serverType), default: []].append(service) - return service + guard let languageService else { + // If a language service fails to start, don't try starting language services with lower precedence. Otherwise + // we get into a situation where eg. `SwiftLanguageService`` fails to start (eg. because the toolchain doesn't + // contain sourcekitd) and the `DocumentationLanguageService` now becomes the primary language service for the + // document, trying to serve documentation, completion etc. which is not intended. + break + } + result.append(languageService) + } + if result.isEmpty { + logger.error("Unable to infer language server type for language '\(language)'") } + return result } - package func languageService( + /// Get the language services that can handle the given document. + /// + /// If we have language services that can handle this document but that haven't been started yet, start them. + package func languageServices( for uri: DocumentURI, _ language: Language, in workspace: Workspace - ) async -> LanguageService? { - if let service = workspace.documentService(for: uri) { - return service + ) async -> [LanguageService] { + let existingLanguageServices = workspace.languageServices(for: uri) + if !existingLanguageServices.isEmpty { + return existingLanguageServices } let toolchain = await workspace.buildServerManager.toolchain( @@ -563,11 +582,12 @@ package actor SourceKitLSPServer { ) guard let toolchain else { logger.error("Failed to determine toolchain for \(uri)") - return nil + return [] } - guard let service = await languageService(for: toolchain, language, in: workspace) else { - logger.error("Failed to create language service for \(uri)") - return nil + let languageServices = await self.languageServices(for: toolchain, language, in: workspace) + + if languageServices.isEmpty { + logger.error("No language service found to handle \(uri.forLogging)") } logger.log( @@ -577,7 +597,23 @@ package actor SourceKitLSPServer { """ ) - return workspace.setDocumentService(for: uri, service) + return workspace.setLanguageServices(for: uri, languageServices) + } + + /// The language service with the highest precedence that can handle the given document. + /// + /// If we have language services that can handle this document but that haven't been started yet, start them. + /// + /// If no language service exists for this document, throw an error. + package func primaryLanguageService( + for uri: DocumentURI, + _ language: Language, + in workspace: Workspace + ) async throws -> LanguageService { + guard let languageService = await languageServices(for: uri, language, in: workspace).first else { + throw ResponseError.unknown("No language service found for \(uri)") + } + return languageService } } @@ -1302,15 +1338,19 @@ extension SourceKitLSPServer { let uri = textDocument.uri let language = textDocument.language - // If we can't create a service, this document is unsupported and we can bail here. - guard let service = await languageService(for: uri, language, in: workspace) else { + let languageServices = await languageServices(for: uri, language, in: workspace) + + if languageServices.isEmpty { + // If we can't create a service, this document is unsupported and we can bail here. return } await workspace.buildServerManager.registerForChangeNotifications(for: uri, language: language) // If the document is ready, we can immediately send the notification. - await service.openDocument(notification, snapshot: snapshot) + for languageService in languageServices { + await languageService.openDocument(notification, snapshot: snapshot) + } } func closeDocument(_ notification: DidCloseTextDocumentNotification) async { @@ -1332,7 +1372,10 @@ extension SourceKitLSPServer { ) return } - await workspace.documentService(for: uri)?.reopenDocument(notification) + + for languageService in workspace.languageServices(for: uri) { + await languageService.reopenDocument(notification) + } } func closeDocument(_ notification: DidCloseTextDocumentNotification, workspace: Workspace) async { @@ -1346,7 +1389,9 @@ extension SourceKitLSPServer { await workspace.buildServerManager.unregisterForChangeNotifications(for: uri) - await workspace.documentService(for: uri)?.closeDocument(notification) + for languageService in workspace.languageServices(for: uri) { + await languageService.closeDocument(notification) + } workspaceQueue.async { self.workspaceForUri[notification.textDocument.uri] = nil @@ -1376,12 +1421,14 @@ extension SourceKitLSPServer { // Already logged failure return } - await workspace.documentService(for: uri)?.changeDocument( - notification, - preEditSnapshot: preEditSnapshot, - postEditSnapshot: postEditSnapshot, - edits: edits - ) + for languageService in workspace.languageServices(for: uri) { + await languageService.changeDocument( + notification, + preEditSnapshot: preEditSnapshot, + postEditSnapshot: postEditSnapshot, + edits: edits + ) + } } /// If the client doesn't support the `window/didChangeActiveDocument` notification, when the client interacts with a @@ -1568,7 +1615,11 @@ extension SourceKitLSPServer { else { return request.item } - return try await documentService(for: uri).completionItemResolve(request) + guard let workspace = await self.workspaceForDocument(uri: uri) else { + throw ResponseError.workspaceNotOpen(uri) + } + let language = try documentManager.latestSnapshot(uri.buildSettingsFile).language + return try await primaryLanguageService(for: uri, language, in: workspace).completionItemResolve(request) } func doccDocumentation( @@ -1767,21 +1818,44 @@ extension SourceKitLSPServer { command: req.command, arguments: req.argumentsWithoutSourceKitMetadata ) - return try await documentService(for: uri).executeCommand(executeCommand) + guard let workspace = await self.workspaceForDocument(uri: uri) else { + throw ResponseError.workspaceNotOpen(uri) + } + let language = try documentManager.latestSnapshot(uri.buildSettingsFile).language + // First, check if we have a language service that explicitly declares support for this command. + if let languageService = await languageServices(for: uri, language, in: workspace) + .first(where: { type(of: $0).builtInCommands.contains(req.command) }) + { + return try await languageService.executeCommand(executeCommand) + } + // Otherwise handle it in the primary language service. This is important to handle eg. commands in clangd, which + // are not declared as built-in commands. + return try await primaryLanguageService(for: uri, language, in: workspace).executeCommand(executeCommand) } func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse { let buildSettingsUri = try ReferenceDocumentURL(from: req.uri).buildSettingsFile - guard let workspace = await workspaceForDocument(uri: buildSettingsUri) else { + guard let workspace = await self.workspaceForDocument(uri: buildSettingsUri) else { throw ResponseError.workspaceNotOpen(buildSettingsUri) } - - guard let languageService = workspace.documentService(for: buildSettingsUri) else { - throw ResponseError.unknown("No Language Service for URI: \(buildSettingsUri)") + let language: Language + + // The document that provided the build settings might no longer be open, so we need to be able to infer the + // language by other means as well. + if let snapshot = try? documentManager.latestSnapshot(buildSettingsUri) { + language = snapshot.language + } else if let target = await workspace.buildServerManager.canonicalTarget(for: buildSettingsUri), + let lang = await workspace.buildServerManager.defaultLanguage(for: buildSettingsUri, in: target) + { + language = lang + } else if let lang = Language(inferredFromFileExtension: buildSettingsUri) { + language = lang + } else { + throw ResponseError.unknown("Unable to infer language for \(buildSettingsUri)") } - return try await languageService.getReferenceDocument(req) + return try await primaryLanguageService(for: buildSettingsUri, language, in: workspace).getReferenceDocument(req) } func codeAction( diff --git a/Sources/SourceKitLSP/SyntacticTestIndex.swift b/Sources/SourceKitLSP/SyntacticTestIndex.swift index b2701dd82..26ca9d6fb 100644 --- a/Sources/SourceKitLSP/SyntacticTestIndex.swift +++ b/Sources/SourceKitLSP/SyntacticTestIndex.swift @@ -239,13 +239,13 @@ actor SyntacticTestIndex { if Task.isCancelled { return } - guard let language = Language(inferredFromFileExtension: uri), - let languageServiceType = languageServiceRegistry.languageService(for: language) - else { + guard let language = Language(inferredFromFileExtension: uri) else { logger.log("Not indexing \(uri.forLogging) because the language service could not be inferred") return } - let testItems = await languageServiceType.syntacticTestItems(in: uri) + let testItems = await languageServiceRegistry.languageServices(for: language).asyncFlatMap { + await $0.syntacticTestItems(in: uri) + } guard !removedFiles.contains(uri) else { // Check whether the file got removed while we were scanning it for tests. If so, don't add it back to diff --git a/Sources/SourceKitLSP/TestDiscovery.swift b/Sources/SourceKitLSP/TestDiscovery.swift index 9afdfb224..acb1b9a4b 100644 --- a/Sources/SourceKitLSP/TestDiscovery.swift +++ b/Sources/SourceKitLSP/TestDiscovery.swift @@ -214,7 +214,7 @@ extension SourceKitLSPServer { return documentManager.fileHasInMemoryModifications(uri) } - let filesWithInMemoryState = documentManager.openDocuments.filter { uri in + let snapshotsWithInMemoryState = documentManager.openDocuments.filter { uri in // Use the index to check for in-memory modifications so we can re-use its cache. If no index exits, ask the // document manager directly. if let index { @@ -222,17 +222,22 @@ extension SourceKitLSPServer { } else { return documentManagerHasInMemoryModifications(uri) } + }.compactMap { uri in + orLog("Getting snapshot of open document") { + try documentManager.latestSnapshot(uri) + } } - let testsFromFilesWithInMemoryState = await filesWithInMemoryState.concurrentMap { (uri) -> [AnnotatedTestItem] in - guard let languageService = workspace.documentService(for: uri) else { - return [] - } - return await orLog("Getting document tests for \(uri)") { + let testsFromFilesWithInMemoryState = await snapshotsWithInMemoryState.concurrentMap { + (snapshot) -> [AnnotatedTestItem] in + // When secondary language services can provide tests, we need to query them for tests as well. For now there is + // too much overhead associated with calling `documentTestsWithoutMergingExtensions` for language services that + // don't have any test discovery functionality. + return await orLog("Getting document tests for \(snapshot.uri)") { try await self.documentTestsWithoutMergingExtensions( - DocumentTestsRequest(textDocument: TextDocumentIdentifier(uri)), + DocumentTestsRequest(textDocument: TextDocumentIdentifier(snapshot.uri)), workspace: workspace, - languageService: languageService + languageService: self.primaryLanguageService(for: snapshot.uri, snapshot.language, in: workspace) ) } ?? [] }.flatMap { $0 } @@ -262,7 +267,7 @@ extension SourceKitLSPServer { // don't need to include results from the syntactic index. return nil } - if filesWithInMemoryState.contains(testItem.location.uri) { + if snapshotsWithInMemoryState.contains(where: { $0.uri == testItem.location.uri }) { // If the file has been modified in the editor, the syntactic index (which indexes on-disk files) is no longer // up-to-date. Include the tests from `testsFromFilesWithInMemoryState`. return nil diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index 41fd7dc05..5d0ea5c35 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -122,7 +122,7 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { let syntacticTestIndex: SyntacticTestIndex /// Language service for an open document, if available. - private let documentService: ThreadSafeBox<[DocumentURI: LanguageService]> = ThreadSafeBox(initialValue: [:]) + private let languageServices: ThreadSafeBox<[DocumentURI: [LanguageService]]> = ThreadSafeBox(initialValue: [:]) /// The `SemanticIndexManager` that keeps track of whose file's index is up-to-date in the workspace and schedules /// indexing and preparation tasks for files with out-of-date index. @@ -426,21 +426,30 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { _ = await (updateSyntacticIndex, updateSemanticIndex) } - func documentService(for uri: DocumentURI) -> LanguageService? { - return documentService.value[uri.buildSettingsFile] + /// The language services that can handle the given document. Callers should try to merge the results from the + /// different language service or prefer results from language services that occur earlier in this array, whichever is + /// more suitable. + func languageServices(for uri: DocumentURI) -> [LanguageService] { + return languageServices.value[uri.buildSettingsFile] ?? [] + } + + /// The language service with the highest precedence that can handle the given document. + func primaryLanguageService(for uri: DocumentURI) -> LanguageService? { + return languageServices(for: uri).first } /// Set a language service for a document uri and returns if none exists already. - /// If a language service already exists for this document, eg. because two requests start creating a language - /// service for a document and race, `newLanguageService` is dropped and the existing language service for the - /// document is returned. - func setDocumentService(for uri: DocumentURI, _ newLanguageService: any LanguageService) -> LanguageService { - return documentService.withLock { service in - if let languageService = service[uri] { + /// + /// If language services already exist for this document, eg. because two requests start creating a language + /// service for a document and race, `newLanguageServices` is dropped and the existing language services for the + /// document are returned. + func setLanguageServices(for uri: DocumentURI, _ newLanguageService: [any LanguageService]) -> [LanguageService] { + return languageServices.withLock { languageServices in + if let languageService = languageServices[uri] { return languageService } - service[uri] = newLanguageService + languageServices[uri] = newLanguageService return newLanguageService } } @@ -451,7 +460,9 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { /// - Changed settings for an already open file package func fileBuildSettingsChanged(_ changedFiles: Set) async { for uri in changedFiles { - await self.documentService(for: uri)?.documentUpdatedBuildSettings(uri) + for languageService in languageServices(for: uri) { + await languageService.documentUpdatedBuildSettings(uri) + } } } @@ -462,11 +473,9 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { var documentsByService: [ObjectIdentifier: (Set, LanguageService)] = [:] for uri in changedFiles { logger.log("Dependencies updated for file \(uri.forLogging)") - guard let languageService = documentService(for: uri) else { - logger.error("No document service exists for \(uri.forLogging)") - continue + for languageService in languageServices(for: uri) { + documentsByService[ObjectIdentifier(languageService), default: ([], languageService)].0.insert(uri) } - documentsByService[ObjectIdentifier(languageService), default: ([], languageService)].0.insert(uri) } for (documents, service) in documentsByService.values { await service.documentDependenciesUpdated(documents) diff --git a/Tests/SourceKitLSPTests/ClangdTests.swift b/Tests/SourceKitLSPTests/ClangdTests.swift index 10d2aa2a9..2f0f6d7e9 100644 --- a/Tests/SourceKitLSPTests/ClangdTests.swift +++ b/Tests/SourceKitLSPTests/ClangdTests.swift @@ -158,8 +158,10 @@ final class ClangdTests: XCTestCase { ) // Monitor clangd to notice when it gets restarted - let clangdServer = try await unwrap( - testClient.server.languageService(for: uri, .c, in: unwrap(testClient.server.workspaceForDocument(uri: uri))) + let clangdServer = try await testClient.server.primaryLanguageService( + for: uri, + .c, + in: unwrap(testClient.server.workspaceForDocument(uri: uri)) ) await clangdServer.addStateChangeHandler { oldState, newState in if oldState == .connectionInterrupted, newState == .connected { diff --git a/Tests/SourceKitLSPTests/CrashRecoveryTests.swift b/Tests/SourceKitLSPTests/CrashRecoveryTests.swift index 99120f2d9..0b9a5ad5c 100644 --- a/Tests/SourceKitLSPTests/CrashRecoveryTests.swift +++ b/Tests/SourceKitLSPTests/CrashRecoveryTests.swift @@ -81,11 +81,13 @@ final class CrashRecoveryTests: XCTestCase { // Crash sourcekitd let swiftLanguageService = - await testClient.server.languageService( - for: uri, - .swift, - in: testClient.server.workspaceForDocument(uri: uri)! - ) as! SwiftLanguageService + try unwrap( + await testClient.server.primaryLanguageService( + for: uri, + .swift, + in: testClient.server.workspaceForDocument(uri: uri)! + ) as? SwiftLanguageService + ) await swiftLanguageService.crash() @@ -119,11 +121,11 @@ final class CrashRecoveryTests: XCTestCase { } private func crashClangd(for testClient: TestSourceKitLSPClient, document docUri: DocumentURI) async throws { - let clangdServer = await testClient.server.languageService( + let clangdServer = try await testClient.server.primaryLanguageService( for: docUri, .cpp, in: testClient.server.workspaceForDocument(uri: docUri)! - )! + ) let clangdCrashed = self.expectation(description: "clangd crashed") let clangdRestarted = self.expectation(description: "clangd restarted") @@ -266,11 +268,11 @@ final class CrashRecoveryTests: XCTestCase { // Keep track of clangd crashes - let clangdServer = await testClient.server.languageService( + let clangdServer = try await testClient.server.primaryLanguageService( for: uri, .cpp, in: testClient.server.workspaceForDocument(uri: uri)! - )! + ) let clangdCrashed = self.expectation(description: "clangd crashed") clangdCrashed.assertForOverFulfill = false @@ -333,11 +335,13 @@ final class CrashRecoveryTests: XCTestCase { ) let swiftLanguageService = - await testClient.server.languageService( - for: uri, - .swift, - in: testClient.server.workspaceForDocument(uri: uri)! - ) as! SwiftLanguageService + try unwrap( + await testClient.server.primaryLanguageService( + for: uri, + .swift, + in: testClient.server.workspaceForDocument(uri: uri)! + ) as? SwiftLanguageService + ) await swiftLanguageService.crash() @@ -371,8 +375,11 @@ final class CrashRecoveryTests: XCTestCase { // Monitor sourcekitd to notice when it gets terminated let swiftService = try await unwrap( - testClient.server.languageService(for: uri, .swift, in: unwrap(testClient.server.workspaceForDocument(uri: uri))) - as? SwiftLanguageService + testClient.server.primaryLanguageService( + for: uri, + .swift, + in: unwrap(testClient.server.workspaceForDocument(uri: uri)) + ) as? SwiftLanguageService ) await swiftService.addStateChangeHandler { oldState, newState in logger.debug("sourcekitd changed state: \(String(describing: oldState)) -> \(String(describing: newState))") diff --git a/Tests/SourceKitLSPTests/LocalClangTests.swift b/Tests/SourceKitLSPTests/LocalClangTests.swift index a36d4f59d..529d769c4 100644 --- a/Tests/SourceKitLSPTests/LocalClangTests.swift +++ b/Tests/SourceKitLSPTests/LocalClangTests.swift @@ -340,11 +340,11 @@ final class LocalClangTests: XCTestCase { struct MyObject * newObject(); """.writeWithRetry(to: XCTUnwrap(headerUri.fileURL)) - let clangdServer = await project.testClient.server.languageService( + let clangdServer = try await project.testClient.server.primaryLanguageService( for: mainUri, .c, in: project.testClient.server.workspaceForDocument(uri: mainUri)! - )! + ) await clangdServer.documentDependenciesUpdated([mainUri]) diff --git a/Tests/SourceKitLSPTests/LocalSwiftTests.swift b/Tests/SourceKitLSPTests/LocalSwiftTests.swift index 1d127db5b..bef7cbac5 100644 --- a/Tests/SourceKitLSPTests/LocalSwiftTests.swift +++ b/Tests/SourceKitLSPTests/LocalSwiftTests.swift @@ -1359,8 +1359,13 @@ final class LocalSwiftTests: XCTestCase { let reusedNodeCallback = self.expectation(description: "reused node callback called") let reusedNodes = ThreadSafeBox<[Syntax]>(initialValue: []) let swiftLanguageService = - await testClient.server.languageService(for: uri, .swift, in: testClient.server.workspaceForDocument(uri: uri)!) - as! SwiftLanguageService + try await unwrap( + testClient.server.primaryLanguageService( + for: uri, + .swift, + in: unwrap(testClient.server.workspaceForDocument(uri: uri)) + ) as? SwiftLanguageService + ) await swiftLanguageService.setReusedNodeCallback { reusedNodes.value.append($0) reusedNodeCallback.fulfill()