//===----------------------------------------------------------------------===// // // 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 @_spi(SourceKitLSP) import SKLogging import SourceKitD import SwiftExtensions import SwiftSourceKitPluginCommon import ToolsProtocolsSwiftExtensions 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 -> 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) } } /// 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) { LoggingScope.configureDefaultLoggingSubsystem("org.swift.sourcekit-lsp.service-plugin") logger.fault("sourcekitd_plugin_initialize has been removed in favor of sourcekitd_plugin_initialize_2") } #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 .appending(components: "sourcekitdInProc.framework", "sourcekitdInProc") if FileManager.default.fileExists(at: inProcUrl) { return try SourceKitD(core: SourceKitDCoreForPlugin(dylibPath: inProcUrl)) } let sourcekitdUrl = frameworkUrl .appending(components: "sourcekitd.framework", "sourcekitd") return try SourceKitD(core: SourceKitDCoreForPlugin(dylibPath: sourcekitdUrl)) } } #endif @_cdecl("sourcekitd_plugin_initialize_2") public func sourcekitd_plugin_initialize_2( _ params: sourcekitd_api_plugin_initialize_params_t, _ parentLibraryPath: UnsafePointer ) { LoggingScope.configureDefaultLoggingSubsystem("org.swift.sourcekit-lsp.service-plugin") let parentLibraryPath = String(cString: parentLibraryPath) #if canImport(Darwin) if parentLibraryPath == "SOURCEKIT_LSP_PLUGIN_PARENT_LIBRARY_RTLD_DEFAULT" { SourceKitD.forPlugin = try! SourceKitD( core: SourceKitDCoreForPlugin(dlhandle: .rtldDefault, path: URL(string: "rtld-default://")!) ) } 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( core: SourceKitDCoreForPlugin(dylibPath: URL(fileURLWithPath: parentLibraryPath)) ) #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 } } } private final class SourceKitDCoreForPlugin: SourceKitDCore, Sendable { let dlHandle: DLHandle let path: URL private let ownsHandle: Bool init(dylibPath: URL) throws { #if os(Windows) let dlopenModes: DLOpenFlags = [] #else let dlopenModes: DLOpenFlags = [.lazy, .local, .noLoad] #endif self.dlHandle = try dlopen(dylibPath.filePath, mode: dlopenModes) self.path = dylibPath self.ownsHandle = true } init(dlhandle: DLHandle, path: URL) { self.dlHandle = dlhandle self.path = path self.ownsHandle = false } deinit { if ownsHandle { try? dlHandle.close() } else { dlHandle.leak() } } func initializeService( api: sourcekitd_api_functions_t, notificationCallback: @escaping @Sendable (sourcekitd_api_response_t) -> Void ) { // Borrowed handle — sourcekitd is already initialized. } }