Skip to content

Commit 441a369

Browse files
authored
Merge pull request #1031 from ahoppen/ahoppen/clang-rename
Support cross-file rename for clang languages
2 parents 6b6228d + bfb9040 commit 441a369

File tree

4 files changed

+195
-7
lines changed

4 files changed

+195
-7
lines changed

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ add_library(LanguageServerProtocol STATIC
5656
Requests/FormattingRequests.swift
5757
Requests/HoverRequest.swift
5858
Requests/ImplementationRequest.swift
59+
Requests/IndexedRenameRequest.swift
5960
Requests/InitializeRequest.swift
6061
Requests/InlayHintRefreshRequest.swift
6162
Requests/InlayHintRequest.swift
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// Rename all occurrences of a symbol named `oldName` to `newName` at the
14+
/// given `positions`.
15+
///
16+
/// The use case of this method is for when the positions to rename are already
17+
/// known, eg. from an index lookup outside of clangd's built-in index. In
18+
/// particular, it determines the edits necessary to rename multi-piece
19+
/// Objective-C selector names.
20+
///
21+
/// `textDocument` is used to determine the language options for the symbol to
22+
/// rename, eg. to decide whether `oldName` and `newName` are Objective-C
23+
/// selectors or normal identifiers.
24+
///
25+
/// This is a clangd extension.
26+
public struct IndexedRenameRequest: TextDocumentRequest, Hashable {
27+
public static let method: String = "workspace/indexedRename"
28+
public typealias Response = WorkspaceEdit?
29+
30+
/// The document in which the declaration to rename is declared. Its compiler
31+
/// arguments are used to infer language settings for the rename.
32+
public var textDocument: TextDocumentIdentifier
33+
34+
/// The old name of the symbol.
35+
public var oldName: String
36+
37+
/// The new name of the symbol.
38+
public var newName: String
39+
40+
/// The positions at which the symbol is known to appear and that should be
41+
/// renamed. The key is a document URI
42+
public var positions: [DocumentURI: [Position]]
43+
44+
public init(
45+
textDocument: TextDocumentIdentifier,
46+
oldName: String,
47+
newName: String,
48+
positions: [DocumentURI: [Position]]
49+
) {
50+
self.textDocument = textDocument
51+
self.oldName = oldName
52+
self.newName = newName
53+
self.positions = positions
54+
}
55+
}
56+
57+
// Workaround for Codable not correctly encoding dictionaries whose keys aren't strings.
58+
extension IndexedRenameRequest: Codable {
59+
private enum CodingKeys: CodingKey {
60+
case textDocument
61+
case oldName
62+
case newName
63+
case positions
64+
}
65+
66+
public init(from decoder: Decoder) throws {
67+
let container = try decoder.container(keyedBy: CodingKeys.self)
68+
69+
self.textDocument = try container.decode(
70+
TextDocumentIdentifier.self,
71+
forKey: IndexedRenameRequest.CodingKeys.textDocument
72+
)
73+
self.oldName = try container.decode(String.self, forKey: IndexedRenameRequest.CodingKeys.oldName)
74+
self.newName = try container.decode(String.self, forKey: IndexedRenameRequest.CodingKeys.newName)
75+
self.positions = try container.decode([String: [Position]].self, forKey: .positions).mapKeys(DocumentURI.init)
76+
}
77+
78+
public func encode(to encoder: Encoder) throws {
79+
var container = encoder.container(keyedBy: CodingKeys.self)
80+
81+
try container.encode(self.textDocument, forKey: IndexedRenameRequest.CodingKeys.textDocument)
82+
try container.encode(self.oldName, forKey: IndexedRenameRequest.CodingKeys.oldName)
83+
try container.encode(self.newName, forKey: IndexedRenameRequest.CodingKeys.newName)
84+
try container.encode(self.positions.mapKeys(\.stringValue), forKey: IndexedRenameRequest.CodingKeys.positions)
85+
86+
}
87+
}
88+
89+
fileprivate extension Dictionary {
90+
func mapKeys<NewKeyType: Hashable>(_ transform: (Key) -> NewKeyType) -> [NewKeyType: Value] {
91+
return [NewKeyType: Value](uniqueKeysWithValues: self.map { (transform($0.key), $0.value) })
92+
}
93+
}

Sources/SourceKitLSP/Rename.swift

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -608,18 +608,40 @@ extension SwiftLanguageServer {
608608
// MARK: - Clang
609609

610610
extension ClangLanguageServerShim {
611-
func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?, oldName: String?) {
612-
let edits = try await forwardRequestToClangd(request)
613-
return (edits ?? WorkspaceEdit(), nil, nil)
611+
func rename(_ renameRequest: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?, oldName: String?) {
612+
async let edits = forwardRequestToClangd(renameRequest)
613+
let symbolInfoRequest = SymbolInfoRequest(
614+
textDocument: renameRequest.textDocument,
615+
position: renameRequest.position
616+
)
617+
let symbolDetail = try await forwardRequestToClangd(symbolInfoRequest).only
618+
return (try await edits ?? WorkspaceEdit(), symbolDetail?.usr, symbolDetail?.name)
614619
}
615620

616621
func editsToRename(
617622
locations renameLocations: [RenameLocation],
618623
in snapshot: DocumentSnapshot,
619-
oldName oldNameString: String,
624+
oldName: String,
620625
newName: String
621626
) async throws -> [TextEdit] {
622-
throw ResponseError.internalError("Global rename not implemented for clangd")
627+
let positions = [
628+
snapshot.uri: renameLocations.compactMap {
629+
snapshot.positionOf(zeroBasedLine: $0.line - 1, utf8Column: $0.utf8Column - 1)
630+
}
631+
]
632+
let request = IndexedRenameRequest(
633+
textDocument: TextDocumentIdentifier(snapshot.uri),
634+
oldName: oldName,
635+
newName: newName,
636+
positions: positions
637+
)
638+
do {
639+
let edits = try await forwardRequestToClangd(request)
640+
return edits?.changes?[snapshot.uri] ?? []
641+
} catch {
642+
logger.error("Failed to get indexed rename edits: \(error.forLogging)")
643+
return []
644+
}
623645
}
624646

625647
public func prepareRename(_ request: PrepareRenameRequest) async throws -> PrepareRenameResponse? {

Tests/SourceKitLSPTests/RenameTests.swift

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ private func apply(edits: [TextEdit], to source: String) -> String {
3636
/// Test that applying the edits returned from the requests always result in `expected`.
3737
private func assertSingleFileRename(
3838
_ markedSource: String,
39+
language: Language? = nil,
3940
newName: String,
4041
expected: String,
4142
testName: String = #function,
@@ -44,7 +45,7 @@ private func assertSingleFileRename(
4445
) async throws {
4546
let testClient = try await TestSourceKitLSPClient()
4647
let uri = DocumentURI.for(.swift, testName: testName)
47-
let positions = testClient.openDocument(markedSource, uri: uri)
48+
let positions = testClient.openDocument(markedSource, uri: uri, language: language)
4849
for marker in positions.allMarkers {
4950
let response: WorkspaceEdit?
5051
do {
@@ -111,6 +112,7 @@ private func assertRenamedSourceMatches(
111112
/// to be placed in a state where there are in-memory changes that haven't been written to disk yet.
112113
private func assertMultiFileRename(
113114
files: [RelativeFileLocation: String],
115+
language: Language? = nil,
114116
newName: String,
115117
expected: [RelativeFileLocation: String],
116118
manifest: String = SwiftPMTestWorkspace.defaultPackageManifest,
@@ -131,7 +133,7 @@ private func assertMultiFileRename(
131133
if markers.isEmpty {
132134
continue
133135
}
134-
let (uri, positions) = try ws.openDocument(fileLocation.fileName)
136+
let (uri, positions) = try ws.openDocument(fileLocation.fileName, language: language)
135137
defer {
136138
ws.testClient.send(DidCloseTextDocumentNotification(textDocument: TextDocumentIdentifier(uri)))
137139
}
@@ -759,4 +761,74 @@ final class RenameTests: XCTestCase {
759761
XCTAssertEqual(range, positions["1️⃣"]..<positions["2️⃣"])
760762
XCTAssertEqual(placeholder, "foo(a:b:)")
761763
}
764+
765+
func testGlobalRenameC() async throws {
766+
try await assertMultiFileRename(
767+
files: [
768+
"Sources/MyLibrary/include/lib.h": """
769+
void 1️⃣do2️⃣Stuff();
770+
""",
771+
"lib.c": """
772+
#include "lib.h"
773+
774+
void 3️⃣doStuff() {
775+
4️⃣doStuff();
776+
}
777+
""",
778+
],
779+
language: .c,
780+
newName: "doRecursiveStuff",
781+
expected: [
782+
"Sources/MyLibrary/include/lib.h": """
783+
void doRecursiveStuff();
784+
""",
785+
"lib.c": """
786+
#include "lib.h"
787+
788+
void doRecursiveStuff() {
789+
doRecursiveStuff();
790+
}
791+
""",
792+
]
793+
)
794+
}
795+
796+
func testGlobalRenameObjC() async throws {
797+
try await assertMultiFileRename(
798+
files: [
799+
"Sources/MyLibrary/include/lib.h": """
800+
@interface Foo
801+
- (int)1️⃣perform2️⃣Action:(int)action 3️⃣wi4️⃣th:(int)value;
802+
@end
803+
""",
804+
"lib.m": """
805+
#include "lib.h"
806+
807+
@implementation Foo
808+
- (int)5️⃣performAction:(int)action 6️⃣with:(int)value {
809+
return [self 7️⃣performAction:action 8️⃣with:value];
810+
}
811+
@end
812+
""",
813+
],
814+
language: .objective_c,
815+
newName: "performNewAction:by:",
816+
expected: [
817+
"Sources/MyLibrary/include/lib.h": """
818+
@interface Foo
819+
- (int)performNewAction:(int)action by:(int)value;
820+
@end
821+
""",
822+
"lib.m": """
823+
#include "lib.h"
824+
825+
@implementation Foo
826+
- (int)performNewAction:(int)action by:(int)value {
827+
return [self performNewAction:action by:value];
828+
}
829+
@end
830+
""",
831+
]
832+
)
833+
}
762834
}

0 commit comments

Comments
 (0)