Skip to content

Commit b80521f

Browse files
committed
Refactor ConvertStoredPropertyToComputed to fetch cursor info lazily
1 parent c3196a6 commit b80521f

5 files changed

Lines changed: 241 additions & 36 deletions

File tree

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
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+
113
import Foundation
214
@_spi(SourceKitLSP) import LanguageServerProtocol
315
import SourceKitLSP
@@ -6,55 +18,47 @@ import SwiftSyntax
618
import SwiftSyntaxBuilder
719

820
extension ConvertStoredPropertyToComputed: SyntaxCodeActionProvider {
9-
1021
static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] {
1122
guard
1223
let variableDecl = scope.innermostNodeContainingRange?.as(VariableDeclSyntax.self)
1324
?? scope.innermostNodeContainingRange?.parent?.as(VariableDeclSyntax.self)
14-
else {
15-
return []
16-
}
17-
18-
var resolvedType: TypeSyntax? = nil
19-
20-
if let firstInfo: CursorInfo = scope.cursorInfo.first,
21-
let annotatedDecl = firstInfo.annotatedDeclaration,
22-
let typeString = extractType(from: annotatedDecl)
23-
{
24-
resolvedType = TypeSyntax(stringLiteral: typeString)
25-
}
25+
else { return [] }
2626

27-
if resolvedType == nil,
28-
let explicitType = variableDecl.bindings.first?.typeAnnotation?.type
29-
{
30-
resolvedType = explicitType
31-
}
27+
if variableDecl.bindings.first?.typeAnnotation?.type != nil {
28+
let context = ConvertStoredPropertyToComputed.Context()
29+
guard let refactored = try? Self.refactor(syntax: variableDecl, in: context) else { return [] }
3230

33-
let context = ConvertStoredPropertyToComputed.Context(type: resolvedType)
31+
let declRange = scope.snapshot.range(of: variableDecl)
32+
let edit = TextEdit(
33+
range: declRange,
34+
newText: refactored.description
35+
)
3436

35-
guard let refactoredDecl = try? Self.refactor(syntax: variableDecl, in: context) else {
36-
return []
37+
return [
38+
CodeAction(
39+
title: "Convert Stored Property to Computed Property",
40+
kind: .refactorInline,
41+
edit: WorkspaceEdit(changes: [scope.snapshot.uri: [edit]])
42+
)
43+
]
3744
}
3845

39-
let edit = TextEdit(
40-
range: scope.snapshot.absolutePositionRange(of: variableDecl.range),
41-
newText: refactoredDecl.description
42-
)
43-
4446
return [
4547
CodeAction(
46-
title: "Convert to computed property",
48+
title: "Convert Stored Property to Computed Property",
4749
kind: .refactorInline,
48-
edit: WorkspaceEdit(changes: [scope.snapshot.uri: [edit]])
50+
command: Command(
51+
title: "Convert Stored Property to Computed Property",
52+
command: "semantic.refactor.convertStoredPropertyToComputed",
53+
arguments: [
54+
.dictionary([
55+
"title": .string("Convert Stored Property to Computed Property"),
56+
"uri": .string(scope.snapshot.uri.stringValue),
57+
"offset": .int(scope.range.lowerBound.utf8Offset),
58+
])
59+
]
60+
)
4961
)
5062
]
5163
}
52-
53-
private static func extractType(from annotatedDecl: String) -> String? {
54-
55-
guard let start = annotatedDecl.range(of: "<type>"),
56-
let end = annotatedDecl.range(of: "</type>")
57-
else { return nil }
58-
return String(annotatedDecl[start.upperBound..<end.lowerBound])
59-
}
6064
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
@_spi(SourceKitLSP) import LanguageServerProtocol
2+
import SourceKitLSP
3+
import SwiftSyntax
4+
5+
package struct ConvertStoredPropertyToComputedCommand: SwiftCommand, Equatable, Sendable {
6+
7+
package static let identifier: String =
8+
"semantic.refactor.convertStoredPropertyToComputed"
9+
10+
package var title: String
11+
let uri: DocumentURI
12+
let offset: Int
13+
14+
init(
15+
title: String = "Convert Stored Property to Computed Property",
16+
uri: DocumentURI,
17+
offset: Int
18+
) {
19+
self.title = title
20+
self.uri = uri
21+
self.offset = offset
22+
}
23+
24+
init?(fromLSPDictionary dictionary: [String: LSPAny]) {
25+
guard
26+
case .string(let uriString) = dictionary["uri"],
27+
case .int(let offsetInt) = dictionary["offset"],
28+
let uri = try? DocumentURI(string: uriString)
29+
else {
30+
return nil
31+
}
32+
33+
if case .string(let titleString) = dictionary["title"] {
34+
self.title = titleString
35+
} else {
36+
self.title = "Convert Stored Property to Computed Property"
37+
}
38+
39+
self.uri = uri
40+
self.offset = offsetInt
41+
}
42+
43+
func encodeToLSPAny() -> LSPAny {
44+
return .dictionary([
45+
"title": .string(title),
46+
"uri": .string(uri.stringValue),
47+
"offset": .int(offset),
48+
])
49+
}
50+
51+
func run(
52+
languageService: SwiftLanguageService
53+
) async throws -> WorkspaceEdit {
54+
return try await languageService
55+
.executeConvertStoredPropertyToComputed(uri: uri, offset: offset)
56+
?? WorkspaceEdit(changes: [:])
57+
}
58+
}

Sources/SwiftLanguageService/SwiftCommand.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ extension SwiftLanguageService {
5151
[
5252
SemanticRefactorCommand.self,
5353
ExpandMacroCommand.self,
54+
ConvertStoredPropertyToComputedCommand.self,
5455
].map { (command: any SwiftCommand.Type) in
5556
command.identifier
5657
}

Sources/SwiftLanguageService/SwiftLanguageService.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ package import SourceKitLSP
2727
import SwiftExtensions
2828
import SwiftParser
2929
import SwiftParserDiagnostics
30+
import SwiftRefactor
3031
package import SwiftSyntax
32+
import SwiftSyntaxBuilder
3133
package import ToolchainRegistry
3234
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions
3335

@@ -1356,3 +1358,78 @@ extension SwiftLanguageService {
13561358
return false
13571359
}
13581360
}
1361+
// MARK: - Refactoring Commands
1362+
1363+
extension SwiftLanguageService {
1364+
// Executes the "Convert Stored Property to Computed" refactoring.
1365+
package func executeConvertStoredPropertyToComputed(
1366+
uri: DocumentURI,
1367+
offset: Int
1368+
) async throws -> WorkspaceEdit? {
1369+
1370+
let snapshot = try documentManager.latestSnapshot(uri)
1371+
let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot)
1372+
let position = AbsolutePosition(utf8Offset: offset)
1373+
1374+
guard let token = syntaxTree.token(at: position),
1375+
let variableDecl = token.parent?.ancestorOrSelf(mapping: {
1376+
$0.as(VariableDeclSyntax.self)
1377+
})
1378+
else {
1379+
return nil
1380+
}
1381+
1382+
guard let binding = variableDecl.bindings.first else {
1383+
return nil
1384+
}
1385+
1386+
// Prefer explicitly declared (syntactic) type.
1387+
var resolvedType = binding.typeAnnotation?.type
1388+
1389+
// Fall back to semantic inference using compiler context when absent.
1390+
if resolvedType == nil {
1391+
1392+
let compileCommand = await self.compileCommand(
1393+
for: uri,
1394+
fallbackAfterTimeout: true
1395+
)
1396+
1397+
let lspPosition = snapshot.position(of: position)
1398+
let (cursorInfoResults, _, _) = try await self.cursorInfo(
1399+
snapshot,
1400+
compileCommand: compileCommand,
1401+
lspPosition..<lspPosition
1402+
)
1403+
1404+
if let annotatedDecl = cursorInfoResults.first?.annotatedDeclaration,
1405+
let typeString = extractType(from: annotatedDecl)
1406+
{
1407+
resolvedType = TypeSyntax(stringLiteral: typeString)
1408+
}
1409+
}
1410+
1411+
guard let resolvedType else {
1412+
return nil
1413+
}
1414+
1415+
let context = ConvertStoredPropertyToComputed.Context(type: resolvedType)
1416+
let refactoredDecl = try ConvertStoredPropertyToComputed.refactor(
1417+
syntax: variableDecl,
1418+
in: context
1419+
)
1420+
let edit = TextEdit(
1421+
range: snapshot.absolutePositionRange(of: variableDecl.range),
1422+
newText: refactoredDecl.description
1423+
)
1424+
return WorkspaceEdit(changes: [uri: [edit]])
1425+
}
1426+
// Extracts type from annotated declaration
1427+
private func extractType(from annotatedDecl: String) -> String? {
1428+
guard let start = annotatedDecl.range(of: "<type>"),
1429+
let end = annotatedDecl.range(of: "</type>")
1430+
else {
1431+
return nil
1432+
}
1433+
return String(annotatedDecl[start.upperBound..<end.lowerBound])
1434+
}
1435+
}

Tests/SourceKitLSPTests/CodeActionTests.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,71 @@ final class CodeActionTests: SourceKitLSPTestCase {
12361236
}
12371237
}
12381238
1239+
func testConvertStoredPropertyToComputedWithTypeAnnotation() async throws {
1240+
try await assertCodeActions(
1241+
"""
1242+
struct S {
1243+
1️⃣var x: Int = 252️⃣
1244+
}
1245+
""",
1246+
markers: ["1️⃣"],
1247+
ranges: [("1️⃣", "2️⃣")],
1248+
exhaustive: false
1249+
) { uri, positions in
1250+
[
1251+
CodeAction(
1252+
title: "Convert Stored Property to Computed Property",
1253+
kind: .refactorInline,
1254+
edit: WorkspaceEdit(
1255+
changes: [
1256+
uri: [
1257+
TextEdit(
1258+
range: Position(line: 0, utf16index: 10)..<Position(line: 1, utf16index: 19),
1259+
newText: "\n var x: Int { 25 }"
1260+
)
1261+
]
1262+
]
1263+
)
1264+
)
1265+
]
1266+
}
1267+
}
1268+
1269+
func testConvertStoredPropertyToComputedWithoutTypeAnnotation() async throws {
1270+
try await assertCodeActions(
1271+
"""
1272+
struct S {
1273+
1️⃣var x = 255️⃣
1274+
}
1275+
""",
1276+
markers: [],
1277+
ranges: [("1️⃣", "5️⃣")],
1278+
exhaustive: false
1279+
) { uri, positions in
1280+
[
1281+
CodeAction(
1282+
title: "Convert Stored Property to Computed Property",
1283+
kind: .refactorInline,
1284+
command: Command(
1285+
title: "Convert Stored Property to Computed Property",
1286+
command: "semantic.refactor.convertStoredPropertyToComputed",
1287+
arguments: [
1288+
.dictionary([
1289+
"title": .string("Convert Stored Property to Computed Property"),
1290+
"uri": .string(uri.stringValue),
1291+
"offset": .int(10),
1292+
]),
1293+
.dictionary([
1294+
"sourcekitlsp_textDocument": .dictionary([
1295+
"uri": .string(uri.stringValue)
1296+
])
1297+
]),
1298+
]
1299+
)
1300+
)
1301+
]
1302+
}
1303+
}
12391304
func testApplyDeMorganLawNegatedAnd() async throws {
12401305
try await assertCodeActions(
12411306
"""

0 commit comments

Comments
 (0)