Skip to content

Commit fda8cc8

Browse files
committed
Add a closing parenthesis to function argument completions
Fixes #782
1 parent 79964c8 commit fda8cc8

File tree

2 files changed

+68
-7
lines changed

2 files changed

+68
-7
lines changed

Sources/SwiftLanguageService/CodeCompletionSession.swift

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -489,17 +489,40 @@ class CodeCompletionSession {
489489
let text = rewriteSourceKitPlaceholders(in: insertText, clientSupportsSnippets: clientSupportsSnippets)
490490
let isInsertTextSnippet = clientSupportsSnippets && text != insertText
491491

492-
let textEdit: TextEdit?
493-
let edit = self.computeCompletionTextEdit(
492+
var textEdit = self.computeCompletionTextEdit(
494493
completionPos: completionPos,
495494
requestPosition: requestPosition,
496495
utf8CodeUnitsToErase: utf8CodeUnitsToErase,
497496
newText: text,
498497
snapshot: snapshot
499498
)
500-
textEdit = edit
501499

502-
if utf8CodeUnitsToErase != 0, filterName != nil, let textEdit = textEdit {
500+
let kind: sourcekitd_api_uid_t? = value[sourcekitd.keys.kind]
501+
let completionKind = kind?.asCompletionItemKind(sourcekitd.values) ?? .value
502+
503+
if completionKind == .method || completionKind == .function, name.first == "(", name.last == ")" {
504+
// sourcekitd makes an assumption that the editor inserts a matching `)` when the user types a `(` to start
505+
// argument completions and thus does not contain the closing parentheses in the insert text. Since we can't
506+
// make that assumption of any editor using SourceKit-LSP, add the closing parenthesis when we are completing
507+
// function arguments, indicated by the completion kind and the completion's name being wrapped in parentheses.
508+
textEdit.newText += ")"
509+
510+
let requestIndex = snapshot.index(of: requestPosition)
511+
if snapshot.text[requestIndex] == ")",
512+
let nextIndex = snapshot.text.index(requestIndex, offsetBy: 1, limitedBy: snapshot.text.endIndex)
513+
{
514+
// Now, in case the editor already added the matching closing parenthesis, replace it by the parenthesis we
515+
// are adding as part of the completion above. While this might seem un-intuitive, it is the behavior that
516+
// VS Code expects. If the text edit's insert text does not contain the ')' and the user types the closing
517+
// parenthesis of a function that takes no arguments, VS Code's completion position is after the closing
518+
// parenthesis but no new completion request is sent since no character has been inserted (only the implicitly
519+
// inserted `)` has been overwritten). VS Code will now delete anything from the position that the completion
520+
// request was run, leaving the user without the closing `)`.
521+
textEdit.range = textEdit.range.lowerBound..<snapshot.position(of: nextIndex)
522+
}
523+
}
524+
525+
if utf8CodeUnitsToErase != 0, filterName != nil {
503526
// To support the case where the client is doing prefix matching on the TextEdit range,
504527
// we need to prepend the deleted text to filterText.
505528
// This also works around a behaviour in VS Code that causes completions to not show up
@@ -545,18 +568,17 @@ class CodeCompletionSession {
545568
nil
546569
}
547570

548-
let kind: sourcekitd_api_uid_t? = value[sourcekitd.keys.kind]
549571
return CompletionItem(
550572
label: name,
551-
kind: kind?.asCompletionItemKind(sourcekitd.values) ?? .value,
573+
kind: completionKind,
552574
detail: typeName,
553575
documentation: nil,
554576
deprecated: notRecommended,
555577
sortText: sortText,
556578
filterText: filterName,
557579
insertText: text,
558580
insertTextFormat: isInsertTextSnippet ? .snippet : .plain,
559-
textEdit: textEdit.map(CompletionItemEdit.textEdit),
581+
textEdit: CompletionItemEdit.textEdit(textEdit),
560582
data: data.encodeToLSPAny()
561583
)
562584
}

Tests/SourceKitLSPTests/SwiftCompletionTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,6 +1443,45 @@ final class SwiftCompletionTests: XCTestCase {
14431443
)
14441444
XCTAssert(completions.items.contains(where: { $0.label.contains("myFancyFunction") }))
14451445
}
1446+
1447+
func testArgumentCompletionContainsClosingParenthesisIfNotPresentInEditor() async throws {
1448+
let testClient = try await TestSourceKitLSPClient()
1449+
let uri = DocumentURI(for: .swift)
1450+
let positions = testClient.openDocument(
1451+
"""
1452+
func foo() {
1453+
foo(1️⃣
1454+
}
1455+
""",
1456+
uri: uri
1457+
)
1458+
1459+
let completions = try await testClient.send(
1460+
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
1461+
)
1462+
let completion = try XCTUnwrap(completions.items.only)
1463+
XCTAssertEqual(completion.textEdit, .textEdit(TextEdit(range: Range(positions["1️⃣"]), newText: ")")))
1464+
}
1465+
1466+
func testArgumentCompletionReplacesExistingClosingParenthesis() async throws {
1467+
// See https://github.com/swiftlang/sourcekit-lsp/issues/782 for why this is important
1468+
let testClient = try await TestSourceKitLSPClient()
1469+
let uri = DocumentURI(for: .swift)
1470+
let positions = testClient.openDocument(
1471+
"""
1472+
func foo() {
1473+
foo(1️⃣)2️⃣
1474+
}
1475+
""",
1476+
uri: uri
1477+
)
1478+
1479+
let completions = try await testClient.send(
1480+
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
1481+
)
1482+
let completion = try XCTUnwrap(completions.items.only)
1483+
XCTAssertEqual(completion.textEdit, .textEdit(TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: ")")))
1484+
}
14461485
}
14471486

14481487
private func countFs(_ response: CompletionList) -> Int {

0 commit comments

Comments
 (0)