Skip to content

Commit 7514ad9

Browse files
authored
Merge pull request #1051 from ahoppen/ahoppen/rename-from-param
Support rename of a function’s name started from a parameter or argument label
2 parents 2177a58 + 4117a77 commit 7514ad9

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)