mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
2856 lines
118 KiB
Swift
2856 lines
118 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 BuildServerIntegration
|
|
@_spi(SourceKitLSP) import BuildServerProtocol
|
|
import Dispatch
|
|
import Foundation
|
|
import IndexStoreDB
|
|
@_spi(SourceKitLSP) package import LanguageServerProtocol
|
|
@_spi(SourceKitLSP) import LanguageServerProtocolExtensions
|
|
@_spi(SourceKitLSP) package import LanguageServerProtocolTransport
|
|
@_spi(SourceKitLSP) import SKLogging
|
|
package import SKOptions
|
|
import SemanticIndex
|
|
import SourceKitD
|
|
import SwiftExtensions
|
|
package import ToolchainRegistry
|
|
@_spi(SourceKitLSP) package import ToolsProtocolsSwiftExtensions
|
|
|
|
import struct TSCBasic.AbsolutePath
|
|
import protocol TSCBasic.FileSystem
|
|
|
|
/// Disambiguate LanguageServerProtocol.Language and IndexstoreDB.Language
|
|
package typealias Language = LanguageServerProtocol.Language
|
|
|
|
/// The SourceKit-LSP 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.
|
|
package actor SourceKitLSPServer {
|
|
package let messageHandlingHelper = QueueBasedMessageHandlerHelper(
|
|
signpostLoggingCategory: "message-handling",
|
|
createLoggingScope: true
|
|
)
|
|
|
|
package let messageHandlingQueue = AsyncQueue<MessageHandlingDependencyTracker>()
|
|
|
|
/// The queue on which we keep track of `inProgressTextDocumentRequests` to ensure updates to
|
|
/// `inProgressTextDocumentRequests` are handled in order.
|
|
package let textDocumentTrackingQueue = AsyncQueue<Serial>()
|
|
|
|
/// The queue on which all modifications of `workspaceForUri` happen. This means that the value of
|
|
/// `workspacesAndIsImplicit` and `workspaceForUri` can't change while executing a closure on `workspaceQueue`.
|
|
private let workspaceQueue = AsyncQueue<Serial>()
|
|
|
|
/// The connection to the editor.
|
|
package nonisolated let client: any Connection
|
|
|
|
/// Set to `true` after the `SourceKitLSPServer` has send the reply to the `InitializeRequest`.
|
|
///
|
|
/// Initialization can be awaited using `waitUntilInitialized`.
|
|
private var initialized: Bool = false
|
|
|
|
private let _options: ThreadSafeBox<SourceKitLSPOptions>
|
|
nonisolated package var options: SourceKitLSPOptions {
|
|
_options.value
|
|
}
|
|
|
|
package let hooks: Hooks
|
|
|
|
let toolchainRegistry: ToolchainRegistry
|
|
|
|
package var capabilityRegistry: CapabilityRegistry?
|
|
|
|
let languageServiceRegistry: LanguageServiceRegistry
|
|
|
|
var languageServices: [LanguageServiceType: [any LanguageService]] = [:]
|
|
|
|
package nonisolated let documentManager = DocumentManager()
|
|
|
|
/// The `TaskScheduler` that schedules all background indexing tasks.
|
|
///
|
|
/// Shared process-wide to ensure the scheduled index operations across multiple workspaces don't exceed the maximum
|
|
/// number of processor cores that the user allocated to background indexing.
|
|
private let indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>
|
|
|
|
/// Implicitly unwrapped optional so we can create an `IndexProgressManager` that has a weak reference to
|
|
/// `SourceKitLSPServer`.
|
|
/// `nonisolated(unsafe)` because `indexProgressManager` will not be modified after it is assigned from the
|
|
/// initializer.
|
|
private(set) nonisolated(unsafe) var indexProgressManager: IndexProgressManager!
|
|
|
|
/// Implicitly unwrapped optional so we can create an `SharedWorkDoneProgressManager` that has a weak reference to
|
|
/// `SourceKitLSPServer`.
|
|
/// `nonisolated(unsafe)` because `sourcekitdCrashedWorkDoneProgress` will not be modified after it is assigned from
|
|
/// the initializer.
|
|
nonisolated(unsafe) package private(set) var sourcekitdCrashedWorkDoneProgress: SharedWorkDoneProgressManager!
|
|
|
|
/// Stores which workspace the given URI has been opened in.
|
|
///
|
|
/// - Important: Must only be modified from `workspaceQueue`. This means that the value of `workspaceForUri`
|
|
/// can't change while executing an operation on `workspaceQueue`.
|
|
private var workspaceForUri: [DocumentURI: WeakWorkspace] = [:]
|
|
|
|
/// The open workspaces.
|
|
///
|
|
/// Implicit workspaces are workspaces that weren't actually specified by the client during initialization or by a
|
|
/// `didChangeWorkspaceFolders` request. Instead, they were opened by sourcekit-lsp because a file could not be
|
|
/// handled by any of the open workspaces but one of the file's parent directories had handling capabilities for it.
|
|
///
|
|
/// - Important: Must only be modified from `workspaceQueue`. This means that the value of `workspacesAndIsImplicit`
|
|
/// can't change while executing an operation on `workspaceQueue`.
|
|
private var workspacesAndIsImplicit: [(workspace: Workspace, isImplicit: Bool)] = [] {
|
|
didSet {
|
|
self.scheduleUpdateOfUriToWorkspace()
|
|
}
|
|
}
|
|
|
|
var workspaces: [Workspace] {
|
|
return workspacesAndIsImplicit.map(\.workspace)
|
|
}
|
|
|
|
package func setWorkspaces(_ newValue: [(workspace: Workspace, isImplicit: Bool)]) {
|
|
workspaceQueue.async {
|
|
self.workspacesAndIsImplicit = newValue
|
|
}
|
|
}
|
|
|
|
/// For all currently handled text document requests a mapping from the document to the corresponding request ID and
|
|
/// the method of the request (ie. the value of `TextDocumentRequest.method`).
|
|
private var inProgressTextDocumentRequests: [DocumentURI: [(id: RequestID, requestMethod: String)]] = [:]
|
|
|
|
var onExit: () -> Void
|
|
|
|
/// The files that we asked the client to watch.
|
|
private var watchers: Set<FileSystemWatcher> = []
|
|
|
|
private static func maxConcurrentIndexingTasksByPriority(
|
|
isIndexingPaused: Bool,
|
|
options: SourceKitLSPOptions
|
|
) -> [(priority: TaskPriority, maxConcurrentTasks: Int)] {
|
|
// Use `processorCount` instead of `activeProcessorCount` here because `activeProcessorCount` may be decreased due
|
|
// to thermal throttling. We don't want to consistently limit the concurrent indexing tasks if SourceKit-LSP was
|
|
// launched during a period of thermal throttling.
|
|
let processorCount = ProcessInfo.processInfo.processorCount
|
|
let lowPriorityCores =
|
|
if isIndexingPaused {
|
|
0
|
|
} else {
|
|
max(
|
|
Int(options.indexOrDefault.maxCoresPercentageToUseForBackgroundIndexingOrDefault * Double(processorCount)),
|
|
1
|
|
)
|
|
}
|
|
return [
|
|
(TaskPriority.medium, processorCount),
|
|
(TaskPriority.low, lowPriorityCores),
|
|
]
|
|
}
|
|
|
|
/// Creates a language server for the given client.
|
|
package init(
|
|
client: any Connection,
|
|
toolchainRegistry: ToolchainRegistry,
|
|
languageServerRegistry: LanguageServiceRegistry,
|
|
options: SourceKitLSPOptions,
|
|
hooks: Hooks,
|
|
onExit: @escaping () -> Void = {}
|
|
) {
|
|
self.toolchainRegistry = toolchainRegistry
|
|
self.languageServiceRegistry = languageServerRegistry
|
|
self._options = ThreadSafeBox(initialValue: options)
|
|
self.hooks = hooks
|
|
self.onExit = onExit
|
|
|
|
self.client = client
|
|
self.indexTaskScheduler = TaskScheduler(
|
|
maxConcurrentTasksByPriority: Self.maxConcurrentIndexingTasksByPriority(isIndexingPaused: false, options: options)
|
|
)
|
|
self.indexProgressManager = nil
|
|
self.indexProgressManager = IndexProgressManager(sourceKitLSPServer: self)
|
|
self.sourcekitdCrashedWorkDoneProgress = SharedWorkDoneProgressManager(
|
|
sourceKitLSPServer: self,
|
|
tokenPrefix: "sourcekitd-crashed",
|
|
title: "SourceKit-LSP: Restoring functionality",
|
|
message: "Please run 'sourcekit-lsp diagnose' to file an issue"
|
|
)
|
|
}
|
|
|
|
/// Await until the server has send the reply to the initialize request.
|
|
package func waitUntilInitialized() async {
|
|
// The polling of `initialized` is not perfect but it should be OK, because
|
|
// - In almost all cases the server should already be initialized.
|
|
// - If it's not initialized, we expect initialization to finish fairly quickly. Even if initialization takes 5s
|
|
// this only results in 50 polls, which is acceptable.
|
|
// Alternative solutions that signal via an async sequence seem overkill here.
|
|
while !initialized {
|
|
do {
|
|
try await Task.sleep(for: .seconds(0.1))
|
|
} catch {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Search through all the parent directories of `uri` and check if any of these directories contain a workspace that
|
|
/// can be handled by with a build server.
|
|
///
|
|
/// The search will not consider any directory that is not a child of any of the directories in `rootUris`. This
|
|
/// prevents us from picking up a workspace that is outside of the folders that the user opened.
|
|
private func findImplicitWorkspace(for uri: DocumentURI) async -> Workspace? {
|
|
guard var url = uri.fileURL?.deletingLastPathComponent() else {
|
|
return nil
|
|
}
|
|
|
|
// Roots of opened workspaces - only consider explicit here (all implicit must necessarily be subdirectories of
|
|
// the explicit workspace roots)
|
|
let workspaceRoots = workspacesAndIsImplicit.filter { !$0.isImplicit }.compactMap { $0.workspace.rootUri?.fileURL }
|
|
|
|
// We want to skip creating another workspace if any existing already has the same config path. This could happen if
|
|
// an existing workspace hasn't reloaded after a new file was added to it (and thus that build server needs to be
|
|
// reloaded).
|
|
let configPaths = await workspacesAndIsImplicit.asyncCompactMap {
|
|
await $0.workspace.buildServerManager.configPath
|
|
}
|
|
|
|
while url.pathComponents.count > 1 && workspaceRoots.contains(where: { $0.isPrefix(of: url) }) {
|
|
defer {
|
|
url.deleteLastPathComponent()
|
|
}
|
|
|
|
let uri = DocumentURI(url)
|
|
let options = SourceKitLSPOptions.merging(base: self.options, workspaceFolder: uri)
|
|
|
|
// Some build servers consider paths outside of the folder (eg. BSP has settings in the home directory). If we
|
|
// allowed those paths, then the very first folder that the file is in would always be its own build server - so
|
|
// skip them in that case.
|
|
guard
|
|
let buildServerSpec = determineBuildServer(
|
|
forWorkspaceFolder: uri,
|
|
onlyConsiderRoot: true,
|
|
options: options,
|
|
hooks: hooks.buildServerHooks
|
|
)
|
|
else {
|
|
continue
|
|
}
|
|
if configPaths.contains(buildServerSpec.configPath) {
|
|
continue
|
|
}
|
|
|
|
// No existing workspace matches this root - create one.
|
|
guard
|
|
let workspace = await orLog(
|
|
"Creating implicit workspace",
|
|
{ try await createWorkspace(workspaceFolder: uri, options: options, buildServerSpec: buildServerSpec) }
|
|
)
|
|
else {
|
|
continue
|
|
}
|
|
return workspace
|
|
}
|
|
return nil
|
|
}
|
|
|
|
package func workspaceForDocument(uri: DocumentURI) async -> Workspace? {
|
|
let uri = uri.buildSettingsFile
|
|
if let cachedWorkspace = self.workspaceForUri[uri]?.value {
|
|
return cachedWorkspace
|
|
}
|
|
|
|
return await self.workspaceQueue.async {
|
|
await self.computeWorkspaceForDocument(uri: uri)
|
|
}.valuePropagatingCancellation
|
|
}
|
|
|
|
/// This method must be executed on `workspaceQueue` to ensure that the file handling capabilities of the
|
|
/// workspaces don't change during the computation. Otherwise, we could run into a race condition like the following:
|
|
/// 1. We don't have an entry for file `a.swift` in `workspaceForUri` and start the computation
|
|
/// 2. We find that the first workspace in `self.workspaces` can handle this file.
|
|
/// 3. During the `await ... .fileHandlingCapability` for a second workspace the file handling capabilities for the
|
|
/// first workspace change, meaning it can no longer handle the document. This resets `workspaceForUri`
|
|
/// assuming that the URI to workspace relation will get re-computed.
|
|
/// 4. But we then set `workspaceForUri[uri]` to the workspace found in step (2), caching an out-of-date result.
|
|
///
|
|
/// Furthermore, the computation of the workspace for a URI can create a new implicit workspace, which modifies
|
|
/// `workspacesAndIsImplicit` and which must only be modified on `workspaceQueue`.
|
|
///
|
|
/// - Important: Must only be invoked from `workspaceQueue`.
|
|
private func computeWorkspaceForDocument(uri: DocumentURI) async -> Workspace? {
|
|
// 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 = await self.workspaces.asyncFirst {
|
|
await !$0.buildServerManager.targets(for: uri).isEmpty
|
|
}
|
|
if bestWorkspace == nil {
|
|
// We weren't able to handle the document with any of the known workspaces. See if any of the document's parent
|
|
// directories contain a workspace that might be able to handle the document
|
|
if let workspace = await self.findImplicitWorkspace(for: uri) {
|
|
logger.log("Opening implicit workspace at \(workspace.rootUri.forLogging) to handle \(uri.forLogging)")
|
|
self.workspacesAndIsImplicit.append((workspace: workspace, isImplicit: true))
|
|
bestWorkspace = workspace
|
|
}
|
|
}
|
|
let workspace = bestWorkspace ?? self.workspaces.first
|
|
self.workspaceForUri[uri] = WeakWorkspace(workspace)
|
|
return workspace
|
|
}
|
|
|
|
/// Check that the entries in `workspaceForUri` are still up-to-date after workspaces might have changed.
|
|
///
|
|
/// For any entries that are not up-to-date, close the document in the old workspace and open it in the new document.
|
|
///
|
|
/// This method returns immediately and schedules the check in the background as a global configuration change.
|
|
/// Requests may still be served by their old workspace until this configuration change is executed by
|
|
/// `SourceKitLSPServer`.
|
|
private func scheduleUpdateOfUriToWorkspace() {
|
|
messageHandlingQueue.async(priority: .low, metadata: .globalConfigurationChange) {
|
|
logger.info("Updating URI to workspace")
|
|
// 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 {
|
|
await self.workspaceQueue.async {
|
|
let oldWorkspace = self.workspaceForUri[docUri]?.value
|
|
let newWorkspace = await self.computeWorkspaceForDocument(uri: docUri)
|
|
guard newWorkspace !== oldWorkspace else {
|
|
return // Nothing to do, workspace didn't change for this document
|
|
}
|
|
guard let snapshot = try? self.documentManager.latestSnapshot(docUri) else {
|
|
return
|
|
}
|
|
if let oldWorkspace = oldWorkspace {
|
|
await self.closeDocument(
|
|
DidCloseTextDocumentNotification(
|
|
textDocument: TextDocumentIdentifier(docUri)
|
|
),
|
|
workspace: oldWorkspace
|
|
)
|
|
}
|
|
logger.info(
|
|
"Changing workspace of \(docUri.forLogging) from \(oldWorkspace?.rootUri?.forLogging) to \(newWorkspace?.rootUri?.forLogging)"
|
|
)
|
|
self.workspaceForUri[docUri] = WeakWorkspace(newWorkspace)
|
|
if let newWorkspace = newWorkspace {
|
|
await self.openDocument(
|
|
DidOpenTextDocumentNotification(
|
|
textDocument: TextDocumentItem(
|
|
uri: docUri,
|
|
language: snapshot.language,
|
|
version: snapshot.version,
|
|
text: snapshot.text
|
|
)
|
|
),
|
|
workspace: newWorkspace
|
|
)
|
|
}
|
|
}.valuePropagatingCancellation
|
|
}
|
|
// `indexProgressManager` iterates over all workspaces in the SourceKitLSPServer. Modifying workspaces might thus
|
|
// update the index progress status.
|
|
self.indexProgressManager.indexProgressStatusDidChange()
|
|
}
|
|
}
|
|
|
|
/// Execute `notificationHandler` with the request as well as the workspace
|
|
/// and language that handle this document.
|
|
private func withLanguageServiceAndWorkspace<NotificationType: TextDocumentNotification>(
|
|
for notification: NotificationType,
|
|
notificationHandler: @escaping (NotificationType, any LanguageService) async -> Void
|
|
) async {
|
|
let doc = notification.textDocument.uri
|
|
guard let workspace = await 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.
|
|
for languageService in workspace.languageServices(for: doc) {
|
|
await notificationHandler(notification, languageService)
|
|
}
|
|
}
|
|
|
|
private func handleRequest<RequestType: TextDocumentRequest>(
|
|
for request: RequestAndReply<RequestType>,
|
|
requestHandler:
|
|
@Sendable @escaping (
|
|
RequestType, Workspace, any LanguageService
|
|
) async throws ->
|
|
RequestType.Response
|
|
) async {
|
|
await request.reply {
|
|
let request = request.params
|
|
let doc = request.textDocument.uri
|
|
guard let workspace = await self.workspaceForDocument(uri: request.textDocument.uri) else {
|
|
throw ResponseError.workspaceNotOpen(request.textDocument.uri)
|
|
}
|
|
let languageServices = workspace.languageServices(for: doc)
|
|
if languageServices.isEmpty {
|
|
throw ResponseError.unknown("No language service for '\(request.textDocument.uri)' found")
|
|
}
|
|
// Return the results from the first language service that doesn't throw a `requestNotImplemented` error.
|
|
for languageService in languageServices {
|
|
do {
|
|
return try await requestHandler(request, workspace, languageService)
|
|
} catch let error as ResponseError where error.code == .requestNotImplemented {
|
|
continue
|
|
}
|
|
}
|
|
throw ResponseError.unknown("No language service implements \(type(of: request).method)")
|
|
}
|
|
}
|
|
|
|
/// Send the given notification to the editor.
|
|
package nonisolated func sendNotificationToClient(_ notification: some NotificationType) {
|
|
client.send(notification)
|
|
}
|
|
|
|
/// Send the given request to the editor.
|
|
package func sendRequestToClient<R: RequestType>(_ request: R) async throws -> R.Response {
|
|
return try await client.send(request)
|
|
}
|
|
|
|
/// After the language service has crashed, send `DidOpenTextDocumentNotification`s to a newly instantiated language service for previously open documents.
|
|
package func reopenDocuments(for languageService: any LanguageService) async {
|
|
for documentUri in self.documentManager.openDocuments {
|
|
guard let workspace = await self.workspaceForDocument(uri: documentUri) else {
|
|
continue
|
|
}
|
|
guard workspace.languageServices(for: documentUri).contains(where: { $0 === languageService }) else {
|
|
continue
|
|
}
|
|
guard let snapshot = try? 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 server manager to start with a clean sheet when
|
|
// re-opening it.
|
|
// This closes and re-opens the document in all of its language services, not just the crashed language service
|
|
// but since crashing language services should be rare, this is acceptable.
|
|
let closeNotification = DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(documentUri))
|
|
await self.closeDocument(closeNotification, workspace: workspace)
|
|
|
|
let textDocument = TextDocumentItem(
|
|
uri: documentUri,
|
|
language: snapshot.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` using the given toolchain has already been
|
|
/// started, return it, otherwise return `nil`.
|
|
private func existingLanguageService(
|
|
_ serverType: any LanguageService.Type,
|
|
toolchain: Toolchain,
|
|
workspace: Workspace
|
|
) -> (any LanguageService)? {
|
|
for languageService in languageServices[LanguageServiceType(serverType), default: []] {
|
|
if languageService.canHandle(workspace: workspace, toolchain: toolchain) {
|
|
return languageService
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Get the language services that can handle the given languages in the given workspace using the given toolchain.
|
|
///
|
|
/// If we have language services that can handle this combination but that haven't been started yet, start them.
|
|
func languageServices(
|
|
for toolchain: Toolchain,
|
|
_ language: Language,
|
|
in workspace: Workspace
|
|
) async -> [any LanguageService] {
|
|
var result: [any LanguageService] = []
|
|
for serverType in languageServiceRegistry.languageServices(for: language) {
|
|
if let languageService = existingLanguageService(serverType, toolchain: toolchain, workspace: workspace) {
|
|
result.append(languageService)
|
|
continue
|
|
}
|
|
|
|
// Start a new service.
|
|
let languageService: (any LanguageService)? = await orLog("failed to start language service") {
|
|
[options = workspace.options, hooks] in
|
|
let service = try await serverType.init(
|
|
sourceKitLSPServer: self,
|
|
toolchain: toolchain,
|
|
options: options,
|
|
hooks: hooks,
|
|
workspace: workspace
|
|
)
|
|
|
|
let pid = Int(ProcessInfo.processInfo.processIdentifier)
|
|
let resp = try await service.initialize(
|
|
InitializeRequest(
|
|
processId: pid,
|
|
rootPath: nil,
|
|
rootURI: workspace.rootUri,
|
|
initializationOptions: nil,
|
|
capabilities: workspace.capabilityRegistry.clientCapabilities,
|
|
trace: .off,
|
|
workspaceFolders: nil
|
|
)
|
|
)
|
|
let languages = languageClass(for: language)
|
|
await self.registerCapabilities(
|
|
for: resp.capabilities,
|
|
languages: languages,
|
|
registry: workspace.capabilityRegistry
|
|
)
|
|
|
|
var syncKind: TextDocumentSyncKind
|
|
switch resp.capabilities.textDocumentSync {
|
|
case .options(let options):
|
|
syncKind = options.change ?? .incremental
|
|
case .kind(let kind):
|
|
syncKind = kind
|
|
default:
|
|
syncKind = .incremental
|
|
}
|
|
guard syncKind == .incremental else {
|
|
throw ResponseError.internalError("non-incremental update not implemented")
|
|
}
|
|
|
|
await service.clientInitialized(InitializedNotification())
|
|
|
|
if let concurrentlyInitializedService = existingLanguageService(
|
|
serverType,
|
|
toolchain: toolchain,
|
|
workspace: workspace
|
|
) {
|
|
// Since we 'await' above, another call to languageService might have
|
|
// happened concurrently, passed the `existingLanguageService` check at
|
|
// the top and started initializing another language service.
|
|
// If this race happened, just shut down our server and return the
|
|
// other one.
|
|
await service.shutdown()
|
|
return concurrentlyInitializedService
|
|
}
|
|
|
|
languageServices[LanguageServiceType(serverType), default: []].append(service)
|
|
return service
|
|
}
|
|
guard let languageService else {
|
|
// If a language service fails to start, don't try starting language services with lower precedence. Otherwise
|
|
// we get into a situation where eg. `SwiftLanguageService`` fails to start (eg. because the toolchain doesn't
|
|
// contain sourcekitd) and the `DocumentationLanguageService` now becomes the primary language service for the
|
|
// document, trying to serve documentation, completion etc. which is not intended.
|
|
break
|
|
}
|
|
result.append(languageService)
|
|
}
|
|
if result.isEmpty {
|
|
logger.error("Unable to infer language server type for language '\(language)'")
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// Get the language services that can handle the given document.
|
|
///
|
|
/// If we have language services that can handle this document but that haven't been started yet, start them.
|
|
package func languageServices(
|
|
for uri: DocumentURI,
|
|
_ language: Language,
|
|
in workspace: Workspace
|
|
) async -> [any LanguageService] {
|
|
let existingLanguageServices = workspace.languageServices(for: uri)
|
|
if !existingLanguageServices.isEmpty {
|
|
return existingLanguageServices
|
|
}
|
|
|
|
let toolchain = await workspace.buildServerManager.toolchain(
|
|
for: await workspace.buildServerManager.canonicalTarget(for: uri),
|
|
language: language
|
|
)
|
|
guard let toolchain else {
|
|
logger.error("Failed to determine toolchain for \(uri)")
|
|
return []
|
|
}
|
|
let languageServices = await self.languageServices(for: toolchain, language, in: workspace)
|
|
|
|
if languageServices.isEmpty {
|
|
logger.error("No language service found to handle \(uri.forLogging)")
|
|
}
|
|
|
|
logger.log(
|
|
"""
|
|
Using toolchain at \(toolchain.path.description) (\(toolchain.identifier, privacy: .public)) \
|
|
for \(uri.forLogging)
|
|
"""
|
|
)
|
|
|
|
return languageServices
|
|
}
|
|
|
|
/// The language service with the highest precedence that can handle the given document.
|
|
///
|
|
/// If we have language services that can handle this document but that haven't been started yet, start them.
|
|
///
|
|
/// If no language service exists for this document, throw an error.
|
|
package func primaryLanguageService(
|
|
for uri: DocumentURI,
|
|
_ language: Language,
|
|
in workspace: Workspace
|
|
) async throws -> any LanguageService {
|
|
guard let languageService = await languageServices(for: uri, language, in: workspace).first else {
|
|
throw ResponseError.unknown("No language service found for \(uri)")
|
|
}
|
|
return languageService
|
|
}
|
|
}
|
|
|
|
// MARK: - MessageHandler
|
|
|
|
extension SourceKitLSPServer: QueueBasedMessageHandler {
|
|
private enum ImplicitTextDocumentRequestCancellationReason {
|
|
case documentChanged
|
|
case documentClosed
|
|
}
|
|
|
|
package nonisolated func didReceive(notification: some NotificationType) {
|
|
let textDocumentUri: DocumentURI
|
|
let cancellationReason: ImplicitTextDocumentRequestCancellationReason
|
|
switch notification {
|
|
case let params as DidChangeTextDocumentNotification:
|
|
textDocumentUri = params.textDocument.uri
|
|
cancellationReason = .documentChanged
|
|
case let params as DidCloseTextDocumentNotification:
|
|
textDocumentUri = params.textDocument.uri
|
|
cancellationReason = .documentClosed
|
|
default:
|
|
return
|
|
}
|
|
textDocumentTrackingQueue.async(priority: .high) {
|
|
await self.cancelTextDocumentRequests(for: textDocumentUri, reason: cancellationReason)
|
|
}
|
|
}
|
|
|
|
/// Cancel all in-progress text document requests for the given document.
|
|
///
|
|
/// As a user makes an edit to a file, these requests are most likely no longer relevant. It also makes sure that a
|
|
/// long-running sourcekitd request can't block the entire language server if the client does not cancel all requests.
|
|
/// For example, consider the following sequence of requests:
|
|
/// - `textDocument/semanticTokens/full` for document A
|
|
/// - `textDocument/didChange` for document A
|
|
/// - `textDocument/formatting` for document A
|
|
///
|
|
/// If the editor is not cancelling the semantic tokens request on edit (like VS Code does), then the `didChange`
|
|
/// notification is blocked on the semantic tokens request finishing. Hence, we also can't run the
|
|
/// `textDocument/formatting` request. Cancelling the semantic tokens on the edit fixes the issue.
|
|
///
|
|
/// This method is a no-op if `cancelTextDocumentRequestsOnEditAndClose` is disabled.
|
|
///
|
|
/// - Important: Should be invoked on `textDocumentTrackingQueue` to ensure that new text document requests are
|
|
/// registered before a notification that triggers cancellation might come in.
|
|
private func cancelTextDocumentRequests(for uri: DocumentURI, reason: ImplicitTextDocumentRequestCancellationReason) {
|
|
guard self.options.cancelTextDocumentRequestsOnEditAndCloseOrDefault else {
|
|
return
|
|
}
|
|
for (requestID, requestMethod) in self.inProgressTextDocumentRequests[uri, default: []] {
|
|
if reason == .documentChanged && requestMethod == CompletionRequest.method {
|
|
// As the user types, we filter the code completion results. Cancelling the completion request on every
|
|
// keystroke means that we will never build the initial list of completion results for this code
|
|
// completion session if building that list takes longer than the user's typing cadence (eg. for global
|
|
// completions) and we will thus not show any completions.
|
|
continue
|
|
}
|
|
logger.info("Implicitly cancelling request \(requestID)")
|
|
self.messageHandlingHelper.cancelRequest(id: requestID)
|
|
}
|
|
}
|
|
|
|
package func handle(notification: some NotificationType) async {
|
|
logger.log("Received notification: \(notification.forLogging)")
|
|
|
|
switch notification {
|
|
case let notification as DidChangeActiveDocumentNotification:
|
|
await self.didChangeActiveDocument(notification)
|
|
case let notification as DidChangeTextDocumentNotification:
|
|
await self.changeDocument(notification)
|
|
case let notification as DidChangeWorkspaceFoldersNotification:
|
|
await self.didChangeWorkspaceFolders(notification)
|
|
case let notification as DidCloseTextDocumentNotification:
|
|
await self.closeDocument(notification)
|
|
case let notification as DidChangeWatchedFilesNotification:
|
|
await self.didChangeWatchedFiles(notification)
|
|
case let notification as DidOpenTextDocumentNotification:
|
|
await self.openDocument(notification)
|
|
case let notification as DidSaveTextDocumentNotification:
|
|
await self.withLanguageServiceAndWorkspace(for: notification, notificationHandler: self.didSaveDocument)
|
|
case let notification as InitializedNotification:
|
|
self.clientInitialized(notification)
|
|
case let notification as ExitNotification:
|
|
await self.exit(notification)
|
|
case let notification as ReopenTextDocumentNotification:
|
|
await self.reopenDocument(notification)
|
|
case let notification as WillSaveTextDocumentNotification:
|
|
await self.withLanguageServiceAndWorkspace(for: notification, notificationHandler: self.willSaveDocument)
|
|
// IMPORTANT: When adding a new entry to this switch, also add it to the `MessageHandlingDependencyTracker` initializer.
|
|
default:
|
|
logger.error("Ignoring unknown notification \(type(of: notification).method)")
|
|
}
|
|
}
|
|
|
|
package nonisolated func didReceive(request: some RequestType, id: RequestID) {
|
|
guard let request = request as? any TextDocumentRequest else {
|
|
return
|
|
}
|
|
textDocumentTrackingQueue.async(priority: .background) {
|
|
await self.registerInProgressTextDocumentRequest(request, id: id)
|
|
}
|
|
}
|
|
|
|
/// - Important: Should be invoked on `textDocumentTrackingQueue` to ensure that new text document requests are
|
|
/// registered before a notification that triggers cancellation might come in.
|
|
private func registerInProgressTextDocumentRequest<T: TextDocumentRequest>(_ request: T, id: RequestID) {
|
|
self.inProgressTextDocumentRequests[request.textDocument.uri, default: []].append((id: id, requestMethod: T.method))
|
|
}
|
|
|
|
package func handle<Request: RequestType>(
|
|
request params: Request,
|
|
id: RequestID,
|
|
reply: @Sendable @escaping (LSPResult<Request.Response>) -> Void
|
|
) async {
|
|
defer {
|
|
if let request = params as? any TextDocumentRequest {
|
|
textDocumentTrackingQueue.async(priority: .background) {
|
|
self.inProgressTextDocumentRequests[request.textDocument.uri, default: []].removeAll { $0.id == id }
|
|
}
|
|
}
|
|
}
|
|
|
|
await self.hooks.preHandleRequest?(params)
|
|
|
|
let startDate = Date()
|
|
|
|
let request = RequestAndReply(params) { result in
|
|
reply(result)
|
|
let endDate = Date()
|
|
Task {
|
|
switch result {
|
|
case .success(let response):
|
|
logger.log(
|
|
"""
|
|
Succeeded (took \(endDate.timeIntervalSince(startDate) * 1000, privacy: .public)ms)
|
|
\(Request.method, privacy: .public)
|
|
\(response.forLogging)
|
|
"""
|
|
)
|
|
case .failure(let error):
|
|
logger.log(
|
|
"""
|
|
Failed (took \(endDate.timeIntervalSince(startDate) * 1000, privacy: .public)ms)
|
|
\(Request.method, privacy: .public)(\(id, privacy: .public))
|
|
\(error.forLogging, privacy: .private)
|
|
"""
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.log("Received request \(id, privacy: .public): \(params.forLogging)")
|
|
|
|
if let textDocumentRequest = params as? any TextDocumentRequest {
|
|
await self.clientInteractedWithDocument(textDocumentRequest.textDocument.uri)
|
|
}
|
|
|
|
switch request {
|
|
case let request as RequestAndReply<CallHierarchyIncomingCallsRequest>:
|
|
await request.reply { try await incomingCalls(request.params) }
|
|
case let request as RequestAndReply<CallHierarchyOutgoingCallsRequest>:
|
|
await request.reply { try await outgoingCalls(request.params) }
|
|
case let request as RequestAndReply<CallHierarchyPrepareRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.prepareCallHierarchy)
|
|
case let request as RequestAndReply<CodeActionRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.codeAction)
|
|
case let request as RequestAndReply<CodeLensRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.codeLens)
|
|
case let request as RequestAndReply<ColorPresentationRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.colorPresentation)
|
|
case let request as RequestAndReply<CompletionRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.completion)
|
|
case let request as RequestAndReply<CompletionItemResolveRequest>:
|
|
await request.reply { try await completionItemResolve(request: request.params) }
|
|
case let request as RequestAndReply<SignatureHelpRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.signatureHelp)
|
|
case let request as RequestAndReply<DeclarationRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.declaration)
|
|
case let request as RequestAndReply<DefinitionRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.definition)
|
|
case let request as RequestAndReply<TypeDefinitionRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.typeDefinition)
|
|
case let request as RequestAndReply<DoccDocumentationRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.doccDocumentation)
|
|
case let request as RequestAndReply<DocumentColorRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentColor)
|
|
case let request as RequestAndReply<DocumentDiagnosticsRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentDiagnostic)
|
|
case let request as RequestAndReply<DocumentFormattingRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentFormatting)
|
|
case let request as RequestAndReply<DocumentRangeFormattingRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentRangeFormatting)
|
|
case let request as RequestAndReply<DocumentOnTypeFormattingRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentOnTypeFormatting)
|
|
case let request as RequestAndReply<DocumentHighlightRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentSymbolHighlight)
|
|
case let request as RequestAndReply<DocumentSemanticTokensDeltaRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentSemanticTokensDelta)
|
|
case let request as RequestAndReply<DocumentSemanticTokensRangeRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentSemanticTokensRange)
|
|
case let request as RequestAndReply<DocumentSemanticTokensRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentSemanticTokens)
|
|
case let request as RequestAndReply<DocumentSymbolRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentSymbol)
|
|
case let request as RequestAndReply<DocumentTestsRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.documentTests)
|
|
case let request as RequestAndReply<ExecuteCommandRequest>:
|
|
await request.reply { try await executeCommand(request.params) }
|
|
case let request as RequestAndReply<FoldingRangeRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.foldingRange)
|
|
case let request as RequestAndReply<GetReferenceDocumentRequest>:
|
|
await request.reply { try await getReferenceDocument(request.params) }
|
|
case let request as RequestAndReply<HoverRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.hover)
|
|
case let request as RequestAndReply<ImplementationRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.implementation)
|
|
case let request as RequestAndReply<IndexedRenameRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.indexedRename)
|
|
case let request as RequestAndReply<InitializeRequest>:
|
|
await request.reply { try await initialize(request.params) }
|
|
// Only set `initialized` to `true` after we have sent the response to the initialize request to the client.
|
|
initialized = true
|
|
case let request as RequestAndReply<InlayHintRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.inlayHint)
|
|
case let request as RequestAndReply<InlayHintResolveRequest>:
|
|
await request.reply { try await inlayHintResolve(request: request.params) }
|
|
case let request as RequestAndReply<IsIndexingRequest>:
|
|
await request.reply { try await self.isIndexing(request.params) }
|
|
case let request as RequestAndReply<OutputPathsRequest>:
|
|
await request.reply { try await outputPaths(request.params) }
|
|
case let request as RequestAndReply<PrepareRenameRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.prepareRename)
|
|
case let request as RequestAndReply<ReferencesRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.references)
|
|
case let request as RequestAndReply<RenameRequest>:
|
|
await request.reply { try await rename(request.params) }
|
|
case let request as RequestAndReply<SetOptionsRequest>:
|
|
await request.reply { try await self.setBackgroundIndexingPaused(request.params) }
|
|
case let request as RequestAndReply<SourceKitOptionsRequest>:
|
|
await request.reply { try await sourceKitOptions(request.params) }
|
|
case let request as RequestAndReply<ShutdownRequest>:
|
|
await request.reply { try await shutdown(request.params) }
|
|
case let request as RequestAndReply<SymbolInfoRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.symbolInfo)
|
|
case let request as RequestAndReply<SynchronizeRequest>:
|
|
await request.reply { try await synchronize(request.params) }
|
|
case let request as RequestAndReply<TriggerReindexRequest>:
|
|
await request.reply { try await triggerReindex(request.params) }
|
|
case let request as RequestAndReply<TypeHierarchyPrepareRequest>:
|
|
await self.handleRequest(for: request, requestHandler: self.prepareTypeHierarchy)
|
|
case let request as RequestAndReply<TypeHierarchySubtypesRequest>:
|
|
await request.reply { try await subtypes(request.params) }
|
|
case let request as RequestAndReply<TypeHierarchySupertypesRequest>:
|
|
await request.reply { try await supertypes(request.params) }
|
|
case let request as RequestAndReply<WorkspaceSymbolsRequest>:
|
|
await request.reply { try await workspaceSymbols(request.params) }
|
|
case let request as RequestAndReply<WorkspaceTestsRequest>:
|
|
await request.reply { try await workspaceTests(request.params) }
|
|
case let request as RequestAndReply<WorkspacePlaygroundsRequest>:
|
|
await request.reply { try await workspacePlaygrounds(request.params) }
|
|
// IMPORTANT: When adding a new entry to this switch, also add it to the `MessageHandlingDependencyTracker` initializer.
|
|
default:
|
|
await request.reply { throw ResponseError.methodNotFound(Request.method) }
|
|
}
|
|
}
|
|
}
|
|
|
|
extension SourceKitLSPServer {
|
|
nonisolated package func logMessageToIndexLog(
|
|
message: String,
|
|
type: WindowMessageType,
|
|
structure: LanguageServerProtocol.StructuredLogKind?
|
|
) {
|
|
self.sendNotificationToClient(
|
|
LogMessageNotification(
|
|
type: type,
|
|
message: message,
|
|
logName: "SourceKit-LSP: Indexing",
|
|
structure: structure
|
|
).representingTaskIDUsingEmojiPrefixIfNecessary(options: options)
|
|
)
|
|
}
|
|
|
|
func fileHandlingCapabilityChanged() {
|
|
logger.log("Scheduling update of URI to workspace because file handling capability of a workspace changed")
|
|
self.scheduleUpdateOfUriToWorkspace()
|
|
}
|
|
}
|
|
|
|
// MARK: - Request and notification handling
|
|
|
|
extension SourceKitLSPServer {
|
|
|
|
// MARK: - General
|
|
|
|
/// Creates a workspace at the given `uri`.
|
|
///
|
|
/// A workspace does not necessarily have any build server attached to it, in which case `buildServerSpec` may be
|
|
/// `nil` - consider eg. a top level workspace folder with multiple SwiftPM projects inside it.
|
|
private func createWorkspace(
|
|
workspaceFolder: DocumentURI,
|
|
options: SourceKitLSPOptions,
|
|
buildServerSpec: BuildServerSpec?
|
|
) async throws -> Workspace {
|
|
guard let capabilityRegistry = capabilityRegistry else {
|
|
struct NoCapabilityRegistryError: Error {}
|
|
logger.log("Cannot open workspace before server is initialized")
|
|
throw NoCapabilityRegistryError()
|
|
}
|
|
|
|
logger.log("Creating workspace at \(workspaceFolder.forLogging)")
|
|
logger.logFullObjectInMultipleLogMessages(header: "Workspace options", options.loggingProxy)
|
|
|
|
let workspace = await Workspace(
|
|
sourceKitLSPServer: self,
|
|
documentManager: self.documentManager,
|
|
rootUri: workspaceFolder,
|
|
capabilityRegistry: capabilityRegistry,
|
|
buildServerSpec: buildServerSpec,
|
|
toolchainRegistry: self.toolchainRegistry,
|
|
options: options,
|
|
hooks: hooks,
|
|
indexTaskScheduler: indexTaskScheduler
|
|
)
|
|
return workspace
|
|
}
|
|
|
|
/// Determines the build server for the given workspace folder and creates a `Workspace` that uses this inferred build
|
|
/// system.
|
|
private func createWorkspaceWithInferredBuildServer(workspaceFolder: DocumentURI) async throws -> Workspace {
|
|
let options = SourceKitLSPOptions.merging(base: self.options, workspaceFolder: workspaceFolder)
|
|
let buildServerSpec = determineBuildServer(
|
|
forWorkspaceFolder: workspaceFolder,
|
|
onlyConsiderRoot: false,
|
|
options: options,
|
|
hooks: hooks.buildServerHooks
|
|
)
|
|
return try await self.createWorkspace(
|
|
workspaceFolder: workspaceFolder,
|
|
options: options,
|
|
buildServerSpec: buildServerSpec
|
|
)
|
|
}
|
|
|
|
func initialize(_ req: InitializeRequest) async throws -> InitializeResult {
|
|
logger.logFullObjectInMultipleLogMessages(header: "Initialize request", AnyRequestType(request: req))
|
|
// If the client can handle `PeekDocumentsRequest`, they can enable the
|
|
// experimental client capability `"workspace/peekDocuments"` through the `req.capabilities.experimental`.
|
|
//
|
|
// The below is a workaround for the vscode-swift extension since it cannot set client capabilities.
|
|
// It passes "workspace/peekDocuments" through the `initializationOptions`.
|
|
var clientCapabilities = req.capabilities
|
|
if case .dictionary(let initializationOptions) = req.initializationOptions {
|
|
let experimentalClientCapabilities = [
|
|
PeekDocumentsRequest.method,
|
|
GetReferenceDocumentRequest.method,
|
|
DidChangeActiveDocumentNotification.method,
|
|
]
|
|
for capabilityName in experimentalClientCapabilities {
|
|
guard let experimentalCapability = initializationOptions[capabilityName] else {
|
|
continue
|
|
}
|
|
var experimentalCapabilities: [String: LSPAny] =
|
|
if case .dictionary(let experimentalCapabilities) = clientCapabilities.experimental {
|
|
experimentalCapabilities
|
|
} else {
|
|
[:]
|
|
}
|
|
experimentalCapabilities[capabilityName] = experimentalCapability
|
|
clientCapabilities.experimental = .dictionary(experimentalCapabilities)
|
|
}
|
|
|
|
// The client announces what CodeLenses it supports, and the LSP will only return
|
|
// ones found in the supportedCommands dictionary.
|
|
if let codeLens = initializationOptions["textDocument/codeLens"],
|
|
case let .dictionary(codeLensConfig) = codeLens,
|
|
case let .dictionary(supportedCommands) = codeLensConfig["supportedCommands"]
|
|
{
|
|
let commandMap = supportedCommands.compactMap { (key, value) in
|
|
if case let .string(clientCommand) = value {
|
|
return (SupportedCodeLensCommand(rawValue: key), clientCommand)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
clientCapabilities.textDocument?.codeLens?.supportedCommands = Dictionary(uniqueKeysWithValues: commandMap)
|
|
}
|
|
}
|
|
|
|
capabilityRegistry = CapabilityRegistry(clientCapabilities: clientCapabilities)
|
|
|
|
let initializeOptions = orLog("Parsing options") { try SourceKitLSPOptions(fromLSPAny: req.initializationOptions) }
|
|
_options.withLock { options in
|
|
options = SourceKitLSPOptions.merging(base: options, override: initializeOptions)
|
|
}
|
|
|
|
logger.log("Initialized SourceKit-LSP")
|
|
logger.logFullObjectInMultipleLogMessages(header: "Global options", options.loggingProxy)
|
|
|
|
await workspaceQueue.async { [hooks] in
|
|
if let workspaceFolders = req.workspaceFolders {
|
|
self.workspacesAndIsImplicit += await workspaceFolders.asyncCompactMap { workspaceFolder in
|
|
await orLog("Creating workspace from workspaceFolders") {
|
|
return (
|
|
workspace: try await self.createWorkspaceWithInferredBuildServer(workspaceFolder: workspaceFolder.uri),
|
|
isImplicit: false
|
|
)
|
|
}
|
|
}
|
|
} else if let uri = req.rootURI {
|
|
await orLog("Creating workspace from rootURI") {
|
|
self.workspacesAndIsImplicit.append(
|
|
(workspace: try await self.createWorkspaceWithInferredBuildServer(workspaceFolder: uri), isImplicit: false)
|
|
)
|
|
}
|
|
} else if let path = req.rootPath {
|
|
let uri = DocumentURI(URL(fileURLWithPath: path))
|
|
await orLog("Creating workspace from rootPath") {
|
|
self.workspacesAndIsImplicit.append(
|
|
(workspace: try await self.createWorkspaceWithInferredBuildServer(workspaceFolder: uri), isImplicit: false)
|
|
)
|
|
}
|
|
}
|
|
|
|
if self.workspaces.isEmpty {
|
|
logger.error("No workspace found")
|
|
|
|
let options = self.options
|
|
let workspace = await Workspace(
|
|
sourceKitLSPServer: self,
|
|
documentManager: self.documentManager,
|
|
rootUri: req.rootURI,
|
|
capabilityRegistry: self.capabilityRegistry!,
|
|
buildServerSpec: nil,
|
|
toolchainRegistry: self.toolchainRegistry,
|
|
options: options,
|
|
hooks: hooks,
|
|
indexTaskScheduler: self.indexTaskScheduler
|
|
)
|
|
|
|
self.workspacesAndIsImplicit.append((workspace: workspace, isImplicit: false))
|
|
}
|
|
}.value
|
|
|
|
assert(!self.workspaces.isEmpty)
|
|
|
|
let result = InitializeResult(
|
|
capabilities: await self.serverCapabilities(
|
|
for: req.capabilities,
|
|
registry: self.capabilityRegistry!,
|
|
options: options
|
|
)
|
|
)
|
|
logger.logFullObjectInMultipleLogMessages(header: "Initialize response", AnyRequestType(request: req))
|
|
return result
|
|
}
|
|
|
|
func serverCapabilities(
|
|
for client: ClientCapabilities,
|
|
registry: CapabilityRegistry,
|
|
options: SourceKitLSPOptions
|
|
) async -> ServerCapabilities {
|
|
let completionOptions =
|
|
await registry.clientHasDynamicCompletionRegistration
|
|
? nil
|
|
: LanguageServerProtocol.CompletionOptions(
|
|
resolveProvider: true,
|
|
triggerCharacters: [".", "("]
|
|
)
|
|
|
|
let signatureHelpOptions =
|
|
await registry.clientHasDynamicSignatureHelpRegistration
|
|
? nil
|
|
: LanguageServerProtocol.SignatureHelpOptions(
|
|
triggerCharacters: ["(", "["],
|
|
// We retrigger on `:` as it's potentially after an argument label which can change the active parameter or signature.
|
|
retriggerCharacters: [",", ":"]
|
|
)
|
|
|
|
let onTypeFormattingOptions =
|
|
options.hasExperimentalFeature(.onTypeFormatting)
|
|
? DocumentOnTypeFormattingOptions(triggerCharacters: ["\n", "\r\n", "\r", "{", "}", ";", ".", ":", "#"])
|
|
: nil
|
|
|
|
let foldingRangeOptions =
|
|
await registry.clientHasDynamicFoldingRangeRegistration
|
|
? nil
|
|
: ValueOrBool<TextDocumentAndStaticRegistrationOptions>.bool(true)
|
|
|
|
let inlayHintOptions =
|
|
await registry.clientHasDynamicInlayHintRegistration
|
|
? nil
|
|
: ValueOrBool.value(InlayHintOptions(resolveProvider: true))
|
|
|
|
let semanticTokensOptions =
|
|
await registry.clientHasDynamicSemanticTokensRegistration
|
|
? nil
|
|
: SemanticTokensOptions(
|
|
legend: SemanticTokensLegend.sourceKitLSPLegend,
|
|
range: .bool(true),
|
|
full: .bool(true)
|
|
)
|
|
|
|
let executeCommandOptions =
|
|
await registry.clientHasDynamicExecuteCommandRegistration
|
|
? nil
|
|
: ExecuteCommandOptions(commands: languageServiceRegistry.languageServices.flatMap { $0.type.builtInCommands })
|
|
|
|
var experimentalCapabilities: [String: LSPAny] = [
|
|
WorkspaceTestsRequest.method: .dictionary(["version": .int(2)]),
|
|
DocumentTestsRequest.method: .dictionary(["version": .int(2)]),
|
|
TriggerReindexRequest.method: .dictionary(["version": .int(1)]),
|
|
GetReferenceDocumentRequest.method: .dictionary(["version": .int(1)]),
|
|
DidChangeActiveDocumentNotification.method: .dictionary(["version": .int(1)]),
|
|
]
|
|
if let toolchain = await toolchainRegistry.preferredToolchain(containing: [\.swiftc]), toolchain.swiftPlay != nil {
|
|
experimentalCapabilities[WorkspacePlaygroundsRequest.method] = .dictionary(["version": .int(1)])
|
|
}
|
|
for (key, value) in languageServiceRegistry.languageServices.flatMap({ $0.type.experimentalCapabilities }) {
|
|
if let existingValue = experimentalCapabilities[key] {
|
|
logger.error(
|
|
"Conflicting experimental capabilities for \(key): \(existingValue.forLogging) vs \(value.forLogging)"
|
|
)
|
|
}
|
|
experimentalCapabilities[key] = value
|
|
}
|
|
|
|
return ServerCapabilities(
|
|
textDocumentSync: .options(
|
|
TextDocumentSyncOptions(
|
|
openClose: true,
|
|
change: .incremental
|
|
)
|
|
),
|
|
hoverProvider: .bool(true),
|
|
completionProvider: completionOptions,
|
|
signatureHelpProvider: signatureHelpOptions,
|
|
definitionProvider: .bool(true),
|
|
typeDefinitionProvider: .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
|
|
)
|
|
),
|
|
codeLensProvider: CodeLensOptions(),
|
|
documentFormattingProvider: .value(DocumentFormattingOptions(workDoneProgress: false)),
|
|
documentRangeFormattingProvider: .value(DocumentRangeFormattingOptions(workDoneProgress: false)),
|
|
documentOnTypeFormattingProvider: onTypeFormattingOptions,
|
|
renameProvider: .value(RenameOptions(prepareProvider: true)),
|
|
colorProvider: .bool(true),
|
|
foldingRangeProvider: foldingRangeOptions,
|
|
declarationProvider: .bool(true),
|
|
executeCommandProvider: executeCommandOptions,
|
|
workspace: WorkspaceServerCapabilities(
|
|
workspaceFolders: .init(
|
|
supported: true,
|
|
changeNotifications: .bool(true)
|
|
)
|
|
),
|
|
callHierarchyProvider: .bool(true),
|
|
typeHierarchyProvider: .bool(true),
|
|
semanticTokensProvider: semanticTokensOptions,
|
|
inlayHintProvider: inlayHintOptions,
|
|
experimental: .dictionary(experimentalCapabilities)
|
|
)
|
|
}
|
|
|
|
func registerCapabilities(
|
|
for server: ServerCapabilities,
|
|
languages: [Language],
|
|
registry: CapabilityRegistry
|
|
) async {
|
|
// IMPORTANT: When adding new capabilities here, also add the value of that capability in `SwiftLanguageService`
|
|
// to SourceKitLSPServer.serverCapabilities. That way the capabilities get registered for all languages in case the
|
|
// client does not support dynamic capability registration.
|
|
|
|
if let completionOptions = server.completionProvider {
|
|
await registry.registerCompletionIfNeeded(options: completionOptions, for: languages, server: self)
|
|
}
|
|
if let signatureHelpOptions = server.signatureHelpProvider {
|
|
await registry.registerSignatureHelpIfNeeded(options: signatureHelpOptions, for: languages, server: self)
|
|
}
|
|
if server.foldingRangeProvider?.isSupported == true {
|
|
await registry.registerFoldingRangeIfNeeded(options: FoldingRangeOptions(), for: languages, server: self)
|
|
}
|
|
if let semanticTokensOptions = server.semanticTokensProvider {
|
|
await registry.registerSemanticTokensIfNeeded(options: semanticTokensOptions, for: languages, server: self)
|
|
}
|
|
if let inlayHintProvider = server.inlayHintProvider, inlayHintProvider.isSupported {
|
|
let options: InlayHintOptions
|
|
switch inlayHintProvider {
|
|
case .bool(true):
|
|
options = InlayHintOptions()
|
|
case .bool(false):
|
|
return
|
|
case .value(let opts):
|
|
options = opts
|
|
}
|
|
await registry.registerInlayHintIfNeeded(options: options, for: languages, server: self)
|
|
}
|
|
// We use the registration for the diagnostics provider to decide whether to enable pull-diagnostics (see comment
|
|
// on `CapabilityRegistry.clientSupportPullDiagnostics`.
|
|
// Thus, we can't statically register this capability in the server options. We need the client's reply to decide
|
|
// whether it supports pull diagnostics.
|
|
if let diagnosticOptions = server.diagnosticProvider {
|
|
await registry.registerDiagnosticIfNeeded(options: diagnosticOptions, for: languages, server: self)
|
|
}
|
|
if let commandOptions = server.executeCommandProvider {
|
|
await registry.registerExecuteCommandIfNeeded(commands: commandOptions.commands, server: self)
|
|
}
|
|
}
|
|
|
|
func clientInitialized(_: InitializedNotification) {
|
|
// Nothing to do.
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// - Note: this method should be safe to call multiple times, since we want to be resilient against multiple
|
|
// possible shutdown sequences, including pipe failure.
|
|
package func prepareForExit() async {
|
|
// We are shutting down / closing all workspaces and language services, so clear the arrays caching them.
|
|
let languageServices = self.languageServices
|
|
self.languageServices = [:]
|
|
|
|
let workspaces = await self.workspaceQueue.async {
|
|
let workspaces = self.workspaces
|
|
self.workspacesAndIsImplicit = []
|
|
return workspaces
|
|
}.valuePropagatingCancellation
|
|
|
|
// Concurrently shut all things down.
|
|
await withTaskGroup(of: Void.self) { taskGroup in
|
|
taskGroup.addTask {
|
|
await orLog("Shutting down index scheduler") {
|
|
await self.indexTaskScheduler.shutDown()
|
|
}
|
|
}
|
|
for service in languageServices.values.flatMap({ $0 }) {
|
|
taskGroup.addTask {
|
|
await service.shutdown()
|
|
}
|
|
}
|
|
for workspace in workspaces {
|
|
taskGroup.addTask {
|
|
await orLog("Shutting down build server") {
|
|
await workspace.buildServerManager.shutdown()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make sure we emit all pending log messages. When we're not using `NonDarwinLogger` this is a no-op.
|
|
await NonDarwinLogger.flush()
|
|
}
|
|
|
|
func shutdown(_ request: ShutdownRequest) async throws -> ShutdownRequest.Response {
|
|
await prepareForExit()
|
|
|
|
// 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.
|
|
return ShutdownRequest.Response()
|
|
}
|
|
|
|
func exit(_ notification: ExitNotification) async {
|
|
// Should have been called in shutdown, but allow misbehaving clients.
|
|
await prepareForExit()
|
|
|
|
// Call onExit only once, and hop off queue to allow the handler to call us back.
|
|
self.onExit()
|
|
}
|
|
|
|
/// Start watching for changes with the given patterns.
|
|
func watchFiles(_ fileWatchers: [FileSystemWatcher]) async {
|
|
await self.waitUntilInitialized()
|
|
if fileWatchers.allSatisfy({ self.watchers.contains($0) }) {
|
|
// All watchers already registered. Nothing to do.
|
|
return
|
|
}
|
|
self.watchers.formUnion(fileWatchers)
|
|
await self.capabilityRegistry?.registerDidChangeWatchedFiles(
|
|
watchers: self.watchers.sorted { $0.globPattern < $1.globPattern },
|
|
server: self
|
|
)
|
|
}
|
|
|
|
func isIndexing(_ request: IsIndexingRequest) async throws -> IsIndexingResponse {
|
|
guard self.options.hasExperimentalFeature(.isIndexingRequest) else {
|
|
throw ResponseError.unknown("\(IsIndexingRequest.method) indexing is an experimental request")
|
|
}
|
|
let isIndexing =
|
|
await workspaces
|
|
.asyncCompactMap { await $0.semanticIndexManager }
|
|
.asyncContains { await $0.progressStatus != .upToDate }
|
|
return IsIndexingResponse(indexing: isIndexing)
|
|
}
|
|
|
|
// MARK: - Text synchronization
|
|
|
|
func openDocument(_ notification: DidOpenTextDocumentNotification) async {
|
|
let uri = notification.textDocument.uri
|
|
guard let workspace = await workspaceForDocument(uri: uri) else {
|
|
logger.error(
|
|
"Received open notification for file '\(uri.forLogging)' without a corresponding workspace, ignoring..."
|
|
)
|
|
return
|
|
}
|
|
await openDocument(notification, workspace: workspace)
|
|
await self.clientInteractedWithDocument(uri)
|
|
}
|
|
|
|
private func openDocument(_ notification: DidOpenTextDocumentNotification, workspace: Workspace) async {
|
|
// Immediately open the document even if the build server isn't ready. This is important since
|
|
// we check that the document is open when we receive messages from the build server.
|
|
let snapshot = orLog("Opening document") {
|
|
try documentManager.open(
|
|
notification.textDocument.uri,
|
|
language: notification.textDocument.language,
|
|
version: notification.textDocument.version,
|
|
text: notification.textDocument.text
|
|
)
|
|
}
|
|
guard let snapshot else {
|
|
// Already logged failure
|
|
return
|
|
}
|
|
|
|
let textDocument = notification.textDocument
|
|
let uri = textDocument.uri
|
|
let language = textDocument.language
|
|
|
|
let languageServices = await languageServices(for: uri, language, in: workspace)
|
|
workspace.setLanguageServices(for: uri, languageServices)
|
|
|
|
if languageServices.isEmpty {
|
|
// If we can't create a service, this document is unsupported and we can bail here.
|
|
return
|
|
}
|
|
|
|
await workspace.buildServerManager.registerForChangeNotifications(for: uri, language: language)
|
|
|
|
// If the document is ready, we can immediately send the notification.
|
|
for languageService in languageServices {
|
|
await languageService.openDocument(notification, snapshot: snapshot)
|
|
}
|
|
}
|
|
|
|
func closeDocument(_ notification: DidCloseTextDocumentNotification) async {
|
|
let uri = notification.textDocument.uri
|
|
guard let workspace = await workspaceForDocument(uri: uri) else {
|
|
logger.error(
|
|
"Received close notification for file '\(uri.forLogging)' without a corresponding workspace, ignoring..."
|
|
)
|
|
return
|
|
}
|
|
await self.closeDocument(notification, workspace: workspace)
|
|
}
|
|
|
|
func reopenDocument(_ notification: ReopenTextDocumentNotification) async {
|
|
let uri = notification.textDocument.uri
|
|
guard let workspace = await workspaceForDocument(uri: uri) else {
|
|
logger.error(
|
|
"Received reopen notification for file '\(uri.forLogging)' without a corresponding workspace, ignoring..."
|
|
)
|
|
return
|
|
}
|
|
|
|
for languageService in workspace.languageServices(for: uri) {
|
|
await languageService.reopenDocument(notification)
|
|
}
|
|
}
|
|
|
|
func closeDocument(_ notification: DidCloseTextDocumentNotification, workspace: Workspace) async {
|
|
// Immediately close the document. We need to be sure to clear our pending work queue in case
|
|
// the build server still isn't ready.
|
|
orLog("failed to close document", level: .error) {
|
|
try documentManager.close(notification.textDocument.uri)
|
|
}
|
|
|
|
let uri = notification.textDocument.uri
|
|
|
|
await workspace.buildServerManager.unregisterForChangeNotifications(for: uri)
|
|
|
|
for languageService in workspace.languageServices(for: uri) {
|
|
await languageService.closeDocument(notification)
|
|
}
|
|
|
|
workspace.removeLanguageServices(for: uri)
|
|
|
|
workspaceQueue.async {
|
|
self.workspaceForUri[notification.textDocument.uri] = nil
|
|
}
|
|
}
|
|
|
|
func changeDocument(_ notification: DidChangeTextDocumentNotification) async {
|
|
let uri = notification.textDocument.uri
|
|
|
|
guard let workspace = await workspaceForDocument(uri: uri) else {
|
|
logger.error(
|
|
"Received change notification for file '\(uri.forLogging)' without a corresponding workspace, ignoring..."
|
|
)
|
|
return
|
|
}
|
|
await self.clientInteractedWithDocument(uri)
|
|
|
|
// If the document is ready, we can handle the change right now.
|
|
let editResult = orLog("Editing document") {
|
|
try documentManager.edit(
|
|
notification.textDocument.uri,
|
|
newVersion: notification.textDocument.version,
|
|
edits: notification.contentChanges
|
|
)
|
|
}
|
|
guard let (preEditSnapshot, postEditSnapshot, edits) = editResult else {
|
|
// Already logged failure
|
|
return
|
|
}
|
|
for languageService in workspace.languageServices(for: uri) {
|
|
await languageService.changeDocument(
|
|
notification,
|
|
preEditSnapshot: preEditSnapshot,
|
|
postEditSnapshot: postEditSnapshot,
|
|
edits: edits
|
|
)
|
|
}
|
|
}
|
|
|
|
/// If the client doesn't support the `window/didChangeActiveDocument` notification, when the client interacts with a
|
|
/// document, infer that this is the currently active document and poke preparation of its target. We don't want to
|
|
/// wait for the preparation to finish because that would cause too big a delay.
|
|
///
|
|
/// In practice, while the user is working on a file, we'll get a text document request for it on a regular basis,
|
|
/// which prepares the files. For files that are open but aren't being worked on (eg. a different tab), we don't
|
|
/// get requests, ensuring that we don't unnecessarily prepare them.
|
|
func clientInteractedWithDocument(_ uri: DocumentURI) async {
|
|
if self.capabilityRegistry?.clientHasExperimentalCapability(DidChangeActiveDocumentNotification.method) ?? false {
|
|
// The client actively notifies us about the currently active document, so we shouldn't infer the active document
|
|
// based on other requests that the client sends us.
|
|
return
|
|
}
|
|
await self.didChangeActiveDocument(
|
|
DidChangeActiveDocumentNotification(textDocument: TextDocumentIdentifier(uri))
|
|
)
|
|
}
|
|
|
|
func didChangeActiveDocument(_ notification: DidChangeActiveDocumentNotification) async {
|
|
guard let activeDocument = notification.textDocument?.uri else {
|
|
// The client no longer has a SourceKit-LSP document open. Mark in-progress preparations as irrelevant to cancel
|
|
// them if they haven't started yet.
|
|
for workspace in workspaces {
|
|
await workspace.semanticIndexManager?.markPreparationForEditorFunctionalityTaskAsIrrelevant()
|
|
}
|
|
return
|
|
}
|
|
let documentWorkspace = await self.workspaceForDocument(uri: activeDocument)
|
|
for workspace in workspaces {
|
|
if workspace === documentWorkspace {
|
|
await workspace.semanticIndexManager?.schedulePreparationForEditorFunctionality(of: activeDocument)
|
|
} else {
|
|
await workspace.semanticIndexManager?.markPreparationForEditorFunctionalityTaskAsIrrelevant()
|
|
}
|
|
}
|
|
}
|
|
|
|
func willSaveDocument(
|
|
_ notification: WillSaveTextDocumentNotification,
|
|
languageService: any LanguageService
|
|
) async {
|
|
await languageService.willSaveDocument(notification)
|
|
}
|
|
|
|
func didSaveDocument(
|
|
_ notification: DidSaveTextDocumentNotification,
|
|
languageService: any LanguageService
|
|
) async {
|
|
await languageService.didSaveDocument(notification)
|
|
}
|
|
|
|
func didChangeWorkspaceFolders(_ notification: DidChangeWorkspaceFoldersNotification) async {
|
|
// There is a theoretical race condition here: While we await in this function,
|
|
// the open documents or workspaces could have changed. Because of this,
|
|
// we might close a document in a workspace that is no longer responsible
|
|
// for it.
|
|
// In practice, it is fine: sourcekit-lsp will not handle any new messages
|
|
// while we are executing this function and thus there's no risk of
|
|
// documents or workspaces changing. To hit the race condition, you need
|
|
// to invoke the API of `SourceKitLSPServer` directly and open documents
|
|
// while this function is executing. Even in such an API use case, hitting
|
|
// that race condition seems very unlikely.
|
|
var preChangeWorkspaces: [DocumentURI: Workspace] = [:]
|
|
for docUri in self.documentManager.openDocuments {
|
|
preChangeWorkspaces[docUri] = await self.workspaceForDocument(uri: docUri)
|
|
}
|
|
await workspaceQueue.async {
|
|
if let removed = notification.event.removed {
|
|
self.workspacesAndIsImplicit.removeAll { workspace in
|
|
// Close all implicit workspaces as well because we could have opened a new explicit workspace that now contains
|
|
// files from a previous implicit workspace.
|
|
return workspace.isImplicit
|
|
|| removed.contains(where: { workspaceFolder in workspace.workspace.rootUri == workspaceFolder.uri })
|
|
}
|
|
}
|
|
if let added = notification.event.added {
|
|
let newWorkspaces = await added.asyncCompactMap { workspaceFolder in
|
|
await orLog("Creating workspace after workspace folder change") {
|
|
try await self.createWorkspaceWithInferredBuildServer(workspaceFolder: workspaceFolder.uri)
|
|
}
|
|
}
|
|
self.workspacesAndIsImplicit += newWorkspaces.map { (workspace: $0, isImplicit: false) }
|
|
}
|
|
}.value
|
|
|
|
// Shut down any language services that are no longer referenced by any workspace.
|
|
await self.shutdownOrphanedLanguageServices()
|
|
}
|
|
|
|
/// Shuts down any language services that are no longer referenced by any open workspace.
|
|
///
|
|
/// This method gathers all language services that are currently referenced by open workspaces
|
|
/// and shuts down any language services that are not in that set.
|
|
private func shutdownOrphanedLanguageServices() async {
|
|
// Gather all language services referenced by open workspaces
|
|
var referencedServices: Set<ObjectIdentifier> = []
|
|
for workspace in workspaces {
|
|
for languageService in workspace.allLanguageServices {
|
|
referencedServices.insert(ObjectIdentifier(languageService))
|
|
}
|
|
}
|
|
|
|
// Find and remove orphaned language services, skipping immortal ones
|
|
var orphanedServices: [any LanguageService] = []
|
|
for (serviceType, services) in languageServices {
|
|
var remainingServices: [any LanguageService] = []
|
|
for service in services {
|
|
if referencedServices.contains(ObjectIdentifier(service)) || type(of: service).isImmortal {
|
|
remainingServices.append(service)
|
|
} else {
|
|
orphanedServices.append(service)
|
|
}
|
|
}
|
|
if remainingServices.count != services.count {
|
|
languageServices[serviceType] = remainingServices.isEmpty ? nil : remainingServices
|
|
}
|
|
}
|
|
|
|
// Shut down orphaned services in a background task to avoid blocking other requests.
|
|
|
|
if !orphanedServices.isEmpty {
|
|
Task {
|
|
for service in orphanedServices {
|
|
logger.info("Shutting down orphaned language service: \(type(of: service))")
|
|
await service.shutdown()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func didChangeWatchedFiles(_ notification: DidChangeWatchedFilesNotification) async {
|
|
// 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 server's build settings
|
|
// (e.g. Package.swift doesn't have build settings but affects build
|
|
// settings). Inform the build server about all file changes.
|
|
await workspaces.concurrentForEach { await $0.filesDidChange(notification.changes) }
|
|
|
|
for languageService in languageServices.values.flatMap(\.self) {
|
|
await languageService.filesDidChange(notification.changes)
|
|
}
|
|
}
|
|
|
|
func setBackgroundIndexingPaused(_ request: SetOptionsRequest) async throws -> VoidResponse {
|
|
guard self.options.hasExperimentalFeature(.setOptionsRequest) else {
|
|
throw ResponseError.unknown("\(SetOptionsRequest.method) indexing is an experimental request")
|
|
}
|
|
if let backgroundIndexingPaused = request.backgroundIndexingPaused {
|
|
await self.indexTaskScheduler.setMaxConcurrentTasksByPriority(
|
|
Self.maxConcurrentIndexingTasksByPriority(isIndexingPaused: backgroundIndexingPaused, options: self.options)
|
|
)
|
|
}
|
|
|
|
return VoidResponse()
|
|
}
|
|
|
|
func sourceKitOptions(_ request: SourceKitOptionsRequest) async throws -> SourceKitOptionsResponse {
|
|
guard options.hasExperimentalFeature(.sourceKitOptionsRequest) else {
|
|
throw ResponseError.unknown("\(SourceKitOptionsRequest.method) is an experimental request")
|
|
}
|
|
let uri = request.textDocument.uri
|
|
guard let workspace = await self.workspaceForDocument(uri: uri) else {
|
|
throw ResponseError.workspaceNotOpen(uri)
|
|
}
|
|
|
|
let target: BuildTargetIdentifier?
|
|
if let requestedTarget = request.target {
|
|
target = BuildTargetIdentifier(uri: requestedTarget)
|
|
} else if let canonicalTarget = await workspace.buildServerManager.canonicalTarget(for: uri) {
|
|
target = canonicalTarget
|
|
} else {
|
|
target = nil
|
|
}
|
|
let didPrepareTarget: Bool?
|
|
if request.prepareTarget, let target, let semanticIndexManager = await workspace.semanticIndexManager {
|
|
didPrepareTarget = await semanticIndexManager.prepareTargetsForSourceKitOptions(target: target)
|
|
} else {
|
|
didPrepareTarget = nil
|
|
}
|
|
let buildSettings = await workspace.buildServerManager.buildSettingsInferredFromMainFile(
|
|
for: request.textDocument.uri,
|
|
target: target,
|
|
language: nil,
|
|
fallbackAfterTimeout: request.allowFallbackSettings
|
|
)
|
|
guard let buildSettings else {
|
|
throw ResponseError.unknown("Unable to determine build settings")
|
|
}
|
|
return SourceKitOptionsResponse(
|
|
compilerArguments: buildSettings.compilerArguments,
|
|
workingDirectory: buildSettings.workingDirectory,
|
|
kind: buildSettings.isFallback ? .fallback : .normal,
|
|
didPrepareTarget: didPrepareTarget,
|
|
data: buildSettings.data
|
|
)
|
|
}
|
|
|
|
func outputPaths(_ request: OutputPathsRequest) async throws -> OutputPathsResponse {
|
|
guard options.hasExperimentalFeature(.outputPathsRequest) else {
|
|
throw ResponseError.unknown("\(OutputPathsRequest.method) is an experimental request")
|
|
}
|
|
guard let workspace = self.workspaces.first(where: { $0.rootUri == request.workspace }) else {
|
|
throw ResponseError.unknown("No workspace with URI \(request.workspace.forLogging) found")
|
|
}
|
|
guard await workspace.buildServerManager.initializationData?.outputPathsProvider ?? false else {
|
|
throw ResponseError.unknown("Build server for \(request.workspace.forLogging) does not support output paths")
|
|
}
|
|
let outputPaths = try await workspace.buildServerManager.outputPaths(in: [
|
|
BuildTargetIdentifier(uri: request.target)
|
|
])
|
|
return OutputPathsResponse(outputPaths: outputPaths)
|
|
}
|
|
|
|
// MARK: - Language features
|
|
|
|
func completion(
|
|
_ req: CompletionRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> CompletionList {
|
|
return try await languageService.completion(req)
|
|
}
|
|
|
|
func completionItemResolve(
|
|
request: CompletionItemResolveRequest
|
|
) async throws -> CompletionItem {
|
|
// Swift completion items specify the URI of the item they originate from in the `data`
|
|
guard case .dictionary(let dict) = request.item.data, case .string(let uriString) = dict["uri"],
|
|
let uri = try? DocumentURI(string: uriString)
|
|
else {
|
|
return request.item
|
|
}
|
|
guard let workspace = await self.workspaceForDocument(uri: uri) else {
|
|
throw ResponseError.workspaceNotOpen(uri)
|
|
}
|
|
let language = try documentManager.latestSnapshot(uri.buildSettingsFile).language
|
|
return try await primaryLanguageService(for: uri, language, in: workspace).completionItemResolve(request)
|
|
}
|
|
|
|
func doccDocumentation(
|
|
_ req: DoccDocumentationRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> DoccDocumentationResponse {
|
|
return try await languageService.doccDocumentation(req)
|
|
}
|
|
|
|
func hover(
|
|
_ req: HoverRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> HoverResponse? {
|
|
return try await languageService.hover(req)
|
|
}
|
|
|
|
func signatureHelp(
|
|
_ req: SignatureHelpRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> SignatureHelp? {
|
|
return try await languageService.signatureHelp(req)
|
|
}
|
|
|
|
/// Handle a workspace/symbol request, returning the SymbolInformation.
|
|
/// - returns: An array with SymbolInformation for each matching symbol in the workspace.
|
|
func workspaceSymbols(_ req: WorkspaceSymbolsRequest) async throws -> [WorkspaceSymbolItem]? {
|
|
// 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 req.query.count >= minWorkspaceSymbolPatternLength else {
|
|
return []
|
|
}
|
|
var symbolsIndexAndWorkspaces: [(symbol: SymbolOccurrence, index: CheckedIndex, workspace: Workspace)] = []
|
|
for workspace in workspaces {
|
|
guard let index = await workspace.index(checkedFor: .deletedFiles) else {
|
|
continue
|
|
}
|
|
index.forEachCanonicalSymbolOccurrence(
|
|
containing: req.query,
|
|
anchorStart: false,
|
|
anchorEnd: false,
|
|
subsequence: true,
|
|
ignoreCase: true
|
|
) { symbol in
|
|
if Task.isCancelled {
|
|
return false
|
|
}
|
|
guard !symbol.location.isSystem && !symbol.roles.contains(.accessorOf) else {
|
|
return true
|
|
}
|
|
symbolsIndexAndWorkspaces.append((symbol, index, workspace))
|
|
return true
|
|
}
|
|
try Task.checkCancellation()
|
|
}
|
|
|
|
return await symbolsIndexAndWorkspaces.sorted(by: { $0.symbol < $1.symbol }).asyncMap {
|
|
(symbolOccurrence, index, workspace) in
|
|
let symbolPosition = Position(
|
|
line: symbolOccurrence.location.line - 1, // 1-based -> 0-based
|
|
// Technically we would need to convert the UTF-8 column to a UTF-16 column. This would require reading the
|
|
// file. In practice they almost always coincide, so we accept the incorrectness here to avoid the file read.
|
|
utf16index: symbolOccurrence.location.utf8Column - 1
|
|
)
|
|
let symbolLocation = Location(uri: symbolOccurrence.location.documentUri, range: Range(symbolPosition))
|
|
let location = await workspace.buildServerManager.locationAdjustedForCopiedFiles(symbolLocation)
|
|
|
|
let containerNames = index.containerNames(of: symbolOccurrence)
|
|
let containerName: String?
|
|
if containerNames.isEmpty {
|
|
containerName = nil
|
|
} else {
|
|
switch symbolOccurrence.symbol.language {
|
|
case .cxx, .c, .objc: containerName = containerNames.joined(separator: "::")
|
|
case .swift: containerName = containerNames.joined(separator: ".")
|
|
}
|
|
}
|
|
|
|
return WorkspaceSymbolItem.symbolInformation(
|
|
SymbolInformation(
|
|
name: symbolOccurrence.symbol.name,
|
|
kind: symbolOccurrence.symbol.kind.asLspSymbolKind(),
|
|
deprecated: nil,
|
|
location: location,
|
|
containerName: containerName
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Forwards a SymbolInfoRequest to the appropriate toolchain service for this document.
|
|
func symbolInfo(
|
|
_ req: SymbolInfoRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [SymbolDetails] {
|
|
return try await languageService.symbolInfo(req)
|
|
}
|
|
|
|
func documentSymbolHighlight(
|
|
_ req: DocumentHighlightRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [DocumentHighlight]? {
|
|
return try await languageService.documentSymbolHighlight(req)
|
|
}
|
|
|
|
func foldingRange(
|
|
_ req: FoldingRangeRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [FoldingRange]? {
|
|
return try await languageService.foldingRange(req)
|
|
}
|
|
|
|
func documentSymbol(
|
|
_ req: DocumentSymbolRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> DocumentSymbolResponse? {
|
|
return try await languageService.documentSymbol(req)
|
|
}
|
|
|
|
func documentColor(
|
|
_ req: DocumentColorRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [ColorInformation] {
|
|
return try await languageService.documentColor(req)
|
|
}
|
|
|
|
func documentSemanticTokens(
|
|
_ req: DocumentSemanticTokensRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> DocumentSemanticTokensResponse? {
|
|
return try await languageService.documentSemanticTokens(req)
|
|
}
|
|
|
|
func documentSemanticTokensDelta(
|
|
_ req: DocumentSemanticTokensDeltaRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> DocumentSemanticTokensDeltaResponse? {
|
|
return try await languageService.documentSemanticTokensDelta(req)
|
|
}
|
|
|
|
func documentSemanticTokensRange(
|
|
_ req: DocumentSemanticTokensRangeRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> DocumentSemanticTokensResponse? {
|
|
return try await languageService.documentSemanticTokensRange(req)
|
|
}
|
|
|
|
func documentFormatting(
|
|
_ req: DocumentFormattingRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [TextEdit]? {
|
|
return try await languageService.documentFormatting(req)
|
|
}
|
|
|
|
func documentRangeFormatting(
|
|
_ req: DocumentRangeFormattingRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [TextEdit]? {
|
|
return try await languageService.documentRangeFormatting(req)
|
|
}
|
|
|
|
func documentOnTypeFormatting(
|
|
_ req: DocumentOnTypeFormattingRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [TextEdit]? {
|
|
return try await languageService.documentOnTypeFormatting(req)
|
|
}
|
|
|
|
func colorPresentation(
|
|
_ req: ColorPresentationRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [ColorPresentation] {
|
|
return try await languageService.colorPresentation(req)
|
|
}
|
|
|
|
func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? {
|
|
guard let uri = req.textDocument?.uri else {
|
|
logger.error("Attempted to perform executeCommand request without an URL")
|
|
return nil
|
|
}
|
|
|
|
let executeCommand = ExecuteCommandRequest(
|
|
command: req.command,
|
|
arguments: req.argumentsWithoutSourceKitMetadata
|
|
)
|
|
guard let workspace = await self.workspaceForDocument(uri: uri) else {
|
|
throw ResponseError.workspaceNotOpen(uri)
|
|
}
|
|
let language = try documentManager.latestSnapshot(uri.buildSettingsFile).language
|
|
// First, check if we have a language service that explicitly declares support for this command.
|
|
if let languageService = await languageServices(for: uri, language, in: workspace)
|
|
.first(where: { type(of: $0).builtInCommands.contains(req.command) })
|
|
{
|
|
return try await languageService.executeCommand(executeCommand)
|
|
}
|
|
// Otherwise handle it in the primary language service. This is important to handle eg. commands in clangd, which
|
|
// are not declared as built-in commands.
|
|
return try await primaryLanguageService(for: uri, language, in: workspace).executeCommand(executeCommand)
|
|
}
|
|
|
|
func getReferenceDocument(_ req: GetReferenceDocumentRequest) async throws -> GetReferenceDocumentResponse {
|
|
let buildSettingsUri = try ReferenceDocumentURL(from: req.uri).buildSettingsFile
|
|
|
|
guard let workspace = await self.workspaceForDocument(uri: buildSettingsUri) else {
|
|
throw ResponseError.workspaceNotOpen(buildSettingsUri)
|
|
}
|
|
let language: Language
|
|
|
|
// The document that provided the build settings might no longer be open, so we need to be able to infer the
|
|
// language by other means as well.
|
|
if let snapshot = try? documentManager.latestSnapshot(buildSettingsUri) {
|
|
language = snapshot.language
|
|
} else if let target = await workspace.buildServerManager.canonicalTarget(for: buildSettingsUri),
|
|
let lang = await workspace.buildServerManager.defaultLanguage(for: buildSettingsUri, in: target)
|
|
{
|
|
language = lang
|
|
} else if let lang = Language(inferredFromFileExtension: buildSettingsUri) {
|
|
language = lang
|
|
} else {
|
|
throw ResponseError.unknown("Unable to infer language for \(buildSettingsUri)")
|
|
}
|
|
|
|
return try await primaryLanguageService(for: buildSettingsUri, language, in: workspace).getReferenceDocument(req)
|
|
}
|
|
|
|
func codeAction(
|
|
_ req: CodeActionRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> CodeActionRequestResponse? {
|
|
let response = try await languageService.codeAction(req)
|
|
return req.injectMetadata(toResponse: response)
|
|
}
|
|
|
|
func codeLens(
|
|
_ req: CodeLensRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [CodeLens] {
|
|
return try await languageService.codeLens(req)
|
|
}
|
|
|
|
func inlayHint(
|
|
_ req: InlayHintRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [InlayHint] {
|
|
return try await languageService.inlayHint(req)
|
|
}
|
|
|
|
func inlayHintResolve(
|
|
request: InlayHintResolveRequest
|
|
) async throws -> InlayHint {
|
|
guard case .dictionary(let dict) = request.inlayHint.data,
|
|
case .string(let uriString) = dict["uri"],
|
|
let uri = try? DocumentURI(string: uriString)
|
|
else {
|
|
return request.inlayHint
|
|
}
|
|
guard let workspace = await self.workspaceForDocument(uri: uri) else {
|
|
return request.inlayHint
|
|
}
|
|
let language = try documentManager.latestSnapshot(uri.buildSettingsFile).language
|
|
return try await primaryLanguageService(for: uri, language, in: workspace).inlayHintResolve(request)
|
|
}
|
|
|
|
func documentDiagnostic(
|
|
_ req: DocumentDiagnosticsRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> DocumentDiagnosticReport {
|
|
return try await languageService.documentDiagnostic(req)
|
|
}
|
|
|
|
func declaration(
|
|
_ req: DeclarationRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> LocationsOrLocationLinksResponse? {
|
|
return try await languageService.declaration(req)
|
|
}
|
|
|
|
func typeDefinition(
|
|
_ req: TypeDefinitionRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> LocationsOrLocationLinksResponse? {
|
|
return try await languageService.typeDefinition(req)
|
|
}
|
|
|
|
/// Returns the result of a `DefinitionRequest` by running a `SymbolInfoRequest`, inspecting
|
|
/// its result and doing index lookups, if necessary.
|
|
///
|
|
/// In contrast to `definition`, this does not fall back to sending a `DefinitionRequest` to the
|
|
/// toolchain language server.
|
|
private func indexBasedDefinition(
|
|
_ req: DefinitionRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [Location] {
|
|
let symbols = try await languageService.symbolInfo(
|
|
SymbolInfoRequest(
|
|
textDocument: req.textDocument,
|
|
position: req.position
|
|
)
|
|
)
|
|
|
|
let canonicalOriginatorLocation = await languageService.canonicalDeclarationPosition(
|
|
of: req.position,
|
|
in: req.textDocument.uri
|
|
)
|
|
|
|
// Returns `true` if `location` points to the same declaration that the definition request was initiated from.
|
|
@Sendable func isAtCanonicalOriginatorLocation(_ location: Location) async -> Bool {
|
|
guard location.uri == req.textDocument.uri, let canonicalOriginatorLocation else {
|
|
return false
|
|
}
|
|
return await languageService.canonicalDeclarationPosition(of: location.range.lowerBound, in: location.uri)
|
|
== canonicalOriginatorLocation
|
|
}
|
|
|
|
var locations = try await symbols.asyncFlatMap { (symbol) -> [Location] in
|
|
var locations: [Location]
|
|
if let bestLocalDeclaration = symbol.bestLocalDeclaration,
|
|
!(symbol.isDynamic ?? true),
|
|
symbol.usr?.hasPrefix("s:") ?? false /* Swift symbols have USRs starting with s: */
|
|
{
|
|
// If we have a known non-dynamic symbol within Swift, we don't need to do an index lookup.
|
|
// For non-Swift symbols, we need to perform an index lookup because the best local declaration will point to
|
|
// a header file but jump-to-definition should prefer the implementation (there's the declaration request to
|
|
// jump to the function's declaration).
|
|
locations = [bestLocalDeclaration]
|
|
} else {
|
|
let index = await workspace.index(checkedFor: .deletedFiles)
|
|
let definitionResult = try await definitionLocations(
|
|
for: symbol,
|
|
originatorUri: req.textDocument.uri,
|
|
index: index,
|
|
languageService: languageService
|
|
)
|
|
locations = definitionResult.locations
|
|
// For dynamic symbols, also include overridden definitions
|
|
if let index, symbol.isDynamic ?? true, !definitionResult.indexOccurrences.isEmpty {
|
|
lazy var transitiveReceiverUsrs: [String]? = {
|
|
if let receiverUsrs = symbol.receiverUsrs {
|
|
return transitiveSubtypeClosure(
|
|
ofUsrs: receiverUsrs,
|
|
index: index
|
|
)
|
|
} else {
|
|
return nil
|
|
}
|
|
}()
|
|
// Use the occurrences already retrieved by definitionLocations to avoid duplicate index lookup
|
|
let overriddenLocations = definitionResult.indexOccurrences.flatMap { occurrence -> [Location] in
|
|
let overriddenUsrs = index.occurrences(relatedToUSR: occurrence.symbol.usr, roles: .overrideOf)
|
|
.map(\.symbol.usr)
|
|
let overriddenSymbolDefinitions = overriddenUsrs.compactMap {
|
|
index.primaryDefinitionOrDeclarationOccurrence(ofUSR: $0)
|
|
}
|
|
// Only contain overrides that are children of one of the receiver types or their subtypes or extensions.
|
|
return overriddenSymbolDefinitions.filter { override in
|
|
override.relations.contains(where: {
|
|
guard $0.roles.contains(.childOf) else {
|
|
return false
|
|
}
|
|
if let transitiveReceiverUsrs, !transitiveReceiverUsrs.contains($0.symbol.usr) {
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
}.compactMap { indexToLSPLocation($0.location) }
|
|
}
|
|
locations += overriddenLocations
|
|
}
|
|
}
|
|
|
|
// If the symbol's location is is where we initiated rename from, also show the declarations that the symbol
|
|
// overrides.
|
|
if let location = locations.only,
|
|
let usr = symbol.usr,
|
|
let index = await workspace.index(checkedFor: .deletedFiles),
|
|
await isAtCanonicalOriginatorLocation(location)
|
|
{
|
|
let baseUSRs = index.occurrences(ofUSR: usr, roles: .overrideOf).flatMap {
|
|
$0.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr)
|
|
}
|
|
locations += baseUSRs.compactMap {
|
|
guard let baseDeclOccurrence = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: $0) else {
|
|
return nil
|
|
}
|
|
return indexToLSPLocation(baseDeclOccurrence.location)
|
|
}
|
|
}
|
|
|
|
return locations
|
|
}
|
|
|
|
// Remove any duplicate locations. We might end up with duplicate locations when performing a definition request
|
|
// on eg. `MyStruct()` when no explicit initializer is declared. In this case we get two symbol infos, one for the
|
|
// declaration of the `MyStruct` type and one for the initializer, which is implicit and thus has the location of
|
|
// the `MyStruct` declaration itself.
|
|
locations = locations.unique
|
|
|
|
// Try removing any results that would point back to the location we are currently at. This ensures that eg. in the
|
|
// following case we only show line 2 when performing jump-to-definition on `TestImpl.doThing`.
|
|
//
|
|
// ```
|
|
// protocol TestProtocol {
|
|
// func doThing()
|
|
// }
|
|
// struct TestImpl: TestProtocol {
|
|
// func doThing() { }
|
|
// }
|
|
// ```
|
|
//
|
|
// If this would result in no locations, don't apply the filter. This way, performing jump-to-definition in the
|
|
// middle of a function's base name takes us to the base name start, indicating that jump-to-definition was able to
|
|
// resolve the location and didn't fail.
|
|
let nonOriginatorLocations = await locations.asyncFilter { await !isAtCanonicalOriginatorLocation($0) }
|
|
if !nonOriginatorLocations.isEmpty {
|
|
locations = nonOriginatorLocations
|
|
}
|
|
return locations
|
|
}
|
|
|
|
func definition(
|
|
_ req: DefinitionRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> LocationsOrLocationLinksResponse? {
|
|
let indexBasedResponse = try await indexBasedDefinition(req, workspace: workspace, languageService: languageService)
|
|
// 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).
|
|
// We are on only calling the language service's `definition` function if your index-based lookup failed.
|
|
// If this fallback request fails, its error is usually not very enlightening. For example the
|
|
// `SwiftLanguageService` will always respond with `unsupported method`. Thus, only log such a failure instead of
|
|
// returning it to the client.
|
|
if indexBasedResponse.isEmpty {
|
|
return await orLog("Fallback definition request", level: .info) {
|
|
let result = try await languageService.definition(req)
|
|
return await workspace.buildServerManager.locationsOrLocationLinksAdjustedForCopiedFiles(result)
|
|
}
|
|
}
|
|
let remappedLocations = await workspace.buildServerManager.locationsAdjustedForCopiedFiles(indexBasedResponse)
|
|
return .locations(remappedLocations)
|
|
}
|
|
|
|
func implementation(
|
|
_ req: ImplementationRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> LocationsOrLocationLinksResponse? {
|
|
let symbols = try await languageService.symbolInfo(
|
|
SymbolInfoRequest(
|
|
textDocument: req.textDocument,
|
|
position: req.position
|
|
)
|
|
)
|
|
guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else {
|
|
return nil
|
|
}
|
|
let locations = symbols.flatMap { (symbol) -> [Location] in
|
|
guard let usr = symbol.usr else { return [] }
|
|
var occurrences = index.occurrences(ofUSR: usr, roles: .baseOf)
|
|
if occurrences.isEmpty {
|
|
occurrences = index.occurrences(relatedToUSR: usr, roles: .overrideOf)
|
|
}
|
|
|
|
return occurrences.compactMap { indexToLSPLocation($0.location) }
|
|
}
|
|
let remappedLocations = await workspace.buildServerManager.locationsAdjustedForCopiedFiles(locations)
|
|
return .locations(remappedLocations.sorted())
|
|
}
|
|
|
|
func references(
|
|
_ req: ReferencesRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [Location] {
|
|
let symbols = try await languageService.symbolInfo(
|
|
SymbolInfoRequest(
|
|
textDocument: req.textDocument,
|
|
position: req.position
|
|
)
|
|
)
|
|
guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else {
|
|
return []
|
|
}
|
|
let locations = symbols.flatMap { (symbol) -> [Location] in
|
|
guard let usr = symbol.usr else { return [] }
|
|
logger.info("Finding references for USR \(usr)")
|
|
var roles: SymbolRole = [.reference]
|
|
if req.context.includeDeclaration {
|
|
roles.formUnion([.declaration, .definition])
|
|
}
|
|
return index.occurrences(ofUSR: usr, roles: roles).compactMap { indexToLSPLocation($0.location) }
|
|
}
|
|
let remappedLocations = await workspace.buildServerManager.locationsAdjustedForCopiedFiles(locations)
|
|
return remappedLocations.unique.sorted()
|
|
}
|
|
|
|
private func indexToLSPCallHierarchyItem(
|
|
definition: SymbolOccurrence,
|
|
index: CheckedIndex
|
|
) -> CallHierarchyItem? {
|
|
guard let location = indexToLSPLocation(definition.location) else {
|
|
return nil
|
|
}
|
|
let name = index.fullyQualifiedName(of: definition)
|
|
let symbol = definition.symbol
|
|
return CallHierarchyItem(
|
|
name: name,
|
|
kind: symbol.kind.asLspSymbolKind(),
|
|
tags: nil,
|
|
detail: nil,
|
|
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: CallHierarchyPrepareRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [CallHierarchyItem]? {
|
|
let symbols = try await languageService.symbolInfo(
|
|
SymbolInfoRequest(
|
|
textDocument: req.textDocument,
|
|
position: req.position
|
|
)
|
|
)
|
|
guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else {
|
|
return nil
|
|
}
|
|
// For call hierarchy preparation we only locate the definition
|
|
let usrs = symbols.compactMap(\.usr)
|
|
|
|
// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
|
|
func indexToLSPCallHierarchyItem2(
|
|
definition: SymbolOccurrence,
|
|
index: CheckedIndex
|
|
) -> CallHierarchyItem? {
|
|
return self.indexToLSPCallHierarchyItem(definition: definition, index: index)
|
|
}
|
|
|
|
// Only return a single call hierarchy item. Returning multiple doesn't make sense because they will all have the
|
|
// same USR (because we query them by USR) and will thus expand to the exact same call hierarchy.
|
|
var callHierarchyItems: [CallHierarchyItem] = []
|
|
for usr in usrs {
|
|
guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else {
|
|
continue
|
|
}
|
|
guard let item = indexToLSPCallHierarchyItem2(definition: definition, index: index) else {
|
|
continue
|
|
}
|
|
callHierarchyItems.append(await workspace.buildServerManager.callHierarchyItemAdjustedForCopiedFiles(item))
|
|
}
|
|
callHierarchyItems.sort(by: { Location(uri: $0.uri, range: $0.range) < Location(uri: $1.uri, range: $1.range) })
|
|
|
|
// Ideally, we should show multiple symbols. But VS Code fails to display call hierarchies with multiple root items,
|
|
// failing with `Cannot read properties of undefined (reading 'map')`. Pick the first one.
|
|
return Array(callHierarchyItems.prefix(1))
|
|
}
|
|
|
|
/// 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 nonisolated 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"],
|
|
let uri = orLog("DocumentURI for call hierarchy item", { try DocumentURI(string: uriString) })
|
|
else {
|
|
return nil
|
|
}
|
|
return (uri: uri, usr: usr)
|
|
}
|
|
|
|
func incomingCalls(_ req: CallHierarchyIncomingCallsRequest) async throws -> [CallHierarchyIncomingCall]? {
|
|
guard let data = extractCallHierarchyItemData(req.item.data),
|
|
let workspace = await self.workspaceForDocument(uri: data.uri),
|
|
let index = await workspace.index(checkedFor: .deletedFiles)
|
|
else {
|
|
return []
|
|
}
|
|
var callableUsrs = [data.usr]
|
|
// Also show calls to the functions that this method overrides. This includes overridden class methods and
|
|
// satisfied protocol requirements.
|
|
callableUsrs += index.occurrences(ofUSR: data.usr, roles: .overrideOf).flatMap { occurrence in
|
|
occurrence.relations.filter { $0.roles.contains(.overrideOf) }.map(\.symbol.usr)
|
|
}
|
|
// callOccurrences are all the places that any of the USRs in callableUsrs is called.
|
|
// We also load the `calledBy` roles to get the method that contains the reference to this call.
|
|
let callOccurrences = callableUsrs.flatMap { index.occurrences(ofUSR: $0, roles: .containedBy) }
|
|
.filter(\.shouldShowInCallHierarchy)
|
|
|
|
// Maps functions that call a USR in `callableUSRs` to all the called occurrences of `callableUSRs` within the
|
|
// function. If a function `foo` calls `bar` multiple times, `callersToCalls[foo]` will contain two call
|
|
// `SymbolOccurrence`s.
|
|
// This way, we can group multiple calls to `bar` within `foo` to a single item with multiple `fromRanges`.
|
|
var callersToCalls: [Symbol: [SymbolOccurrence]] = [:]
|
|
|
|
for call in callOccurrences {
|
|
// Callers are all `calledBy` relations of a call to a USR in `callableUsrs`, ie. all the functions that contain a
|
|
// call to a USR in callableUSRs. In practice, this should always be a single item.
|
|
let callers = call.relations.filter { $0.roles.contains(.containedBy) }.map(\.symbol)
|
|
for caller in callers {
|
|
callersToCalls[caller, default: []].append(call)
|
|
}
|
|
}
|
|
|
|
// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
|
|
func indexToLSPLocation2(_ location: SymbolLocation) -> Location? {
|
|
return indexToLSPLocation(location)
|
|
}
|
|
|
|
// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
|
|
func indexToLSPCallHierarchyItem2(
|
|
definition: SymbolOccurrence,
|
|
index: CheckedIndex
|
|
) -> CallHierarchyItem? {
|
|
return self.indexToLSPCallHierarchyItem(definition: definition, index: index)
|
|
}
|
|
|
|
var calls: [CallHierarchyIncomingCall] = []
|
|
for (caller, callsList) in callersToCalls {
|
|
// Resolve the caller's definition to find its location
|
|
guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: caller.usr) else {
|
|
continue
|
|
}
|
|
|
|
let locations = callsList.compactMap { indexToLSPLocation2($0.location) }.sorted()
|
|
let remappedLocations = await workspace.buildServerManager.locationsAdjustedForCopiedFiles(locations)
|
|
guard !remappedLocations.isEmpty else {
|
|
continue
|
|
}
|
|
|
|
guard let item = indexToLSPCallHierarchyItem2(definition: definition, index: index) else {
|
|
continue
|
|
}
|
|
let remappedItem = await workspace.buildServerManager.callHierarchyItemAdjustedForCopiedFiles(item)
|
|
|
|
calls.append(CallHierarchyIncomingCall(from: remappedItem, fromRanges: remappedLocations.map(\.range)))
|
|
}
|
|
return calls.sorted(by: { $0.from.name < $1.from.name })
|
|
}
|
|
|
|
func outgoingCalls(_ req: CallHierarchyOutgoingCallsRequest) async throws -> [CallHierarchyOutgoingCall]? {
|
|
guard let data = extractCallHierarchyItemData(req.item.data),
|
|
let workspace = await self.workspaceForDocument(uri: data.uri),
|
|
let index = await workspace.index(checkedFor: .deletedFiles)
|
|
else {
|
|
return []
|
|
}
|
|
|
|
// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
|
|
func indexToLSPLocation2(_ location: SymbolLocation) -> Location? {
|
|
return indexToLSPLocation(location)
|
|
}
|
|
|
|
// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
|
|
func indexToLSPCallHierarchyItem2(
|
|
definition: SymbolOccurrence,
|
|
index: CheckedIndex
|
|
) -> CallHierarchyItem? {
|
|
return self.indexToLSPCallHierarchyItem(definition: definition, index: index)
|
|
}
|
|
|
|
let callableUsrs = [data.usr] + index.occurrences(relatedToUSR: data.usr, roles: .accessorOf).map(\.symbol.usr)
|
|
let callOccurrences = callableUsrs.flatMap { index.occurrences(relatedToUSR: $0, roles: .containedBy) }
|
|
.filter(\.shouldShowInCallHierarchy)
|
|
var calls: [CallHierarchyOutgoingCall] = []
|
|
for occurrence in callOccurrences {
|
|
guard occurrence.symbol.kind.isCallable else {
|
|
continue
|
|
}
|
|
guard let location = indexToLSPLocation2(occurrence.location) else {
|
|
continue
|
|
}
|
|
let remappedLocation = await workspace.buildServerManager.locationAdjustedForCopiedFiles(location)
|
|
|
|
// Resolve the callee's definition to find its location
|
|
guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr) else {
|
|
continue
|
|
}
|
|
|
|
guard let item = indexToLSPCallHierarchyItem2(definition: definition, index: index) else {
|
|
continue
|
|
}
|
|
let remappedItem = await workspace.buildServerManager.callHierarchyItemAdjustedForCopiedFiles(item)
|
|
|
|
calls.append(CallHierarchyOutgoingCall(to: remappedItem, fromRanges: [remappedLocation.range]))
|
|
}
|
|
return calls.sorted(by: { $0.to.name < $1.to.name })
|
|
}
|
|
|
|
private func indexToLSPTypeHierarchyItem(
|
|
definition: SymbolOccurrence,
|
|
moduleName: String?,
|
|
index: CheckedIndex
|
|
) -> TypeHierarchyItem? {
|
|
let name: String
|
|
let detail: String?
|
|
|
|
guard let location = indexToLSPLocation(definition.location) else {
|
|
return nil
|
|
}
|
|
|
|
let symbol = definition.symbol
|
|
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).sorted().joined(separator: ", "))"
|
|
}
|
|
// Add the file name and line to the detail string
|
|
if let url = location.uri.fileURL,
|
|
let basename = (try? AbsolutePath(validating: url.filePath))?.basename
|
|
{
|
|
detail = "Extension at \(basename):\(location.range.lowerBound.line + 1)"
|
|
} else if let moduleName = moduleName {
|
|
detail = "Extension in \(moduleName)"
|
|
} else {
|
|
detail = "Extension"
|
|
}
|
|
default:
|
|
name = index.fullyQualifiedName(of: definition)
|
|
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: TypeHierarchyPrepareRequest,
|
|
workspace: Workspace,
|
|
languageService: any LanguageService
|
|
) async throws -> [TypeHierarchyItem]? {
|
|
let symbols = try await languageService.symbolInfo(
|
|
SymbolInfoRequest(
|
|
textDocument: req.textDocument,
|
|
position: req.position
|
|
)
|
|
)
|
|
guard !symbols.isEmpty else {
|
|
return nil
|
|
}
|
|
guard let index = await workspaceForDocument(uri: req.textDocument.uri)?.index(checkedFor: .deletedFiles) else {
|
|
return nil
|
|
}
|
|
let usrs = symbols.filter {
|
|
// Only include references to type. For example, we don't want to find the type hierarchy of a constructor when
|
|
// starting the type hierarchy on `Foo()`.
|
|
// Consider a symbol a class if its kind is `nil`, eg. for a symbol returned by clang's SymbolInfo, which
|
|
// doesn't support the `kind` field.
|
|
switch $0.kind {
|
|
case .class, .enum, .interface, .struct, nil: return true
|
|
default: return false
|
|
}
|
|
}.compactMap(\.usr)
|
|
|
|
// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
|
|
func indexToLSPLocation2(_ location: SymbolLocation) -> Location? {
|
|
return indexToLSPLocation(location)
|
|
}
|
|
|
|
// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
|
|
func indexToLSPTypeHierarchyItem2(
|
|
definition: SymbolOccurrence,
|
|
moduleName: String?,
|
|
index: CheckedIndex
|
|
) -> TypeHierarchyItem? {
|
|
return self.indexToLSPTypeHierarchyItem(
|
|
definition: definition,
|
|
moduleName: moduleName,
|
|
index: index
|
|
)
|
|
}
|
|
|
|
var typeHierarchyItems: [TypeHierarchyItem] = []
|
|
for usr in usrs {
|
|
guard let info = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: usr) else {
|
|
continue
|
|
}
|
|
// Filter symbols based on their kind in the index since the filter on the symbol info response might have
|
|
// returned `nil` for the kind, preventing us from doing any filtering there.
|
|
switch info.symbol.kind {
|
|
case .unknown, .macro, .function, .variable, .field, .enumConstant, .instanceMethod, .classMethod, .staticMethod,
|
|
.instanceProperty, .classProperty, .staticProperty, .constructor, .destructor, .conversionFunction, .parameter,
|
|
.concept, .commentTag:
|
|
continue
|
|
case .module, .namespace, .namespaceAlias, .enum, .struct, .class, .protocol, .extension, .union, .typealias,
|
|
.using:
|
|
break
|
|
}
|
|
|
|
guard indexToLSPLocation2(info.location) != nil else {
|
|
continue
|
|
}
|
|
|
|
let moduleName = info.location.moduleName
|
|
guard let item = indexToLSPTypeHierarchyItem2(definition: info, moduleName: moduleName, index: index) else {
|
|
continue
|
|
}
|
|
typeHierarchyItems.append(await workspace.buildServerManager.typeHierarchyItemAdjustedForCopiedFiles(item))
|
|
}
|
|
typeHierarchyItems.sort(by: { $0.name < $1.name })
|
|
|
|
if typeHierarchyItems.isEmpty {
|
|
// When returning an empty array, VS Code fails with the following two errors. Returning `nil` works around those
|
|
// VS Code-internal errors showing up
|
|
// - MISSING provider
|
|
// - Cannot read properties of null (reading 'kind')
|
|
return nil
|
|
}
|
|
// Ideally, we should show multiple symbols. But VS Code fails to display type hierarchies with multiple root items,
|
|
// failing with `Cannot read properties of undefined (reading 'map')`. Pick the first one.
|
|
return Array(typeHierarchyItems.prefix(1))
|
|
}
|
|
|
|
/// 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 nonisolated 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"],
|
|
let uri = orLog("DocumentURI for type hierarchy item", { try DocumentURI(string: uriString) })
|
|
else {
|
|
return nil
|
|
}
|
|
return (uri: uri, usr: usr)
|
|
}
|
|
|
|
func supertypes(_ req: TypeHierarchySupertypesRequest) async throws -> [TypeHierarchyItem]? {
|
|
guard let data = extractTypeHierarchyItemData(req.item.data),
|
|
let workspace = await self.workspaceForDocument(uri: data.uri),
|
|
let index = await workspace.index(checkedFor: .deletedFiles)
|
|
else {
|
|
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
|
|
if occurrence.relations.count > 1 {
|
|
// When the occurrence has an `extendedBy` relation, it's an extension declaration. An extension can only extend
|
|
// a single type, so there can only be a single relation here.
|
|
logger.fault("Expected at most extendedBy relation but got \(occurrence.relations.count)")
|
|
}
|
|
guard let related = occurrence.relations.sorted().first else {
|
|
return []
|
|
}
|
|
return index.occurrences(relatedToUSR: related.symbol.usr, roles: .baseOf)
|
|
}
|
|
|
|
// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
|
|
func indexToLSPTypeHierarchyItem2(
|
|
definition: SymbolOccurrence,
|
|
moduleName: String?,
|
|
index: CheckedIndex
|
|
) -> TypeHierarchyItem? {
|
|
return self.indexToLSPTypeHierarchyItem(
|
|
definition: definition,
|
|
moduleName: moduleName,
|
|
index: index
|
|
)
|
|
}
|
|
|
|
// Convert occurrences to type hierarchy items
|
|
let occurs = baseOccurs + retroactiveConformanceOccurs
|
|
var types: [TypeHierarchyItem] = []
|
|
for occurrence in occurs {
|
|
// Resolve the supertype's definition to find its location
|
|
guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: occurrence.symbol.usr) else {
|
|
continue
|
|
}
|
|
|
|
let moduleName = definition.location.moduleName
|
|
guard let item = indexToLSPTypeHierarchyItem2(definition: definition, moduleName: moduleName, index: index) else {
|
|
continue
|
|
}
|
|
types.append(await workspace.buildServerManager.typeHierarchyItemAdjustedForCopiedFiles(item))
|
|
}
|
|
return types.sorted(by: { $0.name < $1.name })
|
|
}
|
|
|
|
func subtypes(_ req: TypeHierarchySubtypesRequest) async throws -> [TypeHierarchyItem]? {
|
|
guard let data = extractTypeHierarchyItemData(req.item.data),
|
|
let workspace = await self.workspaceForDocument(uri: data.uri),
|
|
let index = await workspace.index(checkedFor: .deletedFiles)
|
|
else {
|
|
return []
|
|
}
|
|
|
|
// Resolve child types and extensions
|
|
let occurs = index.occurrences(ofUSR: data.usr, roles: [.baseOf, .extendedBy])
|
|
|
|
// TODO: Remove this workaround once https://github.com/swiftlang/swift/issues/75600 is fixed
|
|
func indexToLSPTypeHierarchyItem2(
|
|
definition: SymbolOccurrence,
|
|
moduleName: String?,
|
|
index: CheckedIndex
|
|
) -> TypeHierarchyItem? {
|
|
return self.indexToLSPTypeHierarchyItem(
|
|
definition: definition,
|
|
moduleName: moduleName,
|
|
index: index
|
|
)
|
|
}
|
|
|
|
// Convert occurrences to type hierarchy items
|
|
var types: [TypeHierarchyItem] = []
|
|
for occurrence in occurs {
|
|
if occurrence.relations.count > 1 {
|
|
// An occurrence with a `baseOf` or `extendedBy` relation is an occurrence inside an inheritance clause.
|
|
// Such an occurrence can only be the source of a single type, namely the one that the inheritance clause belongs
|
|
// to.
|
|
logger.fault("Expected at most extendedBy or baseOf relation but got \(occurrence.relations.count)")
|
|
}
|
|
guard let related = occurrence.relations.sorted().first else {
|
|
continue
|
|
}
|
|
|
|
// Resolve the subtype's definition to find its location
|
|
guard let definition = index.primaryDefinitionOrDeclarationOccurrence(ofUSR: related.symbol.usr) else {
|
|
continue
|
|
}
|
|
|
|
let moduleName = definition.location.moduleName
|
|
guard let item = indexToLSPTypeHierarchyItem2(definition: definition, moduleName: moduleName, index: index) else {
|
|
continue
|
|
}
|
|
types.append(await workspace.buildServerManager.typeHierarchyItemAdjustedForCopiedFiles(item))
|
|
}
|
|
return types.sorted { $0.name < $1.name }
|
|
}
|
|
|
|
func synchronize(_ req: SynchronizeRequest) async throws -> VoidResponse {
|
|
if req.buildServerUpdates != nil, !self.options.hasExperimentalFeature(.synchronizeForBuildSystemUpdates) {
|
|
throw ResponseError.unknown("\(SynchronizeRequest.method).buildServerUpdates is an experimental request option")
|
|
}
|
|
if req.copyFileMap != nil, !self.options.hasExperimentalFeature(.synchronizeCopyFileMap) {
|
|
throw ResponseError.unknown("\(SynchronizeRequest.method).copyFileMap is an experimental request option")
|
|
}
|
|
for workspace in workspaces {
|
|
await workspace.synchronize(req)
|
|
}
|
|
return VoidResponse()
|
|
}
|
|
|
|
func triggerReindex(_ req: TriggerReindexRequest) async throws -> VoidResponse {
|
|
for workspace in workspaces {
|
|
await workspace.semanticIndexManager?.scheduleReindex()
|
|
}
|
|
return VoidResponse()
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
package typealias Diagnostic = LanguageServerProtocol.Diagnostic
|
|
|
|
fileprivate extension CheckedIndex {
|
|
/// Take the name of containers into account to form a fully-qualified name for the given symbol.
|
|
/// This means that we will form names of nested types and type-qualify methods.
|
|
func fullyQualifiedName(of symbolOccurrence: SymbolOccurrence) -> String {
|
|
let symbol = symbolOccurrence.symbol
|
|
let containerNames = self.containerNames(of: symbolOccurrence)
|
|
guard let containerName = containerNames.last else {
|
|
// No containers, so nothing to do.
|
|
return symbol.name
|
|
}
|
|
switch symbol.language {
|
|
case .objc where symbol.kind == .instanceMethod || symbol.kind == .instanceProperty:
|
|
return "-[\(containerName) \(symbol.name)]"
|
|
case .objc where symbol.kind == .classMethod || symbol.kind == .classProperty:
|
|
return "+[\(containerName) \(symbol.name)]"
|
|
case .cxx, .c, .objc:
|
|
// C shouldn't have container names for call hierarchy and Objective-C should be covered above.
|
|
// Fall back to using the C++ notation using `::`.
|
|
return (containerNames + [symbol.name]).joined(separator: "::")
|
|
case .swift:
|
|
return (containerNames + [symbol.name]).joined(separator: ".")
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
case .module, .namespace:
|
|
return .namespace
|
|
case .field:
|
|
return .property
|
|
case .constructor:
|
|
return .constructor
|
|
case .destructor:
|
|
return .null
|
|
case .commentTag, .concept, .extension, .macro, .namespaceAlias, .typealias, .union, .unknown, .using:
|
|
return .null
|
|
}
|
|
}
|
|
|
|
var isCallable: Bool {
|
|
switch self {
|
|
case .function, .instanceMethod, .classMethod, .staticMethod, .constructor, .destructor, .conversionFunction:
|
|
return true
|
|
case .unknown, .module, .namespace, .namespaceAlias, .macro, .enum, .struct, .protocol, .extension, .union,
|
|
.typealias, .field, .enumConstant, .parameter, .using, .concept, .commentTag, .variable, .instanceProperty,
|
|
.class, .staticProperty, .classProperty:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate extension SymbolOccurrence {
|
|
/// Whether this is a call-like occurrence that should be shown in the call hierarchy.
|
|
var shouldShowInCallHierarchy: Bool {
|
|
!roles.intersection([.addressOf, .call, .read, .reference, .write]).isEmpty
|
|
}
|
|
}
|
|
|
|
/// Simple struct for pending notifications/requests, including a cancellation handler.
|
|
/// For convenience the notifications/request handlers are type erased via wrapping.
|
|
private struct NotificationRequestOperation {
|
|
let operation: () async -> Void
|
|
let cancellationHandler: (() -> Void)?
|
|
}
|
|
|
|
/// Used to queue up notifications and requests for documents which are blocked
|
|
/// on build server operations such as fetching build settings.
|
|
///
|
|
/// Note: This is not thread safe. Must be called from the `SourceKitLSPServer.queue`.
|
|
private 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 = []
|
|
}
|
|
}
|
|
|
|
/// Returns the USRs of the subtypes of `usrs` as well as their subtypes and extensions, transitively.
|
|
private func transitiveSubtypeClosure(ofUsrs usrs: [String], index: CheckedIndex) -> [String] {
|
|
var result: [String] = []
|
|
for usr in usrs {
|
|
result.append(usr)
|
|
let directSubtypes = index.occurrences(ofUSR: usr, roles: [.baseOf, .extendedBy]).flatMap { occurrence in
|
|
occurrence.relations.filter { $0.roles.contains(.baseOf) || $0.roles.contains(.extendedBy) }.map(\.symbol.usr)
|
|
}
|
|
let transitiveSubtypes = transitiveSubtypeClosure(ofUsrs: directSubtypes, index: index)
|
|
result += transitiveSubtypes
|
|
}
|
|
return result
|
|
}
|
|
|
|
fileprivate extension Sequence where Element: Hashable {
|
|
/// Removes all duplicate elements from the sequence, maintaining order.
|
|
var unique: [Element] {
|
|
var set = Set<Element>()
|
|
return self.filter { set.insert($0).inserted }
|
|
}
|
|
}
|
|
|
|
fileprivate extension URL {
|
|
func isPrefix(of other: URL) -> Bool {
|
|
guard self.pathComponents.count < other.pathComponents.count else {
|
|
return false
|
|
}
|
|
return other.pathComponents[0..<self.pathComponents.count] == self.pathComponents[...]
|
|
}
|
|
}
|