Skip to content

Commit 997ef38

Browse files
committed
Support rename across Swift files
rdar://118995700
1 parent 34a36b4 commit 997ef38

File tree

10 files changed

+418
-71
lines changed

10 files changed

+418
-71
lines changed

Sources/SKSupport/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ add_library(SKSupport STATIC
44
AsyncUtils.swift
55
BuildConfiguration.swift
66
ByteString.swift
7+
Collection+Only.swift
78
Connection+Send.swift
89
dlopen.swift
910
FileSystem.swift
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
public extension Collection {
14+
/// If the collection contains a single element, return it, otherwise `nil`.
15+
var only: Element? {
16+
if !isEmpty && index(after: startIndex) == endIndex {
17+
return self.first!
18+
} else {
19+
return nil
20+
}
21+
}
22+
}

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ add_library(SourceKitLSP STATIC
44
DocumentManager.swift
55
IndexStoreDB+MainFilesProvider.swift
66
ResponseError+Init.swift
7+
Rename.swift
78
Sequence+AsyncMap.swift
89
SourceKitIndexDelegate.swift
910
SourceKitLSPCommandMetadata.swift
@@ -25,7 +26,6 @@ target_sources(SourceKitLSP PRIVATE
2526
Swift/EditorPlaceholder.swift
2627
Swift/OpenInterface.swift
2728
Swift/RelatedIdentifiers.swift
28-
Swift/Rename.swift
2929
Swift/SemanticRefactorCommand.swift
3030
Swift/SemanticRefactoring.swift
3131
Swift/SemanticTokens.swift

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -597,10 +597,6 @@ extension ClangLanguageServerShim {
597597
func executeCommand(_ req: ExecuteCommandRequest) async throws -> LSPAny? {
598598
return try await forwardRequestToClangd(req)
599599
}
600-
601-
func rename(_ request: RenameRequest) async throws -> WorkspaceEdit? {
602-
return try await forwardRequestToClangd(request)
603-
}
604600
}
605601

606602
/// Clang build settings derived from a `FileBuildSettingsChange`.

Sources/SourceKitLSP/Swift/Rename.swift renamed to Sources/SourceKitLSP/Rename.swift

Lines changed: 156 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import IndexStoreDB
1314
import LSPLogging
1415
import LanguageServerProtocol
1516
import SKSupport
1617
import SourceKitD
1718

19+
// MARK: - Helper types
20+
1821
/// A parsed representation of a name that may be disambiguated by its argument labels.
1922
///
2023
/// ### Examples
@@ -268,12 +271,15 @@ fileprivate struct SyntacticRenameName {
268271
}
269272
}
270273

271-
struct RenameLocation {
272-
/// The line of the identifier to be renamed (1-based).
273-
let line: Int
274-
/// The column of the identifier to be renamed in UTF-8 bytes (1-based).
275-
let utf8Column: Int
276-
let usage: RelatedIdentifier.Usage
274+
private extension LineTable {
275+
subscript(range: Range<Position>) -> Substring? {
276+
guard let start = self.stringIndexOf(line: range.lowerBound.line, utf16Column: range.lowerBound.utf16index),
277+
let end = self.stringIndexOf(line: range.upperBound.line, utf16Column: range.upperBound.utf16index)
278+
else {
279+
return nil
280+
}
281+
return self.content[start..<end]
282+
}
277283
}
278284

279285
private extension DocumentSnapshot {
@@ -283,16 +289,112 @@ private extension DocumentSnapshot {
283289
}
284290
}
285291

292+
private extension RenameLocation.Usage {
293+
init(roles: SymbolRole) {
294+
if roles.contains(.definition) || roles.contains(.declaration) {
295+
self = .definition
296+
} else if roles.contains(.call) {
297+
self = .call
298+
} else {
299+
self = .reference
300+
}
301+
}
302+
}
303+
304+
// MARK: - SourceKitServer
305+
306+
extension SourceKitServer {
307+
func rename(_ request: RenameRequest) async throws -> WorkspaceEdit? {
308+
let uri = request.textDocument.uri
309+
guard let workspace = await workspaceForDocument(uri: uri) else {
310+
throw ResponseError.workspaceNotOpen(uri)
311+
}
312+
guard let languageService = workspace.documentService[uri] else {
313+
return nil
314+
}
315+
316+
// Determine the local edits and the USR to rename
317+
let renameResult = try await languageService.rename(request)
318+
var edits = renameResult.edits
319+
if edits.changes == nil {
320+
// Make sure `edits.changes` is non-nil so we can force-unwrap it below.
321+
edits.changes = [:]
322+
}
323+
324+
if let usr = renameResult.usr, let oldName = renameResult.oldName, let index = workspace.index {
325+
// If we have a USR + old name, perform an index lookup to find workspace-wide symbols to rename.
326+
// First, group all occurrences of that USR by the files they occur in.
327+
var locationsByFile: [URL: [RenameLocation]] = [:]
328+
let occurrences = index.occurrences(ofUSR: usr, roles: [.declaration, .definition, .reference])
329+
for occurrence in occurrences {
330+
let url = URL(fileURLWithPath: occurrence.location.path)
331+
let renameLocation = RenameLocation(
332+
line: occurrence.location.line,
333+
utf8Column: occurrence.location.utf8Column,
334+
usage: RenameLocation.Usage(roles: occurrence.roles)
335+
)
336+
locationsByFile[url, default: []].append(renameLocation)
337+
}
338+
339+
// Now, call `editsToRename(locations:in:oldName:newName:)` on the language service to convert these ranges into
340+
// edits.
341+
await withTaskGroup(of: (DocumentURI, [TextEdit])?.self) { taskGroup in
342+
for (url, renameLocations) in locationsByFile {
343+
let uri = DocumentURI(url)
344+
if edits.changes![uri] != nil {
345+
// We already have edits for this document provided by the language service, so we don't need to compute
346+
// rename ranges for it.
347+
continue
348+
}
349+
taskGroup.addTask {
350+
// Create a document snapshot to operate on. If the document is open, load it from the document manager,
351+
// otherwise conjure one from the file on disk. We need the file in memory to perform UTF-8 to UTF-16 column
352+
// conversions.
353+
// We should technically infer the language for the from-disk snapshot. But `editsToRename` doesn't care
354+
// about it, so defaulting to Swift is good enough for now
355+
// If we fail to get edits for one file, log an error and continue but don't fail rename completely.
356+
guard
357+
let snapshot = (try? self.documentManager.latestSnapshot(uri))
358+
?? (try? DocumentSnapshot(url, language: .swift))
359+
else {
360+
logger.error("Failed to get document snapshot for \(uri.forLogging)")
361+
return nil
362+
}
363+
do {
364+
let edits = try await languageService.editsToRename(
365+
locations: renameLocations,
366+
in: snapshot,
367+
oldName: oldName,
368+
newName: request.newName
369+
)
370+
return (uri, edits)
371+
} catch {
372+
logger.error("Failed to get edits for \(uri.forLogging): \(error.forLogging)")
373+
return nil
374+
}
375+
}
376+
}
377+
for await case let (uri, textEdits)? in taskGroup where !textEdits.isEmpty {
378+
precondition(edits.changes![uri] == nil, "We should create tasks for URIs that already have edits")
379+
edits.changes![uri] = textEdits
380+
}
381+
}
382+
}
383+
return edits
384+
}
385+
}
386+
387+
// MARK: - Swift
388+
286389
extension SwiftLanguageServer {
287-
/// From a list of rename locations compute the list of `SyntacticRenameName`s that define which ranges need to be
390+
/// From a list of rename locations compute the list of `SyntacticRenameName`s that define which ranges need to be
288391
/// edited to rename a compound decl name.
289-
///
392+
///
290393
/// - Parameters:
291394
/// - renameLocations: The locations to rename
292-
/// - oldName: The compound decl name that the declaration had before the rename. Used to verify that the rename
395+
/// - oldName: The compound decl name that the declaration had before the rename. Used to verify that the rename
293396
/// locations match that name. Eg. `myFunc(argLabel:otherLabel:)` or `myVar`
294-
/// - snapshot: If the document has been modified from the on-disk version, the current snapshot. `nil` to read the
295-
/// file contents from disk.
397+
/// - snapshot: A `DocumentSnapshot` containing the contents of the file for which to compute the rename ranges.
296398
private func getSyntacticRenameRanges(
297399
renameLocations: [RenameLocation],
298400
oldName: String,
@@ -329,7 +431,7 @@ extension SwiftLanguageServer {
329431
return categorizedRanges.compactMap { SyntacticRenameName($0, in: snapshot, keys: keys) }
330432
}
331433

332-
public func rename(_ request: RenameRequest) async throws -> WorkspaceEdit? {
434+
public func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?, oldName: String?) {
333435
let snapshot = try self.documentManager.latestSnapshot(request.textDocument.uri)
334436

335437
let relatedIdentifiers = try await self.relatedIdentifiers(
@@ -340,7 +442,7 @@ extension SwiftLanguageServer {
340442
guard let oldName = relatedIdentifiers.name else {
341443
throw ResponseError.unknown("Running sourcekit-lsp with a version of sourcekitd that does not support rename")
342444
}
343-
445+
344446
try Task.checkCancellation()
345447

346448
let renameLocations = relatedIdentifiers.relatedIdentifiers.compactMap { (relatedIdentifier) -> RenameLocation? in
@@ -352,19 +454,38 @@ extension SwiftLanguageServer {
352454
}
353455
return RenameLocation(line: position.line + 1, utf8Column: utf8Column + 1, usage: relatedIdentifier.usage)
354456
}
355-
457+
356458
try Task.checkCancellation()
357459

358-
let edits = try await renameRanges(from: renameLocations, in: snapshot, oldName: oldName, newName: try CompoundDeclName(request.newName))
460+
let edits = try await editsToRename(
461+
locations: renameLocations,
462+
in: snapshot,
463+
oldName: oldName,
464+
newName: request.newName
465+
)
466+
467+
try Task.checkCancellation()
359468

360-
return WorkspaceEdit(changes: [
361-
snapshot.uri: edits
362-
])
469+
let usr =
470+
(try? await self.symbolInfo(SymbolInfoRequest(textDocument: request.textDocument, position: request.position)))?
471+
.only?.usr
472+
473+
return (edits: WorkspaceEdit(changes: [snapshot.uri: edits]), usr: usr, oldName: oldName)
363474
}
364475

365-
private func renameRanges(from renameLocations: [RenameLocation], in snapshot: DocumentSnapshot, oldName oldNameString: String, newName: CompoundDeclName) async throws -> [TextEdit] {
366-
let compoundRenameRanges = try await getSyntacticRenameRanges(renameLocations: renameLocations, oldName: oldNameString, in: snapshot)
476+
public func editsToRename(
477+
locations renameLocations: [RenameLocation],
478+
in snapshot: DocumentSnapshot,
479+
oldName oldNameString: String,
480+
newName newNameString: String
481+
) async throws -> [TextEdit] {
482+
let compoundRenameRanges = try await getSyntacticRenameRanges(
483+
renameLocations: renameLocations,
484+
oldName: oldNameString,
485+
in: snapshot
486+
)
367487
let oldName = try CompoundDeclName(oldNameString)
488+
let newName = try CompoundDeclName(newNameString)
368489

369490
try Task.checkCancellation()
370491

@@ -461,13 +582,20 @@ extension SwiftLanguageServer {
461582
}
462583
}
463584

464-
extension LineTable {
465-
subscript(range: Range<Position>) -> Substring? {
466-
guard let start = self.stringIndexOf(line: range.lowerBound.line, utf16Column: range.lowerBound.utf16index),
467-
let end = self.stringIndexOf(line: range.upperBound.line, utf16Column: range.upperBound.utf16index)
468-
else {
469-
return nil
470-
}
471-
return self.content[start..<end]
585+
// MARK: - Clang
586+
587+
extension ClangLanguageServerShim {
588+
func rename(_ request: RenameRequest) async throws -> (edits: WorkspaceEdit, usr: String?, oldName: String?) {
589+
let edits = try await forwardRequestToClangd(request)
590+
return (edits ?? WorkspaceEdit(), nil, nil)
591+
}
592+
593+
func editsToRename(
594+
locations renameLocations: [RenameLocation],
595+
in snapshot: DocumentSnapshot,
596+
oldName oldNameString: String,
597+
newName: String
598+
) async throws -> [TextEdit] {
599+
throw ResponseError.internalError("Global rename not implemented for clangd")
472600
}
473601
}

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,17 +1646,6 @@ extension SourceKitServer {
16461646
return try await languageService.executeCommand(executeCommand)
16471647
}
16481648

1649-
func rename(_ request: RenameRequest) async throws -> WorkspaceEdit? {
1650-
let uri = request.textDocument.uri
1651-
guard let workspace = await workspaceForDocument(uri: uri) else {
1652-
throw ResponseError.workspaceNotOpen(uri)
1653-
}
1654-
guard let languageService = workspace.documentService[uri] else {
1655-
return nil
1656-
}
1657-
return try await languageService.rename(request)
1658-
}
1659-
16601649
func codeAction(
16611650
_ req: CodeActionRequest,
16621651
workspace: Workspace,

Sources/SourceKitLSP/Swift/RelatedIdentifiers.swift

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,11 @@ import LanguageServerProtocol
1515
import SourceKitD
1616

1717
struct RelatedIdentifier {
18-
enum Usage {
19-
/// The definition of a function/subscript/variable/...
20-
case definition
21-
22-
/// The symbol is being referenced.
23-
///
24-
/// This includes
25-
/// - References to variables
26-
/// - Unapplied references to functions (`myStruct.memberFunc`)
27-
/// - Calls to subscripts (`myArray[1]`, location is `[` here, length 1)
28-
case reference
29-
30-
/// A function that is being called.
31-
case call
32-
33-
/// Unknown name usage occurs if we don't have an entry in the index that
34-
/// tells us whether the location is a call, reference or a definition. The
35-
/// most common reasons why this happens is if the editor is adding syntactic
36-
/// results (eg. from comments or string literals).
37-
case unknown
38-
}
3918
let range: Range<Position>
40-
let usage: Usage
19+
let usage: RenameLocation.Usage
4120
}
4221

43-
extension RelatedIdentifier.Usage {
22+
extension RenameLocation.Usage {
4423
fileprivate init?(_ uid: sourcekitd_uid_t?, _ keys: sourcekitd_keys) {
4524
switch uid {
4625
case keys.syntacticRenameDefinition:
@@ -116,7 +95,7 @@ extension SwiftLanguageServer {
11695
let length: Int = value[keys.length],
11796
let end: Position = snapshot.positionOf(utf8Offset: offset + length)
11897
{
119-
let usage = RelatedIdentifier.Usage(value[keys.nameType], keys) ?? .unknown
98+
let usage = RenameLocation.Usage(value[keys.nameType], keys) ?? .unknown
12099
relatedIdentifiers.append(
121100
RelatedIdentifier(range: start..<end, usage: usage)
122101
)

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public struct SwiftCompileCommand: Equatable {
8989

9090
public actor SwiftLanguageServer: ToolchainLanguageServer {
9191
/// The ``SourceKitServer`` instance that created this `ClangLanguageServerShim`.
92-
private weak var sourceKitServer: SourceKitServer?
92+
weak var sourceKitServer: SourceKitServer?
9393

9494
let sourcekitd: SourceKitD
9595

0 commit comments

Comments
 (0)