From 24de753a92f560f991d8d732a07ae2a885a49264 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 + .../SourceKitLSP/Swift/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/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift index d558828b0..7aa7f5039 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift @@ -574,13 +574,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 d02ed6d7b..f73a1bbab 100644 --- a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift +++ b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift @@ -262,10 +262,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 283e08c526a582b2c7b6887c27fa8a44a1b73cb1 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 59bd49884..7df838a85 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 @@ fileprivate struct CompletionResult: Equatable, Sendable { } fileprivate 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 fce98d5b7b2a96770627d9dc41734f0f474b362d 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 ae3f00e92800a91b398a62ffc1a8ba1520c47ee9 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 2b40e78898bf3b1512931f6da7a0f3732cba2042 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 f73a1bbab..f9757005f 100644 --- a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift +++ b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift @@ -262,7 +262,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 7df838a85..710fbfc35 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 db443202dde16a4c78d564fa2d9e5699028ce2cc 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 710fbfc35..0b654654b 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 @@ fileprivate struct CompletionResult: Equatable, Sendable { } fileprivate 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 3c66365bd12afc100c2cc82464cfdad2e15050c0 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 0b654654b..259acbfc1 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 4df7ea11acd69d81651dc8a4b0df922158ee3107 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/SourceKitLSP/Swift/CodeCompletionSession.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift index 7aa7f5039..92ad9e4b7 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift @@ -585,7 +585,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 ed777352189f10d55273b07d53aa15e9efebb341 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 259acbfc1..710fbfc35 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 @@ fileprivate struct CompletionResult: Equatable, Sendable { } fileprivate 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 4f8d69e32809a137d4b07b63b62a4f1a08d52b2b 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 ede127af3..b2374706c 100644 --- a/Sources/SKTestSupport/SkipUnless.swift +++ b/Sources/SKTestSupport/SkipUnless.swift @@ -10,6 +10,7 @@ // //===----------------------------------------------------------------------===// +import Csourcekitd import Foundation import LanguageServerProtocol import LanguageServerProtocolExtensions @@ -250,13 +251,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, @@ -274,6 +270,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 @@ -352,6 +359,18 @@ package actor SkipUnless { return .featureSupported } } + + 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 710fbfc35..9f295a2d1 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 @@ fileprivate struct CompletionResult: Equatable, Sendable { fileprivate 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 d3ed8b1f3792c2fb834c52069747e4f38803d903 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 --- .../Swift/CodeCompletionSession.swift | 3 +- .../SwiftSourceKitPluginTests.swift | 54 ++++++++++--------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift index 92ad9e4b7..0fc1899b0 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift @@ -576,7 +576,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 9f295a2d1..c19f81070 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 9d56a93b324af4608c0c0154ed11e51a9fcadf54 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Thu, 17 Jul 2025 22:03:33 +0300 Subject: [PATCH 12/14] Show full documentation without declaration in completion --- .../Swift/CodeCompletionSession.swift | 2 +- Sources/SourceKitLSP/Swift/CommentXML.swift | 23 +++++++++++---- Tests/SourceKitLSPTests/LocalSwiftTests.swift | 26 +++++++++++++++++ .../SwiftCompletionTests.swift | 29 ++++++++++--------- 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift index 0fc1899b0..6250c93d1 100644 --- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift +++ b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift @@ -587,7 +587,7 @@ 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) + try xmlDocumentationToMarkdown(docFullAsXML, includeDeclaration: false) } } diff --git a/Sources/SourceKitLSP/Swift/CommentXML.swift b/Sources/SourceKitLSP/Swift/CommentXML.swift index 3aee62188..6562c24af 100644 --- a/Sources/SourceKitLSP/Swift/CommentXML.swift +++ b/Sources/SourceKitLSP/Swift/CommentXML.swift @@ -24,13 +24,17 @@ enum CommentXMLError: Error { /// Converts from sourcekit's XML documentation format to Markdown. /// /// This code should go away and sourcekitd should return the Markdown directly. -package func xmlDocumentationToMarkdown(_ xmlString: String) throws -> String { +/// +/// - Parameters: +/// - xmlString: The XML string to convert. +/// - includeDeclaration: If true, the declaration will be included at the top of the output markdown. +package func xmlDocumentationToMarkdown(_ xmlString: String, includeDeclaration: Bool = true) throws -> String { let xml = try XMLDocument(xmlString: xmlString) guard let root = xml.rootElement() else { throw CommentXMLError.noRootElement } - var convert = XMLToMarkdown() + var convert = XMLToMarkdown(includeDeclaration: includeDeclaration) convert.out.reserveCapacity(xmlString.utf16.count) convert.toMarkdown(root) return convert.out @@ -42,6 +46,11 @@ private struct XMLToMarkdown { let indentWidth: Int = 4 var lineNumber: Int = 0 var inParam: Bool = false + let includeDeclaration: Bool + + init(includeDeclaration: Bool) { + self.includeDeclaration = includeDeclaration + } mutating func newlineIfNeeded(count: Int = 1) { if !out.isEmpty && out.last! != "\n" { @@ -74,10 +83,12 @@ private struct XMLToMarkdown { mutating func toMarkdown(_ node: XMLElement) { switch node.name { case "Declaration": - newlineIfNeeded(count: 2) - out += "```swift\n" - toMarkdown(node.children) - out += "\n```\n" + if includeDeclaration { + newlineIfNeeded(count: 2) + out += "```swift\n" + toMarkdown(node.children) + out += "\n```\n" + } case "Name", "USR", "Direction": break diff --git a/Tests/SourceKitLSPTests/LocalSwiftTests.swift b/Tests/SourceKitLSPTests/LocalSwiftTests.swift index e281365a7..9c981b30e 100644 --- a/Tests/SourceKitLSPTests/LocalSwiftTests.swift +++ b/Tests/SourceKitLSPTests/LocalSwiftTests.swift @@ -1149,6 +1149,32 @@ final class LocalSwiftTests: XCTestCase { ) } + func testXMLToMarkdownCommentOmitDeclaration() { + XCTAssertEqual( + try xmlDocumentationToMarkdown( + """ + \ + func pi()\ + \ + Computes π with infinite precision.\ + \ + This function doesn’t terminate.\ + \ + \ + + """, + includeDeclaration: false + ), + """ + Computes π with infinite precision. + + ### Discussion + + This function doesn’t terminate. + """ + ) + } + func testSymbolInfo() async throws { let testClient = try await TestSourceKitLSPClient() let url = URL(fileURLWithPath: "/\(UUID())/a.swift") diff --git a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift index 4007d821e..a48e7810e 100644 --- a/Tests/SourceKitLSPTests/SwiftCompletionTests.swift +++ b/Tests/SourceKitLSPTests/SwiftCompletionTests.swift @@ -40,6 +40,8 @@ final class SwiftCompletionTests: XCTestCase { """ struct S { /// Documentation for `abc`. + /// + /// - Note: This is a note. var abc: Int func test(a: Int) { @@ -71,10 +73,11 @@ final class SwiftCompletionTests: XCTestCase { assertMarkdown( documentation: abc.documentation, expected: """ - ```swift - var abc: Int - ``` Documentation for `abc`. + + ### Discussion + + This is a note. """ ) XCTAssertEqual(abc.filterText, "abc") @@ -99,10 +102,11 @@ final class SwiftCompletionTests: XCTestCase { assertMarkdown( documentation: abc.documentation, expected: """ - ```swift - var abc: Int - ``` Documentation for `abc`. + + ### Discussion + + This is a note. """ ) XCTAssertEqual(abc.filterText, "abc") @@ -126,6 +130,8 @@ final class SwiftCompletionTests: XCTestCase { """ struct S { /// Documentation for `abc`. + /// + /// - Note: This is a note. var abc: Int func test(a: Int) { @@ -182,6 +188,8 @@ final class SwiftCompletionTests: XCTestCase { """ struct S { /// Documentation for `abc`. + /// + /// - Note: This is a note. var abc: Int func test(a: Int) { @@ -1207,12 +1215,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 +1313,7 @@ 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 { From 2f77852d8d7567ce490eb7fba7fe1f4551f5889a Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Thu, 17 Jul 2025 22:09:35 +0300 Subject: [PATCH 13/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 f9757005f..575b693f9 100644 --- a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift +++ b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift @@ -262,17 +262,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 c19f81070..6ea386100 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 c0b4ca225d171fde60db8c1ddd746afe2d87d811 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmoud Date: Thu, 17 Jul 2025 22:11:09 +0300 Subject: [PATCH 14/14] Format files with swift format --- Tests/SourceKitLSPTests/LocalSwiftTests.swift | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Tests/SourceKitLSPTests/LocalSwiftTests.swift b/Tests/SourceKitLSPTests/LocalSwiftTests.swift index 9c981b30e..b2b70aa64 100644 --- a/Tests/SourceKitLSPTests/LocalSwiftTests.swift +++ b/Tests/SourceKitLSPTests/LocalSwiftTests.swift @@ -1150,29 +1150,29 @@ final class LocalSwiftTests: XCTestCase { } func testXMLToMarkdownCommentOmitDeclaration() { - XCTAssertEqual( - try xmlDocumentationToMarkdown( - """ - \ - func pi()\ - \ - Computes π with infinite precision.\ - \ - This function doesn’t terminate.\ - \ - \ - - """, - includeDeclaration: false - ), + XCTAssertEqual( + try xmlDocumentationToMarkdown( """ - Computes π with infinite precision. + \ + func pi()\ + \ + Computes π with infinite precision.\ + \ + This function doesn’t terminate.\ + \ + \ + + """, + includeDeclaration: false + ), + """ + Computes π with infinite precision. - ### Discussion + ### Discussion - This function doesn’t terminate. - """ - ) + This function doesn’t terminate. + """ + ) } func testSymbolInfo() async throws {