diff --git a/Sources/SourceKitD/sourcekitd_uids.swift b/Sources/SourceKitD/sourcekitd_uids.swift index 09a73cdb9..f92b5a28c 100644 --- a/Sources/SourceKitD/sourcekitd_uids.swift +++ b/Sources/SourceKitD/sourcekitd_uids.swift @@ -878,6 +878,8 @@ package struct sourcekitd_api_requests { package let findLocalRenameRanges: sourcekitd_api_uid_t /// `source.request.semantic.refactoring` package let semanticRefactoring: sourcekitd_api_uid_t + /// `source.request.objc.selector` + package let objcSelector: sourcekitd_api_uid_t /// `source.request.enable-compile-notifications` package let enableCompileNotifications: sourcekitd_api_uid_t /// `source.request.test_notification` @@ -951,6 +953,7 @@ package struct sourcekitd_api_requests { findRenameRanges = api.uid_get_from_cstr("source.request.find-syntactic-rename-ranges")! findLocalRenameRanges = api.uid_get_from_cstr("source.request.find-local-rename-ranges")! semanticRefactoring = api.uid_get_from_cstr("source.request.semantic.refactoring")! + objcSelector = api.uid_get_from_cstr("source.request.objc.selector")! enableCompileNotifications = api.uid_get_from_cstr("source.request.enable-compile-notifications")! testNotification = api.uid_get_from_cstr("source.request.test_notification")! collectExpressionType = api.uid_get_from_cstr("source.request.expression.type")! diff --git a/Sources/SwiftLanguageService/ShowObjCSelector.swift b/Sources/SwiftLanguageService/ShowObjCSelector.swift new file mode 100644 index 000000000..c7c8e78cc --- /dev/null +++ b/Sources/SwiftLanguageService/ShowObjCSelector.swift @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 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 +// +//===----------------------------------------------------------------------===// + +import Foundation +@_spi(SourceKitLSP) import LanguageServerProtocol +@_spi(SourceKitLSP) import SKLogging +import SourceKitD +import SourceKitLSP + +extension SwiftLanguageService { + /// Gets the Objective-C selector for the method at the given position. + /// + /// Uses the `source.request.objc.selector` sourcekitd request directly + /// to retrieve the selector string for @objc methods. + func showObjCSelector( + _ command: ShowObjCSelectorCommand + ) async throws -> LSPAny { + let keys = self.keys + + let uri = command.textDocument.uri + let snapshot = try self.documentManager.latestSnapshot(uri) + let position = command.positionRange.lowerBound + let offset = snapshot.utf8Offset(of: position) + + let skreq = sourcekitd.dictionary([ + keys.sourceFile: uri.pseudoPath, + keys.offset: offset, + keys.compilerArgs: await self.compileCommand(for: uri, fallbackAfterTimeout: true)?.compilerArgs + as [any SKDRequestValue]?, + ]) + + let dict = try await send(sourcekitdRequest: \.objcSelector, skreq, snapshot: snapshot) + + guard let selector: String = dict[keys.text] else { + throw ResponseError.unknown("Could not retrieve Objective-C selector at cursor position") + } + + if let sourceKitLSPServer { + sourceKitLSPServer.sendNotificationToClient( + ShowMessageNotification(type: .info, message: selector) + ) + } + + return .string(selector) + } +} diff --git a/Sources/SwiftLanguageService/ShowObjCSelectorCommand.swift b/Sources/SwiftLanguageService/ShowObjCSelectorCommand.swift new file mode 100644 index 000000000..07bb8e225 --- /dev/null +++ b/Sources/SwiftLanguageService/ShowObjCSelectorCommand.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 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 +// +//===----------------------------------------------------------------------===// + +@_spi(SourceKitLSP) package import LanguageServerProtocol + +/// Command to show the Objective-C selector for a Swift method marked with @objc. +package struct ShowObjCSelectorCommand: SwiftCommand { + package static let identifier: String = "show.objc.selector.command" + + package var title = "Show Objective-C Selector" + + package var positionRange: Range + package var textDocument: TextDocumentIdentifier + + package init(positionRange: Range, textDocument: TextDocumentIdentifier) { + self.positionRange = positionRange + self.textDocument = textDocument + } + + package init?(fromLSPDictionary dictionary: [String: LSPAny]) { + guard case .dictionary(let documentDict)? = dictionary[CodingKeys.textDocument.stringValue], + case .string(let title)? = dictionary[CodingKeys.title.stringValue], + case .dictionary(let rangeDict)? = dictionary[CodingKeys.positionRange.stringValue] + else { + return nil + } + + guard let positionRange = Range(fromLSPDictionary: rangeDict), + let textDocument = TextDocumentIdentifier(fromLSPDictionary: documentDict) + else { + return nil + } + + self.init( + title: title, + positionRange: positionRange, + textDocument: textDocument + ) + } + + package init( + title: String, + positionRange: Range, + textDocument: TextDocumentIdentifier + ) { + self.title = title + self.positionRange = positionRange + self.textDocument = textDocument + } + + package func encodeToLSPAny() -> LSPAny { + return .dictionary([ + CodingKeys.title.stringValue: .string(title), + CodingKeys.positionRange.stringValue: positionRange.encodeToLSPAny(), + CodingKeys.textDocument.stringValue: textDocument.encodeToLSPAny(), + ]) + } +} diff --git a/Sources/SwiftLanguageService/SwiftCommand.swift b/Sources/SwiftLanguageService/SwiftCommand.swift index c6a51197a..7ac0b04b8 100644 --- a/Sources/SwiftLanguageService/SwiftCommand.swift +++ b/Sources/SwiftLanguageService/SwiftCommand.swift @@ -51,6 +51,7 @@ extension SwiftLanguageService { [ SemanticRefactorCommand.self, ExpandMacroCommand.self, + ShowObjCSelectorCommand.self, ].map { (command: any SwiftCommand.Type) in command.identifier } diff --git a/Sources/SwiftLanguageService/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift index 3bebc98a0..b72ea5c4f 100644 --- a/Sources/SwiftLanguageService/SwiftLanguageService.swift +++ b/Sources/SwiftLanguageService/SwiftLanguageService.swift @@ -963,13 +963,13 @@ extension SwiftLanguageService { var canInlineMacro = false - var refactorActions = cursorInfoResponse.refactorActions.compactMap { - let lspCommand = $0.asCommand() + var refactorActions: [CodeAction] = cursorInfoResponse.refactorActions.compactMap { action in + let lspCommand = action.asCommand() if !canInlineMacro { - canInlineMacro = $0.actionString == "source.refactoring.kind.inline.macro" + canInlineMacro = action.actionString == "source.refactoring.kind.inline.macro" } - return CodeAction(title: $0.title, kind: .refactor, command: lspCommand) + return CodeAction(title: action.title, kind: .refactor, command: lspCommand) } if canInlineMacro { @@ -979,6 +979,22 @@ extension SwiftLanguageService { refactorActions.append(CodeAction(title: expandMacroCommand.title, kind: .refactor, command: expandMacroCommand)) } + let methodKinds: [SymbolKind] = [.method, .function, .constructor] + let isOnMethod = cursorInfoResponse.cursorInfo.contains { cursorInfo in + if let kind = cursorInfo.symbolInfo.kind { + return methodKinds.contains(kind) + } + return false + } + + if isOnMethod { + let showCommand = ShowObjCSelectorCommand( + positionRange: params.range, + textDocument: params.textDocument + ).asCommand() + refactorActions.append(CodeAction(title: "Show Objective-C Selector", kind: .refactor, command: showCommand)) + } + return refactorActions } @@ -1096,6 +1112,8 @@ extension SwiftLanguageService { try await semanticRefactoring(command) } else if let command = req.swiftCommand(ofType: ExpandMacroCommand.self) { try await expandMacro(command) + } else if let command = req.swiftCommand(ofType: ShowObjCSelectorCommand.self) { + return try await showObjCSelector(command) } else if let command = req.swiftCommand(ofType: RemoveUnusedImportsCommand.self) { try await removeUnusedImports(command) } else {