Skip to content

Commit e6c900e

Browse files
ahoppenPadmashree06
andcommitted
Add refactoring action to convert stored to computed properties
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 swiftlang#2496. Co-Authored-By: Padmashree S S <padmashreess2006@gmail.com>
1 parent d3847f0 commit e6c900e

9 files changed

Lines changed: 371 additions & 23 deletions

Sources/SwiftLanguageService/CodeActions/ConvertCommentToDocComment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ extension ConvertCommentToDocComment: SyntaxRefactoringCodeActionProvider {
3939
static let title = "Convert Comment to Doc Comment"
4040

4141
static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> DeclSyntax? {
42-
let cursorPosition = scope.snapshot.absolutePosition(of: scope.request.range.lowerBound)
42+
let cursorPosition = scope.snapshot.absolutePosition(of: scope.requestedRange.lowerBound)
4343
guard let token = scope.file.token(at: cursorPosition) else {
4444
return nil
4545
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 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+
import Foundation
14+
@_spi(SourceKitLSP) import LanguageServerProtocol
15+
import SourceKitLSP
16+
import SwiftExtensions
17+
import SwiftRefactor
18+
import SwiftSyntax
19+
import SwiftSyntaxBuilder
20+
21+
extension ConvertStoredPropertyToComputed: SyntaxRefactoringCodeActionProvider {
22+
static let title: String = "Convert Stored Property to Computed Property"
23+
24+
static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> VariableDeclSyntax? {
25+
return scope.innermostNodeContainingRange?.as(VariableDeclSyntax.self)
26+
?? scope.innermostNodeContainingRange?.parent?.as(VariableDeclSyntax.self)
27+
}
28+
29+
static func refactoringContext(
30+
for node: VariableDeclSyntax,
31+
in scope: SyntaxCodeActionScope
32+
) async -> SyntaxCodeActionContextResult<Context> {
33+
guard node.bindings.contains(where: { $0.typeAnnotation?.type == nil }) else {
34+
// All types are syntactically specified, we don't need to resolve the semantic type
35+
return .context(Context())
36+
}
37+
guard let binding = node.bindings.only,
38+
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier
39+
else {
40+
// We can only resolve type information for a single variable binding at the moment. If this is variable decl with multiple bindings, still
41+
// offer the refactoring action and introduce placeholders for the type annotation.
42+
return .context(Context())
43+
}
44+
if scope.resolveSupport?.canResolveEdit ?? false {
45+
return .resolveEditLazily
46+
}
47+
// Cursor info reports type as `_` if it cannot determine the type.
48+
if let type = try? await scope.cursorInfo(at: scope.snapshot.position(of: identifier.position)).only?.typeName,
49+
type != "_"
50+
{
51+
return .context(Context(type: "\(raw: type)"))
52+
}
53+
return .context(Context())
54+
}
55+
}

Sources/SwiftLanguageService/CodeActions/RemoveUnusedImports.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,14 @@ extension SwiftLanguageService {
7272

7373
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
7474
guard
75-
let node = SyntaxCodeActionScope(snapshot: snapshot, syntaxTree: syntaxTree, request: request)?
76-
.innermostNodeContainingRange,
75+
let node = SyntaxCodeActionScope(
76+
resolveSupport: nil,
77+
snapshot: snapshot,
78+
syntaxTree: syntaxTree,
79+
requestedRange: request.range,
80+
swiftLanguageService: self
81+
)?
82+
.innermostNodeContainingRange,
7783
node.findParentOfSelf(ofType: ImportDeclSyntax.self, stoppingIf: { _ in false }) != nil
7884
else {
7985
// Only offer the remove unused imports code action on an import statement.

Sources/SwiftLanguageService/CodeActions/SyntaxCodeActionProvider.swift

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,50 +16,88 @@ import SourceKitLSP
1616
import SwiftRefactor
1717
import SwiftSyntax
1818

19+
extension TextDocumentClientCapabilities.CodeAction.ResolveSupportProperties {
20+
var canResolveEdit: Bool {
21+
return self.properties.contains("edit")
22+
}
23+
}
24+
1925
/// Describes types that provide one or more code actions based on purely
2026
/// syntactic information.
2127
protocol SyntaxCodeActionProvider: SendableMetatype {
2228
/// Produce code actions within the given scope. Each code action
2329
/// corresponds to one syntactic transformation that can be performed, such
2430
/// as adding or removing separators from an integer literal.
25-
static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction]
31+
static func codeActions(in scope: SyntaxCodeActionScope) async -> [CodeAction]
2632
}
2733

2834
/// Defines the scope in which a syntactic code action occurs.
2935
struct SyntaxCodeActionScope {
36+
/// Whether the client supports the codeAction/resolve request.
37+
///
38+
/// This is set to `nil` during the `codeAction/resolve` request.
39+
var resolveSupport: TextDocumentClientCapabilities.CodeAction.ResolveSupportProperties?
40+
3041
/// The snapshot of the document on which the code actions will be evaluated.
3142
var snapshot: DocumentSnapshot
3243

33-
/// The actual code action request, which can specify additional parameters
34-
/// to guide the code actions.
35-
var request: CodeActionRequest
36-
3744
/// The source file in which the syntactic code action will operate.
3845
var file: SourceFileSyntax
3946

47+
/// The originally requested range in the original code action request.
48+
///
49+
/// Generally, `range` should be preferred because it performs useful adjustments to extend the range to the start and end of tokens.
50+
var requestedRange: Range<Position>
51+
4052
/// The UTF-8 byte range in the source file in which code actions should be
4153
/// considered, i.e., where the cursor or selection is.
4254
var range: Range<AbsolutePosition>
4355

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

59+
/// The language service from which this code action is going to be resolved.
60+
///
61+
/// Used to to retrieve cursor info if necessary.
62+
private let swiftLanguageService: SwiftLanguageService
63+
4764
init?(
65+
resolveSupport: TextDocumentClientCapabilities.CodeAction.ResolveSupportProperties?,
4866
snapshot: DocumentSnapshot,
4967
syntaxTree file: SourceFileSyntax,
50-
request: CodeActionRequest
68+
requestedRange: Range<Position>,
69+
swiftLanguageService: SwiftLanguageService
5170
) {
71+
self.resolveSupport = resolveSupport
5272
self.snapshot = snapshot
53-
self.request = request
73+
self.requestedRange = requestedRange
5474
self.file = file
5575

56-
guard let left = tokenForRefactoring(at: request.range.lowerBound, snapshot: snapshot, syntaxTree: file),
57-
let right = tokenForRefactoring(at: request.range.upperBound, snapshot: snapshot, syntaxTree: file)
76+
guard let left = tokenForRefactoring(at: requestedRange.lowerBound, snapshot: snapshot, syntaxTree: file),
77+
let right = tokenForRefactoring(at: requestedRange.upperBound, snapshot: snapshot, syntaxTree: file)
5878
else {
5979
return nil
6080
}
6181
self.range = left.position..<right.endPosition
6282
self.innermostNodeContainingRange = findCommonAncestorOrSelf(Syntax(left), Syntax(right))
83+
self.swiftLanguageService = swiftLanguageService
84+
}
85+
86+
/// Retrieve the cursor info in the code action's document at the given position.
87+
///
88+
/// Because this can be an expensive operation, this should only be called after all syntactic checks and if the request does not support lazily
89+
/// resolving of the `edit` properties.
90+
func cursorInfo(at position: Position) async throws -> [CursorInfo] {
91+
let compileCommand = await swiftLanguageService.compileCommand(
92+
for: snapshot.uri,
93+
fallbackAfterTimeout: true
94+
)
95+
96+
return try await swiftLanguageService.cursorInfo(
97+
snapshot,
98+
compileCommand: compileCommand,
99+
position..<position
100+
).cursorInfo
63101
}
64102
}
65103

Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ let allSyntaxCodeActions: [any SyntaxCodeActionProvider.Type] = {
2424
ConvertIfLetToGuard.self,
2525
ConvertIntegerLiteral.self,
2626
ConvertJSONToCodableStruct.self,
27+
ConvertStoredPropertyToComputed.self,
2728
ConvertStringConcatenationToStringInterpolation.self,
2829
ConvertZeroParameterFunctionToComputedProperty.self,
2930
FormatRawStringLiteral.self,

Sources/SwiftLanguageService/CodeActions/SyntaxRefactoringCodeActionProvider.swift

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,44 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
@_spi(SourceKitLSP) import LanguageServerProtocol
14+
@_spi(SourceKitLSP) import SKLogging
1415
import SourceKitLSP
1516
import SwiftRefactor
1617
import SwiftSyntax
1718

19+
/// Data that is included in a `CodeAction` response for which the client should resolve the edit lazily using a `codeAction/resolve` request.
20+
///
21+
/// This data allows us to re-construct the `SyntaxCodeActionScope`.
22+
struct UnresolvedCodeActionData: Codable, LSPAnyCodable {
23+
/// A string representation of the syntax refactoring action's type.
24+
let action: String
25+
26+
/// The document on which the code action should be applied.
27+
let document: VersionedTextDocumentIdentifier
28+
29+
/// The range at which the code action was originally requested.
30+
let range: Range<Position>
31+
32+
init<Metatype: SyntaxRefactoringCodeActionProvider>(
33+
actionType: Metatype.Type,
34+
document: VersionedTextDocumentIdentifier,
35+
range: Range<Position>,
36+
) {
37+
self.action = "\(Metatype.self)"
38+
self.document = document
39+
self.range = range
40+
}
41+
}
42+
43+
enum SyntaxCodeActionContextResult<Context> {
44+
/// The cont
45+
case context(Context)
46+
/// Report a code action without the `edit` properties. The client is expected to send a `codeAction/resolve` request to resolve the edit.
47+
///
48+
/// Must only be returned if the the client can resolve edits.
49+
case resolveEditLazily
50+
}
51+
1852
/// Protocol that adapts a SyntaxRefactoringProvider (that comes from
1953
/// swift-syntax) into a SyntaxCodeActionProvider.
2054
protocol SyntaxRefactoringCodeActionProvider: SyntaxCodeActionProvider, EditRefactoringProvider {
@@ -24,18 +58,45 @@ protocol SyntaxRefactoringCodeActionProvider: SyntaxCodeActionProvider, EditRefa
2458
/// scope.
2559
static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> Input?
2660

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

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

38-
guard let sourceEdits = try? Self.textRefactor(syntax: node, in: refactoringContext(for: scope)) else {
77+
let context: Context
78+
switch await refactoringContext(for: node, in: scope) {
79+
case .context(let c): context = c
80+
case .resolveEditLazily:
81+
guard scope.resolveSupport?.canResolveEdit ?? false else {
82+
logger.fault(
83+
"Refactoring action \(Self.self) requested lazy resolution of edits but client cannot resolve edits"
84+
)
85+
return []
86+
}
87+
return [
88+
CodeAction(
89+
title: Self.title,
90+
kind: .refactorInline,
91+
data: UnresolvedCodeActionData(
92+
actionType: Self.self,
93+
document: VersionedTextDocumentIdentifier(scope.snapshot.uri, version: scope.snapshot.version),
94+
range: scope.requestedRange
95+
).encodeToLSPAny()
96+
)
97+
]
98+
}
99+
guard let sourceEdits = try? Self.textRefactor(syntax: node, in: context) else {
39100
return []
40101
}
41102

@@ -53,9 +114,14 @@ extension SyntaxRefactoringCodeActionProvider {
53114
}
54115
}
55116

117+
/// SyntaxCodeActionProviders with a `Void` context can automatically be adapted provide a code action based on their
118+
/// refactoring operation.
56119
extension SyntaxRefactoringCodeActionProvider where Context == Void {
57-
static func refactoringContext(for scope: SyntaxCodeActionScope) -> Context {
58-
return ()
120+
static func refactoringContext(
121+
for node: Input,
122+
in scope: SyntaxCodeActionScope
123+
) -> SyntaxCodeActionContextResult<Context> {
124+
return .context(())
59125
}
60126
}
61127

Sources/SwiftLanguageService/CursorInfo.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ struct CursorInfo {
2828
/// name and USR.
2929
var symbolInfo: SymbolDetails
3030

31+
/// A human-readable string representation of the symbol at the given location, eg. a variable's type.
32+
var typeName: String?
33+
3134
/// The annotated declaration XML string.
3235
var annotatedDeclaration: String?
3336

@@ -39,10 +42,12 @@ struct CursorInfo {
3942

4043
init(
4144
_ symbolInfo: SymbolDetails,
45+
typeName: String?,
4246
annotatedDeclaration: String?,
4347
documentation: String?
4448
) {
4549
self.symbolInfo = symbolInfo
50+
self.typeName = typeName
4651
self.annotatedDeclaration = annotatedDeclaration
4752
self.documentation = documentation
4853
}
@@ -107,6 +112,7 @@ struct CursorInfo {
107112
receiverUsrs: dict[keys.receivers]?.compactMap { $0[keys.usr] as String? } ?? [],
108113
systemModule: module
109114
),
115+
typeName: dict[keys.typeName],
110116
annotatedDeclaration: dict[keys.annotatedDecl],
111117
documentation: dict[keys.docComment]
112118
)

0 commit comments

Comments
 (0)