10
10
//
11
11
//===----------------------------------------------------------------------===//
12
12
13
+ import IndexStoreDB
13
14
import LSPLogging
14
15
import LanguageServerProtocol
15
16
import SKSupport
16
17
import SourceKitD
17
18
19
+ // MARK: - Helper types
20
+
18
21
/// A parsed representation of a name that may be disambiguated by its argument labels.
19
22
///
20
23
/// ### Examples
@@ -268,12 +271,15 @@ fileprivate struct SyntacticRenameName {
268
271
}
269
272
}
270
273
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
+ }
277
283
}
278
284
279
285
private extension DocumentSnapshot {
@@ -283,16 +289,112 @@ private extension DocumentSnapshot {
283
289
}
284
290
}
285
291
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
+
286
389
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
288
391
/// edited to rename a compound decl name.
289
- ///
392
+ ///
290
393
/// - Parameters:
291
394
/// - 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
293
396
/// 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.
296
398
private func getSyntacticRenameRanges(
297
399
renameLocations: [ RenameLocation ] ,
298
400
oldName: String ,
@@ -329,7 +431,7 @@ extension SwiftLanguageServer {
329
431
return categorizedRanges. compactMap { SyntacticRenameName ( $0, in: snapshot, keys: keys) }
330
432
}
331
433
332
- public func rename( _ request: RenameRequest ) async throws -> WorkspaceEdit ? {
434
+ public func rename( _ request: RenameRequest ) async throws -> ( edits : WorkspaceEdit , usr : String ? , oldName : String ? ) {
333
435
let snapshot = try self . documentManager. latestSnapshot ( request. textDocument. uri)
334
436
335
437
let relatedIdentifiers = try await self . relatedIdentifiers (
@@ -340,7 +442,7 @@ extension SwiftLanguageServer {
340
442
guard let oldName = relatedIdentifiers. name else {
341
443
throw ResponseError . unknown ( " Running sourcekit-lsp with a version of sourcekitd that does not support rename " )
342
444
}
343
-
445
+
344
446
try Task . checkCancellation ( )
345
447
346
448
let renameLocations = relatedIdentifiers. relatedIdentifiers. compactMap { ( relatedIdentifier) -> RenameLocation ? in
@@ -352,19 +454,38 @@ extension SwiftLanguageServer {
352
454
}
353
455
return RenameLocation ( line: position. line + 1 , utf8Column: utf8Column + 1 , usage: relatedIdentifier. usage)
354
456
}
355
-
457
+
356
458
try Task . checkCancellation ( )
357
459
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 ( )
359
468
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)
363
474
}
364
475
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
+ )
367
487
let oldName = try CompoundDeclName ( oldNameString)
488
+ let newName = try CompoundDeclName ( newNameString)
368
489
369
490
try Task . checkCancellation ( )
370
491
@@ -461,13 +582,20 @@ extension SwiftLanguageServer {
461
582
}
462
583
}
463
584
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 " )
472
600
}
473
601
}
0 commit comments