//===----------------------------------------------------------------------===// // // 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() 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 -> ()) -> 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 ) { 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 } } }