mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
317 lines
12 KiB
Swift
317 lines
12 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2024 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
public import Csourcekitd
|
|
import Foundation
|
|
import SKLogging
|
|
import SourceKitD
|
|
import SwiftExtensions
|
|
import SwiftSourceKitPluginCommon
|
|
|
|
private func useNewAPI(for dict: SKDRequestDictionaryReader) -> Bool {
|
|
guard let opts: SKDRequestDictionaryReader = dict[dict.sourcekitd.keys.codeCompleteOptions],
|
|
opts[dict.sourcekitd.keys.useNewAPI] == 1
|
|
else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
final class RequestHandler: Sendable {
|
|
enum HandleRequestResult {
|
|
/// `handleRequest` will call `receiver`.
|
|
case requestHandled
|
|
|
|
/// `handleRequest` will not call `receiver` and a request response should be produced by sourcekitd (not the plugin).
|
|
case handleInSourceKitD
|
|
}
|
|
|
|
let requestHandlingQueue = AsyncQueue<Serial>()
|
|
let sourcekitd: SourceKitD
|
|
let completionProvider: CompletionProvider
|
|
|
|
init(params: sourcekitd_api_plugin_initialize_params_t, completionResultsBufferKind: UInt64, sourcekitd: SourceKitD) {
|
|
let ideInspectionInstance = sourcekitd.servicePluginApi.plugin_initialize_get_swift_ide_inspection_instance(params)
|
|
|
|
self.sourcekitd = sourcekitd
|
|
self.completionProvider = CompletionProvider(
|
|
completionResultsBufferKind: completionResultsBufferKind,
|
|
opaqueIDEInspectionInstance: OpaqueIDEInspectionInstance(ideInspectionInstance),
|
|
sourcekitd: sourcekitd
|
|
)
|
|
}
|
|
|
|
func handleRequest(
|
|
_ dict: SKDRequestDictionaryReader,
|
|
handle: RequestHandle?,
|
|
receiver: @Sendable @escaping (SKDResponse) -> Void
|
|
) -> HandleRequestResult {
|
|
func produceResult(
|
|
body: @escaping @Sendable () async throws -> SKDResponseDictionaryBuilder
|
|
) -> HandleRequestResult {
|
|
withLoggingScope("request-\(handle?.numericValue ?? 0 % 100)") {
|
|
let start = Date()
|
|
logger.debug(
|
|
"""
|
|
Plugin received sourcekitd request (handle: \(handle?.numericValue ?? -1))
|
|
\(dict.description)
|
|
"""
|
|
)
|
|
requestHandlingQueue.async {
|
|
let response: SKDResponse
|
|
do {
|
|
response = try await body().response
|
|
} catch {
|
|
response = SKDResponse.from(error: error, sourcekitd: self.sourcekitd)
|
|
}
|
|
logger.debug(
|
|
"""
|
|
Finished (took \(Date().timeIntervalSince(start))s)
|
|
\(response.description)
|
|
"""
|
|
)
|
|
receiver(response)
|
|
}
|
|
return .requestHandled
|
|
}
|
|
}
|
|
|
|
func sourcekitdProducesResult(body: @escaping @Sendable () async -> Void) -> HandleRequestResult {
|
|
requestHandlingQueue.async {
|
|
await body()
|
|
}
|
|
return .handleInSourceKitD
|
|
}
|
|
|
|
switch dict[sourcekitd.keys.request] as sourcekitd_api_uid_t? {
|
|
case sourcekitd.requests.editorOpen:
|
|
return sourcekitdProducesResult {
|
|
await self.completionProvider.handleDocumentOpen(dict)
|
|
}
|
|
case sourcekitd.requests.editorReplaceText:
|
|
return sourcekitdProducesResult {
|
|
await self.completionProvider.handleDocumentEdit(dict)
|
|
}
|
|
case sourcekitd.requests.editorClose:
|
|
return sourcekitdProducesResult {
|
|
await self.completionProvider.handleDocumentClose(dict)
|
|
}
|
|
|
|
case sourcekitd.requests.codeCompleteOpen:
|
|
guard useNewAPI(for: dict) else {
|
|
return .handleInSourceKitD
|
|
}
|
|
return produceResult {
|
|
try await self.completionProvider.handleCompleteOpen(dict, handle: handle)
|
|
}
|
|
case sourcekitd.requests.codeCompleteUpdate:
|
|
guard useNewAPI(for: dict) else {
|
|
return .handleInSourceKitD
|
|
}
|
|
return produceResult {
|
|
try await self.completionProvider.handleCompleteUpdate(dict)
|
|
}
|
|
case sourcekitd.requests.codeCompleteClose:
|
|
guard useNewAPI(for: dict) else {
|
|
return .handleInSourceKitD
|
|
}
|
|
return produceResult {
|
|
try await self.completionProvider.handleCompleteClose(dict)
|
|
}
|
|
case sourcekitd.requests.codeCompleteDocumentation:
|
|
return produceResult {
|
|
try await self.completionProvider.handleCompletionDocumentation(dict)
|
|
}
|
|
case sourcekitd.requests.codeCompleteDiagnostic:
|
|
return produceResult {
|
|
try await self.completionProvider.handleCompletionDiagnostic(dict)
|
|
}
|
|
case sourcekitd.requests.codeCompleteSetPopularAPI:
|
|
guard useNewAPI(for: dict) else {
|
|
return .handleInSourceKitD
|
|
}
|
|
return produceResult {
|
|
await self.completionProvider.handleSetPopularAPI(dict)
|
|
}
|
|
case sourcekitd.requests.dependencyUpdated:
|
|
return sourcekitdProducesResult {
|
|
await self.completionProvider.handleDependencyUpdated()
|
|
}
|
|
default:
|
|
return .handleInSourceKitD
|
|
}
|
|
}
|
|
|
|
func cancel(_ handle: RequestHandle) {
|
|
logger.debug("Cancelling request with handle \(handle.numericValue)")
|
|
self.completionProvider.cancel(handle: handle)
|
|
}
|
|
}
|
|
|
|
#if compiler(>=6.3)
|
|
#warning("Remove sourcekitd_plugin_initialize when we no longer support toolchains that call it")
|
|
#endif
|
|
|
|
/// Legacy plugin initialization logic in which sourcekitd does not inform the plugin about the sourcekitd path it was
|
|
/// loaded from.
|
|
@_cdecl("sourcekitd_plugin_initialize")
|
|
public func sourcekitd_plugin_initialize(_ params: sourcekitd_api_plugin_initialize_params_t) {
|
|
#if canImport(Darwin)
|
|
var dlInfo = Dl_info()
|
|
dladdr(#dsohandle, &dlInfo)
|
|
let path = String(cString: dlInfo.dli_fname)
|
|
var url = URL(fileURLWithPath: path, isDirectory: false)
|
|
while url.pathExtension != "framework" && url.lastPathComponent != "/" {
|
|
url.deleteLastPathComponent()
|
|
}
|
|
url =
|
|
url
|
|
.deletingLastPathComponent()
|
|
.appendingPathComponent("sourcekitd.framework")
|
|
.appendingPathComponent("sourcekitd")
|
|
if FileManager.default.fileExists(at: url) {
|
|
try! url.filePath.withCString { sourcekitdPath in
|
|
sourcekitd_plugin_initialize_2(params, sourcekitdPath)
|
|
}
|
|
} else {
|
|
// When using a SourceKit plugin from the build directory, we can't find sourcekitd relative to the plugin.
|
|
// Since sourcekitd_plugin_initialize is only called on Darwin from Xcode toolchains, we know that we are getting
|
|
// called from an XPC sourcekitd. Thus, all sourcekitd symbols that we need should be loaded in the current process
|
|
// already and we can use `RTLD_DEFAULT` for the sourcekitd library.
|
|
sourcekitd_plugin_initialize_2(params, "SOURCEKIT_LSP_PLUGIN_PARENT_LIBRARY_RTLD_DEFAULT")
|
|
}
|
|
#else
|
|
fatalError("sourcekitd_plugin_initialize is not supported on non-Darwin platforms")
|
|
#endif
|
|
}
|
|
|
|
#if canImport(Darwin)
|
|
private extension SourceKitD {
|
|
/// When a plugin is initialized, it gets passed the library it was loaded from to `sourcekitd_plugin_initialize_2`.
|
|
///
|
|
/// Since the plugin wants to interact with sourcekitd in-process, it needs to load `sourcekitdInProc`. This function
|
|
/// loads `sourcekitdInProc` relative to the parent library path, if it exists, or `sourcekitd` if `sourcekitdInProc`
|
|
/// doesn't exist (eg. on Linux where `sourcekitd` is already in-process).
|
|
static func inProcLibrary(relativeTo parentLibraryPath: URL) throws -> SourceKitD {
|
|
var frameworkUrl = parentLibraryPath
|
|
|
|
// Remove path components until we reach the `sourcekitd.framework` directory. The plugin might have been loaded
|
|
// from an XPC service, in which case `parentLibraryPath` is
|
|
// `sourcekitd.framework/XPCServices/SourceKitService.xpc/Contents/MacOS/SourceKitService`.
|
|
while frameworkUrl.pathExtension != "framework" {
|
|
guard frameworkUrl.pathComponents.count > 1 else {
|
|
struct NoFrameworkPathError: Error, CustomStringConvertible {
|
|
var parentLibraryPath: URL
|
|
var description: String { "Could not find .framework directory relative to '\(parentLibraryPath)'" }
|
|
}
|
|
throw NoFrameworkPathError(parentLibraryPath: parentLibraryPath)
|
|
}
|
|
frameworkUrl.deleteLastPathComponent()
|
|
}
|
|
frameworkUrl.deleteLastPathComponent()
|
|
|
|
let inProcUrl =
|
|
frameworkUrl
|
|
.appendingPathComponent("sourcekitdInProc.framework")
|
|
.appendingPathComponent("sourcekitdInProc")
|
|
if FileManager.default.fileExists(at: inProcUrl) {
|
|
return try SourceKitD(dylib: inProcUrl, pluginPaths: nil, initialize: false)
|
|
}
|
|
|
|
let sourcekitdUrl =
|
|
frameworkUrl
|
|
.appendingPathComponent("sourcekitd.framework")
|
|
.appendingPathComponent("sourcekitd")
|
|
return try SourceKitD(dylib: sourcekitdUrl, pluginPaths: nil, initialize: false)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
@_cdecl("sourcekitd_plugin_initialize_2")
|
|
public func sourcekitd_plugin_initialize_2(
|
|
_ params: sourcekitd_api_plugin_initialize_params_t,
|
|
_ parentLibraryPath: UnsafePointer<CChar>
|
|
) {
|
|
let parentLibraryPath = String(cString: parentLibraryPath)
|
|
#if canImport(Darwin)
|
|
if parentLibraryPath == "SOURCEKIT_LSP_PLUGIN_PARENT_LIBRARY_RTLD_DEFAULT" {
|
|
SourceKitD.forPlugin = try! SourceKitD(
|
|
dlhandle: .rtldDefault,
|
|
path: URL(string: "rtld-default://")!,
|
|
pluginPaths: nil,
|
|
initialize: false
|
|
)
|
|
} else {
|
|
SourceKitD.forPlugin = try! SourceKitD.inProcLibrary(relativeTo: URL(fileURLWithPath: parentLibraryPath))
|
|
}
|
|
#else
|
|
// On other platforms, sourcekitd is always in process, so we can load it straight away.
|
|
SourceKitD.forPlugin = try! SourceKitD(
|
|
dylib: URL(fileURLWithPath: parentLibraryPath),
|
|
pluginPaths: nil,
|
|
initialize: false
|
|
)
|
|
#endif
|
|
let sourcekitd = SourceKitD.forPlugin
|
|
|
|
let completionResultsBufferKind = sourcekitd.pluginApi.plugin_initialize_custom_buffer_start(params)
|
|
let isClientOnly = sourcekitd.pluginApi.plugin_initialize_is_client_only(params)
|
|
|
|
let uidFromCString = sourcekitd.pluginApi.plugin_initialize_uid_get_from_cstr(params)
|
|
let uidGetCString = sourcekitd.pluginApi.plugin_initialize_uid_get_string_ptr(params)
|
|
|
|
// Depending on linking and loading configuration, we may need to chain the global UID handlers back to the UID
|
|
// handlers in the caller. The extra hop should not matter, since we cache the results.
|
|
if unsafeBitCast(uidFromCString, to: UnsafeRawPointer.self)
|
|
!= unsafeBitCast(sourcekitd.api.uid_get_from_cstr, to: UnsafeRawPointer.self)
|
|
{
|
|
sourcekitd.api.set_uid_handlers(uidFromCString, uidGetCString)
|
|
}
|
|
|
|
sourcekitd.pluginApi.plugin_initialize_register_custom_buffer(
|
|
params,
|
|
completionResultsBufferKind,
|
|
CompletionResultsArray.arrayFuncs.rawValue
|
|
)
|
|
|
|
if isClientOnly {
|
|
return
|
|
}
|
|
|
|
let requestHandler = RequestHandler(
|
|
params: params,
|
|
completionResultsBufferKind: completionResultsBufferKind,
|
|
sourcekitd: sourcekitd
|
|
)
|
|
|
|
sourcekitd.servicePluginApi.plugin_initialize_register_cancellation_handler(params) { handle in
|
|
if let handle = RequestHandle(handle) {
|
|
requestHandler.cancel(handle)
|
|
}
|
|
}
|
|
|
|
sourcekitd.servicePluginApi.plugin_initialize_register_cancellable_request_handler(params) {
|
|
(request, handle, receiver) -> Bool in
|
|
guard let receiver, let request, let dict = SKDRequestDictionaryReader(request, sourcekitd: sourcekitd) else {
|
|
return false
|
|
}
|
|
let handle = RequestHandle(handle)
|
|
|
|
let handledRequest = requestHandler.handleRequest(dict, handle: handle) { receiver($0.underlyingValueRetained()) }
|
|
|
|
switch handledRequest {
|
|
case .requestHandled: return true
|
|
case .handleInSourceKitD: return false
|
|
}
|
|
}
|
|
}
|