Skip to content

Commit 511447c

Browse files
committed
Address code review and add "Add product to export this target" action
1 parent 38e4257 commit 511447c

File tree

3 files changed

+152
-45
lines changed

3 files changed

+152
-45
lines changed

Sources/LanguageServerProtocol/SupportTypes/TextDocumentEdit.swift

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -52,31 +52,4 @@ public struct TextDocumentEdit: Hashable, Codable, Sendable {
5252
self.textDocument = textDocument
5353
self.edits = edits
5454
}
55-
56-
public enum CodingKeys: String, CodingKey {
57-
case kind
58-
case textDocument
59-
case edits
60-
}
61-
62-
public func encode(to encoder: Encoder) throws {
63-
var container = encoder.container(keyedBy: CodingKeys.self)
64-
try container.encode("textDocumentEdit", forKey: .kind)
65-
try container.encode(self.textDocument, forKey: .textDocument)
66-
try container.encode(self.edits, forKey: .edits)
67-
}
68-
69-
public init(from decoder: Decoder) throws {
70-
let container = try decoder.container(keyedBy: CodingKeys.self)
71-
let kind = try container.decode(String.self, forKey: .kind)
72-
guard kind == "textDocumentEdit" else {
73-
throw DecodingError.dataCorruptedError(
74-
forKey: .kind,
75-
in: container,
76-
debugDescription: "Kind of TextDocumentEdit is not 'textDocumentEdit'"
77-
)
78-
}
79-
self.textDocument = try container.decode(OptionalVersionedTextDocumentIdentifier.self, forKey: .textDocument)
80-
self.edits = try container.decode([Edit].self, forKey: .edits)
81-
}
8255
}

Sources/SourceKitLSP/Swift/CodeActions/PackageManifestEdits.swift

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,40 +19,112 @@ import SwiftSyntax
1919
/// Syntactic code action provider to provide refactoring actions that
2020
/// edit a package manifest.
2121
struct PackageManifestEdits: SyntaxCodeActionProvider {
22-
static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] {
23-
guard
24-
let token = scope.firstToken,
25-
let call = token.findEnclosingCall()
26-
else {
27-
return []
22+
static func codeActions(in scope: SyntaxCodeActionScope) -> [CodeAction] {
23+
guard let token = scope.firstToken,
24+
let call = token.findEnclosingCall()
25+
else {
26+
return []
27+
}
28+
29+
var actions = [CodeAction]()
30+
31+
// Add test target(s)
32+
actions.append(
33+
contentsOf: maybeAddTestTargetActions(call: call, in: scope)
34+
)
35+
36+
// Add product(s).
37+
actions.append(
38+
contentsOf: maybeAddProductActions(call: call, in: scope)
39+
)
40+
41+
return actions
2842
}
2943

30-
var actions = [CodeAction]()
44+
/// Produce code actions to add test target(s) if we are currently on
45+
/// a target for which we know how to create a test.
46+
static func maybeAddTestTargetActions(
47+
call: FunctionCallExprSyntax,
48+
in scope: SyntaxCodeActionScope
49+
) -> [CodeAction] {
50+
guard let calledMember = call.findMemberAccessCallee(),
51+
targetsThatAllowTests.contains(calledMember),
52+
let targetName = call.findStringArgument(label: "name")
53+
else {
54+
return []
55+
}
3156

32-
// If there's a target name, offer to create a test target derived from it.
33-
if let targetName = call.findStringArgument(label: "name") {
3457
do {
58+
// Describe the target we are going to create.
3559
let target = try TargetDescription(
3660
name: "\(targetName)Tests",
3761
dependencies: [ .byName(name: targetName, condition: nil) ],
3862
type: .test
3963
)
4064

4165
let edits = try AddTarget.addTarget(target, to: scope.file)
42-
actions.append(
43-
CodeAction(
44-
title: "Add test target for this target",
45-
kind: .refactor,
46-
edit: edits.asWorkspaceEdit(snapshot: scope.snapshot)
47-
)
66+
return [
67+
CodeAction(
68+
title: "Add test target",
69+
kind: .refactor,
70+
edit: edits.asWorkspaceEdit(snapshot: scope.snapshot)
71+
)
72+
]
73+
} catch {
74+
return []
75+
}
76+
}
77+
78+
/// A list of target kinds that allow the creation of tests.
79+
static let targetsThatAllowTests: Set<String> = [
80+
"executableTarget",
81+
"macro",
82+
"target",
83+
]
84+
85+
/// Produce code actions to add a product if we are currently on
86+
/// a target for which we can create a product.
87+
static func maybeAddProductActions(
88+
call: FunctionCallExprSyntax,
89+
in scope: SyntaxCodeActionScope
90+
) -> [CodeAction] {
91+
guard let calledMember = call.findMemberAccessCallee(),
92+
targetsThatAllowProducts.contains(calledMember),
93+
let targetName = call.findStringArgument(label: "name")
94+
else {
95+
return []
96+
}
97+
98+
do {
99+
let type: ProductType = calledMember == "executableTarget"
100+
? .executable
101+
: .library(.automatic)
102+
103+
// Describe the target we are going to create.
104+
let product = try ProductDescription(
105+
name: "\(targetName)",
106+
type: type,
107+
targets: [ targetName ]
48108
)
109+
110+
let edits = try AddProduct.addProduct(product, to: scope.file)
111+
return [
112+
CodeAction(
113+
title: "Add product to export this target",
114+
kind: .refactor,
115+
edit: edits.asWorkspaceEdit(snapshot: scope.snapshot)
116+
)
117+
]
49118
} catch {
50-
// nothing to do
119+
return []
51120
}
52121
}
53122

54-
return actions
55-
}
123+
/// A list of target kinds that allow the creation of tests.
124+
static let targetsThatAllowProducts: Set<String> = [
125+
"executableTarget",
126+
"target",
127+
]
56128
}
57129

58130
fileprivate extension PackageEditResult {
@@ -163,4 +235,16 @@ fileprivate extension FunctionCallExprSyntax {
163235

164236
return nil
165237
}
238+
239+
/// Find the callee when it is a member access expression referencing
240+
/// a declaration when a specific name.
241+
func findMemberAccessCallee() -> String? {
242+
guard let memberAccess = self.calledExpression
243+
.as(MemberAccessExprSyntax.self)
244+
else {
245+
return nil
246+
}
247+
248+
return memberAccess.declName.baseName.text
249+
}
166250
}

Tests/SourceKitLSPTests/CodeActionTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,13 @@ final class CodeActionTests: XCTestCase {
290290
}
291291

292292
XCTAssertTrue(codeActions.contains(expectedCodeAction))
293+
294+
// Make sure we get one of the swift-syntax refactoring actions.
295+
XCTAssertTrue(
296+
codeActions.contains { action in
297+
return action.title == "Convert string literal to minimal number of \'#\'s"
298+
}
299+
)
293300
}
294301

295302
func testSemanticRefactorRangeCodeActionResult() async throws {
@@ -482,4 +489,47 @@ final class CodeActionTests: XCTestCase {
482489
]
483490
XCTAssertEqual(expectedCodeActions, codeActions)
484491
}
492+
493+
func testPackageManifestEditingCodeActionResult() async throws {
494+
let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport())
495+
let uri = DocumentURI.for(.swift)
496+
let positions = testClient.openDocument(
497+
"""
498+
// swift-tools-version: 5.5
499+
let package = Package(
500+
name: "packages",
501+
targets: [
502+
.tar1️⃣get(name: "MyLib"),
503+
]
504+
)
505+
""",
506+
uri: uri
507+
)
508+
509+
let testPosition = positions["1️⃣"]
510+
let request = CodeActionRequest(
511+
range: Range(testPosition),
512+
context: .init(),
513+
textDocument: TextDocumentIdentifier(uri)
514+
)
515+
let result = try await testClient.send(request)
516+
517+
guard case .codeActions(let codeActions) = result else {
518+
XCTFail("Expected code actions")
519+
return
520+
}
521+
522+
// Make sure we get the expected package manifest editing actions.
523+
XCTAssertTrue(
524+
codeActions.contains { action in
525+
return action.title == "Add test target"
526+
}
527+
)
528+
529+
XCTAssertTrue(
530+
codeActions.contains { action in
531+
return action.title == "Add product to export this target"
532+
}
533+
)
534+
}
485535
}

0 commit comments

Comments
 (0)