Files
sourcekit-lsp/Sources/SwiftLanguageService/RefactoringResponse.swift
2025-12-02 12:27:27 +00:00

146 lines
5.6 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import Csourcekitd
@_spi(SourceKitLSP) import LanguageServerProtocol
@_spi(SourceKitLSP) import SKLogging
import SKUtilities
import SourceKitD
import SourceKitLSP
protocol RefactoringResponse {
init(title: String, uri: DocumentURI, refactoringEdits: [RefactoringEdit])
}
extension [RefactoringEdit] {
init?(_ dict: SKDResponseDictionary, _ snapshot: DocumentSnapshot, _ keys: sourcekitd_api_keys) {
guard let categorizedEdits: SKDResponseArray = dict[keys.categorizedEdits] else {
logger.fault("categorizedEdits doesn't exist in response dictionary")
return nil
}
self = []
// swift-format-ignore: ReplaceForEachWithForLoop
// Reference is to `SKDResponseArray.forEach`, not `Array.forEach`.
categorizedEdits.forEach { _, categorizedEdit in
guard let edits: SKDResponseArray = categorizedEdit[keys.edits] else {
logger.fault("edits doesn't exist in categorizedEdit dictionary")
return true
}
// swift-format-ignore: ReplaceForEachWithForLoop
// Reference is to `SKDResponseArray.forEach`, not `Array.forEach`.
edits.forEach { _, edit in
guard let startLine: Int = edit[keys.line],
let startColumn: Int = edit[keys.column],
let endLine: Int = edit[keys.endLine],
let endColumn: Int = edit[keys.endColumn],
let text: String = edit[keys.text]
else {
logger.fault("Failed to deserialise edit dictionary containing values: \(edit)")
return true // continue
}
// The LSP is zero based, but semantic_refactoring is one based.
let startPosition = snapshot.positionOf(
zeroBasedLine: startLine - 1,
utf8Column: startColumn - 1
)
let endPosition = snapshot.positionOf(
zeroBasedLine: endLine - 1,
utf8Column: endColumn - 1
)
// Snippets are only supported in code completion.
// Remove SourceKit placeholders in refactoring actions because they
// can't be represented in the editor properly.
let textWithSnippets = rewriteSourceKitPlaceholders(in: text, clientSupportsSnippets: false)
self.append(
RefactoringEdit(
range: startPosition..<endPosition,
newText: textWithSnippets,
bufferName: edit[keys.bufferName]
)
)
return true
}
return true
}
guard !self.isEmpty else {
logger.error("No refactoring edits found")
return nil
}
}
}
extension RefactoringResponse {
/// Create an instance of `RefactoringResponse` from a sourcekitd semantic
/// refactoring response dictionary, if possible.
///
/// - Parameters:
/// - title: The title of the refactoring action.
/// - dict: Response dictionary to extract information from.
/// - snapshot: The snapshot that triggered the `semantic_refactoring` request.
/// - keys: The sourcekitd key set to use for looking up into `dict`.
init?(_ title: String, _ dict: SKDResponseDictionary, _ snapshot: DocumentSnapshot, _ keys: sourcekitd_api_keys) {
guard let refactoringEdits = [RefactoringEdit](dict, snapshot, keys) else {
return nil
}
self.init(title: title, uri: snapshot.uri, refactoringEdits: refactoringEdits)
}
}
extension SwiftLanguageService {
/// Provides detailed information about the result of a specific refactoring
/// operation.
///
/// Wraps the information returned by sourcekitd's `semantic_refactoring`
/// request, such as the necessary edits and placeholder locations.
///
/// - Parameters:
/// - refactorCommand: The semantic `RefactorCommand` that triggered this request.
/// - Returns: The response of the refactoring
func refactoring(
_ refactorCommand: SemanticRefactorCommand
) async throws -> SemanticRefactoring {
let keys = self.keys
let uri = refactorCommand.textDocument.uri
let snapshot = try self.documentManager.latestSnapshot(uri)
let line = refactorCommand.positionRange.lowerBound.line
let utf16Column = refactorCommand.positionRange.lowerBound.utf16index
let utf8Column = snapshot.lineTable.utf8ColumnAt(line: line, utf16Column: utf16Column)
let skreq = sourcekitd.dictionary([
keys.cancelOnSubsequentRequest: 0,
// Preferred name for e.g. an extracted variable.
// Empty string means sourcekitd chooses a name automatically.
keys.name: "",
keys.sourceFile: uri.pseudoPath,
// LSP is zero based, but this request is 1 based.
keys.line: line + 1,
keys.column: utf8Column + 1,
keys.length: snapshot.utf8OffsetRange(of: refactorCommand.positionRange).count,
keys.actionUID: self.sourcekitd.api.uid_get_from_cstr(refactorCommand.actionString)!,
keys.compilerArgs: await self.compileCommand(for: snapshot.uri, fallbackAfterTimeout: true)?.compilerArgs
as [any SKDRequestValue]?,
])
let dict = try await send(sourcekitdRequest: \.semanticRefactoring, skreq, snapshot: snapshot)
guard let refactor = SemanticRefactoring(refactorCommand.title, dict, snapshot, self.keys) else {
throw SemanticRefactoringError.noEditsNeeded(uri)
}
return refactor
}
}