Skip to content

Commit 37aafd3

Browse files
authored
Merge pull request #2287 from ahoppen/multi-line-completion-item-formatting
2 parents 5b92c82 + e43c808 commit 37aafd3

File tree

2 files changed

+133
-9
lines changed

2 files changed

+133
-9
lines changed

Sources/SwiftLanguageService/CodeCompletionSession.swift

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import SKOptions
1919
import SKUtilities
2020
import SourceKitD
2121
import SourceKitLSP
22+
import SwiftBasicFormat
2223
import SwiftExtensions
2324
import SwiftParser
2425
@_spi(SourceKitLSP) import SwiftRefactor
@@ -396,15 +397,7 @@ class CodeCompletionSession {
396397
return nil
397398
}
398399

399-
let strippedPrefix: String
400-
let exprToExpand: String
401-
if insertText.starts(with: "?.") {
402-
strippedPrefix = "?."
403-
exprToExpand = String(insertText.dropFirst(2))
404-
} else {
405-
strippedPrefix = ""
406-
exprToExpand = insertText
407-
}
400+
let (strippedPrefix, exprToExpand) = extractExpressionToExpand(from: insertText)
408401

409402
// Note we don't need special handling for macro expansions since
410403
// their insertion text doesn't include the '#', so are parsed as
@@ -439,6 +432,33 @@ class CodeCompletionSession {
439432
return String(bytes: expandedBytes, encoding: .utf8)
440433
}
441434

435+
/// Extract the expression to expand by stripping optional chaining prefix if present.
436+
private func extractExpressionToExpand(from insertText: String) -> (strippedPrefix: String, exprToExpand: String) {
437+
if insertText.starts(with: "?.") {
438+
return (strippedPrefix: "?.", exprToExpand: String(insertText.dropFirst(2)))
439+
} else {
440+
return (strippedPrefix: "", exprToExpand: insertText)
441+
}
442+
}
443+
444+
/// If the code completion text returned by sourcekitd, format it using SwiftBasicFormat. This is needed for
445+
/// completion items returned from sourcekitd that already have the trailing closure expanded.
446+
private func formatMultiLineCompletion(insertText: String) -> String? {
447+
// We only need to format the completion result if it's a multi-line completion that needs adjustment of
448+
// indentation.
449+
guard insertText.contains(where: \.isNewline) else {
450+
return nil
451+
}
452+
453+
let (strippedPrefix, exprToExpand) = extractExpressionToExpand(from: insertText)
454+
455+
var parser = Parser(exprToExpand)
456+
let expr = ExprSyntax.parse(from: &parser)
457+
let formatted = expr.formatted(using: ClosureCompletionFormat(indentationWidth: indentationWidth))
458+
459+
return strippedPrefix + formatted.description
460+
}
461+
442462
private func completionsFromSKDResponse(
443463
_ completions: SKDResponseArray,
444464
in snapshot: DocumentSnapshot,
@@ -462,6 +482,8 @@ class CodeCompletionSession {
462482

463483
if let closureExpanded = expandClosurePlaceholders(insertText: insertText) {
464484
insertText = closureExpanded
485+
} else if let multilineFormatted = formatMultiLineCompletion(insertText: insertText) {
486+
insertText = multilineFormatted
465487
}
466488

467489
let text = rewriteSourceKitPlaceholders(in: insertText, clientSupportsSnippets: clientSupportsSnippets)

Tests/SourceKitLSPTests/SwiftCompletionTests.swift

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,108 @@ final class SwiftCompletionTests: XCTestCase {
863863
)
864864
}
865865

866+
func testIndentTrailingClosureBody() async throws {
867+
// sourcekitd returns a completion item with an already expanded closure here. Make sure that we add indentation to
868+
// the body.
869+
try await SkipUnless.sourcekitdSupportsPlugin()
870+
871+
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
872+
let uri = DocumentURI(for: .swift)
873+
let positions = testClient.openDocument(
874+
"""
875+
struct MyArray {
876+
func myasync(execute work: () -> Void) {}
877+
}
878+
879+
func test(x: MyArray) {
880+
x.1️⃣
881+
}
882+
""",
883+
uri: uri
884+
)
885+
let completions = try await testClient.send(
886+
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
887+
)
888+
XCTAssertEqual(
889+
completions.items.clearingUnstableValues.filter { $0.label.contains("myasync {") },
890+
[
891+
CompletionItem(
892+
label: "myasync { code }",
893+
kind: .method,
894+
detail: "Void",
895+
deprecated: false,
896+
filterText: "myasync",
897+
insertText: #"""
898+
myasync {
899+
${1:code}
900+
}
901+
"""#,
902+
insertTextFormat: .snippet,
903+
textEdit: .textEdit(
904+
TextEdit(
905+
range: Range(positions["1️⃣"]),
906+
newText: #"""
907+
myasync {
908+
${1:code}
909+
}
910+
"""#
911+
)
912+
)
913+
)
914+
]
915+
)
916+
}
917+
918+
func testIndentTrailingClosureBodyOnOptional() async throws {
919+
try await SkipUnless.sourcekitdSupportsPlugin()
920+
921+
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
922+
let uri = DocumentURI(for: .swift)
923+
let positions = testClient.openDocument(
924+
"""
925+
struct MyArray {
926+
func myasync(execute work: () -> Void) {}
927+
}
928+
929+
func test(x: MyArray?) {
930+
x1️⃣.2️⃣
931+
}
932+
""",
933+
uri: uri
934+
)
935+
let completions = try await testClient.send(
936+
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
937+
)
938+
XCTAssertEqual(
939+
completions.items.clearingUnstableValues.filter { $0.label.contains("myasync {") },
940+
[
941+
CompletionItem(
942+
label: "myasync { code }",
943+
kind: .method,
944+
detail: "Void",
945+
deprecated: false,
946+
filterText: ".myasync",
947+
insertText: #"""
948+
?.myasync {
949+
${1:code}
950+
}
951+
"""#,
952+
insertTextFormat: .snippet,
953+
textEdit: .textEdit(
954+
TextEdit(
955+
range: positions["1️⃣"]..<positions["2️⃣"],
956+
newText: #"""
957+
?.myasync {
958+
${1:code}
959+
}
960+
"""#
961+
)
962+
)
963+
)
964+
]
965+
)
966+
}
967+
866968
func testExpandClosurePlaceholder() async throws {
867969
try await SkipUnless.sourcekitdSupportsPlugin()
868970

0 commit comments

Comments
 (0)