diff --git a/Sources/SwiftLanguageService/CodeActions/MoveMember.swift b/Sources/SwiftLanguageService/CodeActions/MoveMember.swift new file mode 100644 index 000000000..0116c8309 --- /dev/null +++ b/Sources/SwiftLanguageService/CodeActions/MoveMember.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 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 + +extension CodeActionKind { + static let refactorMove = CodeActionKind(rawValue: "refactor.move") +} + +struct MoveMember: SyntaxCodeActionProvider { + + static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] { + + guard + let member = + scope.innermostNodeContainingRange? + .findParentOfSelf( + ofType: MemberBlockItemSyntax.self, + stoppingIf: { $0.is(SourceFileSyntax.self) } + ) + else { + return [] + } + + return [ + CodeAction( + title: "Move to another type", + kind: .refactorMove, + command: Command( + title: "Move to another type", + command: "swift.moveMember", + arguments: [ + .string(scope.snapshot.uri.stringValue), + .int(member.position.utf8Offset), + .int(member.endPosition.utf8Offset), + ] + ) + ) + ] + } +} diff --git a/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift index e76e788d8..6f5ecec07 100644 --- a/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift +++ b/Sources/SwiftLanguageService/CodeActions/SyntaxCodeActions.swift @@ -29,6 +29,7 @@ let allSyntaxCodeActions: [any SyntaxCodeActionProvider.Type] = { MigrateToNewIfLetSyntax.self, OpaqueParameterToGeneric.self, RemoveSeparatorsFromIntegerLiteral.self, + MoveMember.self, ] #if !NO_SWIFTPM_DEPENDENCY result.append(PackageManifestEdits.self) diff --git a/Tests/SourceKitLSPTests/CodeActionTests.swift b/Tests/SourceKitLSPTests/CodeActionTests.swift index d36dde054..8c4b0a0d8 100644 --- a/Tests/SourceKitLSPTests/CodeActionTests.swift +++ b/Tests/SourceKitLSPTests/CodeActionTests.swift @@ -1637,6 +1637,45 @@ final class CodeActionTests: SourceKitLSPTestCase { [] } } + + func testMoveMemberCodeAction() async throws { + let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) + let uri = DocumentURI(for: .swift) + let positions = testClient.openDocument( + """ + struct MyStruct { + 1️⃣func myFunction() {} + } + """, + uri: uri + ) + + let request = CodeActionRequest( + range: Range(positions["1️⃣"]), + context: .init(), + textDocument: TextDocumentIdentifier(uri) + ) + let result = try await testClient.send(request) + + // Make sure we get a "Move to another type" action. + let moveMemberAction = result?.codeActions?.first { action in + return action.title == "Move to another type" + } + XCTAssertNotNil(moveMemberAction) + } + + func testMoveMemberCodeActionNotOfferedForTopLevelCode() async throws { + try await assertCodeActions( + """ + 1️⃣func topLevelFunction() {} + """, + markers: ["1️⃣"], + exhaustive: false + ) { _, _ in + [] + } + } + func testConvertComputedPropertyToZeroParameterFunction() async throws { let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) let uri = DocumentURI(for: .swift)