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/CopyObjCSelector.swift b/Sources/SwiftLanguageService/CopyObjCSelector.swift new file mode 100644 index 000000000..420b692ee --- /dev/null +++ b/Sources/SwiftLanguageService/CopyObjCSelector.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + /// Executes the refactoring-based copy and extracts the selector string without applying edits. + func copyObjCSelector( + _ command: CopyObjCSelectorCommand + ) async throws -> LSPAny { + let refactorCommand = SemanticRefactorCommand( + title: command.title, + actionString: command.actionString, + positionRange: command.positionRange, + textDocument: command.textDocument + ) + + let semanticRefactor = try await self.refactoring(refactorCommand) + + guard let edit = semanticRefactor.edit.changes?.first?.value.first else { + throw ResponseError.unknown("No selector found at cursor position") + } + + let prefix = "// Objective-C Selector: " + if let range = edit.newText.range(of: prefix) { + let selector = String(edit.newText[range.upperBound...]).trimmingCharacters(in: .whitespacesAndNewlines) + if let sourceKitLSPServer { + // Notify with just the selector text (no prefix, no buttons). + sourceKitLSPServer.sendNotificationToClient( + ShowMessageNotification(type: .info, message: selector) + ) + } + return .string(selector) + } + + throw ResponseError.unknown("Could not extract selector from refactoring result") + } +} diff --git a/Sources/SwiftLanguageService/CopyObjCSelectorCommand.swift b/Sources/SwiftLanguageService/CopyObjCSelectorCommand.swift new file mode 100644 index 000000000..d1a4bb4ce --- /dev/null +++ b/Sources/SwiftLanguageService/CopyObjCSelectorCommand.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import SourceKitD + +package struct CopyObjCSelectorCommand: SwiftCommand { + package static let identifier: String = "copy.objc.selector.command" + + package var title = "Copy Objective-C Selector" + package var actionString = "source.refactoring.kind.copy.objc.selector" + + package var positionRange: Range + package var textDocument: LanguageServerProtocol.TextDocumentIdentifier + + package init(positionRange: Range, textDocument: LanguageServerProtocol.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 .string(let actionString)? = dictionary[CodingKeys.actionString.stringValue], + case .dictionary(let rangeDict)? = dictionary[CodingKeys.positionRange.stringValue] + else { + return nil + } + + guard let positionRange = Range(fromLSPDictionary: rangeDict), + let textDocument = LanguageServerProtocol.TextDocumentIdentifier(fromLSPDictionary: documentDict) + else { + return nil + } + + self.init( + title: title, + actionString: actionString, + positionRange: positionRange, + textDocument: textDocument + ) + } + + package init( + title: String, + actionString: String, + positionRange: Range, + textDocument: LanguageServerProtocol.TextDocumentIdentifier + ) { + self.title = title + self.actionString = actionString + self.positionRange = positionRange + self.textDocument = textDocument + } + + package func encodeToLSPAny() -> LSPAny { + return .dictionary([ + CodingKeys.title.stringValue: .string(title), + CodingKeys.actionString.stringValue: .string(actionString), + CodingKeys.positionRange.stringValue: positionRange.encodeToLSPAny(), + CodingKeys.textDocument.stringValue: textDocument.encodeToLSPAny(), + ]) + } +} diff --git a/Sources/SwiftLanguageService/SwiftCommand.swift b/Sources/SwiftLanguageService/SwiftCommand.swift index c6a51197a..9a92f0a49 100644 --- a/Sources/SwiftLanguageService/SwiftCommand.swift +++ b/Sources/SwiftLanguageService/SwiftCommand.swift @@ -51,6 +51,7 @@ extension SwiftLanguageService { [ SemanticRefactorCommand.self, ExpandMacroCommand.self, + CopyObjCSelectorCommand.self, ].map { (command: any SwiftCommand.Type) in command.identifier } diff --git a/Sources/SwiftLanguageService/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift index 3bebc98a0..551f1beec 100644 --- a/Sources/SwiftLanguageService/SwiftLanguageService.swift +++ b/Sources/SwiftLanguageService/SwiftLanguageService.swift @@ -954,6 +954,8 @@ extension SwiftLanguageService { skreq.set(self.keys.retrieveRefactorActions, to: 1) } + let snapshot = try documentManager.latestSnapshot(params.textDocument.uri) + let cursorInfoResponse = try await cursorInfo( params.textDocument.uri, params.range, @@ -963,13 +965,23 @@ extension SwiftLanguageService { var canInlineMacro = false - var refactorActions = cursorInfoResponse.refactorActions.compactMap { - let lspCommand = $0.asCommand() + var refactorActions: [CodeAction] = cursorInfoResponse.refactorActions.compactMap { action in + if action.actionString == "source.refactoring.kind.copy.objc.selector" { + let copyCommand = CopyObjCSelectorCommand( + title: "Copy Objective-C Selector", + actionString: action.actionString, + positionRange: params.range, + textDocument: params.textDocument + ).asCommand() + return CodeAction(title: "Copy Objective-C Selector", kind: .refactor, command: copyCommand) + } + + 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 { @@ -1096,6 +1108,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: CopyObjCSelectorCommand.self) { + return try await copyObjCSelector(command) } else if let command = req.swiftCommand(ofType: RemoveUnusedImportsCommand.self) { try await removeUnusedImports(command) } else {