Skip to content

Commit e43c808

Browse files
committed
Format multi-line completion items returned by sourcekitd
For closures that don’t take any inputs and that don’t produce output, sourcekitd returns a completion item that has the trailing closure already expanded. Run SwiftBasicFormat on those to enure that the placeholder for the closure’s body is properly indented. Fixes #2285
1 parent 6301397 commit e43c808

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
@@ -831,6 +831,108 @@ final class SwiftCompletionTests: XCTestCase {
831831
)
832832
}
833833

834+
func testIndentTrailingClosureBody() async throws {
835+
// sourcekitd returns a completion item with an already expanded closure here. Make sure that we add indentation to
836+
// the body.
837+
try await SkipUnless.sourcekitdSupportsPlugin()
838+
839+
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
840+
let uri = DocumentURI(for: .swift)
841+
let positions = testClient.openDocument(
842+
"""
843+
struct MyArray {
844+
func myasync(execute work: () -> Void) {}
845+
}
846+
847+
func test(x: MyArray) {
848+
x.1️⃣
849+
}
850+
""",
851+
uri: uri
852+
)
853+
let completions = try await testClient.send(
854+
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
855+
)
856+
XCTAssertEqual(
857+
completions.items.clearingUnstableValues.filter { $0.label.contains("myasync {") },
858+
[
859+
CompletionItem(
860+
label: "myasync { code }",
861+
kind: .method,
862+
detail: "Void",
863+
deprecated: false,
864+
filterText: "myasync",
865+
insertText: #"""
866+
myasync {
867+
${1:code}
868+
}
869+
"""#,
870+
insertTextFormat: .snippet,
871+
textEdit: .textEdit(
872+
TextEdit(
873+
range: Range(positions["1️⃣"]),
874+
newText: #"""
875+
myasync {
876+
${1:code}
877+
}
878+
"""#
879+
)
880+
)
881+
)
882+
]
883+
)
884+
}
885+
886+
func testIndentTrailingClosureBodyOnOptional() async throws {
887+
try await SkipUnless.sourcekitdSupportsPlugin()
888+
889+
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
890+
let uri = DocumentURI(for: .swift)
891+
let positions = testClient.openDocument(
892+
"""
893+
struct MyArray {
894+
func myasync(execute work: () -> Void) {}
895+
}
896+
897+
func test(x: MyArray?) {
898+
x1️⃣.2️⃣
899+
}
900+
""",
901+
uri: uri
902+
)
903+
let completions = try await testClient.send(
904+
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
905+
)
906+
XCTAssertEqual(
907+
completions.items.clearingUnstableValues.filter { $0.label.contains("myasync {") },
908+
[
909+
CompletionItem(
910+
label: "myasync { code }",
911+
kind: .method,
912+
detail: "Void",
913+
deprecated: false,
914+
filterText: ".myasync",
915+
insertText: #"""
916+
?.myasync {
917+
${1:code}
918+
}
919+
"""#,
920+
insertTextFormat: .snippet,
921+
textEdit: .textEdit(
922+
TextEdit(
923+
range: positions["1️⃣"]..<positions["2️⃣"],
924+
newText: #"""
925+
?.myasync {
926+
${1:code}
927+
}
928+
"""#
929+
)
930+
)
931+
)
932+
]
933+
)
934+
}
935+
834936
func testExpandClosurePlaceholder() async throws {
835937
try await SkipUnless.sourcekitdSupportsPlugin()
836938

0 commit comments

Comments
 (0)