Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Sources/SourceKitD/sourcekitd_uids.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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")!
Expand Down
55 changes: 55 additions & 0 deletions Sources/SwiftLanguageService/ShowObjCSelector.swift
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add the new files to CMakeLists.txt for the Windows build?

Original file line number Diff line number Diff line change
@@ -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)
}
}
67 changes: 67 additions & 0 deletions Sources/SwiftLanguageService/ShowObjCSelectorCommand.swift
Original file line number Diff line number Diff line change
@@ -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<Position>
package var textDocument: TextDocumentIdentifier

package init(positionRange: Range<Position>, 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<Position>(fromLSPDictionary: rangeDict),
let textDocument = TextDocumentIdentifier(fromLSPDictionary: documentDict)
else {
return nil
}
Comment on lines +30 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be simplified slightly by using init(fromLSPAny:)

Suggested change
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<Position>(fromLSPDictionary: rangeDict),
let textDocument = TextDocumentIdentifier(fromLSPDictionary: documentDict)
else {
return nil
}
guard let textDocument = TextDocumentIdentifier(fromLSPAny: dictionary[CodingKeys.textDocument.stringValue]),
case .string(let title)? = dictionary[CodingKeys.title.stringValue],
let positionRange = Range<Position>(fromLSPAny: dictionary[CodingKeys.positionRange.stringValue])
else {
return nil
}


self.init(
title: title,
positionRange: positionRange,
textDocument: textDocument
)
}

package init(
title: String,
positionRange: Range<Position>,
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(),
])
}
}
1 change: 1 addition & 0 deletions Sources/SwiftLanguageService/SwiftCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ extension SwiftLanguageService {
[
SemanticRefactorCommand.self,
ExpandMacroCommand.self,
ShowObjCSelectorCommand.self,
].map { (command: any SwiftCommand.Type) in
command.identifier
}
Expand Down
26 changes: 22 additions & 4 deletions Sources/SwiftLanguageService/SwiftLanguageService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that we show the refactoring action on every Swift method, even if it isn’t backed by an Objective-C method and thus the Show Objective-C Selector would just fail? Do we have some indication in the cursor info response already to see whether this method is defined in Objective-C / that we have an Objective-C selector? One ugly idea I have is checking whether the USR starts with s: but there might be nicer implementations as well.


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
}

Expand Down Expand Up @@ -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 {
Expand Down