Skip to content

Commit 0ac09ca

Browse files
committed
Separate signature and parameter documentation using swift-markdown
1 parent 766f2c8 commit 0ac09ca

File tree

6 files changed

+507
-34
lines changed

6 files changed

+507
-34
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,7 @@ var targets: [Target] = [
613613
"ToolchainRegistry",
614614
"TSCExtensions",
615615
.product(name: "IndexStoreDB", package: "indexstore-db"),
616+
.product(name: "Markdown", package: "swift-markdown"),
616617
.product(name: "Crypto", package: "swift-crypto"),
617618
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
618619
]

Sources/SwiftLanguageService/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ add_library(SwiftLanguageService STATIC
4646
GeneratedInterfaceManager.swift
4747
SignatureHelp.swift
4848
AdjustPositionToStartOfArgument.swift
49+
ParametersDocumentationExtractor.swift
4950
)
5051
set_target_properties(SwiftLanguageService PROPERTIES
5152
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
@@ -57,6 +58,7 @@ target_link_libraries(SwiftLanguageService PUBLIC
5758
SourceKitLSP
5859
ToolchainRegistry
5960
IndexStoreDB
61+
Markdown
6062
SwiftSyntax::SwiftBasicFormat
6163
SwiftSyntax::SwiftSyntax
6264
)
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import Foundation
2+
import Markdown
3+
4+
private struct ParametersDocumentationExtractor {
5+
private var parameters = [String: String]()
6+
7+
/// Extracts parameter documentation from a markdown string.
8+
///
9+
/// - Returns: A tuple containing the extracted parameters and the remaining markdown.
10+
mutating func extract(from markdown: String) -> (parameters: [String: String], remaining: String) {
11+
let document = Document(parsing: markdown, options: [.parseBlockDirectives, .parseMinimalDoxygen])
12+
13+
var remainingBlocks = [any BlockMarkup]()
14+
15+
for block in document.blockChildren {
16+
switch block {
17+
case let unorderedList as UnorderedList:
18+
if let newUnorderedList = extract(from: unorderedList) {
19+
remainingBlocks.append(newUnorderedList)
20+
}
21+
case let doxygenParameter as DoxygenParameter:
22+
extract(from: doxygenParameter)
23+
default:
24+
remainingBlocks.append(block)
25+
}
26+
}
27+
28+
let remaining = Document(remainingBlocks).format()
29+
30+
return (parameters, remaining)
31+
}
32+
33+
/// Extracts parameter documentation from a Doxygen parameter command.
34+
private mutating func extract(from doxygenParameter: DoxygenParameter) {
35+
parameters[doxygenParameter.name] = Document(doxygenParameter.blockChildren).format()
36+
}
37+
38+
/// Extracts parameter documentation from an unordered list.
39+
///
40+
/// - Returns: A new UnorderedList with the items that were not added to the parameters if any.
41+
private mutating func extract(from unorderedList: UnorderedList) -> UnorderedList? {
42+
var newItems = [ListItem]()
43+
44+
for item in unorderedList.listItems {
45+
if extractSingle(from: item) || extractOutline(from: item) {
46+
continue
47+
}
48+
49+
newItems.append(item)
50+
}
51+
52+
if newItems.isEmpty {
53+
return nil
54+
}
55+
56+
return UnorderedList(newItems)
57+
}
58+
59+
/// Parameter documentation from a `Parameters:` outline.
60+
///
61+
/// Example:
62+
/// ```markdown
63+
/// - Parameters:
64+
/// - param: description
65+
/// ```
66+
///
67+
/// - Returns: True if the list item has parameter outline documentation, false otherwise.
68+
private mutating func extractOutline(from listItem: ListItem) -> Bool {
69+
guard let firstChild = listItem.child(at: 0) as? Paragraph,
70+
let headingText = firstChild.child(at: 0) as? Text
71+
else {
72+
return false
73+
}
74+
75+
let parametersPrefix = "parameters:"
76+
let headingContent = headingText.string.trimmingCharacters(in: .whitespaces)
77+
78+
guard headingContent.lowercased().hasPrefix(parametersPrefix) else {
79+
return false
80+
}
81+
82+
for child in listItem.children {
83+
guard let nestedList = child as? UnorderedList else {
84+
continue
85+
}
86+
87+
for nestedItem in nestedList.listItems {
88+
if let parameter = extractOutlineItem(from: nestedItem) {
89+
parameters[parameter.name] = parameter.documentation
90+
}
91+
}
92+
}
93+
94+
return true
95+
}
96+
97+
/// Extracts parameter documentation from a single parameter.
98+
///
99+
/// Example:
100+
/// ```markdown
101+
/// - Parameter param: description
102+
/// ```
103+
///
104+
/// - Returns: True if the list item has single parameter documentation, false otherwise.
105+
private mutating func extractSingle(from listItem: ListItem) -> Bool {
106+
guard let paragraph = listItem.child(at: 0) as? Paragraph,
107+
let paragraphText = paragraph.child(at: 0) as? Text
108+
else {
109+
return false
110+
}
111+
112+
let parameterPrefix = "parameter "
113+
let paragraphContent = paragraphText.string
114+
115+
guard paragraphContent.count >= parameterPrefix.count else {
116+
return false
117+
}
118+
119+
let prefixEnd = paragraphContent.index(paragraphContent.startIndex, offsetBy: parameterPrefix.count)
120+
let potentialMatch = paragraphContent[..<prefixEnd].lowercased()
121+
122+
guard potentialMatch == parameterPrefix else {
123+
return false
124+
}
125+
126+
let remainingContent = String(paragraphContent[prefixEnd...]).trimmingCharacters(in: .whitespaces)
127+
128+
guard let parameter = extractParam(firstTextContent: remainingContent, listItem: listItem) else {
129+
return false
130+
}
131+
132+
parameters[parameter.name] = parameter.documentation
133+
134+
return true
135+
}
136+
137+
/// Extracts a parameter field from a list item (used for parameter outline items)
138+
private func extractOutlineItem(from listItem: ListItem) -> (name: String, documentation: String)? {
139+
guard let paragraph = listItem.child(at: 0) as? Paragraph else {
140+
return nil
141+
}
142+
143+
guard let paragraphText = paragraph.child(at: 0) as? Text else {
144+
return nil
145+
}
146+
147+
return extractParam(firstTextContent: paragraphText.string, listItem: listItem)
148+
}
149+
150+
/// Extracts a parameter field from a list item provided the relevant first text content allowing reuse in ``extractOutlineItem`` and ``extractSingle``
151+
///
152+
/// - Parameters:
153+
/// - firstTextContent: The content of the first text child of the list item's first paragraph
154+
/// - listItem: The list item to extract the parameter from
155+
///
156+
/// - Returns: A tuple containing the parameter name and documentation if a parameter was found, nil otherwise.
157+
private func extractParam(
158+
firstTextContent: String,
159+
listItem: ListItem
160+
) -> (name: String, documentation: String)? {
161+
guard let paragraph = listItem.child(at: 0) as? Paragraph else {
162+
return nil
163+
}
164+
165+
let components = firstTextContent.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
166+
167+
guard components.count == 2 else {
168+
return nil
169+
}
170+
171+
let name = String(components[0]).trimmingCharacters(in: .whitespaces)
172+
guard !name.isEmpty else {
173+
return nil
174+
}
175+
176+
let remainingFirstTextContent = String(components[1]).trimmingCharacters(in: .whitespaces)
177+
let remainingParagraphChildren = [Text(remainingFirstTextContent)] + paragraph.inlineChildren.dropFirst()
178+
let remainingChildren = [Paragraph(remainingParagraphChildren)] + listItem.blockChildren.dropFirst()
179+
let documentation = Document(remainingChildren).format()
180+
181+
return (name, documentation)
182+
}
183+
}
184+
185+
/// Extracts parameter documentation from markdown text.
186+
///
187+
/// - Parameter markdown: The markdown text to extract parameters from
188+
/// - Returns: A tuple containing the extracted parameters dictionary and the remaining markdown text
189+
package func extractParametersDocumentation(from markdown: String) -> ([String: String], String) {
190+
var extractor = ParametersDocumentationExtractor()
191+
return extractor.extract(from: markdown)
192+
}

Sources/SwiftLanguageService/SignatureHelp.swift

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,23 @@ fileprivate extension String {
3636
}
3737

3838
fileprivate extension ParameterInformation {
39-
init?(_ parameter: SKDResponseDictionary, _ signatureLabel: String, _ keys: sourcekitd_api_keys) {
39+
init?(
40+
_ parameter: SKDResponseDictionary,
41+
_ signatureLabel: String,
42+
_ keys: sourcekitd_api_keys,
43+
_ parameterDocumentation: [String: String]
44+
) {
4045
guard let nameOffset = parameter[keys.nameOffset] as Int?,
4146
let nameLength = parameter[keys.nameLength] as Int?
4247
else {
4348
return nil
4449
}
4550

4651
let documentation: StringOrMarkupContent? =
47-
if let docComment: String = parameter[keys.docComment] {
48-
.markupContent(MarkupContent(kind: .markdown, value: docComment))
52+
if let name = parameter[keys.name] as String?,
53+
let documentation = parameterDocumentation[name]
54+
{
55+
.markupContent(MarkupContent(kind: .markdown, value: documentation))
4956
} else {
5057
nil
5158
}
@@ -68,7 +75,20 @@ fileprivate extension SignatureInformation {
6875
return nil
6976
}
7077

71-
let parameters = skParameters.compactMap { ParameterInformation($0, label, keys) }
78+
let documentation: StringOrMarkupContent?
79+
let parameterDocumentation: [String: String]
80+
81+
if let docComment: String = signature[keys.docComment] {
82+
let (parameterComments, signatureComment) = extractParametersDocumentation(from: docComment)
83+
84+
documentation = .markupContent(MarkupContent(kind: .markdown, value: signatureComment))
85+
parameterDocumentation = parameterComments
86+
} else {
87+
documentation = nil
88+
parameterDocumentation = [:]
89+
}
90+
91+
let parameters = skParameters.compactMap { ParameterInformation($0, label, keys, parameterDocumentation) }
7292

7393
let activeParameter: Int? =
7494
if let activeParam: Int = signature[keys.activeParameter] {
@@ -88,13 +108,6 @@ fileprivate extension SignatureInformation {
88108
nil
89109
}
90110

91-
let documentation: StringOrMarkupContent? =
92-
if let docComment: String = signature[keys.docComment] {
93-
.markupContent(MarkupContent(kind: .markdown, value: docComment))
94-
} else {
95-
nil
96-
}
97-
98111
self.init(
99112
label: label,
100113
documentation: documentation,

0 commit comments

Comments
 (0)