//===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2014 - 2019 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 LanguageServerProtocol import SKTestSupport import SourceKitLSP import SwiftExtensions import XCTest private typealias CodeActionCapabilities = TextDocumentClientCapabilities.CodeAction private typealias CodeActionLiteralSupport = CodeActionCapabilities.CodeActionLiteralSupport private typealias CodeActionKindCapabilities = CodeActionLiteralSupport.CodeActionKindValueSet private let clientCapabilitiesWithCodeActionSupport: ClientCapabilities = { var documentCapabilities = TextDocumentClientCapabilities() var codeActionCapabilities = CodeActionCapabilities() let codeActionKinds = CodeActionKindCapabilities(valueSet: [.refactor, .quickFix]) let codeActionLiteralSupport = CodeActionLiteralSupport(codeActionKind: codeActionKinds) codeActionCapabilities.codeActionLiteralSupport = codeActionLiteralSupport documentCapabilities.codeAction = codeActionCapabilities documentCapabilities.completion = .init(completionItem: .init(snippetSupport: true)) return ClientCapabilities(workspace: nil, textDocument: documentCapabilities) }() final class CodeActionTests: XCTestCase { func testCodeActionResponseLegacySupport() throws { let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil]) let codeAction = CodeAction(title: "1") let codeAction2 = CodeAction(title: "2", command: command) var capabilities: TextDocumentClientCapabilities.CodeAction var capabilityJson: String var data: Data var response: CodeActionRequestResponse capabilityJson = """ { "dynamicRegistration": true, "codeActionLiteralSupport" : { "codeActionKind": { "valueSet": [] } } } """ data = capabilityJson.data(using: .utf8)! capabilities = try JSONDecoder().decode( TextDocumentClientCapabilities.CodeAction.self, from: data ) response = .init(codeActions: [codeAction, codeAction2], clientCapabilities: capabilities) let actions = try JSONDecoder().decode([CodeAction].self, from: JSONEncoder().encode(response)) XCTAssertEqual(actions, [codeAction, codeAction2]) capabilityJson = """ { "dynamicRegistration": true } """ data = capabilityJson.data(using: .utf8)! capabilities = try JSONDecoder().decode( TextDocumentClientCapabilities.CodeAction.self, from: data ) response = .init(codeActions: [codeAction, codeAction2], clientCapabilities: capabilities) let commands = try JSONDecoder().decode([Command].self, from: JSONEncoder().encode(response)) XCTAssertEqual(commands, [command]) } func testCodeActionResponseIgnoresSupportedKinds() throws { // The client guarantees that unsupported kinds will be handled, and in // practice some clients use `"codeActionKind":{"valueSet":[]}`, since // they support all kinds anyway. So to avoid filtering all actions, we // ignore the supported kinds. let unspecifiedAction = CodeAction(title: "Unspecified") let refactorAction = CodeAction(title: "Refactor", kind: .refactor) let quickfixAction = CodeAction(title: "Quickfix", kind: .quickFix) let actions = [unspecifiedAction, refactorAction, quickfixAction] var capabilities: TextDocumentClientCapabilities.CodeAction var capabilityJson: String var data: Data var response: CodeActionRequestResponse capabilityJson = """ { "dynamicRegistration": true, "codeActionLiteralSupport" : { "codeActionKind": { "valueSet": ["refactor"] } } } """ data = capabilityJson.data(using: .utf8)! capabilities = try JSONDecoder().decode( TextDocumentClientCapabilities.CodeAction.self, from: data ) response = .init(codeActions: actions, clientCapabilities: capabilities) XCTAssertEqual(response, .codeActions([unspecifiedAction, refactorAction, quickfixAction])) capabilityJson = """ { "dynamicRegistration": true, "codeActionLiteralSupport" : { "codeActionKind": { "valueSet": [] } } } """ data = capabilityJson.data(using: .utf8)! capabilities = try JSONDecoder().decode( TextDocumentClientCapabilities.CodeAction.self, from: data ) response = .init(codeActions: actions, clientCapabilities: capabilities) XCTAssertEqual(response, .codeActions([unspecifiedAction, refactorAction, quickfixAction])) } func testCodeActionResponseCommandMetadataInjection() throws { let url = URL(fileURLWithPath: "/a.swift") let textDocument = TextDocumentIdentifier(url) let expectedMetadata: LSPAny = try { let metadata = SourceKitLSPCommandMetadata(textDocument: textDocument) let data = try JSONEncoder().encode(metadata) return try JSONDecoder().decode(LSPAny.self, from: data) }() XCTAssertEqual(expectedMetadata, .dictionary(["sourcekitlsp_textDocument": ["uri": "file:///a.swift"]])) let command = Command(title: "Title", command: "Command", arguments: [1, "text", 2.2, nil]) let codeAction = CodeAction(title: "1") let codeAction2 = CodeAction(title: "2", command: command) let request = CodeActionRequest( range: Position(line: 0, utf16index: 0).. String { var a = "hello" 1️⃣ return a } """, uri: uri ) let request = CodeActionRequest( range: positions["1️⃣"].. String { var a = "1️⃣" return a } """, uri: uri ) let testPosition = positions["1️⃣"] let request = CodeActionRequest( range: Range(testPosition), context: .init(), textDocument: TextDocumentIdentifier(uri) ) let result = try await testClient.send(request) let expectedCommandArgs: LSPAny = [ "actionString": "source.refactoring.kind.localize.string", "positionRange": [ "start": [ "character": .int(testPosition.utf16index), "line": .int(testPosition.line), ], "end": [ "character": .int(testPosition.utf16index), "line": .int(testPosition.line), ], ], "title": "Localize String", "textDocument": ["uri": .string(uri.stringValue)], ] let metadataArguments: LSPAny = ["sourcekitlsp_textDocument": ["uri": .string(uri.stringValue)]] let expectedCommand = Command( title: "Localize String", command: "semantic.refactor.command", arguments: [expectedCommandArgs] + [metadataArguments] ) let expectedCodeAction = CodeAction( title: "Localize String", kind: .refactor, command: expectedCommand ) guard case .codeActions(let codeActions) = result else { XCTFail("Expected code actions") return } XCTAssertTrue(codeActions.contains(expectedCodeAction)) } func testJSONCodableCodeActionResult() async throws { let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( """ 1️⃣{ "name": "Produce", "shelves": [ { "name": "Discount Produce", "product": { "name": "Banana", "points": 200, "description": "A banana that's perfectly ripe." } } ] } """, uri: uri ) let testPosition = positions["1️⃣"] let request = CodeActionRequest( range: Range(testPosition), context: .init(), textDocument: TextDocumentIdentifier(uri) ) let result = try await testClient.send(request) guard case .codeActions(let codeActions) = result else { XCTFail("Expected code actions") return } // Make sure we get a JSON conversion action. let codableAction = codeActions.first { action in return action.title == "Create Codable structs from JSON" } XCTAssertNotNil(codableAction) } func testSemanticRefactorRangeCodeActionResult() async throws { let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( """ func foo() -> String { 1️⃣var a = "hello" return a2️⃣ } """, uri: uri ) let startPosition = positions["1️⃣"] let endPosition = positions["2️⃣"] let request = CodeActionRequest( range: startPosition.. ApplyEditResponse in defer { editReceived.fulfill() } guard let change = request.edit.changes?[uri]?.only else { XCTFail("Expected exactly one edit") return ApplyEditResponse(applied: false, failureReason: "Expected exactly one edit") } XCTAssertEqual( change.newText.trimmingTrailingWhitespace(), """ func foo() { } """ ) return ApplyEditResponse(applied: true, failureReason: nil) } _ = try await testClient.send(ExecuteCommandRequest(command: command.command, arguments: command.arguments)) try await fulfillmentOfOrThrow([editReceived]) } func testAddDocumentationCodeActionResult() async throws { let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( """ 2️⃣func refacto1️⃣r(syntax: DeclSyntax, in context: Void) -> DeclSyntax? { }3️⃣ """, uri: uri ) let testPosition = positions["1️⃣"] let request = CodeActionRequest( range: Range(testPosition), context: .init(), textDocument: TextDocumentIdentifier(uri) ) let result = try await testClient.send(request) guard case .codeActions(let codeActions) = result else { XCTFail("Expected code actions") return } // Make sure we get an add-documentation action. let addDocAction = codeActions.first { action in return action.title == "Add documentation" } XCTAssertNotNil(addDocAction) } func testCodeActionForFixItsProducedBySwiftSyntax() async throws { let project = try await MultiFileTestProject(files: [ "test.swift": "protocol 1️⃣Multi 2️⃣ident 3️⃣{}", "compile_commands.json": "[]", ]) let (uri, positions) = try project.openDocument("test.swift") let report = try await project.testClient.send( DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri)) ) XCTAssertEqual(report.fullReport?.items.count, 1) let codeActions = try XCTUnwrap(report.fullReport?.items.first?.codeActions) let expectedCodeActions = [ CodeAction( title: "Join the identifiers together", kind: .quickFix, edit: WorkspaceEdit( changes: [ uri: [ TextEdit(range: positions["1️⃣"]..(_ input: T1) {}" ) ] ] ), command: nil ) ] } } func testOpaqueParameterToGenericIsNotShownFromTheBody() async throws { try await assertCodeActions( ##""" func someFunction(_ input: some Value) 1️⃣{ 2️⃣print("x") }3️⃣ """##, exhaustive: false ) { uri, positions in [] } } func testConvertJSONToCodable() async throws { try await assertCodeActions( ##""" 1️⃣{ 2️⃣"id": 3️⃣1, "values": 4️⃣["foo", "bar"] }5️⃣ """##, ranges: [("1️⃣", "5️⃣")], exhaustive: false ) { uri, positions in [ CodeAction( title: "Create Codable structs from JSON", kind: .refactorInline, diagnostics: nil, edit: WorkspaceEdit( changes: [ uri: [ TextEdit( range: positions["1️⃣"].. [CodeAction], testName: String = #function, file: StaticString = #filePath, line: UInt = #line ) async throws { let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport) let uri = DocumentURI(for: .swift, testName: testName) let positions = testClient.openDocument(markedText, uri: uri) var ranges = ranges if let markers { ranges += markers.map { ($0, $0) } } else { ranges += extractMarkers(markedText).markers.map(\.key).map { ($0, $0) } } for (startMarker, endMarker) in ranges { let result = try await testClient.send( CodeActionRequest( range: positions[startMarker]..