mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-06 18:24:36 +01:00
1546 lines
46 KiB
Swift
1546 lines
46 KiB
Swift
//===----------------------------------------------------------------------===//
|
||
//
|
||
// This source file is part of the Swift.org open source project
|
||
//
|
||
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
|
||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||
//
|
||
// See https://swift.org/LICENSE.txt for license information
|
||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||
//
|
||
//===----------------------------------------------------------------------===//
|
||
|
||
@_spi(SourceKitLSP) import LanguageServerProtocol
|
||
import SKLogging
|
||
import SKTestSupport
|
||
import SourceKitLSP
|
||
import SwiftExtensions
|
||
import XCTest
|
||
|
||
final class SwiftCompletionTests: SourceKitLSPTestCase {
|
||
// MARK: - Helpers
|
||
|
||
private var snippetCapabilities = ClientCapabilities(
|
||
textDocument: TextDocumentClientCapabilities(
|
||
completion: TextDocumentClientCapabilities.Completion(
|
||
completionItem: TextDocumentClientCapabilities.Completion.CompletionItem(snippetSupport: true)
|
||
)
|
||
)
|
||
)
|
||
|
||
// MARK: - Tests
|
||
|
||
func testCompletionBasic() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
try await SkipUnless.sourcekitdSupportsFullDocumentationInCompletion()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct S {
|
||
/// Documentation for `abc`.
|
||
///
|
||
/// _More_ documentation for `abc`.
|
||
///
|
||
/// Usage:
|
||
/// ```swift
|
||
/// S().abc
|
||
/// ```
|
||
var abc: Int
|
||
|
||
func test(a: Int) {
|
||
self.1️⃣abc
|
||
2️⃣ }
|
||
|
||
func test(_ b: Int) {
|
||
self.abc
|
||
}
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let selfDot = try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"]
|
||
)
|
||
)
|
||
|
||
XCTAssertTrue(selfDot.isIncomplete)
|
||
XCTAssertGreaterThanOrEqual(selfDot.items.count, 2)
|
||
let abc = selfDot.items.first { $0.label == "abc" }
|
||
XCTAssertNotNil(abc)
|
||
if let abc = abc {
|
||
XCTAssertEqual(abc.kind, .property)
|
||
XCTAssertEqual(abc.detail, "Int")
|
||
assertMarkdown(
|
||
documentation: abc.documentation,
|
||
expected: """
|
||
Documentation for `abc`.
|
||
|
||
_More_ documentation for `abc`.
|
||
|
||
Usage:
|
||
```swift
|
||
S().abc
|
||
```
|
||
"""
|
||
)
|
||
XCTAssertEqual(abc.filterText, "abc")
|
||
XCTAssertEqual(abc.textEdit, .textEdit(TextEdit(range: Range(positions["1️⃣"]), newText: "abc")))
|
||
XCTAssertEqual(abc.insertText, "abc")
|
||
XCTAssertEqual(abc.insertTextFormat, .plain)
|
||
}
|
||
|
||
for columnOffset in 1...3 {
|
||
let offsetPosition = positions["1️⃣"].adding(columns: columnOffset)
|
||
let inIdent = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: offsetPosition)
|
||
)
|
||
guard let abc = inIdent.items.first(where: { $0.label == "abc" }) else {
|
||
XCTFail("No completion item with label 'abc'")
|
||
return
|
||
}
|
||
|
||
// If we switch to server-side filtering this will change.
|
||
XCTAssertEqual(abc.kind, .property)
|
||
XCTAssertEqual(abc.detail, "Int")
|
||
assertMarkdown(
|
||
documentation: abc.documentation,
|
||
expected: """
|
||
Documentation for `abc`.
|
||
|
||
_More_ documentation for `abc`.
|
||
|
||
Usage:
|
||
```swift
|
||
S().abc
|
||
```
|
||
"""
|
||
)
|
||
XCTAssertEqual(abc.filterText, "abc")
|
||
XCTAssertEqual(abc.textEdit, .textEdit(TextEdit(range: positions["1️⃣"]..<offsetPosition, newText: "abc")))
|
||
XCTAssertEqual(abc.insertText, "abc")
|
||
XCTAssertEqual(abc.insertTextFormat, .plain)
|
||
}
|
||
|
||
let after = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
|
||
)
|
||
XCTAssertNotEqual(after, selfDot)
|
||
}
|
||
|
||
func testCompletionSnippetSupport() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct S {
|
||
/// Documentation for `abc`.
|
||
var abc: Int
|
||
|
||
func test(a: Int) {
|
||
self.1️⃣abc
|
||
}
|
||
|
||
func test(_ b: Int) {
|
||
self.2️⃣abc
|
||
}
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
func getTestMethodCompletion(_ position: Position, label: String) async throws -> CompletionItem? {
|
||
let selfDot = try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: position
|
||
)
|
||
)
|
||
return selfDot.items.first { $0.label == label }
|
||
}
|
||
|
||
var test = try await getTestMethodCompletion(positions["1️⃣"], label: "test(a: Int)")
|
||
XCTAssertNotNil(test)
|
||
if let test = test {
|
||
XCTAssertEqual(test.kind, .method)
|
||
XCTAssertEqual(test.detail, "Void")
|
||
XCTAssertEqual(test.filterText, "test(a:)")
|
||
XCTAssertEqual(test.textEdit, .textEdit(TextEdit(range: Range(positions["1️⃣"]), newText: "test(a: ${1:Int})")))
|
||
XCTAssertEqual(test.insertText, "test(a: ${1:Int})")
|
||
XCTAssertEqual(test.insertTextFormat, .snippet)
|
||
}
|
||
|
||
test = try await getTestMethodCompletion(positions["2️⃣"], label: "test(b: Int)")
|
||
XCTAssertNotNil(test)
|
||
if let test = test {
|
||
XCTAssertEqual(test.kind, .method)
|
||
XCTAssertEqual(test.detail, "Void")
|
||
XCTAssertEqual(test.filterText, "test(:)")
|
||
XCTAssertEqual(test.textEdit, .textEdit(TextEdit(range: Range(positions["2️⃣"]), newText: "test(${1:Int})")))
|
||
XCTAssertEqual(test.insertText, "test(${1:Int})")
|
||
XCTAssertEqual(test.insertTextFormat, .snippet)
|
||
}
|
||
}
|
||
|
||
func testCompletionNoSnippetSupport() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct S {
|
||
/// Documentation for `abc`.
|
||
var abc: Int
|
||
|
||
func test(a: Int) {
|
||
self.1️⃣abc
|
||
}
|
||
|
||
func test(_ b: Int) {
|
||
self.2️⃣abc
|
||
}
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
func getTestMethodCompletion(_ position: Position, label: String) async throws -> CompletionItem? {
|
||
let selfDot = try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: position
|
||
)
|
||
)
|
||
return selfDot.items.first { $0.label == label }
|
||
}
|
||
|
||
var test = try await getTestMethodCompletion(positions["1️⃣"], label: "test(a: Int)")
|
||
XCTAssertNotNil(test)
|
||
if let test = test {
|
||
XCTAssertEqual(test.kind, .method)
|
||
XCTAssertEqual(test.detail, "Void")
|
||
XCTAssertEqual(test.filterText, "test(a:)")
|
||
XCTAssertEqual(test.textEdit, .textEdit(TextEdit(range: Range(positions["1️⃣"]), newText: "test(a: )")))
|
||
XCTAssertEqual(test.insertText, "test(a: )")
|
||
XCTAssertEqual(test.insertTextFormat, .plain)
|
||
}
|
||
|
||
test = try await getTestMethodCompletion(positions["2️⃣"], label: "test(b: Int)")
|
||
XCTAssertNotNil(test)
|
||
if let test = test {
|
||
XCTAssertEqual(test.kind, .method)
|
||
XCTAssertEqual(test.detail, "Void")
|
||
XCTAssertEqual(test.filterText, "test(:)")
|
||
XCTAssertEqual(test.textEdit, .textEdit(TextEdit(range: Range(positions["2️⃣"]), newText: "test()")))
|
||
XCTAssertEqual(test.insertText, "test()")
|
||
XCTAssertEqual(test.insertTextFormat, .plain)
|
||
}
|
||
}
|
||
|
||
func testCompletionPositionServerFilter() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
testClient.openDocument("foo", uri: uri)
|
||
|
||
for col in 0...3 {
|
||
let inOrAfterFoo = try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: Position(line: 0, utf16index: col)
|
||
)
|
||
)
|
||
XCTAssertTrue(inOrAfterFoo.isIncomplete)
|
||
XCTAssertFalse(inOrAfterFoo.items.isEmpty)
|
||
}
|
||
|
||
let outOfRange1 = try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: Position(line: 0, utf16index: 4)
|
||
)
|
||
)
|
||
XCTAssertTrue(outOfRange1.isIncomplete)
|
||
|
||
let outOfRange2 = try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: Position(line: 1, utf16index: 0)
|
||
)
|
||
)
|
||
XCTAssertTrue(outOfRange2.isIncomplete)
|
||
}
|
||
|
||
func testCompletionOptional() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct Foo {
|
||
let bar: Int
|
||
}
|
||
let a: Foo? = Foo(bar: 1)
|
||
a1️⃣.2️⃣ba
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
for columnOffset in 0...2 {
|
||
let offsetPosition = positions["2️⃣"].adding(columns: columnOffset)
|
||
let response = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: offsetPosition)
|
||
)
|
||
|
||
guard let item = response.items.first(where: { $0.label.contains("bar") }) else {
|
||
XCTFail("No completion item with label 'bar'")
|
||
return
|
||
}
|
||
XCTAssertEqual(item.filterText, ".bar")
|
||
XCTAssertEqual(
|
||
item.textEdit,
|
||
.textEdit(
|
||
TextEdit(range: positions["1️⃣"]..<offsetPosition, newText: "?.bar")
|
||
)
|
||
)
|
||
}
|
||
}
|
||
|
||
func testCompletionOverride() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
class Base {
|
||
func foo() {}
|
||
}
|
||
class C: Base {
|
||
1️⃣func 2️⃣ // don't delete trailing space in this file
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let response = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
|
||
)
|
||
guard let item = response.items.first(where: { $0.label == "foo()" }) else {
|
||
XCTFail("No completion item with label 'foo()'")
|
||
return
|
||
}
|
||
XCTAssertEqual(item.filterText, "func foo()")
|
||
XCTAssertEqual(
|
||
item.textEdit,
|
||
.textEdit(TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "override func foo() {\n\n}"))
|
||
)
|
||
}
|
||
|
||
func testCompletionOverrideInNewLine() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
class Base {
|
||
func foo() {}
|
||
}
|
||
class C: Base {
|
||
func
|
||
1️⃣ // don't delete trailing space in this file
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let response = try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"]
|
||
)
|
||
)
|
||
guard let item = response.items.first(where: { $0.label == "foo()" }) else {
|
||
XCTFail("No completion item with label 'foo()'")
|
||
return
|
||
}
|
||
// If the edit would cross multiple lines, we are currently not replacing any text. It's not technically correct but the best we can do.
|
||
XCTAssertEqual(item.filterText, "foo()")
|
||
XCTAssertEqual(
|
||
item.textEdit,
|
||
.textEdit(TextEdit(range: Range(positions["1️⃣"]), newText: "override func foo() {\n\n}"))
|
||
)
|
||
}
|
||
|
||
func testRefilterAfterIncompleteResults() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct S {
|
||
func fooAbc() {}
|
||
func fooBcd() {}
|
||
func fooCde() {}
|
||
func fooDef() {}
|
||
func fooGoop() {}
|
||
func test() {
|
||
1️⃣ self.f2️⃣cdez
|
||
}
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
assertEqual(
|
||
5,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["2️⃣"],
|
||
context: CompletionContext(triggerKind: .invoked)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
assertEqual(
|
||
3,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["2️⃣"].adding(columns: 1),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
assertEqual(
|
||
2,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["2️⃣"].adding(columns: 2),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
assertEqual(
|
||
1,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["2️⃣"].adding(columns: 3),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
assertEqual(
|
||
0,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["2️⃣"].adding(columns: 4),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
assertEqual(
|
||
2,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["2️⃣"].adding(columns: 2),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
assertEqual(
|
||
1,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["2️⃣"].adding(columns: 3),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
// Trigger kind changed => OK (200 is maxResults since we're outside the member completion)
|
||
assertEqual(
|
||
200,
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"],
|
||
context: CompletionContext(triggerKind: .invoked)
|
||
)
|
||
).items.count
|
||
)
|
||
}
|
||
|
||
func testRefilterAfterIncompleteResultsWithEdits() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct S {
|
||
func fooAbc() {}
|
||
func fooBcd() {}
|
||
func fooCde() {}
|
||
func fooDef() {}
|
||
func fooGoop() {}
|
||
func test() {
|
||
self.f1️⃣z
|
||
}
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
// 'f'
|
||
assertEqual(
|
||
5,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"],
|
||
context: CompletionContext(triggerKind: .invoked)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
// 'fz'
|
||
assertEqual(
|
||
0,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"].adding(columns: 1),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(uri, version: 1),
|
||
contentChanges: [
|
||
.init(range: positions["1️⃣"]..<(positions["1️⃣"].adding(columns: 1)), text: "A ")
|
||
]
|
||
)
|
||
)
|
||
|
||
// 'fA'
|
||
assertEqual(
|
||
1,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"].adding(columns: 1),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
// 'fA '
|
||
assertEqual(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"].adding(columns: 2),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
).items,
|
||
[]
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(uri, version: 1),
|
||
contentChanges: [
|
||
.init(range: positions["1️⃣"]..<(positions["1️⃣"].adding(columns: 1)), text: "Ab")
|
||
]
|
||
)
|
||
)
|
||
|
||
// 'fAb'
|
||
assertEqual(
|
||
1,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"].adding(columns: 1),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(uri, version: 1),
|
||
contentChanges: [
|
||
.init(range: positions["1️⃣"]..<(positions["1️⃣"].adding(columns: 1)), text: "")
|
||
]
|
||
)
|
||
)
|
||
|
||
// 'fb'
|
||
assertEqual(
|
||
2,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"].adding(columns: 1),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(uri, version: 1),
|
||
contentChanges: [
|
||
.init(range: Range(positions["1️⃣"].adding(columns: 1)), text: "d")
|
||
]
|
||
)
|
||
)
|
||
|
||
// 'fbd'
|
||
assertEqual(
|
||
1,
|
||
countFs(
|
||
try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"].adding(columns: 2),
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
}
|
||
|
||
/// Regression test for https://bugs.swift.org/browse/SR-13561 to make sure the a session
|
||
/// close waits for its respective open to finish to prevent a session geting stuck open.
|
||
func testSessionCloseWaitsforOpen() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct S {
|
||
func forSomethingCrazy() {}
|
||
func forSomethingCool() {}
|
||
func test() {
|
||
self.forS1️⃣ome
|
||
}
|
||
func print() {}
|
||
func anotherOne() {
|
||
self.prin2️⃣
|
||
}
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let forSomeComplete = CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["1️⃣"],
|
||
context: CompletionContext(triggerKind: .invoked)
|
||
)
|
||
let printComplete = CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: positions["2️⃣"],
|
||
context: CompletionContext(triggerKind: .invoked)
|
||
)
|
||
|
||
// Code completion for "self.forSome"
|
||
async let forSomeResult = testClient.send(forSomeComplete)
|
||
|
||
// Code completion for "self.prin", previously could immediately invalidate
|
||
// the previous request.
|
||
async let printResult = testClient.send(printComplete)
|
||
|
||
assertEqual(2, countFs(try await forSomeResult))
|
||
assertEqual(1, try await printResult.items.count)
|
||
|
||
// Try code completion for "self.forSome" again to verify that it still works.
|
||
let result = try await testClient.send(forSomeComplete)
|
||
XCTAssertEqual(2, countFs(result))
|
||
}
|
||
|
||
func testCodeCompleteSwiftPackage() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let project = try await SwiftPMTestProject(
|
||
files: [
|
||
"a.swift": """
|
||
struct A {
|
||
func method(a b: Int) {}
|
||
}
|
||
""",
|
||
"b.swift": """
|
||
func test(a: A) {
|
||
a.1️⃣
|
||
}
|
||
""",
|
||
]
|
||
)
|
||
let (uri, positions) = try project.openDocument("b.swift")
|
||
|
||
let testPosition = positions["1️⃣"]
|
||
let results = try await project.testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: testPosition)
|
||
)
|
||
|
||
XCTAssertEqual(
|
||
results.items.clearingUnstableValues,
|
||
[
|
||
CompletionItem(
|
||
label: "method(a: Int)",
|
||
kind: .method,
|
||
detail: "Void",
|
||
deprecated: false,
|
||
filterText: "method(a:)",
|
||
insertText: "method(a: )",
|
||
insertTextFormat: .plain,
|
||
textEdit: .textEdit(
|
||
TextEdit(
|
||
range: Range(testPosition),
|
||
newText: "method(a: )"
|
||
)
|
||
)
|
||
),
|
||
CompletionItem(
|
||
label: "self",
|
||
kind: .keyword,
|
||
detail: "A",
|
||
deprecated: false,
|
||
filterText: "self",
|
||
insertText: "self",
|
||
insertTextFormat: .plain,
|
||
textEdit: .textEdit(
|
||
TextEdit(range: Range(testPosition), newText: "self")
|
||
)
|
||
),
|
||
]
|
||
)
|
||
}
|
||
|
||
func testTriggerFromIncompleteAfterStartingStringLiteral() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
func foo(_ x: String) {}
|
||
|
||
func test() {
|
||
foo1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
// The following is a pattern that VS Code sends. Make sure we don't return an error.
|
||
// - Insert `()``, changing the line to `foo()``
|
||
// - Invoke code completion after `(`
|
||
// - Insert `""`, changing the line to `foo("")`
|
||
// - Insert `d` inside the string literal, changing the line to `foo("d")`
|
||
// - Ask for completion with the `triggerFromIncompleteCompletions` flag set.
|
||
// Since this isn't actually re-filtering but is a completely new code completion session. When we detect this, we
|
||
// should just start a new session and return the results.
|
||
var position = positions["1️⃣"]
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(uri, version: position.utf16index),
|
||
contentChanges: [TextDocumentContentChangeEvent(range: Range(position), text: "()")]
|
||
)
|
||
)
|
||
position.utf16index += 1
|
||
let initialCompletionResults = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: position)
|
||
)
|
||
// Test that we get the "abc" result which makes VS Code think that we are still in the same completion session when doing hte second completion.
|
||
XCTAssert(initialCompletionResults.items.contains(where: { $0.label == #""abc""# }))
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(uri, version: position.utf16index),
|
||
contentChanges: [TextDocumentContentChangeEvent(range: Range(position), text: "\"\"")]
|
||
)
|
||
)
|
||
position.utf16index += 1
|
||
testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(uri, version: position.utf16index),
|
||
contentChanges: [TextDocumentContentChangeEvent(range: Range(position), text: "d")]
|
||
)
|
||
)
|
||
let secondCompletionResults = try await testClient.send(
|
||
CompletionRequest(
|
||
textDocument: TextDocumentIdentifier(uri),
|
||
position: position,
|
||
context: CompletionContext(triggerKind: .triggerFromIncompleteCompletions)
|
||
)
|
||
)
|
||
// We shouldn't be getting code completion results for inside the string literal.
|
||
XCTAssert(secondCompletionResults.items.isEmpty)
|
||
}
|
||
|
||
func testNonAsciiCompletionFilter() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct Foo {
|
||
func 👨👩👦👨👩👦() {}
|
||
func test() {
|
||
self.1️⃣👨👩👦2️⃣
|
||
}
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
|
||
)
|
||
XCTAssertEqual(
|
||
completions.items.clearingUnstableValues,
|
||
[
|
||
CompletionItem(
|
||
label: "👨👩👦👨👩👦()",
|
||
kind: .method,
|
||
detail: "Void",
|
||
deprecated: false,
|
||
filterText: "👨👩👦👨👩👦()",
|
||
insertText: "👨👩👦👨👩👦()",
|
||
insertTextFormat: .plain,
|
||
textEdit: .textEdit(TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: "👨👩👦👨👩👦()"))
|
||
)
|
||
]
|
||
)
|
||
}
|
||
|
||
func testIndentTrailingClosureBody() async throws {
|
||
// sourcekitd returns a completion item with an already expanded closure here. Make sure that we add indentation to
|
||
// the body.
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct MyArray {
|
||
func myasync(execute work: () -> Void) {}
|
||
}
|
||
|
||
func test(x: MyArray) {
|
||
x.1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
XCTAssertEqual(
|
||
completions.items.clearingUnstableValues.filter { $0.label.contains("myasync {") },
|
||
[
|
||
CompletionItem(
|
||
label: "myasync { code }",
|
||
kind: .method,
|
||
detail: "Void",
|
||
deprecated: false,
|
||
filterText: "myasync",
|
||
insertText: #"""
|
||
myasync {
|
||
${1:code}
|
||
}
|
||
"""#,
|
||
insertTextFormat: .snippet,
|
||
textEdit: .textEdit(
|
||
TextEdit(
|
||
range: Range(positions["1️⃣"]),
|
||
newText: #"""
|
||
myasync {
|
||
${1:code}
|
||
}
|
||
"""#
|
||
)
|
||
)
|
||
)
|
||
]
|
||
)
|
||
}
|
||
|
||
func testIndentTrailingClosureBodyOnOptional() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct MyArray {
|
||
func myasync(execute work: () -> Void) {}
|
||
}
|
||
|
||
func test(x: MyArray?) {
|
||
x1️⃣.2️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
|
||
)
|
||
XCTAssertEqual(
|
||
completions.items.clearingUnstableValues.filter { $0.label.contains("myasync {") },
|
||
[
|
||
CompletionItem(
|
||
label: "myasync { code }",
|
||
kind: .method,
|
||
detail: "Void",
|
||
deprecated: false,
|
||
filterText: ".myasync",
|
||
insertText: #"""
|
||
?.myasync {
|
||
${1:code}
|
||
}
|
||
"""#,
|
||
insertTextFormat: .snippet,
|
||
textEdit: .textEdit(
|
||
TextEdit(
|
||
range: positions["1️⃣"]..<positions["2️⃣"],
|
||
newText: #"""
|
||
?.myasync {
|
||
${1:code}
|
||
}
|
||
"""#
|
||
)
|
||
)
|
||
)
|
||
]
|
||
)
|
||
}
|
||
|
||
func testExpandClosurePlaceholder() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct MyArray {
|
||
func myMap(_ body: (Int) -> Bool) {}
|
||
}
|
||
func test(x: MyArray) {
|
||
x.1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
XCTAssertEqual(
|
||
completions.items.clearingUnstableValues.filter { $0.label.contains("myMap") },
|
||
[
|
||
CompletionItem(
|
||
label: "myMap(body: (Int) -> Bool)",
|
||
kind: .method,
|
||
detail: "Void",
|
||
deprecated: false,
|
||
filterText: "myMap(:)",
|
||
insertText: #"""
|
||
myMap(${1:{ ${2:Int} in ${3:Bool} \}})
|
||
"""#,
|
||
insertTextFormat: .snippet,
|
||
textEdit: .textEdit(
|
||
TextEdit(
|
||
range: Range(positions["1️⃣"]),
|
||
newText: #"""
|
||
myMap(${1:{ ${2:Int} in ${3:Bool} \}})
|
||
"""#
|
||
)
|
||
)
|
||
)
|
||
]
|
||
)
|
||
}
|
||
|
||
func testExpandClosurePlaceholderOnOptional() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct MyArray {
|
||
func myMap(_ body: (Int) -> Bool) {}
|
||
}
|
||
func test(x: MyArray?) {
|
||
x1️⃣.2️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"])
|
||
)
|
||
XCTAssertEqual(
|
||
completions.items.clearingUnstableValues.filter { $0.label.contains("myMap") },
|
||
[
|
||
CompletionItem(
|
||
label: "myMap(body: (Int) -> Bool)",
|
||
kind: .method,
|
||
detail: "Void",
|
||
deprecated: false,
|
||
filterText: ".myMap(:)",
|
||
insertText: #"""
|
||
?.myMap(${1:{ ${2:Int} in ${3:Bool} \}})
|
||
"""#,
|
||
insertTextFormat: .snippet,
|
||
textEdit: .textEdit(
|
||
TextEdit(
|
||
range: positions["1️⃣"]..<positions["2️⃣"],
|
||
newText: #"""
|
||
?.myMap(${1:{ ${2:Int} in ${3:Bool} \}})
|
||
"""#
|
||
)
|
||
)
|
||
)
|
||
]
|
||
)
|
||
}
|
||
|
||
func testExpandMultipleClosurePlaceholders() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct MyArray {
|
||
func myMap(_ body: (Int) -> Bool, _ second: (Int) -> String) {}
|
||
}
|
||
func test(x: MyArray) {
|
||
x.1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
XCTAssertEqual(
|
||
completions.items.clearingUnstableValues.filter { $0.label.contains("myMap") },
|
||
[
|
||
CompletionItem(
|
||
label: "myMap(body: (Int) -> Bool, second: (Int) -> String)",
|
||
kind: .method,
|
||
detail: "Void",
|
||
deprecated: false,
|
||
filterText: "myMap(::)",
|
||
insertText: #"""
|
||
myMap(${1:{ ${2:Int} in ${3:Bool} \}}, ${4:{ ${5:Int} in ${6:String} \}})
|
||
"""#,
|
||
insertTextFormat: .snippet,
|
||
textEdit: .textEdit(
|
||
TextEdit(
|
||
range: Range(positions["1️⃣"]),
|
||
newText: #"""
|
||
myMap(${1:{ ${2:Int} in ${3:Bool} \}}, ${4:{ ${5:Int} in ${6:String} \}})
|
||
"""#
|
||
)
|
||
)
|
||
)
|
||
]
|
||
)
|
||
}
|
||
|
||
func testExpandMultipleClosurePlaceholdersWithLabel() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct MyArray {
|
||
func myMap(_ body: (Int) -> Bool, second: (Int) -> String) {}
|
||
}
|
||
func test(x: MyArray) {
|
||
x.1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
XCTAssertEqual(
|
||
completions.items.clearingUnstableValues.filter { $0.label.contains("myMap") },
|
||
[
|
||
CompletionItem(
|
||
label: "myMap(body: (Int) -> Bool, second: (Int) -> String)",
|
||
kind: .method,
|
||
detail: "Void",
|
||
deprecated: false,
|
||
filterText: "myMap(:second:)",
|
||
insertText: #"""
|
||
myMap(${1:{ ${2:Int} in ${3:Bool} \}}, second: ${4:{ ${5:Int} in ${6:String} \}})
|
||
"""#,
|
||
insertTextFormat: .snippet,
|
||
textEdit: .textEdit(
|
||
TextEdit(
|
||
range: Range(positions["1️⃣"]),
|
||
newText: #"""
|
||
myMap(${1:{ ${2:Int} in ${3:Bool} \}}, second: ${4:{ ${5:Int} in ${6:String} \}})
|
||
"""#
|
||
)
|
||
)
|
||
)
|
||
]
|
||
)
|
||
}
|
||
|
||
func testInferIndentationWhenExpandingClosurePlaceholder() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct MyArray {
|
||
func myMap(_ body: (Int) -> Bool) -> Int {
|
||
return 1
|
||
}
|
||
}
|
||
func test(x: MyArray) {
|
||
x.1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
XCTAssertEqual(
|
||
completions.items.filter { $0.label.contains("myMap") }.clearingUnstableValues,
|
||
[
|
||
CompletionItem(
|
||
label: "myMap(body: (Int) -> Bool)",
|
||
kind: .method,
|
||
detail: "Int",
|
||
deprecated: false,
|
||
filterText: "myMap(:)",
|
||
insertText: #"""
|
||
myMap(${1:{ ${2:Int} in ${3:Bool} \}})
|
||
"""#,
|
||
insertTextFormat: .snippet,
|
||
textEdit: .textEdit(
|
||
TextEdit(
|
||
range: Range(positions["1️⃣"]),
|
||
newText: #"""
|
||
myMap(${1:{ ${2:Int} in ${3:Bool} \}})
|
||
"""#
|
||
)
|
||
)
|
||
)
|
||
]
|
||
)
|
||
}
|
||
|
||
func testExpandMacroClosurePlaceholder() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
@freestanding(expression)
|
||
macro myMacroExpr(fn: (Int) -> String) = #externalMacro(module: "", type: "")
|
||
|
||
@freestanding(declaration)
|
||
macro myMacroDecl(fn1: (Int) -> String, fn2: () -> Void) = #externalMacro(module: "", type: "")
|
||
|
||
func test() {
|
||
#1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
XCTAssertEqual(
|
||
completions.items.clearingUnstableValues.filter { $0.label.contains("myMacro") },
|
||
[
|
||
CompletionItem(
|
||
label: "myMacroDecl(fn1: (Int) -> String, fn2: () -> Void)",
|
||
kind: .value,
|
||
detail: "Declaration Macro",
|
||
deprecated: false,
|
||
filterText: "myMacroDecl(fn1:fn2:)",
|
||
insertText: #"""
|
||
myMacroDecl(fn1: ${1:{ ${2:Int} in ${3:String} \}}, fn2: ${4:{ ${5:Void} \}})
|
||
"""#,
|
||
insertTextFormat: .snippet,
|
||
textEdit: .textEdit(
|
||
TextEdit(
|
||
range: Range(positions["1️⃣"]),
|
||
newText: #"""
|
||
myMacroDecl(fn1: ${1:{ ${2:Int} in ${3:String} \}}, fn2: ${4:{ ${5:Void} \}})
|
||
"""#
|
||
)
|
||
)
|
||
),
|
||
CompletionItem(
|
||
label: "myMacroExpr(fn: (Int) -> String)",
|
||
kind: .value,
|
||
detail: "Void",
|
||
deprecated: false,
|
||
filterText: "myMacroExpr(fn:)",
|
||
insertText: #"""
|
||
myMacroExpr(fn: ${1:{ ${2:Int} in ${3:String} \}})
|
||
"""#,
|
||
insertTextFormat: .snippet,
|
||
textEdit: .textEdit(
|
||
TextEdit(
|
||
range: Range(positions["1️⃣"]),
|
||
newText: #"""
|
||
myMacroExpr(fn: ${1:{ ${2:Int} in ${3:String} \}})
|
||
"""#
|
||
)
|
||
)
|
||
),
|
||
]
|
||
)
|
||
}
|
||
|
||
func testCompletionScoring() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct Foo {
|
||
func makeBool() -> Bool { true }
|
||
func makeInt() -> Int { 1 }
|
||
func makeString() -> String { "" }
|
||
}
|
||
func test(foo: Foo) {
|
||
let x: Int = foo.make1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
XCTAssertEqual(
|
||
completions.items.clearingUnstableValues.map(\.label),
|
||
["makeInt()", "makeBool()", "makeString()"]
|
||
)
|
||
}
|
||
|
||
func testCompletionItemResolve() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
try await SkipUnless.sourcekitdSupportsFullDocumentationInCompletion()
|
||
|
||
let capabilities = ClientCapabilities(
|
||
textDocument: TextDocumentClientCapabilities(
|
||
completion: TextDocumentClientCapabilities.Completion(
|
||
completionItem: TextDocumentClientCapabilities.Completion.CompletionItem(
|
||
resolveSupport: TextDocumentClientCapabilities.Completion.CompletionItem.ResolveSupportProperties(
|
||
properties: ["documentation"]
|
||
)
|
||
)
|
||
)
|
||
)
|
||
)
|
||
|
||
let testClient = try await TestSourceKitLSPClient(capabilities: capabilities)
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct Foo {
|
||
/// Creates a true value
|
||
func makeBool() -> Bool { true }
|
||
}
|
||
func test(foo: Foo) {
|
||
foo.make1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
let item = try XCTUnwrap(completions.items.only)
|
||
XCTAssertNil(item.documentation)
|
||
let resolvedItem = try await testClient.send(CompletionItemResolveRequest(item: item))
|
||
assertMarkdown(
|
||
documentation: resolvedItem.documentation,
|
||
expected: "Creates a true value"
|
||
)
|
||
}
|
||
|
||
func testCompletionBriefDocumentationFallback() async throws {
|
||
try await SkipUnless.sourcekitdSupportsPlugin()
|
||
try await SkipUnless.sourcekitdSupportsFullDocumentationInCompletion()
|
||
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
|
||
// We test completion for result builder build functions since they don't have full documentation
|
||
// but still have brief documentation.
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
@resultBuilder
|
||
struct AnyBuilder {
|
||
static func 1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
let item = try XCTUnwrap(completions.items.filter { $0.label.contains("buildBlock") }.only)
|
||
assertMarkdown(
|
||
documentation: item.documentation,
|
||
expected: "Required by every result builder to build combined results from statement blocks"
|
||
)
|
||
}
|
||
|
||
func testCallDefaultedArguments() async throws {
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
struct Foo {
|
||
func makeBool(value: Bool = true) -> Bool { value }
|
||
}
|
||
func test(foo: Foo) {
|
||
foo.make1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
XCTAssertEqual(completions.items.map(\.insertText), ["makeBool()", "makeBool(value: )"])
|
||
}
|
||
|
||
func testCompletionUsingCompileFlagsTxt() async throws {
|
||
let compileFlags =
|
||
if let defaultSDKPath {
|
||
"""
|
||
-DFOO
|
||
-sdk
|
||
\(defaultSDKPath)
|
||
"""
|
||
} else {
|
||
"-DFOO"
|
||
}
|
||
|
||
let project = try await MultiFileTestProject(
|
||
files: [
|
||
"test.swift": """
|
||
func test() {
|
||
#if FOO
|
||
let myVar: String
|
||
#else
|
||
let myVar: Int
|
||
#endif
|
||
print(myVar1️⃣)
|
||
}
|
||
""",
|
||
"compile_flags.txt": compileFlags,
|
||
]
|
||
)
|
||
let (uri, positions) = try project.openDocument("test.swift")
|
||
let completions = try await project.testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
XCTAssertEqual(completions.items.only?.detail, "String")
|
||
}
|
||
|
||
func testSuggestInMemoryFunctionsFromFilesWithinSameModule() async throws {
|
||
let project = try await SwiftPMTestProject(files: [
|
||
"First.swift": """
|
||
myFancyFunc1️⃣
|
||
""",
|
||
"Second.swift": "2️⃣",
|
||
])
|
||
|
||
let (secondUri, secondPositions) = try project.openDocument("Second.swift")
|
||
project.testClient.send(
|
||
DidChangeTextDocumentNotification(
|
||
textDocument: VersionedTextDocumentIdentifier(secondUri, version: 2),
|
||
contentChanges: [
|
||
TextDocumentContentChangeEvent(
|
||
range: Range(secondPositions["2️⃣"]),
|
||
text: """
|
||
func myFancyFunction() {}
|
||
"""
|
||
)
|
||
]
|
||
)
|
||
)
|
||
|
||
// Synchronize to make sure that the edit to Second.swift has been processed since opening First.swift and the
|
||
// completion request do not have any dependencies on the edit of Second.swift.
|
||
try await project.testClient.send(SynchronizeRequest())
|
||
let (firstUri, firstPositions) = try project.openDocument("First.swift")
|
||
let completions = try await project.testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(firstUri), position: firstPositions["1️⃣"])
|
||
)
|
||
XCTAssert(completions.items.contains(where: { $0.label.contains("myFancyFunction") }))
|
||
}
|
||
|
||
func testSubscriptCompletions() async throws {
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
func foo(x: [Int: Int]) {
|
||
x[1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
assertContains(completions.items.map(\.label), "[key: Int, default: Int]")
|
||
}
|
||
|
||
func testArgumentCompletionContainsClosingParenthesisIfNotPresentInEditor() async throws {
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
func foo() {
|
||
foo(1️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
let completion = try XCTUnwrap(completions.items.only)
|
||
XCTAssertEqual(completion.textEdit, .textEdit(TextEdit(range: Range(positions["1️⃣"]), newText: ")")))
|
||
}
|
||
|
||
func testArgumentCompletionReplacesExistingClosingParenthesis() async throws {
|
||
// See https://github.com/swiftlang/sourcekit-lsp/issues/782 for why this is important
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
func foo() {
|
||
foo(1️⃣)2️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
let completion = try XCTUnwrap(completions.items.only)
|
||
XCTAssertEqual(completion.textEdit, .textEdit(TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: ")")))
|
||
}
|
||
|
||
func testArgumentCompletionReplacesExistingClosingParenthesisInNestedCall() async throws {
|
||
let testClient = try await TestSourceKitLSPClient()
|
||
let uri = DocumentURI(for: .swift)
|
||
let positions = testClient.openDocument(
|
||
"""
|
||
func foo() {
|
||
foo(foo(1️⃣)2️⃣
|
||
}
|
||
""",
|
||
uri: uri
|
||
)
|
||
|
||
let completions = try await testClient.send(
|
||
CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
|
||
)
|
||
let completion = try XCTUnwrap(completions.items.only)
|
||
XCTAssertEqual(completion.textEdit, .textEdit(TextEdit(range: positions["1️⃣"]..<positions["2️⃣"], newText: ")")))
|
||
}
|
||
}
|
||
|
||
private func countFs(_ response: CompletionList) -> Int {
|
||
return response.items.filter { $0.label.hasPrefix("f") }.count
|
||
}
|
||
|
||
private func assertMarkdown(
|
||
documentation: StringOrMarkupContent?,
|
||
expected: String,
|
||
file: StaticString = #filePath,
|
||
line: UInt = #line
|
||
) {
|
||
XCTAssertEqual(
|
||
documentation,
|
||
.markupContent(MarkupContent(kind: .markdown, value: expected)),
|
||
file: file,
|
||
line: line
|
||
)
|
||
}
|
||
|
||
fileprivate extension Position {
|
||
func adding(columns: Int) -> Position {
|
||
return Position(line: line, utf16index: utf16index + columns)
|
||
}
|
||
}
|