Files
sourcekit-lsp/Sources/SourceKitLSP/SourceKitServer.swift
Alex Hoppen 5952ddda24 Fix typos
2023-10-02 09:44:18 -07:00

2113 lines
80 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import BuildServerProtocol
import Dispatch
import Foundation
import IndexStoreDB
import LanguageServerProtocol
import LSPLogging
import SKCore
import SKSupport
import PackageLoading
import protocol TSCBasic.FileSystem
import struct TSCBasic.AbsolutePath
import var TSCBasic.localFileSystem
public typealias URL = Foundation.URL
/// Exhaustive enumeration of all toolchain language servers known to SourceKit-LSP.
enum LanguageServerType: Hashable {
case clangd
case swift
init?(language: Language) {
switch language {
case .c, .cpp, .objective_c, .objective_cpp:
self = .clangd
case .swift:
self = .swift
default:
return nil
}
}
/// The `ToolchainLanguageServer` class used to provide functionality for this language class.
var serverType: ToolchainLanguageServer.Type {
switch self {
case .clangd:
return ClangLanguageServerShim.self
case .swift:
return SwiftLanguageServer.self
}
}
}
/// Keeps track of the state to send work done progress updates to the client
final actor WorkDoneProgressState {
private enum State {
/// No `WorkDoneProgress` has been created.
case noProgress
/// We have sent the request to create a `WorkDoneProgress` but havent received a response yet.
case creating
/// A `WorkDoneProgress` has been created.
case created
/// The creation of a `WorkDoneProgress has failed`.
///
/// This causes us to just give up creating any more `WorkDoneProgress` in
/// the future as those will most likely also fail.
case progressCreationFailed
}
/// How many active tasks are running.
///
/// A work done progress should be displayed if activeTasks > 0
private var activeTasks: Int = 0
private var state: State = .noProgress
/// The token by which we track the `WorkDoneProgress`.
private let token: ProgressToken
/// The title that should be displayed to the user in the UI.
private let title: String
init(_ token: String, title: String) {
self.token = ProgressToken.string(token)
self.title = title
}
/// Start a new task, creating a new `WorkDoneProgress` if none is running right now.
///
/// - Parameter server: The server that is used to create the `WorkDoneProgress` on the client
func startProgress(server: SourceKitServer) {
activeTasks += 1
if state == .noProgress {
state = .creating
// Discard the handle. We don't support cancellation of the creation of a work done progress.
_ = server.client.send(CreateWorkDoneProgressRequest(token: token), queue: server.clientCommunicationQueue) { result in
if result.success != nil {
if self.activeTasks == 0 {
// ActiveTasks might have been decreased while we created the `WorkDoneProgress`
self.state = .noProgress
server.client.send(WorkDoneProgress(token: self.token, value: .end(WorkDoneProgressEnd())))
} else {
self.state = .created
server.client.send(WorkDoneProgress(token: self.token, value: .begin(WorkDoneProgressBegin(title: self.title))))
}
} else {
self.state = .progressCreationFailed
}
}
}
}
/// End a new task stated using `startProgress`.
///
/// If this drops the active task count to 0, the work done progress is ended on the client.
///
/// - Parameter server: The server that is used to send and update of the `WorkDoneProgress` to the client
func endProgress(server: SourceKitServer) {
assert(activeTasks > 0, "Unbalanced startProgress/endProgress calls")
activeTasks -= 1
if state == .created && activeTasks == 0 {
server.client.send(WorkDoneProgress(token: token, value: .end(WorkDoneProgressEnd())))
}
}
}
/// The SourceKit language server.
///
/// This is the client-facing language server implementation, providing indexing, multiple-toolchain
/// and cross-language support. Requests may be dispatched to language-specific services or handled
/// centrally, but this is transparent to the client.
public actor SourceKitServer {
// FIXME: (async) We can remove this if we migrate client.send to be async and it thus doesn't take a queue anymore.
/// The queue on which we communicate with the client.
public let clientCommunicationQueue: DispatchQueue = DispatchQueue(label: "language-server-queue", qos: .userInitiated)
public struct RequestCancelKey: Hashable {
public var client: ObjectIdentifier
public var request: RequestID
public init(client: ObjectIdentifier, request: RequestID) {
self.client = client
self.request = request
}
}
/// The set of outstanding requests that may be cancelled.
public var requestCancellation: [RequestCancelKey: CancellationToken] = [:]
/// The connection to the editor.
public let client: Connection
var options: Options
let toolchainRegistry: ToolchainRegistry
var capabilityRegistry: CapabilityRegistry?
var languageServices: [LanguageServerType: [ToolchainLanguageServer]] = [:]
/// Documents that are ready for requests and notifications.
/// This generally means that the `BuildSystem` has notified of us of build settings.
var documentsReady: Set<DocumentURI> = []
private var documentToPendingQueue: [DocumentURI: DocumentNotificationRequestQueue] = [:]
private let documentManager = DocumentManager()
private var packageLoadingWorkDoneProgress = WorkDoneProgressState("SourceKitLSP.SourceKitServer.reloadPackage", title: "Reloading Package")
/// **Public for testing**
public var _documentManager: DocumentManager {
return documentManager
}
/// Caches which workspace a document with the given URI should be opened in.
/// Must only be accessed from `queue`.
private var uriToWorkspaceCache: [DocumentURI: WeakWorkspace] = [:]
private var workspaces: [Workspace] = [] {
didSet {
uriToWorkspaceCache = [:]
}
}
/// **Public for testing**
public var _workspaces: [Workspace] {
get {
return self.workspaces
}
set {
self.workspaces = newValue
}
}
let fs: FileSystem
var onExit: () -> Void
/// Creates a language server for the given client.
public init(client: Connection, fileSystem: FileSystem = localFileSystem, options: Options, onExit: @escaping () -> Void = {}) {
self.fs = fileSystem
self.toolchainRegistry = ToolchainRegistry.shared
self.options = options
self.onExit = onExit
self.client = client
}
public func workspaceForDocument(uri: DocumentURI) -> Workspace? {
if workspaces.count == 1 {
// Special handling: If there is only one workspace, open all files in it.
// This retains the behavior of SourceKit-LSP before it supported multiple workspaces.
return workspaces.first
}
if let cachedWorkspace = uriToWorkspaceCache[uri]?.value {
return cachedWorkspace
}
// Pick the workspace with the best FileHandlingCapability for this file.
// If there is a tie, use the workspace that occurred first in the list.
var bestWorkspace: (workspace: Workspace?, fileHandlingCapability: FileHandlingCapability) = (nil, .unhandled)
for workspace in workspaces {
let fileHandlingCapability = workspace.buildSystemManager.fileHandlingCapability(for: uri)
if fileHandlingCapability > bestWorkspace.fileHandlingCapability {
bestWorkspace = (workspace, fileHandlingCapability)
}
}
uriToWorkspaceCache[uri] = WeakWorkspace(bestWorkspace.workspace)
return bestWorkspace.workspace
}
/// Execute `notificationHandler` once the document that it concerns is ready
/// and has the initial build settings. These build settings might still be
/// incomplete or fallback settings.
private func withReadyDocument<NotificationType: TextDocumentNotification>(
for notification: Notification<NotificationType>,
notificationHandler: @escaping (Notification<NotificationType>, ToolchainLanguageServer) async -> Void
) async {
let doc = notification.params.textDocument.uri
guard let workspace = self.workspaceForDocument(uri: doc) else {
return
}
// 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[doc] else {
return
}
// If the document is ready, we can handle it right now.
guard !self.documentsReady.contains(doc) else {
await notificationHandler(notification, languageService)
return
}
// Not ready to handle it, we'll queue it and handle it later.
self.documentToPendingQueue[doc, default: DocumentNotificationRequestQueue()].add(operation: {
await notificationHandler(notification, languageService)
})
}
/// Execute `notificationHandler` once the document that it concerns is ready
/// and has the initial build settings. These build settings might still be
/// incomplete or fallback settings.
private func withReadyDocument<RequestType: TextDocumentRequest>(
for request: Request<RequestType>,
requestHandler: @escaping (Request<RequestType>, Workspace, ToolchainLanguageServer) async -> Void,
fallback: RequestType.Response
) async {
let doc = request.params.textDocument.uri
guard let workspace = self.workspaceForDocument(uri: doc) else {
return request.reply(.failure(.workspaceNotOpen(doc)))
}
// 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[doc] else {
return request.reply(fallback)
}
// If the document is ready, we can handle it right now.
guard !self.documentsReady.contains(doc) else {
await requestHandler(request, workspace, languageService)
return
}
// Not ready to handle it, we'll queue it and handle it later.
self.documentToPendingQueue[doc, default: DocumentNotificationRequestQueue()].add(operation: {
await requestHandler(request, workspace, languageService)
}, cancellationHandler: {
request.reply(fallback)
})
}
public func _handleUnknown<R>(_ req: Request<R>) {
if req.clientID == ObjectIdentifier(client) {
req.reply(.failure(ResponseError.methodNotFound(R.method)))
return
}
// Unknown requests from a language server are passed on to the client.
let id = client.send(req.params, queue: clientCommunicationQueue) { result in
req.reply(result)
}
req.cancellationToken.addCancellationHandler {
self.client.send(CancelRequestNotification(id: id))
}
}
/// Handle an unknown notification.
public func _handleUnknown<N>(_ note: Notification<N>) {
if note.clientID == ObjectIdentifier(client) {
return
}
// Unknown notifications from a language server are passed on to the client.
client.send(note.params)
}
func toolchain(for uri: DocumentURI, _ language: Language) -> Toolchain? {
let supportsLang = { (toolchain: Toolchain) -> Bool in
// FIXME: the fact that we're looking at clangd/sourcekitd instead of the compiler indicates this method needs a parameter stating what kind of tool we're looking for.
switch language {
case .swift:
return toolchain.sourcekitd != nil
case .c, .cpp, .objective_c, .objective_cpp:
return toolchain.clangd != nil
default:
return false
}
}
if let toolchain = toolchainRegistry.default, supportsLang(toolchain) {
return toolchain
}
for toolchain in toolchainRegistry.toolchains {
if supportsLang(toolchain) {
return toolchain
}
}
return nil
}
/// After the language service has crashed, send `DidOpenTextDocumentNotification`s to a newly instantiated language service for previously open documents.
func reopenDocuments(for languageService: ToolchainLanguageServer) async {
for documentUri in self.documentManager.openDocuments {
guard let workspace = self.workspaceForDocument(uri: documentUri) else {
continue
}
guard workspace.documentService[documentUri] === languageService else {
continue
}
guard let snapshot = self.documentManager.latestSnapshot(documentUri) else {
// The document has been closed since we retrieved its URI. We don't care about it anymore.
continue
}
// Close the document properly in the document manager and build system manager to start with a clean sheet when re-opening it.
let closeNotification = DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(documentUri))
await self.closeDocument(closeNotification, workspace: workspace)
let textDocument = TextDocumentItem(uri: documentUri,
language: snapshot.document.language,
version: snapshot.version,
text: snapshot.text)
await self.openDocument(DidOpenTextDocumentNotification(textDocument: textDocument), workspace: workspace)
}
}
/// If a language service of type `serverType` that can handle `workspace` has
/// already been started, return it, otherwise return `nil`.
private func existingLanguageService(_ serverType: LanguageServerType, workspace: Workspace) -> ToolchainLanguageServer? {
for languageService in languageServices[serverType, default: []] {
if languageService.canHandle(workspace: workspace) {
return languageService
}
}
return nil
}
func languageService(
for toolchain: Toolchain,
_ language: Language,
in workspace: Workspace
) async -> ToolchainLanguageServer? {
guard let serverType = LanguageServerType(language: language) else {
return nil
}
// Pick the first language service that can handle this workspace.
if let languageService = existingLanguageService(serverType, workspace: workspace) {
return languageService
}
// Start a new service.
return await orLog("failed to start language service", level: .error) {
let service = try await SourceKitLSP.languageService(
for: toolchain,
serverType,
options: options,
client: self,
in: workspace,
reopenDocuments: { [weak self] toolchainLanguageServer in
await self?.reopenDocuments(for: toolchainLanguageServer)
},
workspaceForDocument: { [weak self] document in
guard let self else { return nil }
return await self.workspaceForDocument(uri: document)
}
)
guard let service else {
return nil
}
let pid = Int(ProcessInfo.processInfo.processIdentifier)
let resp = try await service.initializeSync(InitializeRequest(
processId: pid,
rootPath: nil,
rootURI: workspace.rootUri,
initializationOptions: nil,
capabilities: workspace.capabilityRegistry.clientCapabilities,
trace: .off,
workspaceFolders: nil))
let languages = languageClass(for: language)
self.registerCapabilities(
for: resp.capabilities, languages: languages, registry: workspace.capabilityRegistry)
// FIXME: store the server capabilities.
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 {
fatalError("non-incremental update not implemented")
}
await service.clientInitialized(InitializedNotification())
if let concurrentlyInitializedService = existingLanguageService(serverType, 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[serverType, default: []].append(service)
return service
}
}
/// **Public for testing purposes only**
public func _languageService(for uri: DocumentURI, _ language: Language, in workspace: Workspace) async -> ToolchainLanguageServer? {
return await languageService(for: uri, language, in: workspace)
}
func languageService(for uri: DocumentURI, _ language: Language, in workspace: Workspace) async -> ToolchainLanguageServer? {
if let service = workspace.documentService[uri] {
return service
}
guard let toolchain = toolchain(for: uri, language),
let service = await languageService(for: toolchain, language, in: workspace)
else {
return nil
}
log("Using toolchain \(toolchain.displayName) (\(toolchain.identifier)) for \(uri)")
if let concurrentlySetService = workspace.documentService[uri] {
// Since we await the construction of `service`, another call to this
// function might have happened and raced us, setting
// `workspace.documentServices[uri]`. If this is the case, return the
// existing value and discard the service that we just retrieved.
return concurrentlySetService
}
workspace.documentService[uri] = service
return service
}
}
// MARK: - MessageHandler
extension SourceKitServer: MessageHandler {
public func handle(_ params: some NotificationType, from clientID: ObjectIdentifier) async {
let notification = Notification(params, clientID: clientID)
self._logNotification(notification)
switch notification {
case let notification as Notification<InitializedNotification>:
self.clientInitialized(notification)
case let notification as Notification<CancelRequestNotification>:
self.cancelRequest(notification)
case let notification as Notification<ExitNotification>:
self.exit(notification)
case let notification as Notification<DidOpenTextDocumentNotification>:
await self.openDocument(notification)
case let notification as Notification<DidCloseTextDocumentNotification>:
await self.closeDocument(notification)
case let notification as Notification<DidChangeTextDocumentNotification>:
await self.changeDocument(notification)
case let notification as Notification<DidChangeWorkspaceFoldersNotification>:
await self.didChangeWorkspaceFolders(notification)
case let notification as Notification<DidChangeWatchedFilesNotification>:
self.didChangeWatchedFiles(notification)
case let notification as Notification<WillSaveTextDocumentNotification>:
await self.withReadyDocument(for: notification, notificationHandler: self.willSaveDocument)
case let notification as Notification<DidSaveTextDocumentNotification>:
await self.withReadyDocument(for: notification, notificationHandler: self.didSaveDocument)
default:
self._handleUnknown(notification)
}
}
public func handle<R: RequestType>(_ params: R, id: RequestID, from clientID: ObjectIdentifier, reply: @escaping (LSPResult<R.Response >) -> Void) async {
let cancellationToken = CancellationToken()
let key = RequestCancelKey(client: clientID, request: id)
self.requestCancellation[key] = cancellationToken
let request = Request(params, id: id, clientID: clientID, cancellation: cancellationToken, reply: { [weak self] result in
if let self {
Task {
await self.stopTrackingCancellationKey(key)
}
}
reply(result)
if let self {
Task {
await self._logResponse(result, id: id, method: R.method)
}
}
})
self._logRequest(request)
switch request {
case let request as Request<InitializeRequest>:
self.initialize(request)
case let request as Request<ShutdownRequest>:
await self.shutdown(request)
case let request as Request<WorkspaceSymbolsRequest>:
self.workspaceSymbols(request)
case let request as Request<PollIndexRequest>:
self.pollIndex(request)
case let request as Request<ExecuteCommandRequest>:
await self.executeCommand(request)
case let request as Request<CallHierarchyIncomingCallsRequest>:
self.incomingCalls(request)
case let request as Request<CallHierarchyOutgoingCallsRequest>:
self.outgoingCalls(request)
case let request as Request<TypeHierarchySupertypesRequest>:
self.supertypes(request)
case let request as Request<TypeHierarchySubtypesRequest>:
self.subtypes(request)
case let request as Request<CompletionRequest>:
await self.withReadyDocument(for: request, requestHandler: self.completion, fallback: CompletionList(isIncomplete: false, items: []))
case let request as Request<HoverRequest>:
await self.withReadyDocument(for: request, requestHandler: self.hover, fallback: nil)
case let request as Request<OpenInterfaceRequest>:
await self.withReadyDocument(for: request, requestHandler: self.openInterface, fallback: nil)
case let request as Request<DeclarationRequest>:
await self.withReadyDocument(for: request, requestHandler: self.declaration, fallback: nil)
case let request as Request<DefinitionRequest>:
await self.withReadyDocument(for: request, requestHandler: self.definition, fallback: .locations([]))
case let request as Request<ReferencesRequest>:
await self.withReadyDocument(for: request, requestHandler: self.references, fallback: [])
case let request as Request<ImplementationRequest>:
await self.withReadyDocument(for: request, requestHandler: self.implementation, fallback: .locations([]))
case let request as Request<CallHierarchyPrepareRequest>:
await self.withReadyDocument(for: request, requestHandler: self.prepareCallHierarchy, fallback: [])
case let request as Request<TypeHierarchyPrepareRequest>:
await self.withReadyDocument(for: request, requestHandler: self.prepareTypeHierarchy, fallback: [])
case let request as Request<SymbolInfoRequest>:
await self.withReadyDocument(for: request, requestHandler: self.symbolInfo, fallback: [])
case let request as Request<DocumentHighlightRequest>:
await self.withReadyDocument(for: request, requestHandler: self.documentSymbolHighlight, fallback: nil)
case let request as Request<FoldingRangeRequest>:
await self.withReadyDocument(for: request, requestHandler: self.foldingRange, fallback: nil)
case let request as Request<DocumentSymbolRequest>:
await self.withReadyDocument(for: request, requestHandler: self.documentSymbol, fallback: nil)
case let request as Request<DocumentColorRequest>:
await self.withReadyDocument(for: request, requestHandler: self.documentColor, fallback: [])
case let request as Request<DocumentSemanticTokensRequest>:
await self.withReadyDocument(for: request, requestHandler: self.documentSemanticTokens, fallback: nil)
case let request as Request<DocumentSemanticTokensDeltaRequest>:
await self.withReadyDocument(for: request, requestHandler: self.documentSemanticTokensDelta, fallback: nil)
case let request as Request<DocumentSemanticTokensRangeRequest>:
await self.withReadyDocument(for: request, requestHandler: self.documentSemanticTokensRange, fallback: nil)
case let request as Request<ColorPresentationRequest>:
await self.withReadyDocument(for: request, requestHandler: self.colorPresentation, fallback: [])
case let request as Request<CodeActionRequest>:
await self.withReadyDocument(for: request, requestHandler: self.codeAction, fallback: nil)
case let request as Request<InlayHintRequest>:
await self.withReadyDocument(for: request, requestHandler: self.inlayHint, fallback: [])
case let request as Request<DocumentDiagnosticsRequest>:
await self.withReadyDocument(for: request, requestHandler: self.documentDiagnostic, fallback: .full(.init(items: [])))
default:
self._handleUnknown(request)
}
}
private func _logRequest<R>(_ request: Request<R>) {
logAsync { currentLevel in
guard currentLevel >= LogLevel.debug else {
return "\(type(of: self)): Request<\(R.method)(\(request.id))>"
}
return "\(type(of: self)): \(request)"
}
}
private func _logNotification<N>(_ notification: Notification<N>) {
logAsync { currentLevel in
guard currentLevel >= LogLevel.debug else {
return "\(type(of: self)): Notification<\(N.method)>"
}
return "\(type(of: self)): \(notification)"
}
}
private func _logResponse<Response>(_ result: LSPResult<Response>, id: RequestID, method: String) {
logAsync { currentLevel in
guard currentLevel >= LogLevel.debug else {
return "\(type(of: self)): Response<\(method)(\(id))>"
}
return """
\(type(of: self)): Response<\(method)(\(id))>(
\(result)
)
"""
}
}
}
// MARK: - Build System Delegate
extension SourceKitServer: BuildSystemDelegate {
// FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified
public nonisolated func buildTargetsChanged(_ changes: [BuildTargetEvent]) {
// TODO: do something with these changes once build target support is in place
}
private func affectedOpenDocumentsForChangeSet(
_ changes: Set<DocumentURI>,
_ documentManager: DocumentManager
) -> Set<DocumentURI> {
// An empty change set is treated as if all open files have been modified.
guard !changes.isEmpty else {
return documentManager.openDocuments
}
return documentManager.openDocuments.intersection(changes)
}
// FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified
/// Non-async variant that executes `fileBuildSettingsChangedImpl` in a new task.
public nonisolated func fileBuildSettingsChanged(_ changedFiles: [DocumentURI: FileBuildSettingsChange]) {
Task {
await self.fileBuildSettingsChangedImpl(changedFiles)
}
}
/// Handle a build settings change notification from the `BuildSystem`.
/// This has two primary cases:
/// - Initial settings reported for a given file, now we can fully open it
/// - Changed settings for an already open file
public func fileBuildSettingsChangedImpl(
_ changedFiles: [DocumentURI: FileBuildSettingsChange]
) async {
for (uri, change) in changedFiles {
// Non-ready documents should be considered open even though we haven't
// opened it with the language service yet.
guard self.documentManager.openDocuments.contains(uri) else { continue }
guard let workspace = self.workspaceForDocument(uri: uri) else {
continue
}
guard self.documentsReady.contains(uri) else {
// Case 1: initial settings for a given file. Now we can process our backlog.
log("Initial build settings received for opened file \(uri)")
guard let service = workspace.documentService[uri] else {
// Unexpected: we should have an existing language service if we've registered for
// change notifications for an opened but non-ready document.
log("No language service for build settings change to non-ready file \(uri)",
level: .error)
// We're in an odd state, cancel pending requests if we have any.
self.documentToPendingQueue[uri]?.cancelAll()
self.documentToPendingQueue[uri] = nil
continue
}
// Notify the language server so it can apply the proper arguments.
await service.documentUpdatedBuildSettings(uri, change: change)
// Catch up on any queued notifications and requests.
while !(documentToPendingQueue[uri]?.queue.isEmpty ?? true) {
// We need to run this loop until convergence since new closures can
// get added to `documentToPendingQueue` while we are awaiting the
// result of a `task.operation()`.
let pendingQueue = documentToPendingQueue[uri]?.queue ?? []
documentToPendingQueue[uri]?.queue = []
for task in pendingQueue {
await task.operation()
}
}
self.documentToPendingQueue[uri] = nil
self.documentsReady.insert(uri)
continue
}
// Case 2: changed settings for an already open file.
log("Build settings changed for opened file \(uri)")
if let service = workspace.documentService[uri] {
await service.documentUpdatedBuildSettings(uri, change: change)
}
}
}
// FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified
public nonisolated func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>) {
Task {
await filesDependenciesUpdatedImpl(changedFiles)
}
}
/// Handle a dependencies updated notification from the `BuildSystem`.
/// We inform the respective language services as long as the given file is open
/// (not queued for opening).
public func filesDependenciesUpdatedImpl(_ changedFiles: Set<DocumentURI>) async {
// Split the changedFiles into the workspaces they belong to.
// Then invoke affectedOpenDocumentsForChangeSet for each workspace with its affected files.
let changedFilesAndWorkspace = changedFiles.map({
return (uri: $0, workspace: self.workspaceForDocument(uri: $0))
})
for workspace in self.workspaces {
let changedFilesForWorkspace = Set(changedFilesAndWorkspace.filter({ $0.workspace === workspace }).map(\.uri))
if changedFilesForWorkspace.isEmpty {
continue
}
for uri in self.affectedOpenDocumentsForChangeSet(changedFilesForWorkspace, self.documentManager) {
// Make sure the document is ready - otherwise the language service won't
// know about the document yet.
guard self.documentsReady.contains(uri) else {
continue
}
log("Dependencies updated for opened file \(uri)")
if let service = workspace.documentService[uri] {
await service.documentDependenciesUpdated(uri)
}
}
}
}
// FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified
public nonisolated func fileHandlingCapabilityChanged() {
Task {
await fileHandlingCapabilityChangedImpl()
}
}
public func fileHandlingCapabilityChangedImpl() {
self.uriToWorkspaceCache = [:]
}
}
// MARK: - Request and notification handling
extension SourceKitServer {
// MARK: - General
/// Creates a workspace at the given `uri`.
private func workspace(uri: DocumentURI) -> Workspace? {
guard let capabilityRegistry = capabilityRegistry else {
log("Cannot open workspace before server is initialized")
return nil
}
return try? Workspace(
documentManager: self.documentManager,
rootUri: uri,
capabilityRegistry: capabilityRegistry,
toolchainRegistry: self.toolchainRegistry,
buildSetup: self.options.buildSetup,
indexOptions: self.options.indexOptions,
reloadPackageStatusCallback: { status in
guard capabilityRegistry.clientCapabilities.window?.workDoneProgress ?? false else {
// Client doesnt support work done progress
return
}
// FIXME: (async) This can cause out-of-order notifications to be sent to the editor
// if the scheduled tasks change order.
// Make `reloadPackageStatusCallback` async and shift the responsibility for
// guaranteeing in-order calls to `reloadPackageStatusCallback` to
// `SwiftPMWorkspace.reloadPackage` once that method is async.
Task {
switch status {
case .start:
await self.packageLoadingWorkDoneProgress.startProgress(server: self)
case .end:
await self.packageLoadingWorkDoneProgress.endProgress(server: self)
}
}
}
)
}
func initialize(_ req: Request<InitializeRequest>) {
if case .dictionary(let options) = req.params.initializationOptions {
if case .bool(let listenToUnitEvents) = options["listenToUnitEvents"] {
self.options.indexOptions.listenToUnitEvents = listenToUnitEvents
}
if case .dictionary(let completionOptions) = options["completion"] {
if case .bool(let serverSideFiltering) = completionOptions["serverSideFiltering"] {
self.options.completionOptions.serverSideFiltering = serverSideFiltering
}
switch completionOptions["maxResults"] {
case .none:
break
case .some(.null):
self.options.completionOptions.maxResults = nil
case .some(.int(let maxResults)):
self.options.completionOptions.maxResults = maxResults
case .some(let invalid):
log("expected null or int for 'maxResults'; got \(invalid)", level: .warning)
}
}
}
capabilityRegistry = CapabilityRegistry(clientCapabilities: req.params.capabilities)
if let workspaceFolders = req.params.workspaceFolders {
self.workspaces.append(contentsOf: workspaceFolders.compactMap({ self.workspace(uri: $0.uri) }))
} else if let uri = req.params.rootURI {
if let workspace = self.workspace(uri: uri) {
self.workspaces.append(workspace)
}
} else if let path = req.params.rootPath {
if let workspace = self.workspace(uri: DocumentURI(URL(fileURLWithPath: path))) {
self.workspaces.append(workspace)
}
}
if self.workspaces.isEmpty {
log("no workspace found", level: .warning)
let workspace = Workspace(
documentManager: self.documentManager,
rootUri: req.params.rootURI,
capabilityRegistry: self.capabilityRegistry!,
toolchainRegistry: self.toolchainRegistry,
buildSetup: self.options.buildSetup,
underlyingBuildSystem: nil,
index: nil,
indexDelegate: nil)
self.workspaces.append(workspace)
}
assert(!self.workspaces.isEmpty)
for workspace in self.workspaces {
workspace.buildSystemManager.delegate = self
}
req.reply(InitializeResult(capabilities:
self.serverCapabilities(for: req.params.capabilities, registry: self.capabilityRegistry!)))
}
func serverCapabilities(
for client: ClientCapabilities,
registry: CapabilityRegistry
) -> ServerCapabilities {
let completionOptions: CompletionOptions?
if registry.clientHasDynamicCompletionRegistration {
// We'll initialize this dynamically instead of statically.
completionOptions = nil
} else {
completionOptions = LanguageServerProtocol.CompletionOptions(
resolveProvider: false,
triggerCharacters: ["."]
)
}
let executeCommandOptions: ExecuteCommandOptions?
if registry.clientHasDynamicExecuteCommandRegistration {
executeCommandOptions = nil
} else {
executeCommandOptions = ExecuteCommandOptions(commands: builtinSwiftCommands)
}
return ServerCapabilities(
textDocumentSync: .options(TextDocumentSyncOptions(
openClose: true,
change: .incremental
)),
hoverProvider: .bool(true),
completionProvider: completionOptions,
definitionProvider: .bool(true),
implementationProvider: .bool(true),
referencesProvider: .bool(true),
documentHighlightProvider: .bool(true),
documentSymbolProvider: .bool(true),
workspaceSymbolProvider: .bool(true),
codeActionProvider: .value(CodeActionServerCapabilities(
clientCapabilities: client.textDocument?.codeAction,
codeActionOptions: CodeActionOptions(codeActionKinds: nil),
supportsCodeActions: true
)),
colorProvider: .bool(true),
foldingRangeProvider: .bool(!registry.clientHasDynamicFoldingRangeRegistration),
declarationProvider: .bool(true),
executeCommandProvider: executeCommandOptions,
workspace: WorkspaceServerCapabilities(workspaceFolders: .init(
supported: true,
changeNotifications: .bool(true)
)),
callHierarchyProvider: .bool(true),
typeHierarchyProvider: .bool(true)
)
}
func registerCapabilities(
for server: ServerCapabilities,
languages: [Language],
registry: CapabilityRegistry
) {
if let completionOptions = server.completionProvider {
registry.registerCompletionIfNeeded(options: completionOptions, for: languages) {
self.dynamicallyRegisterCapability($0, registry)
}
}
if server.foldingRangeProvider?.isSupported == true {
registry.registerFoldingRangeIfNeeded(options: FoldingRangeOptions(), for: languages) {
self.dynamicallyRegisterCapability($0, registry)
}
}
if let semanticTokensOptions = server.semanticTokensProvider {
registry.registerSemanticTokensIfNeeded(options: semanticTokensOptions, for: languages) {
self.dynamicallyRegisterCapability($0, registry)
}
}
if let inlayHintProvider = server.inlayHintProvider,
inlayHintProvider.isSupported {
let options: InlayHintOptions
switch inlayHintProvider {
case .bool(_):
options = InlayHintOptions()
case .value(let opts):
options = opts
}
registry.registerInlayHintIfNeeded(options: options, for: languages) {
self.dynamicallyRegisterCapability($0, registry)
}
}
if let diagnosticOptions = server.diagnosticProvider {
registry.registerDiagnosticIfNeeded(options: diagnosticOptions, for: languages) {
self.dynamicallyRegisterCapability($0, registry)
}
}
if let commandOptions = server.executeCommandProvider {
registry.registerExecuteCommandIfNeeded(commands: commandOptions.commands) {
self.dynamicallyRegisterCapability($0, registry)
}
}
/// This must be a superset of the files that return true for SwiftPM's `Workspace.fileAffectsSwiftOrClangBuildSettings`.
var watchers = FileRuleDescription.builtinRules.flatMap({ $0.fileTypes }).map { fileExtension in
return FileSystemWatcher(globPattern: "**/*.\(fileExtension)", kind: [.create, .delete])
}
watchers.append(FileSystemWatcher(globPattern: "**/Package.swift", kind: [.change]))
watchers.append(FileSystemWatcher(globPattern: "**/compile_commands.json", kind: [.create, .change, .delete]))
watchers.append(FileSystemWatcher(globPattern: "**/compile_flags.txt", kind: [.create, .change, .delete]))
registry.registerDidChangeWatchedFiles(watchers: watchers) {
self.dynamicallyRegisterCapability($0, registry)
}
}
private func dynamicallyRegisterCapability(
_ registration: CapabilityRegistration,
_ registry: CapabilityRegistry
) {
let req = RegisterCapabilityRequest(registrations: [registration])
let _ = client.send(req, queue: clientCommunicationQueue) { result in
if let error = result.failure {
log("Failed to dynamically register for \(registration.method): \(error)", level: .error)
registry.remove(registration: registration)
}
}
}
func clientInitialized(_: Notification<InitializedNotification>) {
// Nothing to do.
}
func cancelRequest(_ notification: Notification<CancelRequestNotification>) {
let key = RequestCancelKey(client: notification.clientID, request: notification.params.id)
requestCancellation[key]?.cancel()
}
/// Stop keeping track of the cancellation handler for the given cancellation key.
func stopTrackingCancellationKey(_ key: RequestCancelKey) {
requestCancellation[key] = nil
}
/// The server is about to exit, and the server should flush any buffered state.
///
/// The server shall not be used to handle more requests (other than possibly
/// `shutdown` and `exit`) and should attempt to flush any buffered state
/// immediately, such as sending index changes to disk.
public func prepareForExit() {
// Note: this method should be safe to call multiple times, since we want to
// be resilient against multiple possible shutdown sequences, including
// pipe failure.
// Close the index, which will flush to disk.
for workspace in self.workspaces {
workspace.buildSystemManager.mainFilesProvider = nil
workspace.index = nil
// Break retain cycle with the BSM.
workspace.buildSystemManager.delegate = nil
}
}
func shutdown(_ request: Request<ShutdownRequest>) async {
prepareForExit()
await withTaskGroup(of: Void.self) { taskGroup in
for service in languageServices.values.flatMap({ $0 }) {
taskGroup.addTask {
await service.shutdown()
}
}
}
// We have a semantic guarantee that no request or notification should be
// sent to an LSP server after the shutdown request. Thus, there's no chance
// that a new language service has been started during the above 'await'
// call.
languageServices = [:]
// Wait for all services to shut down before sending the shutdown response.
// Otherwise we might terminate sourcekit-lsp while it still has open
// connections to the toolchain servers, which could send messages to
// sourcekit-lsp while it is being deallocated, causing crashes.
request.reply(VoidResponse())
}
func exit(_ notification: Notification<ExitNotification>) {
// Should have been called in shutdown, but allow misbehaving clients.
prepareForExit()
// Call onExit only once, and hop off queue to allow the handler to call us back.
let onExit = self.onExit
self.onExit = {}
DispatchQueue.global().async {
onExit()
}
}
// MARK: - Text synchronization
func openDocument(_ note: Notification<DidOpenTextDocumentNotification>) async {
let uri = note.params.textDocument.uri
guard let workspace = workspaceForDocument(uri: uri) else {
log("received open notification for file '\(uri)' without a corresponding workspace, ignoring...", level: .error)
return
}
await openDocument(note.params, workspace: workspace)
}
private func openDocument(_ note: DidOpenTextDocumentNotification, workspace: Workspace) async {
// Immediately open the document even if the build system isn't ready. This is important since
// we check that the document is open when we receive messages from the build system.
documentManager.open(note)
let textDocument = note.textDocument
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 {
return
}
workspace.buildSystemManager.registerForChangeNotifications(for: uri, language: language)
// If the document is ready, we can immediately send the notification.
guard !documentsReady.contains(uri) else {
await service.openDocument(note)
return
}
// Need to queue the open call so we can handle it when ready.
self.documentToPendingQueue[uri, default: DocumentNotificationRequestQueue()].add(operation: {
await service.openDocument(note)
})
}
func closeDocument(_ note: Notification<DidCloseTextDocumentNotification>) async {
let uri = note.params.textDocument.uri
guard let workspace = workspaceForDocument(uri: uri) else {
log("received close notification for file '\(uri)' without a corresponding workspace, ignoring...", level: .error)
return
}
await self.closeDocument(note.params, workspace: workspace)
}
func closeDocument(_ note: DidCloseTextDocumentNotification, workspace: Workspace) async {
// Immediately close the document. We need to be sure to clear our pending work queue in case
// the build system still isn't ready.
documentManager.close(note)
let uri = note.textDocument.uri
workspace.buildSystemManager.unregisterForChangeNotifications(for: uri)
// If the document is ready, we can close it now.
guard !documentsReady.contains(uri) else {
self.documentsReady.remove(uri)
await workspace.documentService[uri]?.closeDocument(note)
return
}
// Clear any queued notifications via their cancellation handlers.
// No need to send the notification since it was never considered opened.
self.documentToPendingQueue[uri]?.cancelAll()
self.documentToPendingQueue[uri] = nil
}
func changeDocument(_ note: Notification<DidChangeTextDocumentNotification>) async {
let uri = note.params.textDocument.uri
guard let workspace = workspaceForDocument(uri: uri) else {
log("received change notification for file '\(uri)' without a corresponding workspace, ignoring...", level: .error)
return
}
// If the document is ready, we can handle the change right now.
guard !documentsReady.contains(uri) else {
documentManager.edit(note.params)
await workspace.documentService[uri]?.changeDocument(note.params)
return
}
// Need to queue the change call so we can handle it when ready.
self.documentToPendingQueue[uri, default: DocumentNotificationRequestQueue()].add(operation: {
self.documentManager.edit(note.params)
await workspace.documentService[uri]?.changeDocument(note.params)
})
}
func willSaveDocument(
_ note: Notification<WillSaveTextDocumentNotification>,
languageService: ToolchainLanguageServer
) async {
await languageService.willSaveDocument(note.params)
}
func didSaveDocument(
_ note: Notification<DidSaveTextDocumentNotification>,
languageService: ToolchainLanguageServer
) async {
await languageService.didSaveDocument(note.params)
}
func didChangeWorkspaceFolders(_ note: Notification<DidChangeWorkspaceFoldersNotification>) async {
var preChangeWorkspaces: [DocumentURI: Workspace] = [:]
for docUri in self.documentManager.openDocuments {
preChangeWorkspaces[docUri] = self.workspaceForDocument(uri: docUri)
}
if let removed = note.params.event.removed {
self.workspaces.removeAll { workspace in
return removed.contains(where: { workspaceFolder in
workspace.rootUri == workspaceFolder.uri
})
}
}
if let added = note.params.event.added {
let newWorkspaces = added.compactMap({ self.workspace(uri: $0.uri) })
for workspace in newWorkspaces {
workspace.buildSystemManager.delegate = self
}
self.workspaces.append(contentsOf: newWorkspaces)
}
// For each document that has moved to a different workspace, close it in
// the old workspace and open it in the new workspace.
for docUri in self.documentManager.openDocuments {
let oldWorkspace = preChangeWorkspaces[docUri]
let newWorkspace = self.workspaceForDocument(uri: docUri)
if newWorkspace !== oldWorkspace {
guard let snapshot = documentManager.latestSnapshot(docUri) else {
continue
}
if let oldWorkspace = oldWorkspace {
await self.closeDocument(DidCloseTextDocumentNotification(
textDocument: TextDocumentIdentifier(docUri)
), workspace: oldWorkspace)
}
if let newWorkspace = newWorkspace {
await self.openDocument(DidOpenTextDocumentNotification(textDocument: TextDocumentItem(
uri: docUri,
language: snapshot.document.language,
version: snapshot.version,
text: snapshot.text
)), workspace: newWorkspace)
}
}
}
}
func didChangeWatchedFiles(_ note: Notification<DidChangeWatchedFilesNotification>) {
// We can't make any assumptions about which file changes a particular build
// system is interested in. Just because it doesn't have build settings for
// a file doesn't mean a file can't affect the build system's build settings
// (e.g. Package.swift doesn't have build settings but affects build
// settings). Inform the build system about all file changes.
for workspace in workspaces {
workspace.buildSystemManager.filesDidChange(note.params.changes)
}
}
// MARK: - Language features
func completion(
_ req: Request<CompletionRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.completion(req)
}
func hover(
_ req: Request<HoverRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.hover(req)
}
func openInterface(
_ req: Request<OpenInterfaceRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.openInterface(req)
}
/// Find all symbols in the workspace that include a string in their name.
/// - returns: An array of SymbolOccurrences that match the string.
func findWorkspaceSymbols(matching: String) -> [SymbolOccurrence] {
// Ignore short queries since they are:
// - noisy and slow, since they can match many symbols
// - normally unintentional, triggered when the user types slowly or if the editor doesn't
// debounce events while the user is typing
guard matching.count >= minWorkspaceSymbolPatternLength else {
return []
}
var symbolOccurenceResults: [SymbolOccurrence] = []
for workspace in workspaces {
workspace.index?.forEachCanonicalSymbolOccurrence(
containing: matching,
anchorStart: false,
anchorEnd: false,
subsequence: true,
ignoreCase: true
) { symbol in
guard !symbol.location.isSystem && !symbol.roles.contains(.accessorOf) else {
return true
}
symbolOccurenceResults.append(symbol)
// FIXME: Once we have cancellation support, we should fetch all results and take the top
// `maxWorkspaceSymbolResults` symbols but bail if cancelled.
//
// Until then, take the first `maxWorkspaceSymbolResults` symbols to limit the impact of
// queries which match many symbols.
return symbolOccurenceResults.count < maxWorkspaceSymbolResults
}
}
return symbolOccurenceResults
}
/// Handle a workspace/symbol request, returning the SymbolInformation.
/// - returns: An array with SymbolInformation for each matching symbol in the workspace.
func workspaceSymbols(_ req: Request<WorkspaceSymbolsRequest>) {
let symbols = findWorkspaceSymbols(
matching: req.params.query
).map({symbolOccurrence -> WorkspaceSymbolItem in
let symbolPosition = Position(
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
utf16index: symbolOccurrence.location.utf8Column - 1)
let symbolLocation = Location(
uri: DocumentURI(URL(fileURLWithPath: symbolOccurrence.location.path)),
range: Range(symbolPosition))
return .symbolInformation(SymbolInformation(
name: symbolOccurrence.symbol.name,
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
deprecated: nil,
location: symbolLocation,
containerName: symbolOccurrence.getContainerName()
))
})
req.reply(symbols)
}
/// Forwards a SymbolInfoRequest to the appropriate toolchain service for this document.
func symbolInfo(
_ req: Request<SymbolInfoRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.symbolInfo(req)
}
func documentSymbolHighlight(
_ req: Request<DocumentHighlightRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.documentSymbolHighlight(req)
}
func foldingRange(
_ req: Request<FoldingRangeRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer) async {
await languageService.foldingRange(req)
}
func documentSymbol(
_ req: Request<DocumentSymbolRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.documentSymbol(req)
}
func documentColor(
_ req: Request<DocumentColorRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.documentColor(req)
}
func documentSemanticTokens(
_ req: Request<DocumentSemanticTokensRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.documentSemanticTokens(req)
}
func documentSemanticTokensDelta(
_ req: Request<DocumentSemanticTokensDeltaRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.documentSemanticTokensDelta(req)
}
func documentSemanticTokensRange(
_ req: Request<DocumentSemanticTokensRangeRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.documentSemanticTokensRange(req)
}
func colorPresentation(
_ req: Request<ColorPresentationRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.colorPresentation(req)
}
func executeCommand(_ req: Request<ExecuteCommandRequest>) async {
guard let uri = req.params.textDocument?.uri else {
log("attempted to perform executeCommand request without an url!", level: .error)
req.reply(nil)
return
}
guard let workspace = workspaceForDocument(uri: uri) else {
req.reply(.failure(.workspaceNotOpen(uri)))
return
}
guard let languageService = workspace.documentService[uri] else {
req.reply(nil)
return
}
// If the document isn't yet ready, queue the request.
guard self.documentsReady.contains(uri) else {
let operation = { [weak self] in
guard let self = self else { return }
// FIXME: (async) This might cause out-of order requests if tasks of the
// same `documentToPendingQueue` are executed out-of-order. To fix this, we should
// always wait for build settings before handling a request and remove
// documentToPendingQueue.
Task {
await self.fowardExecuteCommand(req, languageService: languageService)
}
}
let cancellationHandler = {
req.reply(nil)
}
self.documentToPendingQueue[uri, default: DocumentNotificationRequestQueue()]
.add(operation: operation, cancellationHandler: cancellationHandler)
return
}
await self.fowardExecuteCommand(req, languageService: languageService)
}
func fowardExecuteCommand(
_ req: Request<ExecuteCommandRequest>,
languageService: ToolchainLanguageServer
) async {
let params = req.params
let executeCommand = ExecuteCommandRequest(command: params.command,
arguments: params.argumentsWithoutSourceKitMetadata)
let callback = { (result: Result<ExecuteCommandRequest.Response, ResponseError>) in
req.reply(result)
}
let request = Request(executeCommand, id: req.id, clientID: ObjectIdentifier(self),
cancellation: req.cancellationToken, reply: callback)
await languageService.executeCommand(request)
}
func codeAction(
_ req: Request<CodeActionRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
let codeAction = CodeActionRequest(range: req.params.range, context: req.params.context,
textDocument: req.params.textDocument)
let callback = { (result: Result<CodeActionRequest.Response, ResponseError>) in
switch result {
case .success(let reply):
req.reply(req.params.injectMetadata(toResponse: reply))
default:
req.reply(result)
}
}
let request = Request(codeAction, id: req.id, clientID: ObjectIdentifier(self),
cancellation: req.cancellationToken, reply: callback)
await languageService.codeAction(request)
}
func inlayHint(
_ req: Request<InlayHintRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.inlayHint(req)
}
func documentDiagnostic(
_ req: Request<DocumentDiagnosticsRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
await languageService.documentDiagnostic(req)
}
/// Converts a location from the symbol index to an LSP location.
///
/// - Parameter location: The symbol index location
/// - Returns: The LSP location
private func indexToLSPLocation(_ location: SymbolLocation) -> Location? {
guard !location.path.isEmpty else { return nil }
return Location(
uri: DocumentURI(URL(fileURLWithPath: location.path)),
range: Range(Position(
// 1-based -> 0-based
// Note that we still use max(0, ...) as a fallback if the location is zero.
line: max(0, location.line - 1),
// FIXME: we need to convert the utf8/utf16 column, which may require reading the file!
utf16index: max(0, location.utf8Column - 1)
))
)
}
/// Extracts the locations of an indexed symbol's occurrences,
/// e.g. for definition or reference lookups.
///
/// - Parameters:
/// - result: The symbol to look up
/// - index: The index in which the occurrences will be looked up
/// - useLocalFallback: Whether to consider the best known local declaration if no other locations are found
/// - extractOccurrences: A function fetching the occurrences by the desired roles given a usr from the index
/// - Returns: The resolved symbol locations
private func extractIndexedOccurrences(
result: LSPResult<SymbolInfoRequest.Response>,
index: IndexStoreDB?,
useLocalFallback: Bool = false,
extractOccurrences: (String, IndexStoreDB) -> [SymbolOccurrence]
) -> LSPResult<[(occurrence: SymbolOccurrence?, location: Location)]> {
guard case .success(let symbols) = result else {
return .failure(result.failure!)
}
guard let symbol = symbols.first else {
return .success([])
}
let fallback: [(occurrence: SymbolOccurrence?, location: Location)]
if useLocalFallback, let bestLocalDeclaration = symbol.bestLocalDeclaration {
fallback = [(occurrence: nil, location: bestLocalDeclaration)]
} else {
fallback = []
}
guard let usr = symbol.usr, let index = index else {
return .success(fallback)
}
let occurs = extractOccurrences(usr, index)
let resolved = occurs.compactMap { occur in
indexToLSPLocation(occur.location).map {
(occurrence: occur, location: $0)
}
}
return .success(resolved.isEmpty ? fallback : resolved)
}
func declaration(
_ req: Request<DeclarationRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
guard await languageService.declaration(req) else {
return req.reply(.locations([]))
}
}
func definition(
_ req: Request<DefinitionRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position)
let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index
let callback = { (result: LSPResult<SymbolInfoRequest.Response>) -> Void in
Task {
// If this symbol is a module then generate a textual interface
if case .success(let symbols) = result, let symbol = symbols.first, symbol.kind == .module, let name = symbol.name {
await self.respondWithInterface(req, moduleName: name, symbolUSR: nil, languageService: languageService)
return
}
let extractedResult = self.extractIndexedOccurrences(result: result, index: index, useLocalFallback: true) { (usr, index) in
log("performing indexed jump-to-def with usr \(usr)")
var occurs = index.occurrences(ofUSR: usr, roles: [.definition])
if occurs.isEmpty {
occurs = index.occurrences(ofUSR: usr, roles: [.declaration])
}
return occurs
}
switch extractedResult {
case .success(let resolved):
// if first resolved location is in `.swiftinterface` file. Use moduleName to return
// textual interface
if let firstResolved = resolved.first,
let moduleName = firstResolved.occurrence?.location.moduleName,
firstResolved.location.uri.fileURL?.pathExtension == "swiftinterface" {
await self.respondWithInterface(
req,
moduleName: moduleName,
symbolUSR: firstResolved.occurrence?.symbol.usr,
languageService: languageService
)
return
}
let locs = resolved.map(\.location)
// If we're unable to handle the definition request using our index, see if the
// language service can handle it (e.g. clangd can provide AST based definitions).
guard locs.isEmpty else {
req.reply(.locations(locs))
return
}
let handled = await languageService.definition(req)
guard !handled else { return }
req.reply(.locations([]))
case .failure(let error):
req.reply(.failure(error))
}
}
}
let request = Request(symbolInfo, id: req.id, clientID: ObjectIdentifier(self),
cancellation: req.cancellationToken, reply: callback)
await languageService.symbolInfo(request)
}
func respondWithInterface(
_ req: Request<DefinitionRequest>,
moduleName: String,
symbolUSR: String?,
languageService: ToolchainLanguageServer
) async {
let openInterface = OpenInterfaceRequest(textDocument: req.params.textDocument, name: moduleName, symbolUSR: symbolUSR)
let request = Request(openInterface, id: req.id, clientID: ObjectIdentifier(self),
cancellation: req.cancellationToken, reply: { (result: Result<OpenInterfaceRequest.Response, ResponseError>) in
switch result {
case .success(let interfaceDetails?):
let position = interfaceDetails.position ?? Position(line: 0, utf16index: 0)
let loc = Location(uri: interfaceDetails.uri, range: Range(position))
req.reply(.locations([loc]))
case .success(nil):
req.reply(.failure(.unknown("Could not generate Swift Interface for \(moduleName)")))
case .failure(let error):
req.reply(.failure(error))
}
})
await languageService.openInterface(request)
}
func implementation(
_ req: Request<ImplementationRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position)
let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index
let callback = { (result: LSPResult<SymbolInfoRequest.Response>) in
let extractedResult = self.extractIndexedOccurrences(result: result, index: index) { (usr, index) in
var occurs = index.occurrences(ofUSR: usr, roles: .baseOf)
if occurs.isEmpty {
occurs = index.occurrences(relatedToUSR: usr, roles: .overrideOf)
}
return occurs
}
req.reply(extractedResult.map { .locations($0.map(\.location)) })
}
let request = Request(symbolInfo, id: req.id, clientID: ObjectIdentifier(self),
cancellation: req.cancellationToken, reply: callback)
await languageService.symbolInfo(request)
}
func references(
_ req: Request<ReferencesRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position)
let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index
let callback = { (result: LSPResult<SymbolInfoRequest.Response>) in
let extractedResult = self.extractIndexedOccurrences(result: result, index: index) { (usr, index) in
log("performing indexed jump-to-def with usr \(usr)")
var roles: SymbolRole = [.reference]
if req.params.context.includeDeclaration {
roles.formUnion([.declaration, .definition])
}
return index.occurrences(ofUSR: usr, roles: roles)
}
req.reply(extractedResult.map { $0.map(\.location) })
}
let request = Request(symbolInfo, id: req.id, clientID: ObjectIdentifier(self),
cancellation: req.cancellationToken, reply: callback)
await languageService.symbolInfo(request)
}
private func indexToLSPCallHierarchyItem(
symbol: Symbol,
moduleName: String?,
location: Location
) -> CallHierarchyItem {
CallHierarchyItem(
name: symbol.name,
kind: symbol.kind.asLspSymbolKind(),
tags: nil,
detail: moduleName,
uri: location.uri,
range: location.range,
selectionRange: location.range,
// We encode usr and uri for incoming/outgoing call lookups in the implementation-specific data field
data: .dictionary([
"usr": .string(symbol.usr),
"uri": .string(location.uri.stringValue),
])
)
}
func prepareCallHierarchy(
_ req: Request<CallHierarchyPrepareRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position)
let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index
let callback = { (result: LSPResult<SymbolInfoRequest.Response>) in
// For call hierarchy preparation we only locate the definition
let extractedResult = self.extractIndexedOccurrences(result: result, index: index) { (usr, index) in
index.occurrences(ofUSR: usr, roles: [.definition, .declaration])
}
let items = extractedResult.map { resolved -> [CallHierarchyItem]? in
resolved.compactMap { info -> CallHierarchyItem? in
guard let occurrence = info.occurrence else {
return nil
}
let symbol = occurrence.symbol
return self.indexToLSPCallHierarchyItem(
symbol: symbol,
moduleName: occurrence.location.moduleName,
location: info.location
)
}
}
req.reply(items)
}
let request = Request(symbolInfo, id: req.id, clientID: ObjectIdentifier(self),
cancellation: req.cancellationToken, reply: callback)
await languageService.symbolInfo(request)
}
/// Extracts our implementation-specific data about a call hierarchy
/// item as encoded in `indexToLSPCallHierarchyItem`.
///
/// - Parameter data: The opaque data structure to extract
/// - Returns: The extracted data if successful or nil otherwise
private func extractCallHierarchyItemData(_ rawData: LSPAny?) -> (uri: DocumentURI, usr: String)? {
guard case let .dictionary(data) = rawData,
case let .string(uriString) = data["uri"],
case let .string(usr) = data["usr"] else {
return nil
}
return (
uri: DocumentURI(string: uriString),
usr: usr
)
}
func incomingCalls(_ req: Request<CallHierarchyIncomingCallsRequest>) {
guard let data = extractCallHierarchyItemData(req.params.item.data),
let index = self.workspaceForDocument(uri: data.uri)?.index else {
req.reply([])
return
}
let occurs = index.occurrences(ofUSR: data.usr, roles: .calledBy)
let calls = occurs.compactMap { occurrence -> CallHierarchyIncomingCall? in
guard let location = indexToLSPLocation(occurrence.location),
let related = occurrence.relations.first else {
return nil
}
// Resolve the caller's definition to find its location
let definition = index.occurrences(ofUSR: related.symbol.usr, roles: [.definition, .declaration]).first
let definitionSymbolLocation = definition?.location
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
return CallHierarchyIncomingCall(
from: indexToLSPCallHierarchyItem(
symbol: related.symbol,
moduleName: definitionSymbolLocation?.moduleName,
location: definitionLocation ?? location // Use occurrence location as fallback
),
fromRanges: [location.range]
)
}
req.reply(calls)
}
func outgoingCalls(_ req: Request<CallHierarchyOutgoingCallsRequest>) {
guard let data = extractCallHierarchyItemData(req.params.item.data),
let index = self.workspaceForDocument(uri: data.uri)?.index else {
req.reply([])
return
}
let occurs = index.occurrences(relatedToUSR: data.usr, roles: .calledBy)
let calls = occurs.compactMap { occurrence -> CallHierarchyOutgoingCall? in
guard let location = indexToLSPLocation(occurrence.location) else {
return nil
}
// Resolve the callee's definition to find its location
let definition = index.occurrences(ofUSR: occurrence.symbol.usr, roles: [.definition, .declaration]).first
let definitionSymbolLocation = definition?.location
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
return CallHierarchyOutgoingCall(
to: indexToLSPCallHierarchyItem(
symbol: occurrence.symbol,
moduleName: definitionSymbolLocation?.moduleName,
location: definitionLocation ?? location // Use occurrence location as fallback
),
fromRanges: [location.range]
)
}
req.reply(calls)
}
private func indexToLSPTypeHierarchyItem(
symbol: Symbol,
moduleName: String?,
location: Location,
index: IndexStoreDB
) -> TypeHierarchyItem {
let name: String
let detail: String?
switch symbol.kind {
case .extension:
// Query the conformance added by this extension
let conformances = index.occurrences(relatedToUSR: symbol.usr, roles: .baseOf)
if conformances.isEmpty {
name = symbol.name
} else {
name = "\(symbol.name): \(conformances.map(\.symbol.name).joined(separator: ", "))"
}
// Add the file name and line to the detail string
if
let url = location.uri.fileURL,
let basename = (try? AbsolutePath(validating: url.path))?.basename
{
detail = "Extension at \(basename):\(location.range.lowerBound.line + 1)"
} else if let moduleName = moduleName {
detail = "Extension in \(moduleName)"
} else {
detail = "Extension"
}
default:
name = symbol.name
detail = moduleName
}
return TypeHierarchyItem(
name: name,
kind: symbol.kind.asLspSymbolKind(),
tags: nil,
detail: detail,
uri: location.uri,
range: location.range,
selectionRange: location.range,
// We encode usr and uri for incoming/outgoing type lookups in the implementation-specific data field
data: .dictionary([
"usr": .string(symbol.usr),
"uri": .string(location.uri.stringValue),
])
)
}
func prepareTypeHierarchy(
_ req: Request<TypeHierarchyPrepareRequest>,
workspace: Workspace,
languageService: ToolchainLanguageServer
) async {
let symbolInfo = SymbolInfoRequest(textDocument: req.params.textDocument, position: req.params.position)
guard let index = self.workspaceForDocument(uri: req.params.textDocument.uri)?.index else {
req.reply([])
return
}
let callback = { (result: LSPResult<SymbolInfoRequest.Response>) in
// For type hierarchy preparation we only locate the definition
let extractedResult = self.extractIndexedOccurrences(result: result, index: index) { (usr, index) in
index.occurrences(ofUSR: usr, roles: [.definition, .declaration])
}
let items = extractedResult.map { resolved -> [TypeHierarchyItem]? in
resolved.compactMap { info -> TypeHierarchyItem? in
guard let occurrence = info.occurrence else {
return nil
}
let symbol = occurrence.symbol
return self.indexToLSPTypeHierarchyItem(
symbol: symbol,
moduleName: occurrence.location.moduleName,
location: info.location,
index: index
)
}
}
req.reply(items)
}
let request = Request(symbolInfo, id: req.id, clientID: ObjectIdentifier(self),
cancellation: req.cancellationToken, reply: callback)
await languageService.symbolInfo(request)
}
/// Extracts our implementation-specific data about a type hierarchy
/// item as encoded in `indexToLSPTypeHierarchyItem`.
///
/// - Parameter data: The opaque data structure to extract
/// - Returns: The extracted data if successful or nil otherwise
private func extractTypeHierarchyItemData(_ rawData: LSPAny?) -> (uri: DocumentURI, usr: String)? {
guard case let .dictionary(data) = rawData,
case let .string(uriString) = data["uri"],
case let .string(usr) = data["usr"] else {
return nil
}
return (
uri: DocumentURI(string: uriString),
usr: usr
)
}
func supertypes(_ req: Request<TypeHierarchySupertypesRequest>) {
guard let data = extractTypeHierarchyItemData(req.params.item.data),
let index = self.workspaceForDocument(uri: data.uri)?.index else {
req.reply([])
return
}
// Resolve base types
let baseOccurs = index.occurrences(relatedToUSR: data.usr, roles: .baseOf)
// Resolve retroactive conformances via the extensions
let extensions = index.occurrences(ofUSR: data.usr, roles: .extendedBy)
let retroactiveConformanceOccurs = extensions.flatMap { occurrence -> [SymbolOccurrence] in
guard let related = occurrence.relations.first else {
return []
}
return index.occurrences(relatedToUSR: related.symbol.usr, roles: .baseOf)
}
// Convert occurrences to type hierarchy items
let occurs = baseOccurs + retroactiveConformanceOccurs
let types = occurs.compactMap { occurrence -> TypeHierarchyItem? in
guard let location = indexToLSPLocation(occurrence.location) else {
return nil
}
// Resolve the supertype's definition to find its location
let definition = index.occurrences(ofUSR: occurrence.symbol.usr, roles: [.definition, .declaration]).first
let definitionSymbolLocation = definition?.location
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
return indexToLSPTypeHierarchyItem(
symbol: occurrence.symbol,
moduleName: definitionSymbolLocation?.moduleName,
location: definitionLocation ?? location, // Use occurrence location as fallback
index: index
)
}
req.reply(types)
}
func subtypes(_ req: Request<TypeHierarchySubtypesRequest>) {
guard let data = extractTypeHierarchyItemData(req.params.item.data),
let index = self.workspaceForDocument(uri: data.uri)?.index else {
req.reply([])
return
}
// Resolve child types and extensions
let occurs = index.occurrences(ofUSR: data.usr, roles: [.baseOf, .extendedBy])
// Convert occurrences to type hierarchy items
let types = occurs.compactMap { occurrence -> TypeHierarchyItem? in
guard let location = indexToLSPLocation(occurrence.location),
let related = occurrence.relations.first else {
return nil
}
// Resolve the subtype's definition to find its location
let definition = index.occurrences(ofUSR: related.symbol.usr, roles: [.definition, .declaration]).first
let definitionSymbolLocation = definition.map(\.location)
let definitionLocation = definitionSymbolLocation.flatMap(indexToLSPLocation)
return indexToLSPTypeHierarchyItem(
symbol: related.symbol,
moduleName: definitionSymbolLocation?.moduleName,
location: definitionLocation ?? location, // Use occurrence location as fallback
index: index
)
}
req.reply(types)
}
func pollIndex(_ req: Request<PollIndexRequest>) {
for workspace in workspaces {
workspace.index?.pollForUnitChangesAndWait()
}
req.reply(VoidResponse())
}
}
/// Creates a new connection from `client` to a service for `language` if available, and launches
/// the service. Does *not* send the initialization request.
///
/// - returns: The connection, if a suitable language service is available; otherwise nil.
/// - throws: If there is a suitable service but it fails to launch, throws an error.
func languageService(
for toolchain: Toolchain,
_ languageServerType: LanguageServerType,
options: SourceKitServer.Options,
client: MessageHandler,
in workspace: Workspace,
reopenDocuments: @escaping (ToolchainLanguageServer) async -> Void,
workspaceForDocument: @escaping (DocumentURI) async -> Workspace?
) async throws -> ToolchainLanguageServer? {
let connectionToClient = LocalConnection()
let server = try await languageServerType.serverType.init(
client: connectionToClient,
toolchain: toolchain,
options: options,
workspace: workspace,
reopenDocuments: reopenDocuments,
workspaceForDocument: workspaceForDocument
)
connectionToClient.start(handler: client)
return server
}
private func languageClass(for language: Language) -> [Language] {
switch language {
case .c, .cpp, .objective_c, .objective_cpp:
return [.c, .cpp, .objective_c, .objective_cpp]
case .swift:
return [.swift]
default:
return [language]
}
}
/// Minimum supported pattern length for a `workspace/symbol` request, smaller pattern
/// strings are not queried and instead we return no results.
private let minWorkspaceSymbolPatternLength = 3
/// The maximum number of results to return from a `workspace/symbol` request.
private let maxWorkspaceSymbolResults = 4096
public typealias Notification = LanguageServerProtocol.Notification
public typealias Diagnostic = LanguageServerProtocol.Diagnostic
extension IndexSymbolKind {
func asLspSymbolKind() -> SymbolKind {
switch self {
case .class:
return .class
case .classMethod, .instanceMethod, .staticMethod:
return .method
case .instanceProperty, .staticProperty, .classProperty:
return .property
case .enum:
return .enum
case .enumConstant:
return .enumMember
case .protocol:
return .interface
case .function, .conversionFunction:
return .function
case .variable:
return .variable
case .struct:
return .struct
case .parameter:
return .typeParameter
default:
return .null
}
}
}
extension SymbolOccurrence {
/// Get the name of the symbol that is a parent of this symbol, if one exists
func getContainerName() -> String? {
return relations.first(where: { $0.roles.contains(.childOf) })?.symbol.name
}
}
/// Simple struct for pending notifications/requests, including a cancellation handler.
/// For convenience the notifications/request handlers are type erased via wrapping.
fileprivate struct NotificationRequestOperation {
let operation: () async -> Void
let cancellationHandler: (() -> Void)?
}
/// Used to queue up notifications and requests for documents which are blocked
/// on `BuildSystem` operations such as fetching build settings.
///
/// Note: This is not thread safe. Must be called from the `SourceKitServer.queue`.
fileprivate struct DocumentNotificationRequestQueue {
fileprivate var queue = [NotificationRequestOperation]()
/// Add an operation to the end of the queue.
mutating func add(operation: @escaping () async -> Void, cancellationHandler: (() -> Void)? = nil) {
queue.append(NotificationRequestOperation(operation: operation, cancellationHandler: cancellationHandler))
}
/// Cancel all operations in the queue. No-op for operations without a cancellation
/// handler.
mutating func cancelAll() {
for task in queue {
if let cancellationHandler = task.cancellationHandler {
cancellationHandler()
}
}
queue = []
}
}