mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-10-05 11:35:18 +02:00
This is the first syntactic refactoring action that needs to perform a cursor info request on `codeAction/resolve`, so the majority of this PR is to add infrastructure for that. Based on https://github.com/swiftlang/sourcekit-lsp/pull/2496. Co-Authored-By: Padmashree S S <padmashreess2006@gmail.com>
168 lines
6.4 KiB
Swift
168 lines
6.4 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2026 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
internal import LanguageServerProtocol
|
|
internal import SourceKitLSP
|
|
import SwiftBasicFormat
|
|
import SwiftExtensions
|
|
import SwiftOperators
|
|
import SwiftRefactor
|
|
import SwiftSyntax
|
|
|
|
struct SwapBinaryOperands: SyntaxRefactoringCodeActionProvider {
|
|
package static let title: String = "Swap operands"
|
|
|
|
package static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> ExprSyntax? {
|
|
guard let node = scope.innermostNodeContainingRange else {
|
|
return nil
|
|
}
|
|
|
|
guard
|
|
let opExpr = node.findParentOfSelf(
|
|
ofType: ExprSyntax.self,
|
|
stoppingIf: { $0.is(CodeBlockItemSyntax.self) },
|
|
matching: { $0.is(BinaryOperatorExprSyntax.self) }
|
|
)?.as(BinaryOperatorExprSyntax.self)
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
// Only offer the refactoring when the operator participates in a
|
|
// binary expression that can be folded into an InfixOperatorExprSyntax.
|
|
guard
|
|
opExpr.parent?.is(InfixOperatorExprSyntax.self) == true
|
|
|| opExpr.parent?.as(ExprListSyntax.self)?.parent?.is(SequenceExprSyntax.self) == true
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
// Only offer the refactoring when the cursor or selection targets the
|
|
// operator token. This prevents offering the action when the cursor is placed on
|
|
// either operand or in surrounding trivia.
|
|
let tokenRange = opExpr.operator.trimmedRange
|
|
guard scope.range.overlapsOrTouches(tokenRange) else {
|
|
return nil
|
|
}
|
|
|
|
return ExprSyntax(opExpr)
|
|
}
|
|
|
|
package static func textRefactor(syntax opExpr: ExprSyntax, in context: Void) throws -> [SourceEdit] {
|
|
guard let binOp = opExpr.as(BinaryOperatorExprSyntax.self) else {
|
|
throw RefactoringNotApplicableError("Selected expression is not a binary operator")
|
|
}
|
|
|
|
// Locate the smallest expression that may contain the operator under the cursor.
|
|
// SequenceExprSyntax needs to be folded before precedence-aware rewriting can occur.
|
|
let exprToFold: ExprSyntax
|
|
if let infixExpr = binOp.parent?.as(InfixOperatorExprSyntax.self) {
|
|
exprToFold = ExprSyntax(infixExpr)
|
|
} else if let seqExpr = binOp.parent?.as(ExprListSyntax.self)?.parent?.as(SequenceExprSyntax.self) {
|
|
exprToFold = ExprSyntax(seqExpr)
|
|
} else {
|
|
throw RefactoringNotApplicableError("Could not find an infix or sequence expression to fold")
|
|
}
|
|
|
|
let foldedExpr: ExprSyntax
|
|
|
|
// Fold operator precedence so nested expressions such as
|
|
// `1 + -2 * 5` become a structured tree where the selected
|
|
// infix operator can be identified reliably.
|
|
if exprToFold.is(SequenceExprSyntax.self) {
|
|
guard let folded = OperatorTable.standardOperators.foldAll(exprToFold, errorHandler: { _ in }).as(ExprSyntax.self)
|
|
else {
|
|
throw RefactoringNotApplicableError("Failed to fold operator sequence")
|
|
}
|
|
foldedExpr = folded
|
|
} else {
|
|
foldedExpr = exprToFold.detached
|
|
}
|
|
|
|
let currentOperatorText = binOp.operator.text
|
|
|
|
// Calculate the relative UTF-8 offset.
|
|
let targetRelativeOffset =
|
|
binOp.operator.positionAfterSkippingLeadingTrivia.utf8Offset - exprToFold.position.utf8Offset
|
|
let targetPosition = AbsolutePosition(utf8Offset: foldedExpr.position.utf8Offset + targetRelativeOffset)
|
|
|
|
guard
|
|
let token = foldedExpr.token(at: targetPosition),
|
|
let infixExpr = token.parent?.as(BinaryOperatorExprSyntax.self)?.parent?.as(InfixOperatorExprSyntax.self)
|
|
else {
|
|
throw RefactoringNotApplicableError("Could not locate the target operator in the folded tree")
|
|
}
|
|
|
|
// Avoid offering the refactoring for malformed expressions such as
|
|
// `a +` or `+ b` while the user is still typing.
|
|
guard !infixExpr.hasError else {
|
|
throw RefactoringNotApplicableError("Expression is malformed or incomplete")
|
|
}
|
|
|
|
let newOperatorText: String
|
|
|
|
// Comparison operators must be inverted when operands are swapped.
|
|
// Symmetric operators can be reused unchanged.
|
|
switch currentOperatorText {
|
|
case "<": newOperatorText = ">"
|
|
case ">": newOperatorText = "<"
|
|
case "<=": newOperatorText = ">="
|
|
case ">=": newOperatorText = "<="
|
|
case "+", "*", "==", "!=", "===", "!==", "&&", "||", "&", "|", "^":
|
|
newOperatorText = currentOperatorText
|
|
default:
|
|
throw RefactoringNotApplicableError("Operator '\(currentOperatorText)' cannot be swapped")
|
|
}
|
|
|
|
let leftOperand = infixExpr.leftOperand
|
|
let rightOperand = infixExpr.rightOperand
|
|
|
|
// Preserve operand trivia so whitespace and comments remain attached
|
|
// to the same side of the expression after swapping.
|
|
var newLeft = rightOperand
|
|
newLeft.leadingTrivia = leftOperand.leadingTrivia
|
|
newLeft.trailingTrivia = leftOperand.trailingTrivia
|
|
|
|
var newRight = leftOperand
|
|
newRight.leadingTrivia = rightOperand.leadingTrivia
|
|
newRight.trailingTrivia = rightOperand.trailingTrivia
|
|
|
|
guard let targetBinOp = infixExpr.operator.as(BinaryOperatorExprSyntax.self) else {
|
|
throw RefactoringNotApplicableError("Failed to cast operator to BinaryOperatorExprSyntax")
|
|
}
|
|
let newToken = targetBinOp.operator.with(\.tokenKind, .binaryOperator(newOperatorText))
|
|
let newOperatorExpr = ExprSyntax(targetBinOp.with(\.operator, newToken))
|
|
|
|
let newInfix =
|
|
infixExpr
|
|
.with(\.leftOperand, newLeft)
|
|
.with(\.operator, newOperatorExpr)
|
|
.with(\.rightOperand, newRight)
|
|
|
|
// Calculate the absolute range of the targeted expression in the original source
|
|
// by applying its relative offset from the detached folded tree.
|
|
let rootOffset = exprToFold.position.utf8Offset
|
|
let foldedRootOffset = foldedExpr.position.utf8Offset
|
|
|
|
let startOffset = rootOffset + (infixExpr.position.utf8Offset - foldedRootOffset)
|
|
let endOffset = rootOffset + (infixExpr.endPosition.utf8Offset - foldedRootOffset)
|
|
|
|
let editRange = AbsolutePosition(utf8Offset: startOffset)..<AbsolutePosition(utf8Offset: endOffset)
|
|
|
|
return [
|
|
SourceEdit(
|
|
range: editRange,
|
|
replacement: newInfix.description
|
|
)
|
|
]
|
|
}
|
|
}
|