Skip to content

Commit 44a8a3c

Browse files
committed
Add Copy Objective-C Selector code action
1 parent d449cb7 commit 44a8a3c

File tree

5 files changed

+172
-0
lines changed

5 files changed

+172
-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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,11 +949,15 @@ extension SwiftLanguageService {
949949
}.flatMap { $0 }
950950
}
951951

952+
// most likely we will go from here ?
952953
func retrieveRefactorCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] {
953954
let additionalCursorInfoParameters: ((SKDRequestDictionary) -> Void) = { skreq in
954955
skreq.set(self.keys.retrieveRefactorActions, to: 1)
955956
}
956957

958+
let snapshot = try documentManager.latestSnapshot(params.textDocument.uri)
959+
960+
// here we are returning for the server the available options?
957961
let cursorInfoResponse = try await cursorInfo(
958962
params.textDocument.uri,
959963
params.range,
@@ -979,9 +983,52 @@ extension SwiftLanguageService {
979983
refactorActions.append(CodeAction(title: expandMacroCommand.title, kind: .refactor, command: expandMacroCommand))
980984
}
981985

986+
if let copySelectorAction = await makeCopyObjCSelectorAction(
987+
range: params.range,
988+
textDocument: params.textDocument,
989+
snapshot: snapshot
990+
) {
991+
refactorActions.append(copySelectorAction)
992+
}
993+
982994
return refactorActions
983995
}
984996

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

0 commit comments

Comments
 (0)