diff --git a/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift b/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift new file mode 100644 index 000000000..28a7e2833 --- /dev/null +++ b/Sources/SwiftLanguageService/CodeActions/ConvertStoredPropertyToComputed.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// 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 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 [] } + + if variableDecl.bindings.first?.typeAnnotation?.type != nil { + let context = ConvertStoredPropertyToComputed.Context() + guard let refactored = try? Self.refactor(syntax: variableDecl, in: context) else { return [] } + + let declRange = scope.snapshot.range(of: variableDecl) + let edit = TextEdit( + range: declRange, + newText: refactored.description + ) + + return [ + CodeAction( + title: "Convert Stored Property to Computed Property", + kind: .refactorInline, + edit: WorkspaceEdit(changes: [scope.snapshot.uri: [edit]]) + ) + ] + } + + return [ + CodeAction( + title: "Convert Stored Property to Computed Property", + kind: .refactorInline, + 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/CodeActions/SyntaxCodeActionProvider.swift b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActionProvider.swift index 4379a503c..ae151bbbc 100644 --- a/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActionProvider.swift +++ b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActionProvider.swift @@ -44,14 +44,18 @@ struct SyntaxCodeActionScope { /// The innermost node that contains the entire selected source range var innermostNodeContainingRange: Syntax? + var cursorInfo: [CursorInfo] = [] + init?( snapshot: DocumentSnapshot, syntaxTree file: SourceFileSyntax, - request: CodeActionRequest + request: CodeActionRequest, + cursorInfo: [CursorInfo] = [] ) { self.snapshot = snapshot self.request = request self.file = file + self.cursorInfo = cursorInfo guard let left = tokenForRefactoring(at: request.range.lowerBound, snapshot: snapshot, syntaxTree: file), let right = tokenForRefactoring(at: request.range.upperBound, snapshot: snapshot, syntaxTree: file) diff --git a/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift index a481a5950..ece556576 100644 --- a/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift +++ b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift @@ -24,6 +24,7 @@ let allSyntaxCodeActions: [any SyntaxCodeActionProvider.Type] = { ConvertIfLetToGuard.self, ConvertIntegerLiteral.self, ConvertJSONToCodableStruct.self, + ConvertStoredPropertyToComputed.self, ConvertStringConcatenationToStringInterpolation.self, ConvertZeroParameterFunctionToComputedProperty.self, FormatRawStringLiteral.self, diff --git a/Sources/SwiftLanguageService/SwiftLanguageService.swift b/Sources/SwiftLanguageService/SwiftLanguageService.swift index c54bffe47..278fd2fe5 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 @@ -982,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] { @@ -1392,3 +1410,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..