From 8d42228f29cac548dd84dbeb5ec8f448e4f46741 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Tue, 15 Jul 2025 21:14:55 +0300 Subject: [PATCH 01/14] Fetch full documentation in completion items --- .../include/CodeCompletionSwiftInterop.h | 6 ++++++ Sources/SourceKitD/sourcekitd_functions.swift | 1 + .../CodeCompletionSession.swift | 12 +++++++++++- .../ASTCompletion/CompletionSession.swift | 11 +++++++++++ .../SwiftSourceKitPlugin/CompletionProvider.swift | 13 ++++++++++--- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h b/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h index eb676cb9f..0264c4060 100644 --- a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h +++ b/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h @@ -324,6 +324,12 @@ typedef struct { void (^_Null_unspecified handler)(const char *_Null_unspecified) ); + void (*_Nonnull completion_item_get_doc_full_copy)( + _Null_unspecified swiftide_api_completion_response_t, + _Null_unspecified swiftide_api_completion_item_t, + void (^_Null_unspecified handler)(char *_Null_unspecified) + ); + void (*_Nonnull completion_item_get_associated_usrs)( _Null_unspecified swiftide_api_completion_response_t, _Null_unspecified swiftide_api_completion_item_t, diff --git a/Sources/SourceKitD/sourcekitd_functions.swift b/Sources/SourceKitD/sourcekitd_functions.swift index 6edb883c2..c6d8bd6f1 100644 --- a/Sources/SourceKitD/sourcekitd_functions.swift +++ b/Sources/SourceKitD/sourcekitd_functions.swift @@ -146,6 +146,7 @@ extension sourcekitd_ide_api_functions_t { completion_item_get_source_text: try loadRequired("swiftide_completion_item_get_source_text"), completion_item_get_type_name: try loadRequired("swiftide_completion_item_get_type_name"), completion_item_get_doc_brief: try loadRequired("swiftide_completion_item_get_doc_brief"), + completion_item_get_doc_full_copy: try loadRequired("swiftide_completion_item_get_doc_full_copy"), completion_item_get_associated_usrs: try loadRequired("swiftide_completion_item_get_associated_usrs"), completion_item_get_kind: try loadRequired("swiftide_completion_item_get_kind"), completion_item_get_associated_kind: try loadRequired("swiftide_completion_item_get_associated_kind"), diff --git a/Sources/SwiftLanguageService/CodeCompletionSession.swift b/Sources/SwiftLanguageService/CodeCompletionSession.swift index 3f0a85406..42b646c32 100644 --- a/Sources/SwiftLanguageService/CodeCompletionSession.swift +++ b/Sources/SwiftLanguageService/CodeCompletionSession.swift @@ -575,13 +575,23 @@ class CodeCompletionSession { fileContents: nil ) } - if let docString: String = documentationResponse?[sourcekitd.keys.docBrief] { + + if let response = documentationResponse, + let docString = documentationString(from: response, sourcekitd: sourcekitd) { item.documentation = .markupContent(MarkupContent(kind: .markdown, value: docString)) } } return item } + private static func documentationString(from response: SKDResponseDictionary, sourcekitd: SourceKitD) -> String? { + if let docFullAsXML: String = response[sourcekitd.keys.docFullAsXML] { + return try? xmlDocumentationToMarkdown(docFullAsXML) + } + + return response[sourcekitd.keys.docBrief] + } + private func computeCompletionTextEdit( completionPos: Position, requestPosition: Position, diff --git a/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift b/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift index dcd89f548..41fa6f894 100644 --- a/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift +++ b/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift @@ -241,6 +241,17 @@ struct ExtendedCompletionInfo { return result } + var fullDocumentation: String? { + var result: String? = nil + session.sourcekitd.ideApi.completion_item_get_doc_full_copy(session.response, rawItem) { + if let cstr = $0 { + result = String(cString: cstr) + free(cstr) + } + } + return result + } + var associatedUSRs: [String] { var result: [String] = [] session.sourcekitd.ideApi.completion_item_get_associated_usrs(session.response, rawItem) { ptr, len in diff --git a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift index cf248b9ea..f8dbf3438 100644 --- a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift +++ b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift @@ -264,10 +264,17 @@ actor CompletionProvider { func handleCompletionDocumentation(_ request: SKDRequestDictionaryReader) throws -> SKDResponseDictionaryBuilder { let info = try handleExtendedCompletionRequest(request) - return request.sourcekitd.responseDictionary([ - request.sourcekitd.keys.docBrief: info.briefDocumentation, - request.sourcekitd.keys.associatedUSRs: info.associatedUSRs as [SKDResponseValue]?, + var response = request.sourcekitd.responseDictionary([ + request.sourcekitd.keys.associatedUSRs: info.associatedUSRs as [SKDResponseValue]? ]) + + if let fullDocumentation = info.fullDocumentation { + response.set(request.sourcekitd.keys.docFullAsXML, to: fullDocumentation) + } else { + response.set(request.sourcekitd.keys.docBrief, to: info.briefDocumentation) + } + + return response } func handleCompletionDiagnostic(_ dict: SKDRequestDictionaryReader) throws -> SKDResponseDictionaryBuilder { From 4533f615e22c56c2d0673254b72c4628531c1e8f Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Tue, 15 Jul 2025 21:58:27 +0300 Subject: [PATCH 02/14] Fix completion tests to assert on full documentation --- .../SwiftCompletionTests.swift | 40 ++++++++++++++++--- .../SwiftSourceKitPluginTests.swift | 31 +++++++++++--- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index d08e0233c..72e5e2e91 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -67,7 +67,15 @@ final class SwiftCompletionTests: XCTestCase { if let abc = abc { XCTAssertEqual(abc.kind, .property) XCTAssertEqual(abc.detail, "Int") - XCTAssertEqual(abc.documentation, .markupContent(MarkupContent(kind: .markdown, value: "Documentation for abc."))) + assertMarkdown( + documentation: abc.documentation, + expected: """ + ```swift + var abc: Int + ``` + Documentation for `abc`. + """ + ) XCTAssertEqual(abc.filterText, "abc") XCTAssertEqual(abc.textEdit, .textEdit(TextEdit(range: Range(positions["1️⃣"]), newText: "abc"))) XCTAssertEqual(abc.insertText, "abc") @@ -87,7 +95,15 @@ final class SwiftCompletionTests: XCTestCase { // If we switch to server-side filtering this will change. XCTAssertEqual(abc.kind, .property) XCTAssertEqual(abc.detail, "Int") - XCTAssertEqual(abc.documentation, .markupContent(MarkupContent(kind: .markdown, value: "Documentation for abc."))) + assertMarkdown( + documentation: abc.documentation, + expected: """ + ```swift + var abc: Int + ``` + Documentation for `abc`. + """ + ) XCTAssertEqual(abc.filterText, "abc") XCTAssertEqual(abc.textEdit, .textEdit(TextEdit(range: positions["1️⃣"].. Bool + ``` + Creates a true value + """ ) } @@ -1253,6 +1274,15 @@ 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))) +} + fileprivate extension Position { func adding(columns: Int) -> Position { return Position(line: line, utf16index: utf16index + columns) diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index 7607492c2..fbb608660 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -203,7 +203,7 @@ final class SwiftSourceKitPluginTests: XCTestCase { XCTAssertEqual(result2.items.count, 1) XCTAssertEqual(result2.items[0].name, "") let doc = try await sourcekitd.completeDocumentation(id: result2.items[0].id) - XCTAssertEqual(doc.docBrief, nil) + XCTAssertEqual(doc.docFullAsXML, nil) } func testMultipleFiles() async throws { @@ -436,15 +436,34 @@ final class SwiftSourceKitPluginTests: XCTestCase { let sym3 = try unwrap(result.items.first(where: { $0.name == "foo3()" }), "did not find foo3; got \(result.items)") let sym1Doc = try await sourcekitd.completeDocumentation(id: sym1.id) - XCTAssertEqual(sym1Doc.docBrief, "Protocol P foo1") + XCTAssertEqual(sym1Doc.docFullAsXML, + "" + + "foo1()" + + "s:1a1PP4foo1yyF" + + "func foo1()" + + "" + + "Protocol P foo1" + + "" + + "This documentation comment was inherited from P." + + "" + + "" + + "") XCTAssertEqual(sym1Doc.associatedUSRs, ["s:1a1SV4foo1yyF", "s:1a1PP4foo1yyF"]) let sym2Doc = try await sourcekitd.completeDocumentation(id: sym2.id) - XCTAssertEqual(sym2Doc.docBrief, "Struct S foo2") + XCTAssertEqual(sym2Doc.docFullAsXML, + "" + + "foo2()" + + "s:1a1SV4foo2yyF" + + "func foo2()" + + "" + + "Struct S foo2" + + "" + + "") XCTAssertEqual(sym2Doc.associatedUSRs, ["s:1a1SV4foo2yyF"]) let sym3Doc = try await sourcekitd.completeDocumentation(id: sym3.id) - XCTAssertNil(sym3Doc.docBrief) + XCTAssertNil(sym3Doc.docFullAsXML) XCTAssertEqual(sym3Doc.associatedUSRs, ["s:1a1SV4foo3yyF"]) } @@ -1766,12 +1785,12 @@ private struct CompletionResult: Equatable, Sendable { } private struct CompletionDocumentation { - var docBrief: String? = nil + var docFullAsXML: String? = nil var associatedUSRs: [String] = [] init(_ dict: SKDResponseDictionary) { let keys = dict.sourcekitd.keys - self.docBrief = dict[keys.docBrief] + self.docFullAsXML = dict[keys.docFullAsXML] self.associatedUSRs = dict[keys.associatedUSRs]?.asStringArray ?? [] } } From 915e5d29a2c15a00a34751042d2996ccb2440993 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Tue, 15 Jul 2025 22:34:58 +0300 Subject: [PATCH 03/14] Test completion item resolve documentation falls back to brief doc if full doc is null --- .../SwiftCompletionTests.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index 72e5e2e91..b967dda71 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -1214,6 +1214,33 @@ final class SwiftCompletionTests: XCTestCase { ) } + func testCompletionBriefDocumentationFallback() async throws { + try await SkipUnless.sourcekitdSupportsPlugin() + + 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) From 2cffd1cd3f9e185f0bd064cc8c82fcbc45209fbb Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Wed, 16 Jul 2025 22:53:33 +0300 Subject: [PATCH 04/14] Make completion_item_get_doc_full_copy optional for backward compatibility --- Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h | 8 ++++---- Sources/SourceKitD/sourcekitd_functions.swift | 2 +- .../ASTCompletion/CompletionSession.swift | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h b/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h index 0264c4060..593de322a 100644 --- a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h +++ b/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h @@ -324,10 +324,10 @@ typedef struct { void (^_Null_unspecified handler)(const char *_Null_unspecified) ); - void (*_Nonnull completion_item_get_doc_full_copy)( - _Null_unspecified swiftide_api_completion_response_t, - _Null_unspecified swiftide_api_completion_item_t, - void (^_Null_unspecified handler)(char *_Null_unspecified) + void (*_Nullable completion_item_get_doc_full_copy)( + _Nonnull swiftide_api_completion_response_t, + _Nonnull swiftide_api_completion_item_t, + void (^_Nonnull handler)(char *_Nullable) ); void (*_Nonnull completion_item_get_associated_usrs)( diff --git a/Sources/SourceKitD/sourcekitd_functions.swift b/Sources/SourceKitD/sourcekitd_functions.swift index c6d8bd6f1..14362819d 100644 --- a/Sources/SourceKitD/sourcekitd_functions.swift +++ b/Sources/SourceKitD/sourcekitd_functions.swift @@ -146,7 +146,7 @@ extension sourcekitd_ide_api_functions_t { completion_item_get_source_text: try loadRequired("swiftide_completion_item_get_source_text"), completion_item_get_type_name: try loadRequired("swiftide_completion_item_get_type_name"), completion_item_get_doc_brief: try loadRequired("swiftide_completion_item_get_doc_brief"), - completion_item_get_doc_full_copy: try loadRequired("swiftide_completion_item_get_doc_full_copy"), + completion_item_get_doc_full_copy: loadOptional("swiftide_completion_item_get_doc_full_copy"), completion_item_get_associated_usrs: try loadRequired("swiftide_completion_item_get_associated_usrs"), completion_item_get_kind: try loadRequired("swiftide_completion_item_get_kind"), completion_item_get_associated_kind: try loadRequired("swiftide_completion_item_get_associated_kind"), diff --git a/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift b/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift index 41fa6f894..0ad8214f3 100644 --- a/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift +++ b/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift @@ -243,7 +243,7 @@ struct ExtendedCompletionInfo { var fullDocumentation: String? { var result: String? = nil - session.sourcekitd.ideApi.completion_item_get_doc_full_copy(session.response, rawItem) { + session.sourcekitd.ideApi.completion_item_get_doc_full_copy?(session.response, rawItem) { if let cstr = $0 { result = String(cString: cstr) free(cstr) From 04b6169a67c8b5a1bd4c2b7e2279fa2fbc016465 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Wed, 16 Jul 2025 22:54:26 +0300 Subject: [PATCH 05/14] Use multiline string & turn var into let in handleCompletionDocumentation --- .../CompletionProvider.swift | 2 +- .../SwiftSourceKitPluginTests.swift | 42 ++++++++++--------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift index f8dbf3438..09a25605b 100644 --- a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift +++ b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift @@ -264,7 +264,7 @@ actor CompletionProvider { func handleCompletionDocumentation(_ request: SKDRequestDictionaryReader) throws -> SKDResponseDictionaryBuilder { let info = try handleExtendedCompletionRequest(request) - var response = request.sourcekitd.responseDictionary([ + let response = request.sourcekitd.responseDictionary([ request.sourcekitd.keys.associatedUSRs: info.associatedUSRs as [SKDResponseValue]? ]) diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index fbb608660..94a771ca3 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -437,29 +437,33 @@ final class SwiftSourceKitPluginTests: XCTestCase { let sym1Doc = try await sourcekitd.completeDocumentation(id: sym1.id) XCTAssertEqual(sym1Doc.docFullAsXML, - "" - + "foo1()" - + "s:1a1PP4foo1yyF" - + "func foo1()" - + "" - + "Protocol P foo1" - + "" - + "This documentation comment was inherited from P." - + "" - + "" - + "") + """ + \ + foo1()\ + s:1a1PP4foo1yyF\ + func foo1()\ + \ + Protocol P foo1\ + \ + This documentation comment was inherited from P.\ + \ + \ + + """) XCTAssertEqual(sym1Doc.associatedUSRs, ["s:1a1SV4foo1yyF", "s:1a1PP4foo1yyF"]) let sym2Doc = try await sourcekitd.completeDocumentation(id: sym2.id) XCTAssertEqual(sym2Doc.docFullAsXML, - "" - + "foo2()" - + "s:1a1SV4foo2yyF" - + "func foo2()" - + "" - + "Struct S foo2" - + "" - + "") + """ + \ + foo2()\ + s:1a1SV4foo2yyF\ + func foo2()\ + \ + Struct S foo2\ + \ + + """) XCTAssertEqual(sym2Doc.associatedUSRs, ["s:1a1SV4foo2yyF"]) let sym3Doc = try await sourcekitd.completeDocumentation(id: sym3.id) From b3b01d7860a2c9eb380e50e913be4dddc2fbfa86 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Thu, 17 Jul 2025 01:29:37 +0300 Subject: [PATCH 06/14] Assert appropriate completion doc based on full doc support in sourcekitd --- .../SwiftCompletionTests.swift | 45 ++++++++-- .../SwiftSourceKitPluginTests.swift | 86 +++++++++++++------ 2 files changed, 99 insertions(+), 32 deletions(-) diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index b967dda71..8a729310d 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -10,10 +10,13 @@ // //===----------------------------------------------------------------------===// +import Csourcekitd import LanguageServerProtocol import SKTestSupport +import SourceKitD import SourceKitLSP import SwiftExtensions +import ToolchainRegistry import XCTest final class SwiftCompletionTests: XCTestCase { @@ -67,9 +70,10 @@ final class SwiftCompletionTests: XCTestCase { if let abc = abc { XCTAssertEqual(abc.kind, .property) XCTAssertEqual(abc.detail, "Int") - assertMarkdown( + try await assertDocumentation( documentation: abc.documentation, - expected: """ + expectedBrief: "Documentation for abc.", + expectedFull: """ ```swift var abc: Int ``` @@ -95,9 +99,10 @@ final class SwiftCompletionTests: XCTestCase { // If we switch to server-side filtering this will change. XCTAssertEqual(abc.kind, .property) XCTAssertEqual(abc.detail, "Int") - assertMarkdown( + try await assertDocumentation( documentation: abc.documentation, - expected: """ + expectedBrief: "Documentation for abc.", + expectedFull: """ ```swift var abc: Int ``` @@ -1203,9 +1208,10 @@ final class SwiftCompletionTests: XCTestCase { let item = try XCTUnwrap(completions.items.only) XCTAssertNil(item.documentation) let resolvedItem = try await testClient.send(CompletionItemResolveRequest(item: item)) - assertMarkdown( + try await assertDocumentation( documentation: resolvedItem.documentation, - expected: """ + expectedBrief: "Creates a true value", + expectedFull: """ ```swift func makeBool() -> Bool ``` @@ -1217,6 +1223,9 @@ final class SwiftCompletionTests: XCTestCase { func testCompletionBriefDocumentationFallback() async throws { try await SkipUnless.sourcekitdSupportsPlugin() + let fullDocumentationSupported = try await sourcekitdSupportsFullDocumentation() + try XCTSkipUnless(fullDocumentationSupported) + let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI(for: .swift) @@ -1310,6 +1319,30 @@ private func assertMarkdown( XCTAssertEqual(documentation, .markupContent(MarkupContent(kind: .markdown, value: expected))) } +/// Asserts that documentation matches the expected values based on whether full documentation is supported in sourcekitd or not. +private func assertDocumentation( + documentation: StringOrMarkupContent?, + expectedBrief: String, + expectedFull: String, + file: StaticString = #filePath, + line: UInt = #line +) async throws { + let supportsFullDocumentation = try await sourcekitdSupportsFullDocumentation() + let expected = supportsFullDocumentation ? expectedFull : expectedBrief + + assertMarkdown(documentation: documentation, expected: expected, file: file, line: line) +} + +private func sourcekitdSupportsFullDocumentation() async throws -> Bool { + let sourcekitdPath = await ToolchainRegistry.forTesting.default!.sourcekitd! + let sourcekitd = try await SourceKitD.getOrCreate( + dylibPath: sourcekitdPath, + pluginPaths: sourceKitPluginPaths + ) + + return sourcekitd.ideApi.completion_item_get_doc_full_copy != nil +} + fileprivate extension Position { func adding(columns: Int) -> Position { return Position(line: line, utf16index: utf16index + columns) diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index 94a771ca3..33d2a8adf 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -203,7 +203,8 @@ final class SwiftSourceKitPluginTests: XCTestCase { XCTAssertEqual(result2.items.count, 1) XCTAssertEqual(result2.items[0].name, "") let doc = try await sourcekitd.completeDocumentation(id: result2.items[0].id) - XCTAssertEqual(doc.docFullAsXML, nil) + XCTAssertNil(doc.docFullAsXML) + XCTAssertNil(doc.docBrief) } func testMultipleFiles() async throws { @@ -436,38 +437,47 @@ final class SwiftSourceKitPluginTests: XCTestCase { let sym3 = try unwrap(result.items.first(where: { $0.name == "foo3()" }), "did not find foo3; got \(result.items)") let sym1Doc = try await sourcekitd.completeDocumentation(id: sym1.id) - XCTAssertEqual(sym1Doc.docFullAsXML, - """ - \ - foo1()\ - s:1a1PP4foo1yyF\ - func foo1()\ - \ - Protocol P foo1\ - \ - This documentation comment was inherited from P.\ - \ - \ - - """) + assertDocumentation( + full: sourcekitd.supportsFullDocumentationInCompletion, + documentation: sym1Doc, + expectedBrief: "Protocol P foo1", + expectedFull: """ + \ + foo1()\ + s:1a1PP4foo1yyF\ + func foo1()\ + \ + Protocol P foo1\ + \ + This documentation comment was inherited from P.\ + \ + \ + + """ + ) XCTAssertEqual(sym1Doc.associatedUSRs, ["s:1a1SV4foo1yyF", "s:1a1PP4foo1yyF"]) let sym2Doc = try await sourcekitd.completeDocumentation(id: sym2.id) - XCTAssertEqual(sym2Doc.docFullAsXML, - """ - \ - foo2()\ - s:1a1SV4foo2yyF\ - func foo2()\ - \ - Struct S foo2\ - \ - - """) + assertDocumentation( + full: sourcekitd.supportsFullDocumentationInCompletion, + documentation: sym2Doc, + expectedBrief: "Struct S foo2", + expectedFull: """ + \ + foo2()\ + s:1a1SV4foo2yyF\ + func foo2()\ + \ + Struct S foo2\ + \ + + """ + ) XCTAssertEqual(sym2Doc.associatedUSRs, ["s:1a1SV4foo2yyF"]) let sym3Doc = try await sourcekitd.completeDocumentation(id: sym3.id) XCTAssertNil(sym3Doc.docFullAsXML) + XCTAssertNil(sym3Doc.docBrief) XCTAssertEqual(sym3Doc.associatedUSRs, ["s:1a1SV4foo3yyF"]) } @@ -1789,11 +1799,13 @@ private struct CompletionResult: Equatable, Sendable { } private struct CompletionDocumentation { + var docBrief: String? = nil var docFullAsXML: String? = nil var associatedUSRs: [String] = [] init(_ dict: SKDResponseDictionary) { let keys = dict.sourcekitd.keys + self.docBrief = dict[keys.docBrief] self.docFullAsXML = dict[keys.docFullAsXML] self.associatedUSRs = dict[keys.associatedUSRs]?.asStringArray ?? [] } @@ -2072,6 +2084,10 @@ fileprivate extension SourceKitD { try await openDocument(path, contents: textWithoutMarker, compilerArguments: [path]) return (positions["1️⃣"], recent) } + + nonisolated var supportsFullDocumentationInCompletion: Bool { + return ideApi.completion_item_get_doc_full_copy != nil + } } private struct ExpectationNotFulfilledError: Error {} @@ -2094,3 +2110,21 @@ private func runAsync(_ body: @escaping @Sendable () async throws - } return try result.get() } + +/// Asserts that documentation matches the expected values based on whether full documentation is supported in sourcekitd or not. +private func assertDocumentation( + full: Bool, + documentation: CompletionDocumentation, + expectedBrief: String, + expectedFull: String, + file: StaticString = #filePath, + line: UInt = #line +) { + if full { + XCTAssertEqual(documentation.docFullAsXML, expectedFull, file: file, line: line) + XCTAssertNil(documentation.docBrief, "Expected brief documentation to not be available", file: file, line: line) + } else { + XCTAssertEqual(documentation.docBrief, expectedBrief, file: file, line: line) + XCTAssertNil(documentation.docFullAsXML, "Expected full documentation to not be available", file: file, line: line) + } +} From 80637748675345fc3c8ba7dc99f65d55db1aa849 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Thu, 17 Jul 2025 10:36:45 +0300 Subject: [PATCH 07/14] Replace completion_item_get_doc_full_copy with completion_item_get_doc_full --- Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h | 4 ++-- Sources/SourceKitD/sourcekitd_functions.swift | 2 +- .../ASTCompletion/CompletionSession.swift | 3 +-- Tests/SourceKitLSPTests/SwiftCompletionTests.swift | 2 +- .../SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h b/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h index 593de322a..b9465ef04 100644 --- a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h +++ b/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h @@ -324,10 +324,10 @@ typedef struct { void (^_Null_unspecified handler)(const char *_Null_unspecified) ); - void (*_Nullable completion_item_get_doc_full_copy)( + void (*_Nullable completion_item_get_doc_full)( _Nonnull swiftide_api_completion_response_t, _Nonnull swiftide_api_completion_item_t, - void (^_Nonnull handler)(char *_Nullable) + void (^_Nonnull handler)(const char *_Nullable) ); void (*_Nonnull completion_item_get_associated_usrs)( diff --git a/Sources/SourceKitD/sourcekitd_functions.swift b/Sources/SourceKitD/sourcekitd_functions.swift index 14362819d..705d7f3d5 100644 --- a/Sources/SourceKitD/sourcekitd_functions.swift +++ b/Sources/SourceKitD/sourcekitd_functions.swift @@ -146,7 +146,7 @@ extension sourcekitd_ide_api_functions_t { completion_item_get_source_text: try loadRequired("swiftide_completion_item_get_source_text"), completion_item_get_type_name: try loadRequired("swiftide_completion_item_get_type_name"), completion_item_get_doc_brief: try loadRequired("swiftide_completion_item_get_doc_brief"), - completion_item_get_doc_full_copy: loadOptional("swiftide_completion_item_get_doc_full_copy"), + completion_item_get_doc_full: loadOptional("swiftide_completion_item_get_doc_full"), completion_item_get_associated_usrs: try loadRequired("swiftide_completion_item_get_associated_usrs"), completion_item_get_kind: try loadRequired("swiftide_completion_item_get_kind"), completion_item_get_associated_kind: try loadRequired("swiftide_completion_item_get_associated_kind"), diff --git a/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift b/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift index 0ad8214f3..a8d392d77 100644 --- a/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift +++ b/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift @@ -243,10 +243,9 @@ struct ExtendedCompletionInfo { var fullDocumentation: String? { var result: String? = nil - session.sourcekitd.ideApi.completion_item_get_doc_full_copy?(session.response, rawItem) { + session.sourcekitd.ideApi.completion_item_get_doc_full?(session.response, rawItem) { if let cstr = $0 { result = String(cString: cstr) - free(cstr) } } return result diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index 8a729310d..358b103a3 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -1340,7 +1340,7 @@ private func sourcekitdSupportsFullDocumentation() async throws -> Bool { pluginPaths: sourceKitPluginPaths ) - return sourcekitd.ideApi.completion_item_get_doc_full_copy != nil + return sourcekitd.ideApi.completion_item_get_doc_full != nil } fileprivate extension Position { diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index 33d2a8adf..5e6d8dc59 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -2086,7 +2086,7 @@ fileprivate extension SourceKitD { } nonisolated var supportsFullDocumentationInCompletion: Bool { - return ideApi.completion_item_get_doc_full_copy != nil + return ideApi.completion_item_get_doc_full != nil } } From bc76da3868a95635fa7bd889d4146e6fec0b42ff Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Thu, 17 Jul 2025 10:39:44 +0300 Subject: [PATCH 08/14] Log errors on xmlDocumentationToMarkdown in documentationString --- Sources/SwiftLanguageService/CodeCompletionSession.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftLanguageService/CodeCompletionSession.swift b/Sources/SwiftLanguageService/CodeCompletionSession.swift index 42b646c32..4b64a9422 100644 --- a/Sources/SwiftLanguageService/CodeCompletionSession.swift +++ b/Sources/SwiftLanguageService/CodeCompletionSession.swift @@ -586,7 +586,9 @@ class CodeCompletionSession { private static func documentationString(from response: SKDResponseDictionary, sourcekitd: SourceKitD) -> String? { if let docFullAsXML: String = response[sourcekitd.keys.docFullAsXML] { - return try? xmlDocumentationToMarkdown(docFullAsXML) + return orLog("Converting XML documentation to markdown") { + try xmlDocumentationToMarkdown(docFullAsXML) + } } return response[sourcekitd.keys.docBrief] From 313880055a55d0e114e0025a6fbe27252aa12f4f Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Thu, 17 Jul 2025 10:50:15 +0300 Subject: [PATCH 09/14] Revert "Assert appropriate completion doc based on full doc support in sourcekitd" This reverts commit db443202dde16a4c78d564fa2d9e5699028ce2cc. --- .../SwiftCompletionTests.swift | 45 ++-------- .../SwiftSourceKitPluginTests.swift | 86 ++++++------------- 2 files changed, 32 insertions(+), 99 deletions(-) diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index 358b103a3..b967dda71 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -10,13 +10,10 @@ // //===----------------------------------------------------------------------===// -import Csourcekitd import LanguageServerProtocol import SKTestSupport -import SourceKitD import SourceKitLSP import SwiftExtensions -import ToolchainRegistry import XCTest final class SwiftCompletionTests: XCTestCase { @@ -70,10 +67,9 @@ final class SwiftCompletionTests: XCTestCase { if let abc = abc { XCTAssertEqual(abc.kind, .property) XCTAssertEqual(abc.detail, "Int") - try await assertDocumentation( + assertMarkdown( documentation: abc.documentation, - expectedBrief: "Documentation for abc.", - expectedFull: """ + expected: """ ```swift var abc: Int ``` @@ -99,10 +95,9 @@ final class SwiftCompletionTests: XCTestCase { // If we switch to server-side filtering this will change. XCTAssertEqual(abc.kind, .property) XCTAssertEqual(abc.detail, "Int") - try await assertDocumentation( + assertMarkdown( documentation: abc.documentation, - expectedBrief: "Documentation for abc.", - expectedFull: """ + expected: """ ```swift var abc: Int ``` @@ -1208,10 +1203,9 @@ final class SwiftCompletionTests: XCTestCase { let item = try XCTUnwrap(completions.items.only) XCTAssertNil(item.documentation) let resolvedItem = try await testClient.send(CompletionItemResolveRequest(item: item)) - try await assertDocumentation( + assertMarkdown( documentation: resolvedItem.documentation, - expectedBrief: "Creates a true value", - expectedFull: """ + expected: """ ```swift func makeBool() -> Bool ``` @@ -1223,9 +1217,6 @@ final class SwiftCompletionTests: XCTestCase { func testCompletionBriefDocumentationFallback() async throws { try await SkipUnless.sourcekitdSupportsPlugin() - let fullDocumentationSupported = try await sourcekitdSupportsFullDocumentation() - try XCTSkipUnless(fullDocumentationSupported) - let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI(for: .swift) @@ -1319,30 +1310,6 @@ private func assertMarkdown( XCTAssertEqual(documentation, .markupContent(MarkupContent(kind: .markdown, value: expected))) } -/// Asserts that documentation matches the expected values based on whether full documentation is supported in sourcekitd or not. -private func assertDocumentation( - documentation: StringOrMarkupContent?, - expectedBrief: String, - expectedFull: String, - file: StaticString = #filePath, - line: UInt = #line -) async throws { - let supportsFullDocumentation = try await sourcekitdSupportsFullDocumentation() - let expected = supportsFullDocumentation ? expectedFull : expectedBrief - - assertMarkdown(documentation: documentation, expected: expected, file: file, line: line) -} - -private func sourcekitdSupportsFullDocumentation() async throws -> Bool { - let sourcekitdPath = await ToolchainRegistry.forTesting.default!.sourcekitd! - let sourcekitd = try await SourceKitD.getOrCreate( - dylibPath: sourcekitdPath, - pluginPaths: sourceKitPluginPaths - ) - - return sourcekitd.ideApi.completion_item_get_doc_full != nil -} - fileprivate extension Position { func adding(columns: Int) -> Position { return Position(line: line, utf16index: utf16index + columns) diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index 5e6d8dc59..94a771ca3 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -203,8 +203,7 @@ final class SwiftSourceKitPluginTests: XCTestCase { XCTAssertEqual(result2.items.count, 1) XCTAssertEqual(result2.items[0].name, "") let doc = try await sourcekitd.completeDocumentation(id: result2.items[0].id) - XCTAssertNil(doc.docFullAsXML) - XCTAssertNil(doc.docBrief) + XCTAssertEqual(doc.docFullAsXML, nil) } func testMultipleFiles() async throws { @@ -437,47 +436,38 @@ final class SwiftSourceKitPluginTests: XCTestCase { let sym3 = try unwrap(result.items.first(where: { $0.name == "foo3()" }), "did not find foo3; got \(result.items)") let sym1Doc = try await sourcekitd.completeDocumentation(id: sym1.id) - assertDocumentation( - full: sourcekitd.supportsFullDocumentationInCompletion, - documentation: sym1Doc, - expectedBrief: "Protocol P foo1", - expectedFull: """ - \ - foo1()\ - s:1a1PP4foo1yyF\ - func foo1()\ - \ - Protocol P foo1\ - \ - This documentation comment was inherited from P.\ - \ - \ - - """ - ) + XCTAssertEqual(sym1Doc.docFullAsXML, + """ + \ + foo1()\ + s:1a1PP4foo1yyF\ + func foo1()\ + \ + Protocol P foo1\ + \ + This documentation comment was inherited from P.\ + \ + \ + + """) XCTAssertEqual(sym1Doc.associatedUSRs, ["s:1a1SV4foo1yyF", "s:1a1PP4foo1yyF"]) let sym2Doc = try await sourcekitd.completeDocumentation(id: sym2.id) - assertDocumentation( - full: sourcekitd.supportsFullDocumentationInCompletion, - documentation: sym2Doc, - expectedBrief: "Struct S foo2", - expectedFull: """ - \ - foo2()\ - s:1a1SV4foo2yyF\ - func foo2()\ - \ - Struct S foo2\ - \ - - """ - ) + XCTAssertEqual(sym2Doc.docFullAsXML, + """ + \ + foo2()\ + s:1a1SV4foo2yyF\ + func foo2()\ + \ + Struct S foo2\ + \ + + """) XCTAssertEqual(sym2Doc.associatedUSRs, ["s:1a1SV4foo2yyF"]) let sym3Doc = try await sourcekitd.completeDocumentation(id: sym3.id) XCTAssertNil(sym3Doc.docFullAsXML) - XCTAssertNil(sym3Doc.docBrief) XCTAssertEqual(sym3Doc.associatedUSRs, ["s:1a1SV4foo3yyF"]) } @@ -1799,13 +1789,11 @@ private struct CompletionResult: Equatable, Sendable { } private struct CompletionDocumentation { - var docBrief: String? = nil var docFullAsXML: String? = nil var associatedUSRs: [String] = [] init(_ dict: SKDResponseDictionary) { let keys = dict.sourcekitd.keys - self.docBrief = dict[keys.docBrief] self.docFullAsXML = dict[keys.docFullAsXML] self.associatedUSRs = dict[keys.associatedUSRs]?.asStringArray ?? [] } @@ -2084,10 +2072,6 @@ fileprivate extension SourceKitD { try await openDocument(path, contents: textWithoutMarker, compilerArguments: [path]) return (positions["1️⃣"], recent) } - - nonisolated var supportsFullDocumentationInCompletion: Bool { - return ideApi.completion_item_get_doc_full != nil - } } private struct ExpectationNotFulfilledError: Error {} @@ -2110,21 +2094,3 @@ private func runAsync(_ body: @escaping @Sendable () async throws - } return try result.get() } - -/// Asserts that documentation matches the expected values based on whether full documentation is supported in sourcekitd or not. -private func assertDocumentation( - full: Bool, - documentation: CompletionDocumentation, - expectedBrief: String, - expectedFull: String, - file: StaticString = #filePath, - line: UInt = #line -) { - if full { - XCTAssertEqual(documentation.docFullAsXML, expectedFull, file: file, line: line) - XCTAssertNil(documentation.docBrief, "Expected brief documentation to not be available", file: file, line: line) - } else { - XCTAssertEqual(documentation.docBrief, expectedBrief, file: file, line: line) - XCTAssertNil(documentation.docFullAsXML, "Expected full documentation to not be available", file: file, line: line) - } -} From 02da9fe728a2d74d2f735a70f7ff5c35aa2d29e5 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Thu, 17 Jul 2025 11:16:16 +0300 Subject: [PATCH 10/14] Skip tests relying on full documentation unless supported --- Sources/SKTestSupport/SkipUnless.swift | 33 +++++++++++++++---- .../SwiftCompletionTests.swift | 3 ++ .../SwiftSourceKitPluginTests.swift | 9 ++++- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/Sources/SKTestSupport/SkipUnless.swift b/Sources/SKTestSupport/SkipUnless.swift index b4ece6593..7f85a341d 100644 --- a/Sources/SKTestSupport/SkipUnless.swift +++ b/Sources/SKTestSupport/SkipUnless.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import Csourcekitd import Foundation import LanguageServerProtocol import LanguageServerProtocolExtensions @@ -251,13 +252,8 @@ package actor SkipUnless { line: UInt = #line ) async throws { return try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 2), file: file, line: line) { - guard let sourcekitdPath = await ToolchainRegistry.forTesting.default?.sourcekitd else { - throw GenericError("Could not find SourceKitD") - } - let sourcekitd = try await SourceKitD.getOrCreate( - dylibPath: sourcekitdPath, - pluginPaths: try sourceKitPluginPaths - ) + let sourcekitd = try await getSourceKitD() + do { let response = try await sourcekitd.send( \.codeCompleteSetPopularAPI, @@ -275,6 +271,17 @@ package actor SkipUnless { } } + package static func sourcekitdSupportsFullDocumentationInCompletion( + file: StaticString = #filePath, + line: UInt = #line + ) async throws { + return try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 2), file: file, line: line) { + let sourcekitd = try await getSourceKitD() + + return sourcekitd.ideApi.completion_item_get_doc_full != nil + } + } + package static func canLoadPluginsBuiltByToolchain( file: StaticString = #filePath, line: UInt = #line @@ -386,6 +393,18 @@ package actor SkipUnless { ) } } + + private static func getSourceKitD() async throws -> SourceKitD { + guard let sourcekitdPath = await ToolchainRegistry.forTesting.default?.sourcekitd else { + throw GenericError("Could not find SourceKitD") + } + let sourcekitd = try await SourceKitD.getOrCreate( + dylibPath: sourcekitdPath, + pluginPaths: try sourceKitPluginPaths + ) + + return sourcekitd + } } // MARK: - Parsing Swift compiler version diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index b967dda71..4007d821e 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -31,6 +31,7 @@ final class SwiftCompletionTests: XCTestCase { func testCompletionBasic() async throws { try await SkipUnless.sourcekitdSupportsPlugin() + try await SkipUnless.sourcekitdSupportsFullDocumentationInCompletion() let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI(for: .swift) @@ -1170,6 +1171,7 @@ final class SwiftCompletionTests: XCTestCase { func testCompletionItemResolve() async throws { try await SkipUnless.sourcekitdSupportsPlugin() + try await SkipUnless.sourcekitdSupportsFullDocumentationInCompletion() let capabilities = ClientCapabilities( textDocument: TextDocumentClientCapabilities( @@ -1216,6 +1218,7 @@ final class SwiftCompletionTests: XCTestCase { func testCompletionBriefDocumentationFallback() async throws { try await SkipUnless.sourcekitdSupportsPlugin() + try await SkipUnless.sourcekitdSupportsFullDocumentationInCompletion() let testClient = try await TestSourceKitLSPClient() let uri = DocumentURI(for: .swift) diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index 94a771ca3..39411fe71 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -46,6 +46,8 @@ final class SwiftSourceKitPluginTests: XCTestCase { func testBasicCompletion() async throws { try await SkipUnless.sourcekitdSupportsPlugin() + try await SkipUnless.sourcekitdSupportsFullDocumentationInCompletion() + let sourcekitd = try await getSourceKitD() let path = scratchFilePath() let positions = try await sourcekitd.openDocument( @@ -203,7 +205,8 @@ final class SwiftSourceKitPluginTests: XCTestCase { XCTAssertEqual(result2.items.count, 1) XCTAssertEqual(result2.items[0].name, "") let doc = try await sourcekitd.completeDocumentation(id: result2.items[0].id) - XCTAssertEqual(doc.docFullAsXML, nil) + XCTAssertNil(doc.docFullAsXML) + XCTAssertNil(doc.docBrief) } func testMultipleFiles() async throws { @@ -403,6 +406,8 @@ final class SwiftSourceKitPluginTests: XCTestCase { func testDocumentation() async throws { try await SkipUnless.sourcekitdSupportsPlugin() + try await SkipUnless.sourcekitdSupportsFullDocumentationInCompletion() + let sourcekitd = try await getSourceKitD() let path = scratchFilePath() let positions = try await sourcekitd.openDocument( @@ -1790,11 +1795,13 @@ private struct CompletionResult: Equatable, Sendable { private struct CompletionDocumentation { var docFullAsXML: String? = nil + var docBrief: String? = nil var associatedUSRs: [String] = [] init(_ dict: SKDResponseDictionary) { let keys = dict.sourcekitd.keys self.docFullAsXML = dict[keys.docFullAsXML] + self.docBrief = dict[keys.docBrief] self.associatedUSRs = dict[keys.associatedUSRs]?.asStringArray ?? [] } } From c5a6b1cf679c559c9a99bf074d8aaa0f918c7c9f Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Thu, 17 Jul 2025 20:19:57 +0300 Subject: [PATCH 11/14] Format changes with swift-format --- .../CodeCompletionSession.swift | 3 +- .../SwiftSourceKitPluginTests.swift | 54 ++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Sources/SwiftLanguageService/CodeCompletionSession.swift b/Sources/SwiftLanguageService/CodeCompletionSession.swift index 4b64a9422..f33dda60f 100644 --- a/Sources/SwiftLanguageService/CodeCompletionSession.swift +++ b/Sources/SwiftLanguageService/CodeCompletionSession.swift @@ -577,7 +577,8 @@ class CodeCompletionSession { } if let response = documentationResponse, - let docString = documentationString(from: response, sourcekitd: sourcekitd) { + let docString = documentationString(from: response, sourcekitd: sourcekitd) + { item.documentation = .markupContent(MarkupContent(kind: .markdown, value: docString)) } } diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index 39411fe71..ccece241f 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -441,34 +441,38 @@ final class SwiftSourceKitPluginTests: XCTestCase { let sym3 = try unwrap(result.items.first(where: { $0.name == "foo3()" }), "did not find foo3; got \(result.items)") let sym1Doc = try await sourcekitd.completeDocumentation(id: sym1.id) - XCTAssertEqual(sym1Doc.docFullAsXML, - """ - \ - foo1()\ - s:1a1PP4foo1yyF\ - func foo1()\ - \ - Protocol P foo1\ - \ - This documentation comment was inherited from P.\ - \ - \ - - """) + XCTAssertEqual( + sym1Doc.docFullAsXML, + """ + \ + foo1()\ + s:1a1PP4foo1yyF\ + func foo1()\ + \ + Protocol P foo1\ + \ + This documentation comment was inherited from P.\ + \ + \ + + """ + ) XCTAssertEqual(sym1Doc.associatedUSRs, ["s:1a1SV4foo1yyF", "s:1a1PP4foo1yyF"]) let sym2Doc = try await sourcekitd.completeDocumentation(id: sym2.id) - XCTAssertEqual(sym2Doc.docFullAsXML, - """ - \ - foo2()\ - s:1a1SV4foo2yyF\ - func foo2()\ - \ - Struct S foo2\ - \ - - """) + XCTAssertEqual( + sym2Doc.docFullAsXML, + """ + \ + foo2()\ + s:1a1SV4foo2yyF\ + func foo2()\ + \ + Struct S foo2\ + \ + + """ + ) XCTAssertEqual(sym2Doc.associatedUSRs, ["s:1a1SV4foo2yyF"]) let sym3Doc = try await sourcekitd.completeDocumentation(id: sym3.id) From 61b819495530bc387f2483f2812702836d3f0270 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Thu, 17 Jul 2025 22:09:35 +0300 Subject: [PATCH 12/14] Return both breif & full doc in codeCompleteDocumentation request --- .../SwiftSourceKitPlugin/CompletionProvider.swift | 14 ++++---------- .../SwiftSourceKitPluginTests.swift | 3 +++ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift index 09a25605b..2b64e0770 100644 --- a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift +++ b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift @@ -264,17 +264,11 @@ actor CompletionProvider { func handleCompletionDocumentation(_ request: SKDRequestDictionaryReader) throws -> SKDResponseDictionaryBuilder { let info = try handleExtendedCompletionRequest(request) - let response = request.sourcekitd.responseDictionary([ - request.sourcekitd.keys.associatedUSRs: info.associatedUSRs as [SKDResponseValue]? + return request.sourcekitd.responseDictionary([ + request.sourcekitd.keys.docBrief: info.briefDocumentation, + request.sourcekitd.keys.docFullAsXML: info.fullDocumentation, + request.sourcekitd.keys.associatedUSRs: info.associatedUSRs as [SKDResponseValue]?, ]) - - if let fullDocumentation = info.fullDocumentation { - response.set(request.sourcekitd.keys.docFullAsXML, to: fullDocumentation) - } else { - response.set(request.sourcekitd.keys.docBrief, to: info.briefDocumentation) - } - - return response } func handleCompletionDiagnostic(_ dict: SKDRequestDictionaryReader) throws -> SKDResponseDictionaryBuilder { diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index ccece241f..8cfaccc41 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -457,6 +457,7 @@ final class SwiftSourceKitPluginTests: XCTestCase { """ ) + XCTAssertEqual(sym1Doc.docBrief, "Protocol P foo1") XCTAssertEqual(sym1Doc.associatedUSRs, ["s:1a1SV4foo1yyF", "s:1a1PP4foo1yyF"]) let sym2Doc = try await sourcekitd.completeDocumentation(id: sym2.id) @@ -473,10 +474,12 @@ final class SwiftSourceKitPluginTests: XCTestCase { """ ) + XCTAssertEqual(sym2Doc.docBrief, "Struct S foo2") XCTAssertEqual(sym2Doc.associatedUSRs, ["s:1a1SV4foo2yyF"]) let sym3Doc = try await sourcekitd.completeDocumentation(id: sym3.id) XCTAssertNil(sym3Doc.docFullAsXML) + XCTAssertNil(sym3Doc.docBrief) XCTAssertEqual(sym3Doc.associatedUSRs, ["s:1a1SV4foo3yyF"]) } From 0bcd943fc4f7692329af89ccbc90c93993440896 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Sat, 16 Aug 2025 18:49:45 +0300 Subject: [PATCH 13/14] Use raw documentation for full documentation in completion --- .../include/CodeCompletionSwiftInterop.h | 14 ++++--- Sources/SKTestSupport/SkipUnless.swift | 2 +- Sources/SourceKitD/sourcekitd_functions.swift | 3 +- .../CodeCompletionSession.swift | 6 +-- .../ASTCompletion/CompletionSession.swift | 14 ++++++- .../CompletionProvider.swift | 3 +- .../SwiftCompletionTests.swift | 37 +++++++++++++------ .../SwiftSourceKitPluginTests.swift | 6 +++ 8 files changed, 60 insertions(+), 25 deletions(-) diff --git a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h b/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h index b9465ef04..fefd676fa 100644 --- a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h +++ b/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h @@ -324,11 +324,15 @@ typedef struct { void (^_Null_unspecified handler)(const char *_Null_unspecified) ); - void (*_Nullable completion_item_get_doc_full)( - _Nonnull swiftide_api_completion_response_t, - _Nonnull swiftide_api_completion_item_t, - void (^_Nonnull handler)(const char *_Nullable) - ); + void (*_Nullable completion_item_get_doc_full_as_xml)( + _Nonnull swiftide_api_completion_response_t, + _Nonnull swiftide_api_completion_item_t, + void (^_Nonnull handler)(const char *_Nullable)); + + void (*_Nullable completion_item_get_doc_raw)( + _Nonnull swiftide_api_completion_response_t, + _Nonnull swiftide_api_completion_item_t, + void (^_Nonnull handler)(const char *_Nullable)); void (*_Nonnull completion_item_get_associated_usrs)( _Null_unspecified swiftide_api_completion_response_t, diff --git a/Sources/SKTestSupport/SkipUnless.swift b/Sources/SKTestSupport/SkipUnless.swift index 7f85a341d..691a2a6ab 100644 --- a/Sources/SKTestSupport/SkipUnless.swift +++ b/Sources/SKTestSupport/SkipUnless.swift @@ -278,7 +278,7 @@ package actor SkipUnless { return try await shared.skipUnlessSupportedByToolchain(swiftVersion: SwiftVersion(6, 2), file: file, line: line) { let sourcekitd = try await getSourceKitD() - return sourcekitd.ideApi.completion_item_get_doc_full != nil + return sourcekitd.ideApi.completion_item_get_doc_raw != nil } } diff --git a/Sources/SourceKitD/sourcekitd_functions.swift b/Sources/SourceKitD/sourcekitd_functions.swift index 705d7f3d5..c3d5a3b0e 100644 --- a/Sources/SourceKitD/sourcekitd_functions.swift +++ b/Sources/SourceKitD/sourcekitd_functions.swift @@ -146,7 +146,8 @@ extension sourcekitd_ide_api_functions_t { completion_item_get_source_text: try loadRequired("swiftide_completion_item_get_source_text"), completion_item_get_type_name: try loadRequired("swiftide_completion_item_get_type_name"), completion_item_get_doc_brief: try loadRequired("swiftide_completion_item_get_doc_brief"), - completion_item_get_doc_full: loadOptional("swiftide_completion_item_get_doc_full"), + completion_item_get_doc_full_as_xml: loadOptional("swiftide_completion_item_get_doc_full_as_xml"), + completion_item_get_doc_raw: loadOptional("swiftide_completion_item_get_doc_raw"), completion_item_get_associated_usrs: try loadRequired("swiftide_completion_item_get_associated_usrs"), completion_item_get_kind: try loadRequired("swiftide_completion_item_get_kind"), completion_item_get_associated_kind: try loadRequired("swiftide_completion_item_get_associated_kind"), diff --git a/Sources/SwiftLanguageService/CodeCompletionSession.swift b/Sources/SwiftLanguageService/CodeCompletionSession.swift index f33dda60f..e0b31c7a2 100644 --- a/Sources/SwiftLanguageService/CodeCompletionSession.swift +++ b/Sources/SwiftLanguageService/CodeCompletionSession.swift @@ -586,10 +586,8 @@ class CodeCompletionSession { } private static func documentationString(from response: SKDResponseDictionary, sourcekitd: SourceKitD) -> String? { - if let docFullAsXML: String = response[sourcekitd.keys.docFullAsXML] { - return orLog("Converting XML documentation to markdown") { - try xmlDocumentationToMarkdown(docFullAsXML) - } + if let docComment: String = response[sourcekitd.keys.docComment] { + return docComment } return response[sourcekitd.keys.docBrief] diff --git a/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift b/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift index a8d392d77..e8620731b 100644 --- a/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift +++ b/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift @@ -241,9 +241,19 @@ struct ExtendedCompletionInfo { return result } - var fullDocumentation: String? { + var fullDocumentationAsXML: String? { var result: String? = nil - session.sourcekitd.ideApi.completion_item_get_doc_full?(session.response, rawItem) { + session.sourcekitd.ideApi.completion_item_get_doc_full_as_xml?(session.response, rawItem) { + if let cstr = $0 { + result = String(cString: cstr) + } + } + return result + } + + var rawDocumentation: String? { + var result: String? = nil + session.sourcekitd.ideApi.completion_item_get_doc_raw?(session.response, rawItem) { if let cstr = $0 { result = String(cString: cstr) } diff --git a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift index 2b64e0770..878ca8673 100644 --- a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift +++ b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift @@ -266,7 +266,8 @@ actor CompletionProvider { return request.sourcekitd.responseDictionary([ request.sourcekitd.keys.docBrief: info.briefDocumentation, - request.sourcekitd.keys.docFullAsXML: info.fullDocumentation, + request.sourcekitd.keys.docFullAsXML: info.fullDocumentationAsXML, + request.sourcekitd.keys.docComment: info.rawDocumentation, request.sourcekitd.keys.associatedUSRs: info.associatedUSRs as [SKDResponseValue]?, ]) } diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index 4007d821e..cf57ce410 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -40,6 +40,13 @@ final class SwiftCompletionTests: XCTestCase { """ struct S { /// Documentation for `abc`. + /// + /// _More_ documentation for `abc`. + /// + /// Usage: + /// ```swift + /// S().abc + /// ``` var abc: Int func test(a: Int) { @@ -71,10 +78,14 @@ final class SwiftCompletionTests: XCTestCase { assertMarkdown( documentation: abc.documentation, expected: """ + Documentation for `abc`. + + _More_ documentation for `abc`. + + Usage: ```swift - var abc: Int + S().abc ``` - Documentation for `abc`. """ ) XCTAssertEqual(abc.filterText, "abc") @@ -99,10 +110,14 @@ final class SwiftCompletionTests: XCTestCase { assertMarkdown( documentation: abc.documentation, expected: """ + Documentation for `abc`. + + _More_ documentation for `abc`. + + Usage: ```swift - var abc: Int + S().abc ``` - Documentation for `abc`. """ ) XCTAssertEqual(abc.filterText, "abc") @@ -1207,12 +1222,7 @@ final class SwiftCompletionTests: XCTestCase { let resolvedItem = try await testClient.send(CompletionItemResolveRequest(item: item)) assertMarkdown( documentation: resolvedItem.documentation, - expected: """ - ```swift - func makeBool() -> Bool - ``` - Creates a true value - """ + expected: "Creates a true value" ) } @@ -1310,7 +1320,12 @@ private func assertMarkdown( file: StaticString = #filePath, line: UInt = #line ) { - XCTAssertEqual(documentation, .markupContent(MarkupContent(kind: .markdown, value: expected))) + XCTAssertEqual( + documentation, + .markupContent(MarkupContent(kind: .markdown, value: expected)), + file: file, + line: line + ) } fileprivate extension Position { diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift index 8cfaccc41..4104a9871 100644 --- a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift +++ b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift @@ -205,6 +205,7 @@ final class SwiftSourceKitPluginTests: XCTestCase { XCTAssertEqual(result2.items.count, 1) XCTAssertEqual(result2.items[0].name, "") let doc = try await sourcekitd.completeDocumentation(id: result2.items[0].id) + XCTAssertNil(doc.docComment) XCTAssertNil(doc.docFullAsXML) XCTAssertNil(doc.docBrief) } @@ -441,6 +442,7 @@ final class SwiftSourceKitPluginTests: XCTestCase { let sym3 = try unwrap(result.items.first(where: { $0.name == "foo3()" }), "did not find foo3; got \(result.items)") let sym1Doc = try await sourcekitd.completeDocumentation(id: sym1.id) + XCTAssertEqual(sym1Doc.docComment, "Protocol P foo1") XCTAssertEqual( sym1Doc.docFullAsXML, """ @@ -461,6 +463,7 @@ final class SwiftSourceKitPluginTests: XCTestCase { XCTAssertEqual(sym1Doc.associatedUSRs, ["s:1a1SV4foo1yyF", "s:1a1PP4foo1yyF"]) let sym2Doc = try await sourcekitd.completeDocumentation(id: sym2.id) + XCTAssertEqual(sym2Doc.docComment, "Struct S foo2") XCTAssertEqual( sym2Doc.docFullAsXML, """ @@ -478,6 +481,7 @@ final class SwiftSourceKitPluginTests: XCTestCase { XCTAssertEqual(sym2Doc.associatedUSRs, ["s:1a1SV4foo2yyF"]) let sym3Doc = try await sourcekitd.completeDocumentation(id: sym3.id) + XCTAssertNil(sym3Doc.docComment) XCTAssertNil(sym3Doc.docFullAsXML) XCTAssertNil(sym3Doc.docBrief) XCTAssertEqual(sym3Doc.associatedUSRs, ["s:1a1SV4foo3yyF"]) @@ -1801,12 +1805,14 @@ private struct CompletionResult: Equatable, Sendable { } private struct CompletionDocumentation { + var docComment: String? = nil var docFullAsXML: String? = nil var docBrief: String? = nil var associatedUSRs: [String] = [] init(_ dict: SKDResponseDictionary) { let keys = dict.sourcekitd.keys + self.docComment = dict[keys.docComment] self.docFullAsXML = dict[keys.docFullAsXML] self.docBrief = dict[keys.docBrief] self.associatedUSRs = dict[keys.associatedUSRs]?.asStringArray ?? [] From a8d36798c50d9d7916fcffa3e76fe01b49448723 Mon Sep 17 00:00:00 2001 From: Ahmed Elrefaey <68241710+a7medev@users.noreply.github.com> Date: Mon, 18 Aug 2025 12:40:26 +0300 Subject: [PATCH 14/14] Simplify return of SkipUnless.getSourceKitD Co-authored-by: Alex Hoppen --- Sources/SKTestSupport/SkipUnless.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/SKTestSupport/SkipUnless.swift b/Sources/SKTestSupport/SkipUnless.swift index 691a2a6ab..17383def1 100644 --- a/Sources/SKTestSupport/SkipUnless.swift +++ b/Sources/SKTestSupport/SkipUnless.swift @@ -398,12 +398,10 @@ package actor SkipUnless { guard let sourcekitdPath = await ToolchainRegistry.forTesting.default?.sourcekitd else { throw GenericError("Could not find SourceKitD") } - let sourcekitd = try await SourceKitD.getOrCreate( + return try await SourceKitD.getOrCreate( dylibPath: sourcekitdPath, pluginPaths: try sourceKitPluginPaths ) - - return sourcekitd } }