Skip to content

Commit 4117a77

Browse files
committed
Support rename of a function’s name started from a parameter or argument label
When starting rename from a function parameter’s label, we should be performing a rename of the function signature. To do that, we need to do some adjustments of th rename positions in order to get the position of the function’s base name and use that for the rename requests sent to sourcekitd. rdar://119875277
1 parent eca0979 commit 4117a77

File tree

5 files changed

+306
-75
lines changed

5 files changed

+306
-75
lines changed

Sources/SKTestSupport/SkipUnless.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public enum SkipUnless {
127127
) {
128128
let testClient = try await TestSourceKitLSPClient()
129129
let uri = DocumentURI.for(.swift)
130-
let positions = testClient.openDocument("void 1️⃣test() {}", uri: uri)
130+
let positions = testClient.openDocument("func 1️⃣test() {}", uri: uri)
131131
do {
132132
_ = try await testClient.send(
133133
RenameRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"], newName: "test2")

Sources/SourceKitLSP/Rename.swift

Lines changed: 193 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import LSPLogging
1515
import LanguageServerProtocol
1616
import SKSupport
1717
import SourceKitD
18+
import SwiftSyntax
1819

1920
// MARK: - Helper types
2021

@@ -465,7 +466,7 @@ extension SwiftLanguageServer {
465466
}
466467
}
467468

468-
/// A name that has a represenentation both in Swift and clang-based languages.
469+
/// A name that has a representation both in Swift and clang-based languages.
469470
///
470471
/// These names might differ. For example, an Objective-C method gets translated by the clang importer to form the Swift
471472
/// name or it could have a `SWIFT_NAME` attribute that defines the method's name in Swift. Similarly, a Swift symbol
@@ -504,7 +505,7 @@ public struct CrossLanguageName {
504505

505506
// MARK: - SourceKitServer
506507

507-
/// The kinds of symbol occurrance roles that should be renamed.
508+
/// The kinds of symbol occurrence roles that should be renamed.
508509
fileprivate let renameRoles: SymbolRole = [.declaration, .definition, .reference]
509510

510511
extension DocumentManager {
@@ -747,17 +748,14 @@ extension SourceKitServer {
747748
workspace: Workspace,
748749
languageService: ToolchainLanguageServer
749750
) async throws -> PrepareRenameResponse? {
750-
guard var prepareRenameResult = try await languageService.prepareRename(request) else {
751+
guard let languageServicePrepareRename = try await languageService.prepareRename(request) else {
751752
return nil
752753
}
753-
754-
let symbolInfo = try await languageService.symbolInfo(
755-
SymbolInfoRequest(textDocument: request.textDocument, position: request.position)
756-
)
754+
var prepareRenameResult = languageServicePrepareRename.prepareRename
757755

758756
guard
759757
let index = workspace.index,
760-
let usr = symbolInfo.only?.usr,
758+
let usr = languageServicePrepareRename.usr,
761759
let oldName = try await self.getCrossLanguageName(forUsr: usr, workspace: workspace, index: index)
762760
else {
763761
return prepareRenameResult
@@ -826,11 +824,169 @@ extension SwiftLanguageServer {
826824
return categorizedRanges.compactMap { SyntacticRenameName($0, in: snapshot, keys: keys, values: values) }
827825
}
828826

827+
/// If `position` is on an argument label or a parameter name, find the position of the function's base name.
828+
private func findFunctionBaseNamePosition(of position: Position, in snapshot: DocumentSnapshot) async -> Position? {
829+
class TokenFinder: SyntaxAnyVisitor {
830+
/// The position at which the token should be found.
831+
let position: AbsolutePosition
832+
833+
/// Once found, the token at the requested position.
834+
var foundToken: TokenSyntax?
835+
836+
init(position: AbsolutePosition) {
837+
self.position = position
838+
super.init(viewMode: .sourceAccurate)
839+
}
840+
841+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
842+
guard (node.position..<node.endPosition).contains(position) else {
843+
// Node doesn't contain the position. No point visiting it.
844+
return .skipChildren
845+
}
846+
guard foundToken == nil else {
847+
// We have already found a token. No point visiting this one
848+
return .skipChildren
849+
}
850+
return .visitChildren
851+
}
852+
853+
override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
854+
if (token.position..<token.endPosition).contains(position) {
855+
self.foundToken = token
856+
}
857+
return .skipChildren
858+
}
859+
860+
/// Dedicated entry point for `TokenFinder`.
861+
static func findToken(at position: AbsolutePosition, in tree: some SyntaxProtocol) -> TokenSyntax? {
862+
let finder = TokenFinder(position: position)
863+
finder.walk(tree)
864+
return finder.foundToken
865+
}
866+
}
867+
868+
let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot)
869+
guard let absolutePosition = snapshot.position(of: position) else {
870+
return nil
871+
}
872+
guard let token = TokenFinder.findToken(at: absolutePosition, in: tree) else {
873+
return nil
874+
}
875+
876+
// The node that contains the function's base name. This might be an expression like `self.doStuff`.
877+
// The start position of the last token in this node will be used as the base name position.
878+
var baseNode: Syntax? = nil
879+
880+
switch token.keyPathInParent {
881+
case \LabeledExprSyntax.label:
882+
let callLike = token.parent(as: LabeledExprSyntax.self)?.parent(as: LabeledExprListSyntax.self)?.parent
883+
switch callLike?.as(SyntaxEnum.self) {
884+
case .attribute(let attribute):
885+
baseNode = Syntax(attribute.attributeName)
886+
case .functionCallExpr(let functionCall):
887+
baseNode = Syntax(functionCall.calledExpression)
888+
case .macroExpansionDecl(let macroExpansionDecl):
889+
baseNode = Syntax(macroExpansionDecl.macroName)
890+
case .macroExpansionExpr(let macroExpansionExpr):
891+
baseNode = Syntax(macroExpansionExpr.macroName)
892+
case .subscriptCallExpr(let subscriptCall):
893+
baseNode = Syntax(subscriptCall.leftSquare)
894+
default:
895+
break
896+
}
897+
case \FunctionParameterSyntax.firstName:
898+
let parameterClause =
899+
token
900+
.parent(as: FunctionParameterSyntax.self)?
901+
.parent(as: FunctionParameterListSyntax.self)?
902+
.parent(as: FunctionParameterClauseSyntax.self)
903+
if let functionSignature = parameterClause?.parent(as: FunctionSignatureSyntax.self) {
904+
switch functionSignature.parent?.as(SyntaxEnum.self) {
905+
case .functionDecl(let functionDecl):
906+
baseNode = Syntax(functionDecl.name)
907+
case .initializerDecl(let initializerDecl):
908+
baseNode = Syntax(initializerDecl.initKeyword)
909+
case .macroDecl(let macroDecl):
910+
baseNode = Syntax(macroDecl.name)
911+
default:
912+
break
913+
}
914+
} else if let subscriptDecl = parameterClause?.parent(as: SubscriptDeclSyntax.self) {
915+
baseNode = Syntax(subscriptDecl.subscriptKeyword)
916+
}
917+
case \DeclNameArgumentSyntax.name:
918+
let declReference =
919+
token
920+
.parent(as: DeclNameArgumentSyntax.self)?
921+
.parent(as: DeclNameArgumentListSyntax.self)?
922+
.parent(as: DeclNameArgumentsSyntax.self)?
923+
.parent(as: DeclReferenceExprSyntax.self)
924+
baseNode = Syntax(declReference?.baseName)
925+
default:
926+
break
927+
}
928+
929+
if let lastToken = baseNode?.lastToken(viewMode: .sourceAccurate),
930+
let position = snapshot.position(of: lastToken.positionAfterSkippingLeadingTrivia)
931+
{
932+
return position
933+
}
934+
return nil
935+
}
936+
937+
/// When the user requested a rename at `position` in `snapshot`, determine the position at which the rename should be
938+
/// performed internally and USR of the symbol to rename.
939+
///
940+
/// This is necessary to adjust the rename position when renaming function parameters. For example when invoking
941+
/// rename on `x` in `foo(x:)`, we need to perform a rename of `foo` in sourcekitd so that we can rename the function
942+
/// parameter.
943+
///
944+
/// The position might be `nil` if there is no local position in the file that refers to the base name to be renamed.
945+
/// This happens if renaming a function parameter of `MyStruct(x:)` where `MyStruct` is defined outside of the current
946+
/// file. In this case, there is no base name that refers to the initializer of `MyStruct`. When `position` is `nil`
947+
/// a pure index-based rename from the usr USR or `symbolDetails` needs to be performed and no `relatedIdentifiers`
948+
/// request can be used to rename symbols in the current file.
949+
func symbolToRename(
950+
at position: Position,
951+
in snapshot: DocumentSnapshot
952+
) async -> (position: Position?, usr: String?) {
953+
let symbolInfo = try? await self.symbolInfo(
954+
SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: position)
955+
)
956+
957+
guard let baseNamePosition = await findFunctionBaseNamePosition(of: position, in: snapshot) else {
958+
return (position, symbolInfo?.only?.usr)
959+
}
960+
if let onlySymbol = symbolInfo?.only, onlySymbol.kind == .constructor {
961+
// We have a rename like `MyStruct(x: 1)`, invoked from `x`.
962+
if let bestLocalDeclaration = onlySymbol.bestLocalDeclaration, bestLocalDeclaration.uri == snapshot.uri {
963+
// If the initializer is declared within the same file, we can perform rename in the current file based on
964+
// the declaration's location.
965+
return (bestLocalDeclaration.range.lowerBound, onlySymbol.usr)
966+
}
967+
// Otherwise, we don't have a reference to the base name of the initializer and we can't use related
968+
// identifiers to perform the rename.
969+
// Return `nil` for the position to perform a pure index-based rename.
970+
return (nil, onlySymbol.usr)
971+
}
972+
// Adjust the symbol info to the symbol info of the base name.
973+
// This ensures that we get the symbol info of the function's base instead of the parameter.
974+
let baseNameSymbolInfo = try? await self.symbolInfo(
975+
SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: baseNamePosition)
976+
)
977+
return (baseNamePosition, baseNameSymbolInfo?.only?.usr)
978+
}
979+
829980
public func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) {
830981
let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri)
831982

983+
let (renamePosition, usr) = await symbolToRename(at: request.position, in: snapshot)
984+
guard let renamePosition else {
985+
return (edits: WorkspaceEdit(), usr: usr)
986+
}
987+
832988
let relatedIdentifiersResponse = try await self.relatedIdentifiers(
833-
at: request.position,
989+
at: renamePosition,
834990
in: snapshot,
835991
includeNonEditableBaseNames: true
836992
)
@@ -862,10 +1018,6 @@ extension SwiftLanguageServer {
8621018

8631019
try Task.checkCancellation()
8641020

865-
let usr =
866-
(try? await self.symbolInfo(SymbolInfoRequest(textDocument: request.textDocument, position: request.position)))?
867-
.only?.usr
868-
8691021
return (edits: WorkspaceEdit(changes: [snapshot.uri: edits]), usr: usr)
8701022
}
8711023

@@ -998,25 +1150,29 @@ extension SwiftLanguageServer {
9981150
}
9991151
}
10001152

1001-
public func prepareRename(_ request: PrepareRenameRequest) async throws -> PrepareRenameResponse? {
1153+
public func prepareRename(
1154+
_ request: PrepareRenameRequest
1155+
) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? {
10021156
let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri)
10031157

1158+
let (renamePosition, usr) = await symbolToRename(at: request.position, in: snapshot)
1159+
guard let renamePosition else {
1160+
return nil
1161+
}
1162+
10041163
let response = try await self.relatedIdentifiers(
1005-
at: request.position,
1164+
at: renamePosition,
10061165
in: snapshot,
10071166
includeNonEditableBaseNames: true
10081167
)
10091168
guard let name = response.name else {
10101169
throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename")
10111170
}
1012-
guard let range = response.relatedIdentifiers.first(where: { $0.range.contains(request.position) })?.range
1171+
guard let range = response.relatedIdentifiers.first(where: { $0.range.contains(renamePosition) })?.range
10131172
else {
10141173
return nil
10151174
}
1016-
return PrepareRenameResponse(
1017-
range: range,
1018-
placeholder: name
1019-
)
1175+
return (PrepareRenameResponse(range: range, placeholder: name), usr)
10201176
}
10211177
}
10221178

@@ -1065,7 +1221,22 @@ extension ClangLanguageServerShim {
10651221
}
10661222
}
10671223

1068-
public func prepareRename(_ request: PrepareRenameRequest) async throws -> PrepareRenameResponse? {
1069-
return try await forwardRequestToClangd(request)
1224+
public func prepareRename(
1225+
_ request: PrepareRenameRequest
1226+
) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? {
1227+
guard let prepareRename = try await forwardRequestToClangd(request) else {
1228+
return nil
1229+
}
1230+
let symbolInfo = try await forwardRequestToClangd(
1231+
SymbolInfoRequest(textDocument: request.textDocument, position: request.position)
1232+
)
1233+
return (prepareRename, symbolInfo.only?.usr)
1234+
}
1235+
}
1236+
1237+
fileprivate extension SyntaxProtocol {
1238+
/// Returns the parent node and casts it to the specified type.
1239+
func parent<S: SyntaxProtocol>(as syntaxType: S.Type) -> S? {
1240+
return parent?.as(S.self)
10701241
}
10711242
}

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,9 @@ extension sourcekitd_uid_t {
10371037
return .namespace
10381038
case vals.refModule:
10391039
return .module
1040+
case vals.refConstructor,
1041+
vals.declConstructor:
1042+
return .constructor
10401043
default:
10411044
return nil
10421045
}

Sources/SourceKitLSP/ToolchainLanguageServer.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ public protocol ToolchainLanguageServer: AnyObject {
163163
) async throws -> [TextEdit]
164164

165165
/// Return compound decl name that will be used as a placeholder for a rename request at a specific position.
166-
func prepareRename(_ request: PrepareRenameRequest) async throws -> PrepareRenameResponse?
166+
func prepareRename(
167+
_ request: PrepareRenameRequest
168+
) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)?
167169

168170
func indexedRename(_ request: IndexedRenameRequest) async throws -> WorkspaceEdit?
169171

0 commit comments

Comments
 (0)