diff --git a/Sources/SKTestSupport/CompletionItem+clearingUnstableValues.swift b/Sources/SKTestSupport/CompletionItem+clearingUnstableValues.swift new file mode 100644 index 000000000..e549ed894 --- /dev/null +++ b/Sources/SKTestSupport/CompletionItem+clearingUnstableValues.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 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 +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6) +package import LanguageServerProtocol +#else +import LanguageServerProtocol +#endif + +extension Array { + /// Remove `sortText` and `data` from all completion items as these are not stable across runs. Instead, sort items + /// by `sortText` to ensure we test them in the order that an editor would display them in. + package var clearingUnstableValues: [CompletionItem] { + return + self + .sorted(by: { ($0.sortText ?? "") < ($1.sortText ?? "") }) + .map { + var item = $0 + item.sortText = nil + item.data = nil + return item + } + } +} diff --git a/Sources/SourceKitLSP/Swift/CodeCompletion.swift b/Sources/SourceKitLSP/Swift/CodeCompletion.swift index 4b0ff3aed..ff0415d37 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletion.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletion.swift @@ -29,8 +29,7 @@ extension SwiftLanguageService { let snapshot = try documentManager.latestSnapshot(req.textDocument.uri) let completionPos = await adjustPositionToStartOfIdentifier(req.position, in: snapshot) - let offset = snapshot.utf8Offset(of: completionPos) - let filterText = String(snapshot.text[snapshot.indexOf(utf8Offset: offset).. LSPAny { + var dict: [String: LSPAny] = [:] + if let id { + dict["id"] = .int(id) + } + return .dictionary(dict) + } +} + /// Represents a code-completion session for a given source location that can be efficiently /// re-filtered by calling `update()`. /// @@ -98,7 +123,6 @@ class CodeCompletionSession { options: SourceKitLSPOptions, indentationWidth: Trivia?, completionPosition: Position, - completionUtf8Offset: Int, cursorPosition: Position, compileCommand: SwiftCompileCommand?, clientSupportsSnippets: Bool, @@ -107,8 +131,9 @@ class CodeCompletionSession { let task = completionQueue.asyncThrowing { if let session = completionSessions[ObjectIdentifier(sourcekitd)], session.state == .open { let isCompatible = - session.snapshot.uri == snapshot.uri && session.utf8StartOffset == completionUtf8Offset - && session.position == completionPosition && session.compileCommand == compileCommand + session.snapshot.uri == snapshot.uri + && session.position == completionPosition + && session.compileCommand == compileCommand && session.clientSupportsSnippets == clientSupportsSnippets if isCompatible { @@ -128,7 +153,6 @@ class CodeCompletionSession { snapshot: snapshot, options: options, indentationWidth: indentationWidth, - utf8Offset: completionUtf8Offset, position: completionPosition, compileCommand: compileCommand, clientSupportsSnippets: clientSupportsSnippets @@ -161,7 +185,6 @@ class CodeCompletionSession { private let options: SourceKitLSPOptions /// The inferred indentation width of the source file the completion is being performed in private let indentationWidth: Trivia? - private let utf8StartOffset: Int private let position: Position private let compileCommand: SwiftCompileCommand? private let clientSupportsSnippets: Bool @@ -180,7 +203,6 @@ class CodeCompletionSession { snapshot: DocumentSnapshot, options: SourceKitLSPOptions, indentationWidth: Trivia?, - utf8Offset: Int, position: Position, compileCommand: SwiftCompileCommand?, clientSupportsSnippets: Bool @@ -189,7 +211,6 @@ class CodeCompletionSession { self.options = options self.indentationWidth = indentationWidth self.snapshot = snapshot - self.utf8StartOffset = utf8Offset self.position = position self.compileCommand = compileCommand self.clientSupportsSnippets = clientSupportsSnippets @@ -197,7 +218,7 @@ class CodeCompletionSession { private func open( filterText: String, - position: Position, + position cursorPosition: Position, in snapshot: DocumentSnapshot ) async throws -> CompletionList { logger.info("Opening code completion session: \(self.description) filter=\(filterText)") @@ -205,14 +226,15 @@ class CodeCompletionSession { throw ResponseError(code: .invalidRequest, message: "open must use the original snapshot") } + let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) let req = sourcekitd.dictionary([ keys.request: sourcekitd.requests.codeCompleteOpen, - keys.offset: utf8StartOffset, + keys.line: sourcekitdPosition.line, + keys.column: sourcekitdPosition.utf8Column, keys.name: uri.pseudoPath, keys.sourceFile: uri.pseudoPath, keys.sourceText: snapshot.text, keys.codeCompleteOptions: optionsDictionary(filterText: filterText), - keys.compilerArgs: compileCommand?.compilerArgs as [SKDRequestValue]?, ]) let dict = try await sourcekitd.send( @@ -228,11 +250,11 @@ class CodeCompletionSession { try Task.checkCancellation() - return self.completionsFromSKDResponse( + return await self.completionsFromSKDResponse( completions, in: snapshot, completionPos: self.position, - requestPosition: position, + requestPosition: cursorPosition, isIncomplete: true ) } @@ -243,10 +265,13 @@ class CodeCompletionSession { in snapshot: DocumentSnapshot ) async throws -> CompletionList { logger.info("Updating code completion session: \(self.description) filter=\(filterText)") + let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) let req = sourcekitd.dictionary([ keys.request: sourcekitd.requests.codeCompleteUpdate, - keys.offset: utf8StartOffset, + keys.line: sourcekitdPosition.line, + keys.column: sourcekitdPosition.utf8Column, keys.name: uri.pseudoPath, + keys.sourceFile: uri.pseudoPath, keys.codeCompleteOptions: optionsDictionary(filterText: filterText), ]) @@ -259,7 +284,7 @@ class CodeCompletionSession { return CompletionList(isIncomplete: false, items: []) } - return self.completionsFromSKDResponse( + return await self.completionsFromSKDResponse( completions, in: snapshot, completionPos: self.position, @@ -281,6 +306,7 @@ class CodeCompletionSession { // Filtering options. keys.filterText: filterText, keys.requestLimit: 200, + keys.useNewAPI: 1, ]) return dict } @@ -291,9 +317,12 @@ class CodeCompletionSession { // Already closed, nothing to do. break case .open: + let sourcekitdPosition = snapshot.sourcekitdPosition(of: self.position) let req = sourcekitd.dictionary([ keys.request: sourcekitd.requests.codeCompleteClose, - keys.offset: utf8StartOffset, + keys.line: sourcekitdPosition.line, + keys.column: sourcekitdPosition.utf8Column, + keys.sourceFile: snapshot.uri.pseudoPath, keys.name: snapshot.uri.pseudoPath, ]) logger.info("Closing code completion session: \(self.description)") @@ -356,7 +385,10 @@ class CodeCompletionSession { completionPos: Position, requestPosition: Position, isIncomplete: Bool - ) -> CompletionList { + ) async -> CompletionList { + let sourcekitd = self.sourcekitd + let keys = sourcekitd.keys + let completionItems = completions.compactMap { (value: SKDResponseDictionary) -> CompletionItem? in guard let name: String = value[keys.description], var insertText: String = value[keys.sourceText] @@ -366,7 +398,6 @@ class CodeCompletionSession { var filterName: String? = value[keys.name] let typeName: String? = value[sourcekitd.keys.typeName] - let docBrief: String? = value[sourcekitd.keys.docBrief] let utf8CodeUnitsToErase: Int = value[sourcekitd.keys.numBytesToErase] ?? 0 if let closureExpanded = expandClosurePlaceholders(insertText: insertText) { @@ -398,22 +429,64 @@ class CodeCompletionSession { // Map SourceKit's not_recommended field to LSP's deprecated let notRecommended = (value[sourcekitd.keys.notRecommended] ?? 0) != 0 + let sortText: String? + if let semanticScore: Double = value[sourcekitd.keys.semanticScore] { + // sourcekitd returns numeric completion item scores with a higher score being better. LSP's sort text is + // lexicographical. Map the numeric score to a lexicographically sortable score by subtracting it from 5_000. + // This gives us a valid range of semantic scores from -5_000 to 5_000 that can be sorted correctly + // lexicographically. This should be sufficient as semantic scores are typically single-digit. + var lexicallySortableScore = 5_000 - semanticScore + if lexicallySortableScore < 0 { + logger.fault("Semantic score out-of-bounds: \(semanticScore, privacy: .public)") + lexicallySortableScore = 0 + } + if lexicallySortableScore >= 10_000 { + logger.fault("Semantic score out-of-bounds: \(semanticScore, privacy: .public)") + lexicallySortableScore = 9_999.99999999 + } + sortText = String(format: "%013.8f", lexicallySortableScore) + "-\(name)" + } else { + sortText = nil + } + + let data = CompletionItemData(id: value[keys.identifier] as Int?) + let kind: sourcekitd_api_uid_t? = value[sourcekitd.keys.kind] return CompletionItem( label: name, kind: kind?.asCompletionItemKind(sourcekitd.values) ?? .value, detail: typeName, - documentation: docBrief != nil ? .markupContent(MarkupContent(kind: .markdown, value: docBrief!)) : nil, + documentation: nil, deprecated: notRecommended, - sortText: nil, + sortText: sortText, filterText: filterName, insertText: text, insertTextFormat: isInsertTextSnippet ? .snippet : .plain, - textEdit: textEdit.map(CompletionItemEdit.textEdit) + textEdit: textEdit.map(CompletionItemEdit.textEdit), + data: data.encodeToLSPAny() ) } - return CompletionList(isIncomplete: isIncomplete, items: completionItems) + // TODO: Only compute documentation if the client doesn't support `completionItem/resolve` + // (https://github.com/swiftlang/sourcekit-lsp/issues/1935) + let withDocumentation = await completionItems.asyncMap { item in + var item = item + + if let itemId = CompletionItemData(fromLSPAny: item.data)?.id { + let req = sourcekitd.dictionary([ + keys.request: sourcekitd.requests.codeCompleteDocumentation, + keys.identifier: itemId, + ]) + let documentationResponse = try? await sourcekitd.send(req, timeout: .seconds(1), fileContents: snapshot.text) + if let docString: String = documentationResponse?[keys.docBrief] { + item.documentation = .markupContent(MarkupContent(kind: .markdown, value: docString)) + } + } + + return item + } + + return CompletionList(isIncomplete: isIncomplete, items: withDocumentation) } private func computeCompletionTextEdit( diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift index c6cdee74e..5df1fb58a 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageService.swift @@ -1163,6 +1163,16 @@ extension SwiftLanguageService: SKDNotificationHandler { // MARK: - Position conversion +/// A line:column position as it is used in sourcekitd, using UTF-8 for the column index and using a one-based line and +/// column number. +struct SourceKitDPosition { + /// Line number within a document (one-based). + public var line: Int + + /// UTF-8 code-unit offset from the start of a line (1-based). + public var utf8Column: Int +} + extension DocumentSnapshot { // MARK: String.Index <-> Raw UTF-8 @@ -1380,6 +1390,23 @@ extension DocumentSnapshot { ) } + // MARK: Position <-> SourceKitDPosition + + func sourcekitdPosition( + of position: Position, + callerFile: StaticString = #fileID, + callerLine: UInt = #line + ) -> SourceKitDPosition { + let utf8Column = lineTable.utf8ColumnAt( + line: position.line, + utf16Column: position.utf16index, + callerFile: callerFile, + callerLine: callerLine + ) + // FIXME: Introduce new type for UTF-8 based positions + return SourceKitDPosition(line: position.line + 1, utf8Column: utf8Column + 1) + } + // MAR: Position <-> SymbolLocation /// Converts the given UTF-8-offset-based `SymbolLocation` to a UTF-16-based line:column `Position`. diff --git a/Sources/SwiftSourceKitPlugin/ASTCompletion/ASTCompletionItem.swift b/Sources/SwiftSourceKitPlugin/ASTCompletion/ASTCompletionItem.swift index 8cf9b0a19..a10b2c7c3 100644 --- a/Sources/SwiftSourceKitPlugin/ASTCompletion/ASTCompletionItem.swift +++ b/Sources/SwiftSourceKitPlugin/ASTCompletion/ASTCompletionItem.swift @@ -13,6 +13,7 @@ import CompletionScoring import Csourcekitd import Foundation +import SKLogging import SourceKitD import SwiftExtensions @@ -392,12 +393,12 @@ extension CompletionItem { self.typeName = astItem.typeName(in: session) var editRange = completionReplaceRange if astItem.numBytesToErase(in: session) > 0 { - var newCol = editRange.lowerBound.utf8Column - astItem.numBytesToErase(in: session) - if newCol < 1 { - assertionFailure("num_bytes_to_erase crosses line boundary") - newCol = 1 + let newCol = editRange.lowerBound.utf8Column - astItem.numBytesToErase(in: session) + if newCol >= 1 { + editRange = Position(line: editRange.lowerBound.line, utf8Column: newCol).. CompletionItem? { let selfDot = try await testClient.send( @@ -133,49 +132,51 @@ final class SwiftCompletionTests: XCTestCase { return selfDot.items.first { $0.label == label } } - var test = try await getTestMethodCompletion(Position(line: 5, utf16index: 9), label: "test(a: Int)") + 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: Position(line: 5, utf16index: 9).. CompletionItem? { let selfDot = try await testClient.send( @@ -187,40 +188,32 @@ final class SwiftCompletionTests: XCTestCase { return selfDot.items.first { $0.label == label } } - var test = try await getTestMethodCompletion(Position(line: 5, utf16index: 9), label: "test(a: Int)") + 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: Position(line: 5, utf16index: 9).. Bool)", kind: .method, detail: "Void", deprecated: false, - sortText: nil, filterText: "myMap(:)", insertText: #""" myMap(${1:{ ${2:Int} in ${3:Bool} \}}) @@ -881,6 +876,8 @@ final class SwiftCompletionTests: XCTestCase { } func testExpandClosurePlaceholderOnOptional() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( @@ -898,14 +895,13 @@ final class SwiftCompletionTests: XCTestCase { CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["2️⃣"]) ) XCTAssertEqual( - completions.items.filter { $0.label.contains("myMap") }, + completions.items.clearingUnstableValues.filter { $0.label.contains("myMap") }, [ CompletionItem( - label: "?.myMap(body: (Int) -> Bool)", + label: "myMap(body: (Int) -> Bool)", kind: .method, detail: "Void", deprecated: false, - sortText: nil, filterText: ".myMap(:)", insertText: #""" ?.myMap(${1:{ ${2:Int} in ${3:Bool} \}}) @@ -925,6 +921,8 @@ final class SwiftCompletionTests: XCTestCase { } func testExpandMultipleClosurePlaceholders() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( @@ -942,14 +940,13 @@ final class SwiftCompletionTests: XCTestCase { CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"]) ) XCTAssertEqual( - completions.items.filter { $0.label.contains("myMap") }, + completions.items.clearingUnstableValues.filter { $0.label.contains("myMap") }, [ CompletionItem( label: "myMap(body: (Int) -> Bool, second: (Int) -> String)", kind: .method, detail: "Void", deprecated: false, - sortText: nil, filterText: "myMap(::)", insertText: #""" myMap(${1:{ ${2:Int} in ${3:Bool} \}}, ${4:{ ${5:Int} in ${6:String} \}}) @@ -969,6 +966,8 @@ final class SwiftCompletionTests: XCTestCase { } func testExpandMultipleClosurePlaceholdersWithLabel() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( @@ -986,14 +985,13 @@ final class SwiftCompletionTests: XCTestCase { CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"]) ) XCTAssertEqual( - completions.items.filter { $0.label.contains("myMap") }, + completions.items.clearingUnstableValues.filter { $0.label.contains("myMap") }, [ CompletionItem( label: "myMap(body: (Int) -> Bool, second: (Int) -> String)", kind: .method, detail: "Void", deprecated: false, - sortText: nil, filterText: "myMap(:second:)", insertText: #""" myMap(${1:{ ${2:Int} in ${3:Bool} \}}, second: ${4:{ ${5:Int} in ${6:String} \}}) @@ -1013,6 +1011,8 @@ final class SwiftCompletionTests: XCTestCase { } func testInferIndentationWhenExpandingClosurePlaceholder() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + let testClient = try await TestSourceKitLSPClient(capabilities: snippetCapabilities) let uri = DocumentURI(for: .swift) let positions = testClient.openDocument( @@ -1032,14 +1032,13 @@ final class SwiftCompletionTests: XCTestCase { CompletionRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"]) ) XCTAssertEqual( - completions.items.filter { $0.label.contains("myMap") }, + completions.items.filter { $0.label.contains("myMap") }.clearingUnstableValues, [ CompletionItem( label: "myMap(body: (Int) -> Bool)", kind: .method, detail: "Int", deprecated: false, - sortText: nil, filterText: "myMap(:)", insertText: #""" myMap(${1:{ ${2:Int} in ${3:Bool} \}}) @@ -1057,8 +1056,41 @@ final class SwiftCompletionTests: XCTestCase { ] ) } + + 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()"] + ) + } } private func countFs(_ response: CompletionList) -> Int { return response.items.filter { $0.label.hasPrefix("f") }.count } + +fileprivate extension Position { + func adding(columns: Int) -> Position { + return Position(line: line, utf16index: utf16index + columns) + } +} diff --git a/Tests/SourceKitLSPTests/SwiftPMIntegrationTests.swift b/Tests/SourceKitLSPTests/SwiftPMIntegrationTests.swift index c1c7f89c4..3456a3d57 100644 --- a/Tests/SourceKitLSPTests/SwiftPMIntegrationTests.swift +++ b/Tests/SourceKitLSPTests/SwiftPMIntegrationTests.swift @@ -20,6 +20,8 @@ import XCTest final class SwiftPMIntegrationTests: XCTestCase { func testSwiftPMIntegration() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + let project = try await SwiftPMTestProject( files: [ "Lib.swift": """ @@ -60,7 +62,7 @@ final class SwiftPMIntegrationTests: XCTestCase { ) XCTAssertEqual( - completions.items, + completions.items.clearingUnstableValues, [ CompletionItem( label: "foo()", @@ -93,6 +95,8 @@ final class SwiftPMIntegrationTests: XCTestCase { } func testAddFile() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + let project = try await SwiftPMTestProject( files: [ "Lib.swift": """ @@ -144,7 +148,7 @@ final class SwiftPMIntegrationTests: XCTestCase { ) XCTAssertEqual( - completions.items, + completions.items.clearingUnstableValues, [ CompletionItem( label: "foo()", @@ -185,6 +189,8 @@ final class SwiftPMIntegrationTests: XCTestCase { } func testNestedPackage() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + let project = try await MultiFileTestProject(files: [ "pkg/Sources/lib/lib.swift": "", "pkg/Package.swift": """ @@ -220,7 +226,7 @@ final class SwiftPMIntegrationTests: XCTestCase { ) XCTAssertEqual( - result.items, + result.items.clearingUnstableValues, [ CompletionItem( label: "bar()", diff --git a/Tests/SourceKitLSPTests/WorkspaceTests.swift b/Tests/SourceKitLSPTests/WorkspaceTests.swift index fd3e2bd9d..474474141 100644 --- a/Tests/SourceKitLSPTests/WorkspaceTests.swift +++ b/Tests/SourceKitLSPTests/WorkspaceTests.swift @@ -25,6 +25,8 @@ import XCTest final class WorkspaceTests: XCTestCase { func testMultipleSwiftPMWorkspaces() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + // The package manifest is the same for both packages we open. let packageManifest = """ // swift-tools-version: 5.7 @@ -88,14 +90,13 @@ final class WorkspaceTests: XCTestCase { ) XCTAssertEqual( - completions.items, + completions.items.clearingUnstableValues, [ CompletionItem( label: "foo()", kind: .method, detail: "Void", deprecated: false, - sortText: nil, filterText: "foo()", insertText: "foo()", insertTextFormat: .plain, @@ -108,7 +109,6 @@ final class WorkspaceTests: XCTestCase { kind: .keyword, detail: "Lib", deprecated: false, - sortText: nil, filterText: "self", insertText: "self", insertTextFormat: .plain, @@ -126,7 +126,7 @@ final class WorkspaceTests: XCTestCase { ) XCTAssertEqual( - otherCompletions.items, + otherCompletions.items.clearingUnstableValues, [ CompletionItem( label: "sayHello()", @@ -134,7 +134,6 @@ final class WorkspaceTests: XCTestCase { detail: "Void", documentation: nil, deprecated: false, - sortText: nil, filterText: "sayHello()", insertText: "sayHello()", insertTextFormat: .plain, @@ -148,7 +147,6 @@ final class WorkspaceTests: XCTestCase { detail: "FancyLib", documentation: nil, deprecated: false, - sortText: nil, filterText: "self", insertText: "self", insertTextFormat: .plain, @@ -237,6 +235,8 @@ final class WorkspaceTests: XCTestCase { } func testSwiftPMPackageInSubfolder() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + let packageManifest = """ // swift-tools-version: 5.7 @@ -281,7 +281,7 @@ final class WorkspaceTests: XCTestCase { ) XCTAssertEqual( - otherCompletions.items, + otherCompletions.items.clearingUnstableValues, [ CompletionItem( label: "sayHello()", @@ -289,7 +289,6 @@ final class WorkspaceTests: XCTestCase { detail: "Void", documentation: nil, deprecated: false, - sortText: nil, filterText: "sayHello()", insertText: "sayHello()", insertTextFormat: .plain, @@ -303,7 +302,6 @@ final class WorkspaceTests: XCTestCase { detail: "FancyLib", documentation: nil, deprecated: false, - sortText: nil, filterText: "self", insertText: "self", insertTextFormat: .plain, @@ -316,6 +314,8 @@ final class WorkspaceTests: XCTestCase { } func testNestedSwiftPMWorkspacesWithoutDedicatedWorkspaceFolder() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + // The package manifest is the same for both packages we open. let packageManifest = """ // swift-tools-version: 5.7 @@ -374,14 +374,13 @@ final class WorkspaceTests: XCTestCase { ) XCTAssertEqual( - completions.items, + completions.items.clearingUnstableValues, [ CompletionItem( label: "foo()", kind: .method, detail: "Void", deprecated: false, - sortText: nil, filterText: "foo()", insertText: "foo()", insertTextFormat: .plain, @@ -394,7 +393,6 @@ final class WorkspaceTests: XCTestCase { kind: .keyword, detail: "Lib", deprecated: false, - sortText: nil, filterText: "self", insertText: "self", insertTextFormat: .plain, @@ -414,7 +412,7 @@ final class WorkspaceTests: XCTestCase { ) XCTAssertEqual( - otherCompletions.items, + otherCompletions.items.clearingUnstableValues, [ CompletionItem( label: "sayHello()", @@ -422,7 +420,6 @@ final class WorkspaceTests: XCTestCase { detail: "Void", documentation: nil, deprecated: false, - sortText: nil, filterText: "sayHello()", insertText: "sayHello()", insertTextFormat: .plain, @@ -436,7 +433,6 @@ final class WorkspaceTests: XCTestCase { detail: "FancyLib", documentation: nil, deprecated: false, - sortText: nil, filterText: "self", insertText: "self", insertTextFormat: .plain, @@ -587,6 +583,8 @@ final class WorkspaceTests: XCTestCase { } func testMixedPackage() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + let project = try await SwiftPMTestProject( files: [ "clib/include/clib.h": """ @@ -637,6 +635,8 @@ final class WorkspaceTests: XCTestCase { } func testChangeWorkspaceFolders() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + let project = try await MultiFileTestProject( files: [ "subdir/Sources/otherPackage/otherPackage.swift": """ @@ -730,7 +730,7 @@ final class WorkspaceTests: XCTestCase { ) XCTAssertEqual( - postChangeWorkspaceResponse.items, + postChangeWorkspaceResponse.items.clearingUnstableValues, [ CompletionItem( label: "helloWorld()", @@ -738,7 +738,6 @@ final class WorkspaceTests: XCTestCase { detail: "Void", documentation: nil, deprecated: false, - sortText: nil, filterText: "helloWorld()", insertText: "helloWorld()", insertTextFormat: .plain, @@ -755,7 +754,6 @@ final class WorkspaceTests: XCTestCase { detail: "Package", documentation: nil, deprecated: false, - sortText: nil, filterText: "self", insertText: "self", insertTextFormat: .plain, @@ -770,6 +768,8 @@ final class WorkspaceTests: XCTestCase { ) } func testIntegrationTest() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + // This test is doing the same as `test-sourcekit-lsp` in the `swift-integration-tests` repo. let project = try await SwiftPMTestProject( @@ -843,7 +843,7 @@ final class WorkspaceTests: XCTestCase { CompletionRequest(textDocument: TextDocumentIdentifier(mainUri), position: mainPositions["3️⃣"]) ) XCTAssertEqual( - swiftCompletionResponse.items, + swiftCompletionResponse.items.clearingUnstableValues, [ CompletionItem( label: "foo()",