Skip to content

Commit 67d3137

Browse files
committed
Add Copy Objective-C Selector code action
1 parent d449cb7 commit 67d3137

File tree

5 files changed

+173
-0
lines changed

5 files changed

+173
-0
lines changed

Sources/SourceKitD/sourcekitd_uids.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,8 @@ package struct sourcekitd_api_requests {
878878
package let findLocalRenameRanges: sourcekitd_api_uid_t
879879
/// `source.request.semantic.refactoring`
880880
package let semanticRefactoring: sourcekitd_api_uid_t
881+
/// `source.request.objc.selector`
882+
package let objcSelector: sourcekitd_api_uid_t
881883
/// `source.request.enable-compile-notifications`
882884
package let enableCompileNotifications: sourcekitd_api_uid_t
883885
/// `source.request.test_notification`
@@ -951,6 +953,7 @@ package struct sourcekitd_api_requests {
951953
findRenameRanges = api.uid_get_from_cstr("source.request.find-syntactic-rename-ranges")!
952954
findLocalRenameRanges = api.uid_get_from_cstr("source.request.find-local-rename-ranges")!
953955
semanticRefactoring = api.uid_get_from_cstr("source.request.semantic.refactoring")!
956+
objcSelector = api.uid_get_from_cstr("source.request.objc.selector")!
954957
enableCompileNotifications = api.uid_get_from_cstr("source.request.enable-compile-notifications")!
955958
testNotification = api.uid_get_from_cstr("source.request.test_notification")!
956959
collectExpressionType = api.uid_get_from_cstr("source.request.expression.type")!
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
@_spi(SourceKitLSP) import LanguageServerProtocol
15+
@_spi(SourceKitLSP) import SKLogging
16+
import SourceKitD
17+
import SourceKitLSP
18+
19+
extension SwiftLanguageService {
20+
/// Executes the direct SourceKitD request to fetch the Objective-C selector at the cursor position.
21+
func copyObjCSelector(
22+
_ command: CopyObjCSelectorCommand
23+
) async throws -> LSPAny {
24+
let snapshot = try documentManager.latestSnapshot(command.textDocument.uri)
25+
let compileCommand = await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: true)
26+
let offset = snapshot.utf8Offset(of: command.positionRange.lowerBound)
27+
28+
let keys = self.keys
29+
let skreq = sourcekitd.dictionary([:])
30+
skreq.set(keys.offset, to: offset)
31+
skreq.set(keys.sourceFile, to: snapshot.uri.sourcekitdSourceFile)
32+
if let primaryFile = snapshot.uri.primaryFile?.pseudoPath {
33+
skreq.set(keys.primaryFile, to: primaryFile)
34+
}
35+
if let compilerArgs = compileCommand?.compilerArgs {
36+
skreq.set(keys.compilerArgs, to: compilerArgs as [any SKDRequestValue])
37+
}
38+
39+
let dict = try await send(sourcekitdRequest: \.objcSelector, skreq, snapshot: snapshot)
40+
if let selectorText: String = dict[keys.text] {
41+
return .string(selectorText)
42+
}
43+
44+
throw ResponseError.unknown("No Objective-C selector found at cursor position")
45+
}
46+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
@_spi(SourceKitLSP) package import LanguageServerProtocol
14+
import SourceKitD
15+
16+
package struct CopyObjCSelectorCommand: SwiftCommand {
17+
package static let identifier: String = "copy.objc.selector.command"
18+
19+
package var title = "Copy Objective-C Selector"
20+
package var actionString = "source.refactoring.kind.copy.objc.selector"
21+
22+
package var positionRange: Range<Position>
23+
package var textDocument: LanguageServerProtocol.TextDocumentIdentifier
24+
25+
package init(positionRange: Range<Position>, textDocument: LanguageServerProtocol.TextDocumentIdentifier) {
26+
self.positionRange = positionRange
27+
self.textDocument = textDocument
28+
}
29+
30+
package init?(fromLSPDictionary dictionary: [String: LSPAny]) {
31+
guard case .dictionary(let documentDict)? = dictionary[CodingKeys.textDocument.stringValue],
32+
case .string(let title)? = dictionary[CodingKeys.title.stringValue],
33+
case .string(let actionString)? = dictionary[CodingKeys.actionString.stringValue],
34+
case .dictionary(let rangeDict)? = dictionary[CodingKeys.positionRange.stringValue]
35+
else {
36+
return nil
37+
}
38+
39+
guard let positionRange = Range<Position>(fromLSPDictionary: rangeDict),
40+
let textDocument = LanguageServerProtocol.TextDocumentIdentifier(fromLSPDictionary: documentDict)
41+
else {
42+
return nil
43+
}
44+
45+
self.init(
46+
title: title,
47+
actionString: actionString,
48+
positionRange: positionRange,
49+
textDocument: textDocument
50+
)
51+
}
52+
53+
package init(
54+
title: String,
55+
actionString: String,
56+
positionRange: Range<Position>,
57+
textDocument: LanguageServerProtocol.TextDocumentIdentifier
58+
) {
59+
self.title = title
60+
self.actionString = actionString
61+
self.positionRange = positionRange
62+
self.textDocument = textDocument
63+
}
64+
65+
package func encodeToLSPAny() -> LSPAny {
66+
return .dictionary([
67+
CodingKeys.title.stringValue: .string(title),
68+
CodingKeys.actionString.stringValue: .string(actionString),
69+
CodingKeys.positionRange.stringValue: positionRange.encodeToLSPAny(),
70+
CodingKeys.textDocument.stringValue: textDocument.encodeToLSPAny(),
71+
])
72+
}
73+
}

Sources/SwiftLanguageService/SwiftCommand.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ extension SwiftLanguageService {
5151
[
5252
SemanticRefactorCommand.self,
5353
ExpandMacroCommand.self,
54+
CopyObjCSelectorCommand.self,
5455
].map { (command: any SwiftCommand.Type) in
5556
command.identifier
5657
}

Sources/SwiftLanguageService/SwiftLanguageService.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,8 @@ extension SwiftLanguageService {
954954
skreq.set(self.keys.retrieveRefactorActions, to: 1)
955955
}
956956

957+
let snapshot = try documentManager.latestSnapshot(params.textDocument.uri)
958+
957959
let cursorInfoResponse = try await cursorInfo(
958960
params.textDocument.uri,
959961
params.range,
@@ -979,9 +981,55 @@ extension SwiftLanguageService {
979981
refactorActions.append(CodeAction(title: expandMacroCommand.title, kind: .refactor, command: expandMacroCommand))
980982
}
981983

984+
if let copySelectorAction = await makeCopyObjCSelectorAction(
985+
range: params.range,
986+
textDocument: params.textDocument,
987+
snapshot: snapshot
988+
) {
989+
refactorActions.append(copySelectorAction)
990+
}
991+
982992
return refactorActions
983993
}
984994

995+
private func makeCopyObjCSelectorAction(
996+
range: Range<Position>,
997+
textDocument: LanguageServerProtocol.TextDocumentIdentifier,
998+
snapshot: DocumentSnapshot
999+
) async -> CodeAction? {
1000+
let compileCommand = await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: true)
1001+
let offset = snapshot.utf8Offset(of: range.lowerBound)
1002+
1003+
let keys = self.keys
1004+
let skreq = sourcekitd.dictionary([
1005+
keys.offset: offset,
1006+
keys.sourceFile: snapshot.uri.sourcekitdSourceFile,
1007+
keys.primaryFile: primaryFile,
1008+
keys.compilerArgs: compilerArgs as [any SKDRequestValue]
1009+
])
1010+
if let primaryFile = snapshot.uri.primaryFile?.pseudoPath {
1011+
skreq.set(keys.primaryFile, to: primaryFile)
1012+
}
1013+
if let compilerArgs = compileCommand?.compilerArgs {
1014+
skreq.set(keys.compilerArgs, to: compilerArgs as [any SKDRequestValue])
1015+
}
1016+
1017+
do {
1018+
let dict = try await send(sourcekitdRequest: \.objcSelector, skreq, snapshot: snapshot)
1019+
if dict[keys.text] as String? != nil {
1020+
let copyCommand = CopyObjCSelectorCommand(
1021+
positionRange: range,
1022+
textDocument: textDocument
1023+
).asCommand()
1024+
return CodeAction(title: "Copy Objective-C Selector", kind: .refactor, command: copyCommand)
1025+
}
1026+
} catch {
1027+
logger.debug("CopyObjCSelector: applicability check failed: \(error.forLogging)")
1028+
}
1029+
1030+
return nil
1031+
}
1032+
9851033
func retrieveQuickFixCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] {
9861034
let snapshot = try await self.latestSnapshot(for: params.textDocument.uri)
9871035
let buildSettings = await self.compileCommand(for: params.textDocument.uri, fallbackAfterTimeout: true)
@@ -1096,6 +1144,8 @@ extension SwiftLanguageService {
10961144
try await semanticRefactoring(command)
10971145
} else if let command = req.swiftCommand(ofType: ExpandMacroCommand.self) {
10981146
try await expandMacro(command)
1147+
} else if let command = req.swiftCommand(ofType: CopyObjCSelectorCommand.self) {
1148+
return try await copyObjCSelector(command)
10991149
} else if let command = req.swiftCommand(ofType: RemoveUnusedImportsCommand.self) {
11001150
try await removeUnusedImports(command)
11011151
} else {

0 commit comments

Comments
 (0)