Skip to content

Commit 8371d9d

Browse files
authored
Merge pull request #1072 from ahoppen/ahoppen/expand-trailing-closure
Expand trailing closures of code completion items
2 parents 4dea061 + 90c124c commit 8371d9d

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
@@ -61,6 +61,7 @@ target_link_libraries(SourceKitLSP PUBLIC
6161
SwiftSyntax::SwiftIDEUtils
6262
SwiftSyntax::SwiftParser
6363
SwiftSyntax::SwiftParserDiagnostics
64+
SwiftSyntax::SwiftRefactor
6465
SwiftSyntax::SwiftSyntax)
6566
target_link_libraries(SourceKitLSP PRIVATE
6667
$<$<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)