Skip to content

Commit 884b1f5

Browse files
authored
Merge pull request #1082 from ahoppen/ahoppen/rename-param-vars
Rename references to function parameters inside function bodies
2 parents dec57f9 + 6b2b702 commit 884b1f5

File tree

8 files changed

+1248
-963
lines changed

8 files changed

+1248
-963
lines changed

Sources/SKSupport/LineTable.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,20 @@ extension LineTable {
141141
return content.utf16.index(lineSlice.startIndex, offsetBy: utf16Column, limitedBy: lineSlice.endIndex)
142142
}
143143

144+
/// Returns `String.Index` of given logical position.
145+
///
146+
/// - parameter line: Line number (zero-based).
147+
/// - parameter utf8Column: UTF-8 column offset (zero-based).
148+
@inlinable
149+
public func stringIndexOf(line: Int, utf8Column: Int) -> String.Index? {
150+
guard line < count else {
151+
// Line out of range.
152+
return nil
153+
}
154+
let lineSlice = self[line]
155+
return content.utf8.index(lineSlice.startIndex, offsetBy: utf8Column, limitedBy: lineSlice.endIndex)
156+
}
157+
144158
/// Returns UTF8 buffer offset of given logical position.
145159
///
146160
/// - parameter line: Line number (zero-based).
@@ -153,6 +167,18 @@ extension LineTable {
153167
return content.utf8.distance(from: content.startIndex, to: stringIndex)
154168
}
155169

170+
/// Returns UTF8 buffer offset of given logical position.
171+
///
172+
/// - parameter line: Line number (zero-based).
173+
/// - parameter utf8Column: UTF-8 column offset (zero-based).
174+
@inlinable
175+
public func utf8OffsetOf(line: Int, utf8Column: Int) -> Int? {
176+
guard let stringIndex = stringIndexOf(line: line, utf8Column: utf8Column) else {
177+
return nil
178+
}
179+
return content.utf8.distance(from: content.startIndex, to: stringIndex)
180+
}
181+
156182
/// Returns logical position of given source offset.
157183
///
158184
/// - parameter utf8Offset: UTF-8 buffer offset (zero-based).

Sources/SourceKitLSP/Rename.swift

Lines changed: 165 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -284,14 +284,6 @@ private extension DocumentSnapshot {
284284
let contents = try String(contentsOf: url)
285285
self.init(uri: DocumentURI(url), language: language, version: 0, lineTable: LineTable(contents))
286286
}
287-
288-
func position(of renameLocation: RenameLocation) -> Position? {
289-
return positionOf(zeroBasedLine: renameLocation.line - 1, utf8Column: renameLocation.utf8Column - 1)
290-
}
291-
292-
func position(of symbolLocation: SymbolLocation) -> Position? {
293-
return positionOf(zeroBasedLine: symbolLocation.line - 1, utf8Column: symbolLocation.utf8Column - 1)
294-
}
295287
}
296288

297289
private extension RenameLocation.Usage {
@@ -713,21 +705,27 @@ extension SourceKitServer {
713705
logger.error("Failed to get document snapshot for \(uri.forLogging)")
714706
return nil
715707
}
716-
do {
717-
guard let languageService = await self.languageService(for: uri, language, in: workspace) else {
718-
return nil
719-
}
720-
let edits = try await languageService.editsToRename(
721-
locations: renameLocations,
722-
in: snapshot,
723-
oldName: oldName,
708+
guard let languageService = await self.languageService(for: uri, language, in: workspace) else {
709+
return nil
710+
}
711+
712+
var edits: [TextEdit] =
713+
await orLog("Getting edits for rename location") {
714+
return try await languageService.editsToRename(
715+
locations: renameLocations,
716+
in: snapshot,
717+
oldName: oldName,
718+
newName: newName
719+
)
720+
} ?? []
721+
for location in renameLocations where location.usage == .definition {
722+
edits += await languageService.editsToRenameParametersInFunctionBody(
723+
snapshot: snapshot,
724+
renameLocation: location,
724725
newName: newName
725726
)
726-
return (uri, edits)
727-
} catch {
728-
logger.error("Failed to get edits for \(uri.forLogging): \(error.forLogging)")
729-
return nil
730727
}
728+
return (uri, edits)
731729
}.compactMap { $0 }
732730
for (uri, editsForUri) in urisAndEdits {
733731
precondition(
@@ -826,50 +824,11 @@ extension SwiftLanguageServer {
826824

827825
/// If `position` is on an argument label or a parameter name, find the position of the function's base name.
828826
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-
868827
let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot)
869-
guard let absolutePosition = snapshot.position(of: position) else {
828+
guard let absolutePosition = snapshot.absolutePosition(of: position) else {
870829
return nil
871830
}
872-
guard let token = TokenFinder.findToken(at: absolutePosition, in: tree) else {
831+
guard let token = tree.token(at: absolutePosition) else {
873832
return nil
874833
}
875834

@@ -990,37 +949,140 @@ extension SwiftLanguageServer {
990949
in: snapshot,
991950
includeNonEditableBaseNames: true
992951
)
993-
guard let oldName = relatedIdentifiersResponse.name else {
952+
guard let oldNameString = relatedIdentifiersResponse.name else {
994953
throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename")
995954
}
996955

997-
try Task.checkCancellation()
998-
999-
let renameLocations = relatedIdentifiersResponse.relatedIdentifiers.compactMap {
1000-
(relatedIdentifier) -> RenameLocation? in
1001-
let position = relatedIdentifier.range.lowerBound
1002-
guard let utf8Column = snapshot.lineTable.utf8ColumnAt(line: position.line, utf16Column: position.utf16index)
1003-
else {
1004-
logger.fault("Unable to find UTF-8 column for \(position.line):\(position.utf16index)")
1005-
return nil
1006-
}
1007-
return RenameLocation(line: position.line + 1, utf8Column: utf8Column + 1, usage: relatedIdentifier.usage)
1008-
}
956+
let renameLocations = relatedIdentifiersResponse.renameLocations(in: snapshot)
1009957

1010958
try Task.checkCancellation()
1011959

1012-
let edits = try await editsToRename(
960+
let oldName = CrossLanguageName(clangName: nil, swiftName: oldNameString, definitionLanguage: .swift)
961+
let newName = CrossLanguageName(clangName: nil, swiftName: request.newName, definitionLanguage: .swift)
962+
var edits = try await editsToRename(
1013963
locations: renameLocations,
1014964
in: snapshot,
1015-
oldName: CrossLanguageName(clangName: nil, swiftName: oldName, definitionLanguage: .swift),
1016-
newName: CrossLanguageName(clangName: nil, swiftName: request.newName, definitionLanguage: .swift)
965+
oldName: oldName,
966+
newName: newName
1017967
)
1018-
1019-
try Task.checkCancellation()
968+
if let compoundSwiftName = oldName.compoundSwiftName, !compoundSwiftName.parameters.isEmpty {
969+
// If we are doing a function rename, run `renameParametersInFunctionBody` for every occurrence of the rename
970+
// location within the current file. If the location is not a function declaration, it will exit early without
971+
// invoking sourcekitd, so it's OK to do this performance-wise.
972+
for renameLocation in renameLocations {
973+
edits += await editsToRenameParametersInFunctionBody(
974+
snapshot: snapshot,
975+
renameLocation: renameLocation,
976+
newName: newName
977+
)
978+
}
979+
}
1020980

1021981
return (edits: WorkspaceEdit(changes: [snapshot.uri: edits]), usr: usr)
1022982
}
1023983

984+
public func editsToRenameParametersInFunctionBody(
985+
snapshot: DocumentSnapshot,
986+
renameLocation: RenameLocation,
987+
newName: CrossLanguageName
988+
) async -> [TextEdit] {
989+
guard let position = snapshot.absolutePosition(of: renameLocation) else {
990+
logger.fault("Failed to convert \(renameLocation.line):\(renameLocation.utf8Column) to AbsolutePosition")
991+
return []
992+
}
993+
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
994+
let token = syntaxTree.token(at: position)
995+
let parameterClause: FunctionParameterClauseSyntax?
996+
switch token?.keyPathInParent {
997+
case \FunctionDeclSyntax.name:
998+
parameterClause = token?.parent(as: FunctionDeclSyntax.self)?.signature.parameterClause
999+
case \InitializerDeclSyntax.initKeyword:
1000+
parameterClause = token?.parent(as: InitializerDeclSyntax.self)?.signature.parameterClause
1001+
case \SubscriptDeclSyntax.subscriptKeyword:
1002+
parameterClause = token?.parent(as: SubscriptDeclSyntax.self)?.parameterClause
1003+
default:
1004+
parameterClause = nil
1005+
}
1006+
guard let parameterClause else {
1007+
// We are not at a function-like definition. Nothing to rename.
1008+
return []
1009+
}
1010+
guard let newSwiftNameString = newName.swiftName else {
1011+
logger.fault(
1012+
"Cannot rename at \(renameLocation.line):\(renameLocation.utf8Column) because new name is not a Swift name"
1013+
)
1014+
return []
1015+
}
1016+
let newSwiftName = CompoundDeclName(newSwiftNameString)
1017+
1018+
var edits: [TextEdit] = []
1019+
for (index, parameter) in parameterClause.parameters.enumerated() {
1020+
guard parameter.secondName == nil else {
1021+
// The parameter has a second name. The function signature only renames the first name and the function body
1022+
// refers to the second name. Nothing to do.
1023+
continue
1024+
}
1025+
let oldParameterName = parameter.firstName.text
1026+
guard index < newSwiftName.parameters.count else {
1027+
// We don't have a new name for this parameter. Nothing to do.
1028+
continue
1029+
}
1030+
let newParameterName = newSwiftName.parameters[index].stringOrEmpty
1031+
guard !newParameterName.isEmpty else {
1032+
// We are changing the parameter to an empty name. This will retain the current external parameter name as the
1033+
// new second name, so nothing to do in the function body.
1034+
continue
1035+
}
1036+
guard newParameterName != oldParameterName else {
1037+
// This parameter wasn't modified. Nothing to do.
1038+
continue
1039+
}
1040+
1041+
let oldCrossLanguageParameterName = CrossLanguageName(
1042+
clangName: nil,
1043+
swiftName: oldParameterName,
1044+
definitionLanguage: .swift
1045+
)
1046+
let newCrossLanguageParameterName = CrossLanguageName(
1047+
clangName: nil,
1048+
swiftName: newParameterName,
1049+
definitionLanguage: .swift
1050+
)
1051+
1052+
guard let parameterPosition = snapshot.position(of: parameter.positionAfterSkippingLeadingTrivia) else {
1053+
logger.fault("Failed to convert position of \(parameter.firstName.text) to line-column")
1054+
continue
1055+
}
1056+
1057+
let parameterRenameEdits = await orLog("Renaming parameter") {
1058+
// Once we have lexical scope lookup in swift-syntax, this can be a purely syntactic rename.
1059+
// We know that the parameters are variables and thus there can't be overloads that need to be resolved by the
1060+
// type checker.
1061+
let relatedIdentifiersResponse = try await self.relatedIdentifiers(
1062+
at: parameterPosition,
1063+
in: snapshot,
1064+
includeNonEditableBaseNames: false
1065+
)
1066+
1067+
let parameterRenameLocations = relatedIdentifiersResponse.renameLocations(in: snapshot)
1068+
1069+
return try await editsToRename(
1070+
locations: parameterRenameLocations,
1071+
in: snapshot,
1072+
oldName: oldCrossLanguageParameterName,
1073+
newName: newCrossLanguageParameterName
1074+
)
1075+
}
1076+
guard let parameterRenameEdits else {
1077+
continue
1078+
}
1079+
// Exclude the edit that renames the parameter itself. The parameter gets renamed as part of the function
1080+
// declaration.
1081+
edits += parameterRenameEdits.filter { !$0.range.contains(parameterPosition) }
1082+
}
1083+
return edits
1084+
}
1085+
10241086
/// Return the edit that needs to be performed for the given syntactic rename piece to rename it from
10251087
/// `oldParameter` to `newParameter`.
10261088
/// Returns `nil` if no edit needs to be performed.
@@ -1232,6 +1294,15 @@ extension ClangLanguageServerShim {
12321294
)
12331295
return (prepareRename, symbolInfo.only?.usr)
12341296
}
1297+
1298+
public func editsToRenameParametersInFunctionBody(
1299+
snapshot: DocumentSnapshot,
1300+
renameLocation: RenameLocation,
1301+
newName: CrossLanguageName
1302+
) async -> [TextEdit] {
1303+
// When renaming a clang function name, we don't need to rename any references to the arguments.
1304+
return []
1305+
}
12351306
}
12361307

12371308
fileprivate extension SyntaxProtocol {
@@ -1240,3 +1311,18 @@ fileprivate extension SyntaxProtocol {
12401311
return parent?.as(S.self)
12411312
}
12421313
}
1314+
1315+
fileprivate extension RelatedIdentifiersResponse {
1316+
func renameLocations(in snapshot: DocumentSnapshot) -> [RenameLocation] {
1317+
return self.relatedIdentifiers.compactMap {
1318+
(relatedIdentifier) -> RenameLocation? in
1319+
let position = relatedIdentifier.range.lowerBound
1320+
guard let utf8Column = snapshot.lineTable.utf8ColumnAt(line: position.line, utf16Column: position.utf16index)
1321+
else {
1322+
logger.fault("Unable to find UTF-8 column for \(position.description, privacy: .public)")
1323+
return nil
1324+
}
1325+
return RenameLocation(line: position.line + 1, utf8Column: utf8Column + 1, usage: relatedIdentifier.usage)
1326+
}
1327+
}
1328+
}

Sources/SourceKitLSP/Swift/AdjustPositionToStartOfIdentifier.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ extension SwiftLanguageServer {
5353
in snapshot: DocumentSnapshot
5454
) async -> Position {
5555
let tree = await self.syntaxTreeManager.syntaxTree(for: snapshot)
56-
guard let swiftSyntaxPosition = snapshot.position(of: position) else {
56+
guard let swiftSyntaxPosition = snapshot.absolutePosition(of: position) else {
5757
return position
5858
}
5959
let visitor = StartOfIdentifierFinder(position: swiftSyntaxPosition)

0 commit comments

Comments
 (0)