Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ extension ConvertCommentToDocComment: SyntaxRefactoringCodeActionProvider {
static let title = "Convert Comment to Doc Comment"

static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> DeclSyntax? {
let cursorPosition = scope.snapshot.absolutePosition(of: scope.request.range.lowerBound)
let cursorPosition = scope.snapshot.absolutePosition(of: scope.requestedRange.lowerBound)
guard let token = scope.file.token(at: cursorPosition) else {
return nil
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//===----------------------------------------------------------------------===//
//
// 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 Foundation
@_spi(SourceKitLSP) import LanguageServerProtocol
import SourceKitLSP
import SwiftExtensions
import SwiftRefactor
import SwiftSyntax
import SwiftSyntaxBuilder

extension ConvertStoredPropertyToComputed: SyntaxRefactoringCodeActionProvider {
static let title: String = "Convert Stored Property to Computed Property"

static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> VariableDeclSyntax? {
return scope.innermostNodeContainingRange?.as(VariableDeclSyntax.self)
?? scope.innermostNodeContainingRange?.parent?.as(VariableDeclSyntax.self)
}

static func refactoringContext(
for node: VariableDeclSyntax,
in scope: SyntaxCodeActionScope
) async -> SyntaxCodeActionContextResult<Context> {
guard node.bindings.contains(where: { $0.typeAnnotation?.type == nil }) else {
// All types are syntactically specified, we don't need to resolve the semantic type
return .context(Context())
}
guard let binding = node.bindings.only,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier
else {
// We can only resolve type information for a single variable binding at the moment. If this is variable decl with multiple bindings, still
// offer the refactoring action and introduce placeholders for the type annotation.
return .context(Context())
}
if scope.resolveSupport?.canResolveEdit ?? false {
return .resolveEditLazily
}
// Cursor info reports type as `_` if it cannot determine the type.
if let type = try? await scope.cursorInfo(at: scope.snapshot.position(of: identifier.position)).only?.typeName,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in #2588 (comment), I don't think we should allow arbitrary cursorInfo in textDocument/codeAction request.
If the client don't support codeAction/resolve for .edit, imo we should drop this action.

type != "_"
{
return .context(Context(type: "\(raw: type)"))
}
return .context(Context())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,14 @@ extension SwiftLanguageService {

let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
guard
let node = SyntaxCodeActionScope(snapshot: snapshot, syntaxTree: syntaxTree, request: request)?
.innermostNodeContainingRange,
let node = SyntaxCodeActionScope(
resolveSupport: nil,
snapshot: snapshot,
syntaxTree: syntaxTree,
requestedRange: request.range,
swiftLanguageService: self
)?
.innermostNodeContainingRange,
node.findParentOfSelf(ofType: ImportDeclSyntax.self, stoppingIf: { _ in false }) != nil
else {
// Only offer the remove unused imports code action on an import statement.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,50 +16,88 @@ import SourceKitLSP
import SwiftRefactor
import SwiftSyntax

extension TextDocumentClientCapabilities.CodeAction.ResolveSupportProperties {
var canResolveEdit: Bool {
return self.properties.contains("edit")
}
}

/// Describes types that provide one or more code actions based on purely
/// syntactic information.
protocol SyntaxCodeActionProvider: SendableMetatype {
/// Produce code actions within the given scope. Each code action
/// corresponds to one syntactic transformation that can be performed, such
/// as adding or removing separators from an integer literal.
static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction]
static func codeActions(in scope: SyntaxCodeActionScope) async -> [CodeAction]
}

/// Defines the scope in which a syntactic code action occurs.
struct SyntaxCodeActionScope {
/// Whether the client supports the codeAction/resolve request.
///
/// This is set to `nil` during the `codeAction/resolve` request.
var resolveSupport: TextDocumentClientCapabilities.CodeAction.ResolveSupportProperties?

/// The snapshot of the document on which the code actions will be evaluated.
var snapshot: DocumentSnapshot

/// The actual code action request, which can specify additional parameters
/// to guide the code actions.
var request: CodeActionRequest

/// The source file in which the syntactic code action will operate.
var file: SourceFileSyntax

/// The originally requested range in the original code action request.
///
/// Generally, `range` should be preferred because it performs useful adjustments to extend the range to the start and end of tokens.
var requestedRange: Range<Position>

/// The UTF-8 byte range in the source file in which code actions should be
/// considered, i.e., where the cursor or selection is.
var range: Range<AbsolutePosition>

/// The innermost node that contains the entire selected source range
var innermostNodeContainingRange: Syntax?

/// The language service from which this code action is going to be resolved.
///
/// Used to to retrieve cursor info if necessary.
private let swiftLanguageService: SwiftLanguageService

init?(
resolveSupport: TextDocumentClientCapabilities.CodeAction.ResolveSupportProperties?,
snapshot: DocumentSnapshot,
syntaxTree file: SourceFileSyntax,
request: CodeActionRequest
requestedRange: Range<Position>,
swiftLanguageService: SwiftLanguageService
) {
self.resolveSupport = resolveSupport
self.snapshot = snapshot
self.request = request
self.requestedRange = requestedRange
self.file = file

guard let left = tokenForRefactoring(at: request.range.lowerBound, snapshot: snapshot, syntaxTree: file),
let right = tokenForRefactoring(at: request.range.upperBound, snapshot: snapshot, syntaxTree: file)
guard let left = tokenForRefactoring(at: requestedRange.lowerBound, snapshot: snapshot, syntaxTree: file),
let right = tokenForRefactoring(at: requestedRange.upperBound, snapshot: snapshot, syntaxTree: file)
else {
return nil
}
self.range = left.position..<right.endPosition
self.innermostNodeContainingRange = findCommonAncestorOrSelf(Syntax(left), Syntax(right))
self.swiftLanguageService = swiftLanguageService
}

/// Retrieve the cursor info in the code action's document at the given position.
///
/// Because this can be an expensive operation, this should only be called after all syntactic checks and if the request does not support lazily
/// resolving of the `edit` properties.
func cursorInfo(at position: Position) async throws -> [CursorInfo] {
let compileCommand = await swiftLanguageService.compileCommand(
for: snapshot.uri,
fallbackAfterTimeout: true
)

return try await swiftLanguageService.cursorInfo(
snapshot,
compileCommand: compileCommand,
position..<position
).cursorInfo
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let allSyntaxCodeActions: [any SyntaxCodeActionProvider.Type] = {
ConvertIfLetToGuard.self,
ConvertIntegerLiteral.self,
ConvertJSONToCodableStruct.self,
ConvertStoredPropertyToComputed.self,
ConvertStringConcatenationToStringInterpolation.self,
ConvertZeroParameterFunctionToComputedProperty.self,
FormatRawStringLiteral.self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,44 @@
//===----------------------------------------------------------------------===//

@_spi(SourceKitLSP) import LanguageServerProtocol
@_spi(SourceKitLSP) import SKLogging
import SourceKitLSP
import SwiftRefactor
import SwiftSyntax

/// Data that is included in a `CodeAction` response for which the client should resolve the edit lazily using a `codeAction/resolve` request.
///
/// This data allows us to re-construct the `SyntaxCodeActionScope`.
struct UnresolvedCodeActionData: Codable, LSPAnyCodable {
/// A string representation of the syntax refactoring action's type.
let action: String

/// The document on which the code action should be applied.
let document: VersionedTextDocumentIdentifier

/// The range at which the code action was originally requested.
let range: Range<Position>

init<Metatype: SyntaxRefactoringCodeActionProvider>(
actionType: Metatype.Type,
document: VersionedTextDocumentIdentifier,
range: Range<Position>,
) {
self.action = "\(Metatype.self)"
self.document = document
self.range = range
}
}

enum SyntaxCodeActionContextResult<Context> {
/// The cont
case context(Context)
/// Report a code action without the `edit` properties. The client is expected to send a `codeAction/resolve` request to resolve the edit.
///
/// Must only be returned if the the client can resolve edits.
case resolveEditLazily
}

/// Protocol that adapts a SyntaxRefactoringProvider (that comes from
/// swift-syntax) into a SyntaxCodeActionProvider.
protocol SyntaxRefactoringCodeActionProvider: SyntaxCodeActionProvider, EditRefactoringProvider {
Expand All @@ -24,18 +58,45 @@ protocol SyntaxRefactoringCodeActionProvider: SyntaxCodeActionProvider, EditRefa
/// scope.
static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> Input?

static func refactoringContext(for scope: SyntaxCodeActionScope) -> Context
/// Retrieve the refactoring context to refactor the given node in the given scope.
///
/// Throwing an error from this method causes the code action to be reported without any workspace edits. The client is expected to send a
/// `codeAction/resolve` request when the user selects the code action in order to retrieve the semantic information and compute the actual edits.
static func refactoringContext(
for node: Input,
in scope: SyntaxCodeActionScope
) async -> SyntaxCodeActionContextResult<Context>
}

/// SyntaxCodeActionProviders with a \c Void context can automatically be
/// adapted provide a code action based on their refactoring operation.
extension SyntaxRefactoringCodeActionProvider {
static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] {
static func codeActions(in scope: SyntaxCodeActionScope) async -> [CodeAction] {
guard let node = nodeToRefactor(in: scope) else {
return []
}

guard let sourceEdits = try? Self.textRefactor(syntax: node, in: refactoringContext(for: scope)) else {
let context: Context
switch await refactoringContext(for: node, in: scope) {
case .context(let c): context = c
case .resolveEditLazily:
guard scope.resolveSupport?.canResolveEdit ?? false else {
logger.fault(
"Refactoring action \(Self.self) requested lazy resolution of edits but client cannot resolve edits"
)
return []
}
return [
CodeAction(
title: Self.title,
kind: .refactorInline,
data: UnresolvedCodeActionData(
actionType: Self.self,
document: VersionedTextDocumentIdentifier(scope.snapshot.uri, version: scope.snapshot.version),
range: scope.requestedRange
).encodeToLSPAny()
)
]
}
guard let sourceEdits = try? Self.textRefactor(syntax: node, in: context) else {
return []
}

Expand All @@ -53,9 +114,14 @@ extension SyntaxRefactoringCodeActionProvider {
}
}

/// SyntaxCodeActionProviders with a `Void` context can automatically be adapted provide a code action based on their
/// refactoring operation.
extension SyntaxRefactoringCodeActionProvider where Context == Void {
static func refactoringContext(for scope: SyntaxCodeActionScope) -> Context {
return ()
static func refactoringContext(
for node: Input,
in scope: SyntaxCodeActionScope
) -> SyntaxCodeActionContextResult<Context> {
return .context(())
}
}

Expand Down
6 changes: 6 additions & 0 deletions Sources/SwiftLanguageService/CursorInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ struct CursorInfo {
/// name and USR.
var symbolInfo: SymbolDetails

/// A human-readable string representation of the symbol at the given location, eg. a variable's type.
var typeName: String?

/// The annotated declaration XML string.
var annotatedDeclaration: String?

Expand All @@ -39,10 +42,12 @@ struct CursorInfo {

init(
_ symbolInfo: SymbolDetails,
typeName: String?,
annotatedDeclaration: String?,
documentation: String?
) {
self.symbolInfo = symbolInfo
self.typeName = typeName
self.annotatedDeclaration = annotatedDeclaration
self.documentation = documentation
}
Expand Down Expand Up @@ -107,6 +112,7 @@ struct CursorInfo {
receiverUsrs: dict[keys.receivers]?.compactMap { $0[keys.usr] as String? } ?? [],
systemModule: module
),
typeName: dict[keys.typeName],
annotatedDeclaration: dict[keys.annotatedDecl],
documentation: dict[keys.docComment]
)
Expand Down
Loading