Skip to content

Commit 7780ec5

Browse files
committed
Make sure the range returned by PrepareRenameRequest always contains the position from which the rename was initiated
VS Code rejects the placeholder name returned by the `PrepareRenameRequest`. When initiating rename on `x` in the following, this means that VS Code picks its own internal symbol name as the placeholder text, opening the rename textbox with `x`. ```swift func foo( x: Int ) {} ``` Users then enter `y` in the expectation to rename only the parameter but sourcekit-lsp expects a function name and thus renames `foo` to `y(x:)`, which is unexpected. rdar://125551489
1 parent 3e51f70 commit 7780ec5

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)