mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
2114 lines
80 KiB
Swift
2114 lines
80 KiB
Swift
//===----------------------------------------------------------------------===//
|
||
//
|
||
// 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
|
||
|
||
private struct WeakWorkspace {
|
||
weak var value: Workspace?
|
||
}
|
||
|
||
/// 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 haven’t received a respose 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(value: 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 SourceKitLSP.languageService(
|
||
for: toolchain,
|
||
serverType,
|
||
options: options,
|
||
client: self,
|
||
in: workspace,
|
||
reopenDocuments: { [weak self] toolchainLanguageServer in
|
||
guard let self else { return }
|
||
Task {
|
||
await self.reopenDocuments(for: toolchainLanguageServer)
|
||
}
|
||
}
|
||
)
|
||
|
||
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 consutrction of `service`, another call to this
|
||
// function might have happened and raced us, setting
|
||
// `workspace.documentServices[uri]`. If this is the case, return the
|
||
// exising 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 converence 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 doesn’t 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) -> Void
|
||
) throws -> ToolchainLanguageServer? {
|
||
let connectionToClient = LocalConnection()
|
||
|
||
let server = try languageServerType.serverType.init(
|
||
client: connectionToClient,
|
||
toolchain: toolchain,
|
||
options: options,
|
||
workspace: workspace,
|
||
reopenDocuments: reopenDocuments
|
||
)
|
||
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 = []
|
||
}
|
||
}
|