@@ -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 }
0 commit comments