Skip to content

Commit d1d312c

Browse files
authored
Merge pull request #1163 from ahoppen/prepare-rename-should-contain-originator-range
Make sure the range returned by `PrepareRenameRequest` always contains the position from which the rename was initiated
2 parents 3e51f70 + 7780ec5 commit d1d312c

File tree

3 files changed

+91
-39
lines changed

3 files changed

+91
-39
lines changed

Sources/SourceKitLSP/Rename.swift

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -927,8 +927,10 @@ extension SwiftLanguageService {
927927
return categorizedRanges.compactMap { SyntacticRenameName($0, in: snapshot, keys: keys, values: values) }
928928
}
929929

930-
/// If `position` is on an argument label or a parameter name, find the position of the function's base name.
931-
private func findFunctionBaseNamePosition(of position: Position, in snapshot: DocumentSnapshot) async -> Position? {
930+
/// If `position` is on an argument label or a parameter name, find the range from the function's base name to the
931+
/// token that terminates the arguments or parameters of the function. Typically, this is the closing ')' but it can
932+
/// also be a closing ']' for subscripts or the end of a trailing closure.
933+
private func findFunctionLikeRange(of position: Position, in snapshot: DocumentSnapshot) async -> Range<Position>? {
932934
let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot)
933935
guard let absolutePosition = snapshot.absolutePosition(of: position) else {
934936
return nil
@@ -939,22 +941,28 @@ extension SwiftLanguageService {
939941

940942
// The node that contains the function's base name. This might be an expression like `self.doStuff`.
941943
// The start position of the last token in this node will be used as the base name position.
942-
var baseNode: Syntax? = nil
944+
var startToken: TokenSyntax? = nil
945+
var endToken: TokenSyntax? = nil
943946

944947
switch token.keyPathInParent {
945948
case \LabeledExprSyntax.label:
946949
let callLike = token.parent(as: LabeledExprSyntax.self)?.parent(as: LabeledExprListSyntax.self)?.parent
947950
switch callLike?.as(SyntaxEnum.self) {
948951
case .attribute(let attribute):
949-
baseNode = Syntax(attribute.attributeName)
952+
startToken = attribute.attributeName.lastToken(viewMode: .sourceAccurate)
953+
endToken = attribute.lastToken(viewMode: .sourceAccurate)
950954
case .functionCallExpr(let functionCall):
951-
baseNode = Syntax(functionCall.calledExpression)
955+
startToken = functionCall.calledExpression.lastToken(viewMode: .sourceAccurate)
956+
endToken = functionCall.lastToken(viewMode: .sourceAccurate)
952957
case .macroExpansionDecl(let macroExpansionDecl):
953-
baseNode = Syntax(macroExpansionDecl.macroName)
958+
startToken = macroExpansionDecl.macroName
959+
endToken = macroExpansionDecl.lastToken(viewMode: .sourceAccurate)
954960
case .macroExpansionExpr(let macroExpansionExpr):
955-
baseNode = Syntax(macroExpansionExpr.macroName)
961+
startToken = macroExpansionExpr.macroName
962+
endToken = macroExpansionExpr.lastToken(viewMode: .sourceAccurate)
956963
case .subscriptCallExpr(let subscriptCall):
957-
baseNode = Syntax(subscriptCall.leftSquare)
964+
startToken = subscriptCall.leftSquare
965+
endToken = subscriptCall.lastToken(viewMode: .sourceAccurate)
958966
default:
959967
break
960968
}
@@ -967,16 +975,20 @@ extension SwiftLanguageService {
967975
if let functionSignature = parameterClause?.parent(as: FunctionSignatureSyntax.self) {
968976
switch functionSignature.parent?.as(SyntaxEnum.self) {
969977
case .functionDecl(let functionDecl):
970-
baseNode = Syntax(functionDecl.name)
978+
startToken = functionDecl.name
979+
endToken = functionSignature.parameterClause.rightParen
971980
case .initializerDecl(let initializerDecl):
972-
baseNode = Syntax(initializerDecl.initKeyword)
981+
startToken = initializerDecl.initKeyword
982+
endToken = functionSignature.parameterClause.rightParen
973983
case .macroDecl(let macroDecl):
974-
baseNode = Syntax(macroDecl.name)
984+
startToken = macroDecl.name
985+
endToken = functionSignature.parameterClause.rightParen
975986
default:
976987
break
977988
}
978989
} else if let subscriptDecl = parameterClause?.parent(as: SubscriptDeclSyntax.self) {
979-
baseNode = Syntax(subscriptDecl.subscriptKeyword)
990+
startToken = subscriptDecl.subscriptKeyword
991+
endToken = subscriptDecl.parameterClause.rightParen
980992
}
981993
case \DeclNameArgumentSyntax.name:
982994
let declReference =
@@ -985,21 +997,24 @@ extension SwiftLanguageService {
985997
.parent(as: DeclNameArgumentListSyntax.self)?
986998
.parent(as: DeclNameArgumentsSyntax.self)?
987999
.parent(as: DeclReferenceExprSyntax.self)
988-
baseNode = Syntax(declReference?.baseName)
1000+
startToken = declReference?.baseName
1001+
endToken = declReference?.argumentNames?.rightParen
9891002
default:
9901003
break
9911004
}
9921005

993-
if let lastToken = baseNode?.lastToken(viewMode: .sourceAccurate),
994-
let position = snapshot.position(of: lastToken.positionAfterSkippingLeadingTrivia)
1006+
if let startToken, let endToken,
1007+
let startPosition = snapshot.position(of: startToken.positionAfterSkippingLeadingTrivia),
1008+
let endPosition = snapshot.position(of: endToken.endPositionBeforeTrailingTrivia)
9951009
{
996-
return position
1010+
return startPosition..<endPosition
9971011
}
9981012
return nil
9991013
}
10001014

10011015
/// When the user requested a rename at `position` in `snapshot`, determine the position at which the rename should be
1002-
/// performed internally and USR of the symbol to rename.
1016+
/// performed internally, the USR of the symbol to rename and the range to rename that should be returned to the
1017+
/// editor.
10031018
///
10041019
/// This is necessary to adjust the rename position when renaming function parameters. For example when invoking
10051020
/// rename on `x` in `foo(x:)`, we need to perform a rename of `foo` in sourcekitd so that we can rename the function
@@ -1010,41 +1025,49 @@ extension SwiftLanguageService {
10101025
/// file. In this case, there is no base name that refers to the initializer of `MyStruct`. When `position` is `nil`
10111026
/// a pure index-based rename from the usr USR or `symbolDetails` needs to be performed and no `relatedIdentifiers`
10121027
/// request can be used to rename symbols in the current file.
1028+
///
1029+
/// `position` might be at a different location in the source file than where the user initiated the rename.
1030+
/// For example, `position` could point to the definition of a function within the file when rename was initiated on
1031+
/// a call.
1032+
///
1033+
/// If a `range` is returned, this is an expanded range that contains both the symbol to rename as well as the
1034+
/// position at which the rename was requested. For example, when rename was initiated from the argument label of a
1035+
/// function call, the `range` will contain the entire function call from the base name to the closing `)`.
10131036
func symbolToRename(
10141037
at position: Position,
10151038
in snapshot: DocumentSnapshot
1016-
) async -> (position: Position?, usr: String?) {
1039+
) async -> (position: Position?, usr: String?, functionLikeRange: Range<Position>?) {
10171040
let symbolInfo = try? await self.symbolInfo(
10181041
SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: position)
10191042
)
10201043

1021-
guard let baseNamePosition = await findFunctionBaseNamePosition(of: position, in: snapshot) else {
1022-
return (position, symbolInfo?.only?.usr)
1044+
guard let functionLikeRange = await findFunctionLikeRange(of: position, in: snapshot) else {
1045+
return (position, symbolInfo?.only?.usr, nil)
10231046
}
10241047
if let onlySymbol = symbolInfo?.only, onlySymbol.kind == .constructor {
10251048
// We have a rename like `MyStruct(x: 1)`, invoked from `x`.
10261049
if let bestLocalDeclaration = onlySymbol.bestLocalDeclaration, bestLocalDeclaration.uri == snapshot.uri {
10271050
// If the initializer is declared within the same file, we can perform rename in the current file based on
10281051
// the declaration's location.
1029-
return (bestLocalDeclaration.range.lowerBound, onlySymbol.usr)
1052+
return (bestLocalDeclaration.range.lowerBound, onlySymbol.usr, functionLikeRange)
10301053
}
10311054
// Otherwise, we don't have a reference to the base name of the initializer and we can't use related
10321055
// identifiers to perform the rename.
10331056
// Return `nil` for the position to perform a pure index-based rename.
1034-
return (nil, onlySymbol.usr)
1057+
return (nil, onlySymbol.usr, functionLikeRange)
10351058
}
10361059
// Adjust the symbol info to the symbol info of the base name.
10371060
// This ensures that we get the symbol info of the function's base instead of the parameter.
10381061
let baseNameSymbolInfo = try? await self.symbolInfo(
1039-
SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: baseNamePosition)
1062+
SymbolInfoRequest(textDocument: TextDocumentIdentifier(snapshot.uri), position: functionLikeRange.lowerBound)
10401063
)
1041-
return (baseNamePosition, baseNameSymbolInfo?.only?.usr)
1064+
return (functionLikeRange.lowerBound, baseNameSymbolInfo?.only?.usr, functionLikeRange)
10421065
}
10431066

10441067
public func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?) {
10451068
let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri)
10461069

1047-
let (renamePosition, usr) = await symbolToRename(at: request.position, in: snapshot)
1070+
let (renamePosition, usr, _) = await symbolToRename(at: request.position, in: snapshot)
10481071
guard let renamePosition else {
10491072
return (edits: WorkspaceEdit(), usr: usr)
10501073
}
@@ -1342,7 +1365,7 @@ extension SwiftLanguageService {
13421365
) async throws -> (prepareRename: PrepareRenameResponse, usr: String?)? {
13431366
let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri)
13441367

1345-
let (renamePosition, usr) = await symbolToRename(at: request.position, in: snapshot)
1368+
let (renamePosition, usr, functionLikeRange) = await symbolToRename(at: request.position, in: snapshot)
13461369
guard let renamePosition else {
13471370
return nil
13481371
}
@@ -1358,11 +1381,11 @@ extension SwiftLanguageService {
13581381
if name.hasSuffix("()") {
13591382
name = String(name.dropLast(2))
13601383
}
1361-
guard let range = response.relatedIdentifiers.first(where: { $0.range.contains(renamePosition) })?.range
1384+
guard let relatedIdentRange = response.relatedIdentifiers.first(where: { $0.range.contains(renamePosition) })?.range
13621385
else {
13631386
return nil
13641387
}
1365-
return (PrepareRenameResponse(range: range, placeholder: name), usr)
1388+
return (PrepareRenameResponse(range: functionLikeRange ?? relatedIdentRange, placeholder: name), usr)
13661389
}
13671390
}
13681391

Tests/SourceKitLSPTests/RenameAssertions.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,23 @@ func assertSingleFileRename(
5454
let prepareRenameResponse = try await testClient.send(
5555
PrepareRenameRequest(textDocument: TextDocumentIdentifier(uri), position: positions[marker])
5656
)
57-
XCTAssertEqual(
58-
prepareRenameResponse?.placeholder,
59-
expectedPrepareRenamePlaceholder,
60-
"Prepare rename placeholder does not match while performing rename at \(marker)",
61-
file: file,
62-
line: line
63-
)
57+
if let prepareRenameResponse {
58+
XCTAssertEqual(
59+
prepareRenameResponse.placeholder,
60+
expectedPrepareRenamePlaceholder,
61+
"Prepare rename placeholder does not match while performing rename at \(marker)",
62+
file: file,
63+
line: line
64+
)
65+
XCTAssert(
66+
prepareRenameResponse.range.contains(positions[marker]),
67+
"Prepare rename range \(prepareRenameResponse.range) does not contain rename position \(positions[marker])",
68+
file: file,
69+
line: line
70+
)
71+
} else {
72+
XCTFail("Expected non-nil prepareRename response", file: file, line: line)
73+
}
6474

6575
let response = try await testClient.send(
6676
RenameRequest(

Tests/SourceKitLSPTests/RenameTests.swift

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,12 @@ final class RenameTests: XCTestCase {
6666
)
6767
}
6868

69-
func testFoo() async throws {
69+
func testRenameFromFunctionParameter() async throws {
7070
try await assertSingleFileRename(
7171
"""
72-
func foo(5️⃣x: Int) {}
73-
foo(x: 1)
74-
_ = foo(x:)
72+
func foo(1️⃣x: Int) {}
73+
foo(2️⃣x: 1)
74+
_ = foo(3️⃣x:)
7575
_ = foo
7676
""",
7777
newName: "bar(y:)",
@@ -85,6 +85,25 @@ final class RenameTests: XCTestCase {
8585
)
8686
}
8787

88+
func testRenameFromFunctionParameterOnSeparateLine() async throws {
89+
try await assertSingleFileRename(
90+
"""
91+
func foo(
92+
1️⃣x: Int
93+
) {}
94+
foo(x: 1)
95+
""",
96+
newName: "bar(y:)",
97+
expectedPrepareRenamePlaceholder: "foo(x:)",
98+
expected: """
99+
func bar(
100+
y: Int
101+
) {}
102+
bar(y: 1)
103+
"""
104+
)
105+
}
106+
88107
func testSecondParameterNameIfMatches() async throws {
89108
try await assertSingleFileRename(
90109
"""

0 commit comments

Comments
 (0)