From 59f96208b78a3b510581ad281721cce06d1e8fc5 Mon Sep 17 00:00:00 2001 From: Padmashree06 Date: Tue, 24 Feb 2026 16:08:34 +0530 Subject: [PATCH 1/3] Add convertStoredPropertyToComputed in CodeActions --- .../ConvertStoredPropertyToComputed.swift | 60 +++++++++++++++++++ .../SyntaxCodeActionProvider.swift | 6 +- .../CodeActions/SyntaxCodeActions.swift | 1 + 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift diff --git a/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift b/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift new file mode 100644 index 000000000..78450f8fd --- /dev/null +++ b/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift @@ -0,0 +1,60 @@ +import Foundation +@_spi(SourceKitLSP) import LanguageServerProtocol +import SourceKitLSP +import SwiftRefactor +import SwiftSyntax +import SwiftSyntaxBuilder + +extension ConvertStoredPropertyToComputed: SyntaxCodeActionProvider { + + static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] { + guard + let variableDecl = scope.innermostNodeContainingRange?.as(VariableDeclSyntax.self) + ?? scope.innermostNodeContainingRange?.parent?.as(VariableDeclSyntax.self) + else { + return [] + } + + var resolvedType: TypeSyntax? = nil + + if let firstInfo: CursorInfo = scope.cursorInfo.first, + let annotatedDecl = firstInfo.annotatedDeclaration, + let typeString = extractType(from: annotatedDecl) + { + resolvedType = TypeSyntax(stringLiteral: typeString) + } + + if resolvedType == nil, + let explicitType = variableDecl.bindings.first?.typeAnnotation?.type + { + resolvedType = explicitType + } + + let context = ConvertStoredPropertyToComputed.Context(type: resolvedType) + + guard let refactoredDecl = try? Self.refactor(syntax: variableDecl, in: context) else { + return [] + } + + let edit = TextEdit( + range: scope.snapshot.absolutePositionRange(of: variableDecl.range), + newText: refactoredDecl.description + ) + + return [ + CodeAction( + title: "Convert to computed property", + kind: .refactorInline, + edit: WorkspaceEdit(changes: [scope.snapshot.uri: [edit]]) + ) + ] + } + + private static func extractType(from annotatedDecl: String) -> String? { + + guard let start = annotatedDecl.range(of: ""), + let end = annotatedDecl.range(of: "") + else { return nil } + return String(annotatedDecl[start.upperBound.. Date: Wed, 18 Mar 2026 14:06:22 +0530 Subject: [PATCH 2/3] Refactor ConvertStoredPropertyToComputed to fetch cursor info lazily --- .../ConvertStoredPropertyToComputed.swift | 76 +++++++++--------- ...redPropertyToComputedRefactorCommand.swift | 70 +++++++++++++++++ .../SwiftLanguageService/SwiftCommand.swift | 1 + .../SwiftLanguageService.swift | 77 +++++++++++++++++++ Tests/SourceKitLSPTests/CodeActionTests.swift | 65 ++++++++++++++++ 5 files changed, 253 insertions(+), 36 deletions(-) create mode 100644 Sources/SwiftLanguageService/ConvertStoredPropertyToComputedRefactorCommand.swift diff --git a/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift b/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift index 78450f8fd..16ad4266e 100644 --- a/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift +++ b/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// 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 @@ -6,55 +18,47 @@ import SwiftSyntax import SwiftSyntaxBuilder extension ConvertStoredPropertyToComputed: SyntaxCodeActionProvider { - static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] { guard let variableDecl = scope.innermostNodeContainingRange?.as(VariableDeclSyntax.self) ?? scope.innermostNodeContainingRange?.parent?.as(VariableDeclSyntax.self) - else { - return [] - } - - var resolvedType: TypeSyntax? = nil - - if let firstInfo: CursorInfo = scope.cursorInfo.first, - let annotatedDecl = firstInfo.annotatedDeclaration, - let typeString = extractType(from: annotatedDecl) - { - resolvedType = TypeSyntax(stringLiteral: typeString) - } + else { return [] } - if resolvedType == nil, - let explicitType = variableDecl.bindings.first?.typeAnnotation?.type - { - resolvedType = explicitType - } + if variableDecl.bindings.first?.typeAnnotation?.type != nil { + let context = ConvertStoredPropertyToComputed.Context() + guard let refactored = try? Self.refactor(syntax: variableDecl, in: context) else { return [] } - let context = ConvertStoredPropertyToComputed.Context(type: resolvedType) + let declRange = scope.snapshot.range(of: variableDecl) + let edit = TextEdit( + range: declRange, + newText: refactored.description + ) - guard let refactoredDecl = try? Self.refactor(syntax: variableDecl, in: context) else { - return [] + return [ + CodeAction( + title: "Convert Stored Property to Computed Property", + kind: .refactorInline, + edit: WorkspaceEdit(changes: [scope.snapshot.uri: [edit]]) + ) + ] } - let edit = TextEdit( - range: scope.snapshot.absolutePositionRange(of: variableDecl.range), - newText: refactoredDecl.description - ) - return [ CodeAction( - title: "Convert to computed property", + title: "Convert Stored Property to Computed Property", kind: .refactorInline, - edit: WorkspaceEdit(changes: [scope.snapshot.uri: [edit]]) + command: Command( + title: "Convert Stored Property to Computed Property", + command: "semantic.refactor.convertStoredPropertyToComputed", + arguments: [ + .dictionary([ + "title": .string("Convert Stored Property to Computed Property"), + "uri": .string(scope.snapshot.uri.stringValue), + "offset": .int(scope.range.lowerBound.utf8Offset), + ]) + ] + ) ) ] } - - private static func extractType(from annotatedDecl: String) -> String? { - - guard let start = annotatedDecl.range(of: ""), - let end = annotatedDecl.range(of: "") - else { return nil } - return String(annotatedDecl[start.upperBound.. LSPAny { + return .dictionary([ + "title": .string(title), + "uri": .string(uri.stringValue), + "offset": .int(offset), + ]) + } + + func run( + languageService: SwiftLanguageService + ) async throws -> WorkspaceEdit { + return try await languageService + .executeConvertStoredPropertyToComputed(uri: uri, offset: offset) + ?? WorkspaceEdit(changes: [:]) + } +} diff --git a/Sources/SwiftLanguageService/SwiftCommand.swift b/Sources/SwiftLanguageService/SwiftCommand.swift index dd0dc52c2..ac000e1c3 100644 --- a/Sources/SwiftLanguageService/SwiftCommand.swift +++ b/Sources/SwiftLanguageService/SwiftCommand.swift @@ -51,6 +51,7 @@ extension SwiftLanguageService { [ SemanticRefactorCommand.self, ExpandMacroCommand.self, + ConvertStoredPropertyToComputedCommand.self, ].map { (command: any SwiftCommand.Type) in command.identifier } diff --git a/Sources/SwiftLanguageService/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift index c54bffe47..16426dc1f 100644 --- a/Sources/SwiftLanguageService/SwiftLanguageService.swift +++ b/Sources/SwiftLanguageService/SwiftLanguageService.swift @@ -27,7 +27,9 @@ package import SourceKitLSP import SwiftExtensions @_spi(ExperimentalLanguageFeatures) public import SwiftParser import SwiftParserDiagnostics +import SwiftRefactor package import SwiftSyntax +import SwiftSyntaxBuilder package import ToolchainRegistry @_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions @@ -1392,3 +1394,78 @@ extension SwiftLanguageService { return false } } +// MARK: - Refactoring Commands + +extension SwiftLanguageService { + // Executes the "Convert Stored Property to Computed" refactoring. + package func executeConvertStoredPropertyToComputed( + uri: DocumentURI, + offset: Int + ) async throws -> WorkspaceEdit? { + + let snapshot = try documentManager.latestSnapshot(uri) + let syntaxTree = await syntaxTreeManager.syntaxTree(for: snapshot) + let position = AbsolutePosition(utf8Offset: offset) + + guard let token = syntaxTree.token(at: position), + let variableDecl = token.parent?.ancestorOrSelf(mapping: { + $0.as(VariableDeclSyntax.self) + }) + else { + return nil + } + + guard let binding = variableDecl.bindings.first else { + return nil + } + + // Prefer explicitly declared (syntactic) type. + var resolvedType = binding.typeAnnotation?.type + + // Fall back to semantic inference using compiler context when absent. + if resolvedType == nil { + + let compileCommand = await self.compileCommand( + for: uri, + fallbackAfterTimeout: true + ) + + let lspPosition = snapshot.position(of: position) + let (cursorInfoResults, _, _) = try await self.cursorInfo( + snapshot, + compileCommand: compileCommand, + lspPosition.. String? { + guard let start = annotatedDecl.range(of: ""), + let end = annotatedDecl.range(of: "") + else { + return nil + } + return String(annotatedDecl[start.upperBound.. Date: Sun, 12 Apr 2026 19:14:41 +0530 Subject: [PATCH 3/3] Modify workflow according to codeAction/resolve --- .../ConvertStoredPropertyToComputed.swift | 16 ++--- ...redPropertyToComputedRefactorCommand.swift | 70 ------------------- .../SwiftLanguageService/SwiftCommand.swift | 1 - .../SwiftLanguageService.swift | 18 ++++- Tests/SourceKitLSPTests/CodeActionTests.swift | 20 ++---- 5 files changed, 29 insertions(+), 96 deletions(-) delete mode 100644 Sources/SwiftLanguageService/ConvertStoredPropertyToComputedRefactorCommand.swift diff --git a/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift b/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift index 16ad4266e..28a7e2833 100644 --- a/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift +++ b/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift @@ -47,17 +47,11 @@ extension ConvertStoredPropertyToComputed: SyntaxCodeActionProvider { CodeAction( title: "Convert Stored Property to Computed Property", kind: .refactorInline, - command: Command( - title: "Convert Stored Property to Computed Property", - command: "semantic.refactor.convertStoredPropertyToComputed", - arguments: [ - .dictionary([ - "title": .string("Convert Stored Property to Computed Property"), - "uri": .string(scope.snapshot.uri.stringValue), - "offset": .int(scope.range.lowerBound.utf8Offset), - ]) - ] - ) + data: .dictionary([ + "action": .string("Convert Stored Property to Computed Property"), + "uri": .string(scope.snapshot.uri.stringValue), + "offset": .int(scope.range.lowerBound.utf8Offset), + ]) ) ] } diff --git a/Sources/SwiftLanguageService/ConvertStoredPropertyToComputedRefactorCommand.swift b/Sources/SwiftLanguageService/ConvertStoredPropertyToComputedRefactorCommand.swift deleted file mode 100644 index f868d8ee2..000000000 --- a/Sources/SwiftLanguageService/ConvertStoredPropertyToComputedRefactorCommand.swift +++ /dev/null @@ -1,70 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2018 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 -// -//===----------------------------------------------------------------------===// - -@_spi(SourceKitLSP) import LanguageServerProtocol -import SourceKitLSP -import SwiftSyntax - -package struct ConvertStoredPropertyToComputedCommand: SwiftCommand, Equatable, Sendable { - - package static let identifier: String = - "semantic.refactor.convertStoredPropertyToComputed" - - package var title: String - let uri: DocumentURI - let offset: Int - - init( - title: String = "Convert Stored Property to Computed Property", - uri: DocumentURI, - offset: Int - ) { - self.title = title - self.uri = uri - self.offset = offset - } - - init?(fromLSPDictionary dictionary: [String: LSPAny]) { - guard - case .string(let uriString) = dictionary["uri"], - case .int(let offsetInt) = dictionary["offset"], - let uri = try? DocumentURI(string: uriString) - else { - return nil - } - - if case .string(let titleString) = dictionary["title"] { - self.title = titleString - } else { - self.title = "Convert Stored Property to Computed Property" - } - - self.uri = uri - self.offset = offsetInt - } - - func encodeToLSPAny() -> LSPAny { - return .dictionary([ - "title": .string(title), - "uri": .string(uri.stringValue), - "offset": .int(offset), - ]) - } - - func run( - languageService: SwiftLanguageService - ) async throws -> WorkspaceEdit { - return try await languageService - .executeConvertStoredPropertyToComputed(uri: uri, offset: offset) - ?? WorkspaceEdit(changes: [:]) - } -} diff --git a/Sources/SwiftLanguageService/SwiftCommand.swift b/Sources/SwiftLanguageService/SwiftCommand.swift index ac000e1c3..dd0dc52c2 100644 --- a/Sources/SwiftLanguageService/SwiftCommand.swift +++ b/Sources/SwiftLanguageService/SwiftCommand.swift @@ -51,7 +51,6 @@ extension SwiftLanguageService { [ SemanticRefactorCommand.self, ExpandMacroCommand.self, - ConvertStoredPropertyToComputedCommand.self, ].map { (command: any SwiftCommand.Type) in command.identifier } diff --git a/Sources/SwiftLanguageService/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift index 16426dc1f..278fd2fe5 100644 --- a/Sources/SwiftLanguageService/SwiftLanguageService.swift +++ b/Sources/SwiftLanguageService/SwiftLanguageService.swift @@ -984,7 +984,23 @@ extension SwiftLanguageService { } package func codeActionResolve(_ req: CodeActionResolveRequest) async throws -> CodeAction { - return req.codeAction + var codeAction = req.codeAction + + guard case let .dictionary(data) = codeAction.data, + case let .string(action) = data["action"], + action == "Convert Stored Property to Computed Property" + else { + return codeAction + } + guard case let .string(uriString) = data["uri"], + let uri = try? DocumentURI(string: uriString), + case let .int(offset) = data["offset"] + else { + return codeAction + } + let edit = try await self.executeConvertStoredPropertyToComputed(uri: uri, offset: offset) + codeAction.edit = edit + return codeAction } func retrieveRefactorCodeActions(_ params: CodeActionRequest) async throws -> [CodeAction] { diff --git a/Tests/SourceKitLSPTests/CodeActionTests.swift b/Tests/SourceKitLSPTests/CodeActionTests.swift index 9ef757d8c..81d849c5f 100644 --- a/Tests/SourceKitLSPTests/CodeActionTests.swift +++ b/Tests/SourceKitLSPTests/CodeActionTests.swift @@ -1321,26 +1321,20 @@ final class CodeActionTests: SourceKitLSPTestCase { CodeAction( title: "Convert Stored Property to Computed Property", kind: .refactorInline, - command: Command( - title: "Convert Stored Property to Computed Property", - command: "semantic.refactor.convertStoredPropertyToComputed", - arguments: [ - .dictionary([ - "title": .string("Convert Stored Property to Computed Property"), + data: + .dictionary([ + "textDocument": .dictionary(["uri": .string(uri.stringValue)]), + "underlyingData": .dictionary([ + "action": .string("Convert Stored Property to Computed Property"), "uri": .string(uri.stringValue), "offset": .int(10), ]), - .dictionary([ - "sourcekitlsp_textDocument": .dictionary([ - "uri": .string(uri.stringValue) - ]) - ]), - ] - ) + ]) ) ] } } + func testApplyDeMorganLawNegatedAnd() async throws { try await assertCodeActions( """