diff --git a/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h b/Sources/Csourcekitd/include/CodeCompletionSwiftInterop.h
index eb676cb9f..b9465ef04 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 (*_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 (*_Nonnull completion_item_get_associated_usrs)(
_Null_unspecified swiftide_api_completion_response_t,
_Null_unspecified swiftide_api_completion_item_t,
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/Sources/SourceKitD/sourcekitd_functions.swift b/Sources/SourceKitD/sourcekitd_functions.swift
index 6edb883c2..705d7f3d5 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: 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/SourceKitLSP/Swift/CodeCompletionSession.swift b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift
index d558828b0..6250c93d1 100644
--- a/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift
+++ b/Sources/SourceKitLSP/Swift/CodeCompletionSession.swift
@@ -574,13 +574,26 @@ 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 orLog("Converting XML documentation to markdown") {
+ try xmlDocumentationToMarkdown(docFullAsXML, includeDeclaration: false)
+ }
+ }
+
+ return response[sourcekitd.keys.docBrief]
+ }
+
private func computeCompletionTextEdit(
completionPos: Position,
requestPosition: Position,
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/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift b/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift
index dcd89f548..a8d392d77 100644
--- a/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift
+++ b/Sources/SwiftSourceKitPlugin/ASTCompletion/CompletionSession.swift
@@ -241,6 +241,16 @@ struct ExtendedCompletionInfo {
return result
}
+ var fullDocumentation: String? {
+ var result: String? = nil
+ session.sourcekitd.ideApi.completion_item_get_doc_full?(session.response, rawItem) {
+ if let cstr = $0 {
+ result = String(cString: 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..575b693f9 100644
--- a/Sources/SwiftSourceKitPlugin/CompletionProvider.swift
+++ b/Sources/SwiftSourceKitPlugin/CompletionProvider.swift
@@ -264,6 +264,7 @@ actor CompletionProvider {
return request.sourcekitd.responseDictionary([
request.sourcekitd.keys.docBrief: info.briefDocumentation,
+ request.sourcekitd.keys.docFullAsXML: info.fullDocumentation,
request.sourcekitd.keys.associatedUSRs: info.associatedUSRs as [SKDResponseValue]?,
])
}
diff --git a/Tests/SourceKitLSPTests/LocalSwiftTests.swift b/Tests/SourceKitLSPTests/LocalSwiftTests.swift
index e281365a7..b2b70aa64 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 d08e0233c..a48e7810e 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)
@@ -39,6 +40,8 @@ final class SwiftCompletionTests: XCTestCase {
"""
struct S {
/// Documentation for `abc`.
+ ///
+ /// - Note: This is a note.
var abc: Int
func test(a: Int) {
@@ -67,7 +70,16 @@ 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: """
+ Documentation for `abc`.
+
+ ### Discussion
+
+ This is a note.
+ """
+ )
XCTAssertEqual(abc.filterText, "abc")
XCTAssertEqual(abc.textEdit, .textEdit(TextEdit(range: Range(positions["1️⃣"]), newText: "abc")))
XCTAssertEqual(abc.insertText, "abc")
@@ -87,7 +99,16 @@ 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: """
+ Documentation for `abc`.
+
+ ### Discussion
+
+ This is a note.
+ """
+ )
XCTAssertEqual(abc.filterText, "abc")
XCTAssertEqual(abc.textEdit, .textEdit(TextEdit(range: positions["1️⃣"].. Int {
return response.items.filter { $0.label.hasPrefix("f") }.count
}
+private func assertMarkdown(
+ documentation: StringOrMarkupContent?,
+ expected: String,
+ file: StaticString = #filePath,
+ line: UInt = #line
+) {
+ XCTAssertEqual(documentation, .markupContent(MarkupContent(kind: .markdown, value: expected)), file: file, line: line)
+}
+
fileprivate extension Position {
func adding(columns: Int) -> Position {
return Position(line: line, utf16index: utf16index + columns)
diff --git a/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift b/Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift
index 59bd49884..6ea386100 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.docBrief, 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(
@@ -436,14 +441,44 @@ 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.docBrief, "Protocol P foo1")
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.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"])
}
@@ -1766,11 +1801,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 ?? []
}