Skip to content

Commit 90c124c

Browse files
committed
Expand trailing closures of code completion items
Editors other than Xcode don’t have a notion of editor placeholders or their expansion, so we can’t produce results with editor placeholders and expect the user to expand them while completing the function arguments. Instead, expand all trailing closure placeholders when producing the code completion results. The generated expansion is currently not formatted with respect to the file’s indentaiton. Since we don’t want to launch `swift-format` for every completion item, this formatting will need to be done using `BasicFormat` which needs to infer the file’s indentation. Doing so will be non-trivial work on its own and will be done in a follow-up PR. rdar://121130170
1 parent 4269e90 commit 90c124c

File tree

6 files changed

+320
-14
lines changed

6 files changed

+320
-14
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ let package = Package(
296296
.product(name: "SwiftIDEUtils", package: "swift-syntax"),
297297
.product(name: "SwiftParser", package: "swift-syntax"),
298298
.product(name: "SwiftParserDiagnostics", package: "swift-syntax"),
299+
.product(name: "SwiftRefactor", package: "swift-syntax"),
299300
.product(name: "SwiftSyntax", package: "swift-syntax"),
300301
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
301302
],

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ target_link_libraries(SourceKitLSP PUBLIC
6060
SwiftSyntax::SwiftIDEUtils
6161
SwiftSyntax::SwiftParser
6262
SwiftSyntax::SwiftParserDiagnostics
63+
SwiftSyntax::SwiftRefactor
6364
SwiftSyntax::SwiftSyntax)
6465
target_link_libraries(SourceKitLSP PRIVATE
6566
$<$<NOT:$<PLATFORM_ID:Darwin>>:FoundationXML>)

Sources/SourceKitLSP/Swift/CodeCompletion.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ extension SwiftLanguageServer {
4646
return try await CodeCompletionSession.completionList(
4747
sourcekitd: sourcekitd,
4848
snapshot: snapshot,
49+
syntaxTreeParseResult: syntaxTreeManager.incrementalParseResult(for: snapshot),
4950
completionPosition: completionPos,
5051
completionUtf8Offset: offset,
5152
cursorPosition: req.position,

Sources/SourceKitLSP/Swift/CodeCompletionSession.swift

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import LSPLogging
1515
import LanguageServerProtocol
1616
import SKSupport
1717
import SourceKitD
18+
import SwiftParser
19+
@_spi(SourceKitLSP) import SwiftRefactor
20+
import SwiftSyntax
1821

1922
/// Represents a code-completion session for a given source location that can be efficiently
2023
/// re-filtered by calling `update()`.
@@ -88,6 +91,7 @@ class CodeCompletionSession {
8891
static func completionList(
8992
sourcekitd: any SourceKitD,
9093
snapshot: DocumentSnapshot,
94+
syntaxTreeParseResult: IncrementalParseResult,
9195
completionPosition: Position,
9296
completionUtf8Offset: Int,
9397
cursorPosition: Position,
@@ -119,6 +123,7 @@ class CodeCompletionSession {
119123
let session = CodeCompletionSession(
120124
sourcekitd: sourcekitd,
121125
snapshot: snapshot,
126+
syntaxTreeParseResult: syntaxTreeParseResult,
122127
utf8Offset: completionUtf8Offset,
123128
position: completionPosition,
124129
compileCommand: compileCommand,
@@ -135,6 +140,7 @@ class CodeCompletionSession {
135140

136141
private let sourcekitd: any SourceKitD
137142
private let snapshot: DocumentSnapshot
143+
private let syntaxTreeParseResult: IncrementalParseResult
138144
private let utf8StartOffset: Int
139145
private let position: Position
140146
private let compileCommand: SwiftCompileCommand?
@@ -152,12 +158,14 @@ class CodeCompletionSession {
152158
private init(
153159
sourcekitd: any SourceKitD,
154160
snapshot: DocumentSnapshot,
161+
syntaxTreeParseResult: IncrementalParseResult,
155162
utf8Offset: Int,
156163
position: Position,
157164
compileCommand: SwiftCompileCommand?,
158165
clientSupportsSnippets: Bool
159166
) {
160167
self.sourcekitd = sourcekitd
168+
self.syntaxTreeParseResult = syntaxTreeParseResult
161169
self.snapshot = snapshot
162170
self.utf8StartOffset = utf8Offset
163171
self.position = position
@@ -271,6 +279,54 @@ class CodeCompletionSession {
271279

272280
// MARK: - Helpers
273281

282+
private func expandClosurePlaceholders(
283+
insertText: String,
284+
utf8CodeUnitsToErase: Int,
285+
requestPosition: Position
286+
) -> String? {
287+
guard insertText.contains("<#") && insertText.contains("->") else {
288+
// Fast path: There is no closure placeholder to expand
289+
return nil
290+
}
291+
guard requestPosition.line < snapshot.lineTable.count else {
292+
logger.error("Request position is past the last line")
293+
return nil
294+
}
295+
296+
let indentationOfLine = snapshot.lineTable[requestPosition.line].prefix(while: { $0.isWhitespace })
297+
298+
let strippedPrefix: String
299+
let exprToExpand: String
300+
if insertText.starts(with: "?.") {
301+
strippedPrefix = "?."
302+
exprToExpand = indentationOfLine + String(insertText.dropFirst(2))
303+
} else {
304+
strippedPrefix = ""
305+
exprToExpand = indentationOfLine + insertText
306+
}
307+
308+
var parser = Parser(exprToExpand)
309+
let expr = ExprSyntax.parse(from: &parser)
310+
guard let call = OutermostFunctionCallFinder.findOutermostFunctionCall(in: expr),
311+
let expandedCall = ExpandEditorPlaceholdersToTrailingClosures.refactor(syntax: call)
312+
else {
313+
return nil
314+
}
315+
316+
let bytesToExpand = Array(exprToExpand.utf8)
317+
318+
var expandedBytes: [UInt8] = []
319+
// Add the prefix that we stripped of to allow expression parsing
320+
expandedBytes += strippedPrefix.utf8
321+
// Add any part of the expression that didn't end up being part of the function call
322+
expandedBytes += bytesToExpand[0..<call.position.utf8Offset]
323+
// Add the expanded function call excluding the added `indentationOfLine`
324+
expandedBytes += expandedCall.syntaxTextBytes[indentationOfLine.utf8.count...]
325+
// Add any trailing text that didn't end up being part of the function call
326+
expandedBytes += bytesToExpand[call.endPosition.utf8Offset...]
327+
return String(bytes: expandedBytes, encoding: .utf8)
328+
}
329+
274330
private func completionsFromSKDResponse(
275331
_ completions: SKDResponseArray,
276332
in snapshot: DocumentSnapshot,
@@ -286,9 +342,19 @@ class CodeCompletionSession {
286342
}
287343

288344
var filterName: String? = value[keys.name]
289-
let insertText: String? = value[keys.sourceText]
345+
var insertText: String? = value[keys.sourceText]
290346
let typeName: String? = value[sourcekitd.keys.typeName]
291347
let docBrief: String? = value[sourcekitd.keys.docBrief]
348+
let utf8CodeUnitsToErase: Int = value[sourcekitd.keys.numBytesToErase] ?? 0
349+
350+
if let insertTextUnwrapped = insertText {
351+
insertText =
352+
expandClosurePlaceholders(
353+
insertText: insertTextUnwrapped,
354+
utf8CodeUnitsToErase: utf8CodeUnitsToErase,
355+
requestPosition: requestPosition
356+
) ?? insertText
357+
}
292358

293359
let text = insertText.map {
294360
rewriteSourceKitPlaceholders(inString: $0, clientSupportsSnippets: clientSupportsSnippets)
@@ -297,8 +363,6 @@ class CodeCompletionSession {
297363

298364
let textEdit: TextEdit?
299365
if let text = text {
300-
let utf8CodeUnitsToErase: Int = value[sourcekitd.keys.numBytesToErase] ?? 0
301-
302366
textEdit = self.computeCompletionTextEdit(
303367
completionPos: completionPos,
304368
requestPosition: requestPosition,
@@ -411,3 +475,39 @@ extension CodeCompletionSession: CustomStringConvertible {
411475
"\(uri.pseudoPath):\(position)"
412476
}
413477
}
478+
479+
fileprivate class OutermostFunctionCallFinder: SyntaxAnyVisitor {
480+
/// Once a `FunctionCallExprSyntax` has been visited, that syntax node.
481+
var foundCall: FunctionCallExprSyntax?
482+
483+
private func shouldVisit(_ node: some SyntaxProtocol) -> Bool {
484+
if foundCall != nil {
485+
return false
486+
}
487+
return true
488+
}
489+
490+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
491+
guard shouldVisit(node) else {
492+
return .skipChildren
493+
}
494+
return .visitChildren
495+
}
496+
497+
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
498+
guard shouldVisit(node) else {
499+
return .skipChildren
500+
}
501+
foundCall = node
502+
return .skipChildren
503+
}
504+
505+
/// Find the innermost `FunctionCallExprSyntax` that contains `position`.
506+
static func findOutermostFunctionCall(
507+
in tree: some SyntaxProtocol
508+
) -> FunctionCallExprSyntax? {
509+
let finder = OutermostFunctionCallFinder(viewMode: .sourceAccurate)
510+
finder.walk(tree)
511+
return finder.foundCall
512+
}
513+
}

Sources/SourceKitLSP/Swift/SyntaxTreeManager.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,19 @@ actor SyntaxTreeManager {
7171

7272
/// Get the SwiftSyntax tree for the given document snapshot.
7373
func syntaxTree(for snapshot: DocumentSnapshot) async -> SourceFileSyntax {
74+
return await incrementalParseResult(for: snapshot).tree
75+
}
76+
77+
/// Get the `IncrementalParseResult` for the given document snapshot.
78+
func incrementalParseResult(for snapshot: DocumentSnapshot) async -> IncrementalParseResult {
7479
if let syntaxTreeComputation = computation(for: snapshot.id) {
75-
return await syntaxTreeComputation.value.tree
80+
return await syntaxTreeComputation.value
7681
}
7782
let task = Task {
7883
return Parser.parseIncrementally(source: snapshot.text, parseTransition: nil)
7984
}
8085
setComputation(for: snapshot.id, computation: task)
81-
return await task.value.tree
86+
return await task.value
8287
}
8388

8489
/// Register that we have made an edit to an old document snapshot.

0 commit comments

Comments
 (0)