Skip to content

Commit e90d614

Browse files
committed
Report MARK comments in the document symbols request
Fixes #963 rdar://117811210
1 parent 67c4530 commit e90d614

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

Sources/SourceKitLSP/Swift/DocumentSymbols.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import Foundation
1314
import LSPLogging
1415
import LanguageServerProtocol
1516
import SwiftSyntax
@@ -109,6 +110,51 @@ fileprivate final class DocumentSymbolsFinder: SyntaxAnyVisitor {
109110
)
110111
}
111112

113+
private func visit(_ trivia: Trivia, position: AbsolutePosition) {
114+
var position = position
115+
for piece in trivia.pieces {
116+
defer {
117+
position = position.advanced(by: piece.sourceLength.utf8Length)
118+
}
119+
switch piece {
120+
case .lineComment(let commentText), .blockComment(let commentText):
121+
let trimmedComment = commentText.trimmingPrefix(while: { $0 == "/" || $0 == "*" || $0 == " " })
122+
if trimmedComment.starts(with: "MARK: ") {
123+
let markText = String(trimmedComment[trimmedComment.index(trimmedComment.startIndex, offsetBy: 6)...])
124+
guard let rangeLowerBound = snapshot.position(of: position),
125+
let rangeUpperBound = snapshot.position(of: position.advanced(by: piece.sourceLength.utf8Length))
126+
else {
127+
break
128+
}
129+
let documentSymbolName: String
130+
switch piece {
131+
case .lineComment:
132+
documentSymbolName = markText.trimmingCharacters(in: CharacterSet(["/", "*"]).union(.whitespaces))
133+
case .blockComment: documentSymbolName = markText.trimmingCharacters(in: .whitespaces)
134+
default: preconditionFailure("Trivia piece kind not covered in the outer case")
135+
}
136+
result.append(
137+
DocumentSymbol(
138+
name: documentSymbolName,
139+
kind: .namespace,
140+
range: rangeLowerBound..<rangeUpperBound,
141+
selectionRange: rangeLowerBound..<rangeUpperBound,
142+
children: nil
143+
)
144+
)
145+
}
146+
default:
147+
break
148+
}
149+
}
150+
}
151+
152+
override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind {
153+
self.visit(node.leadingTrivia, position: node.position)
154+
self.visit(node.trailingTrivia, position: node.endPositionBeforeTrailingTrivia)
155+
return .skipChildren
156+
}
157+
112158
override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind {
113159
let rangeEnd =
114160
if let parameterClause = node.parameterClause {
@@ -253,3 +299,15 @@ fileprivate extension TokenKind {
253299
}
254300
}
255301
}
302+
303+
fileprivate extension String {
304+
/// Compatibility function of the `trimmingPrefix` function in the stdlib. We should be able to remove this once we
305+
/// only support macOS 13+
306+
func trimmingPrefix(while condition: (Character) -> Bool) -> Substring {
307+
var result = self[...]
308+
while let first = result.first, condition(first) {
309+
result = result[result.index(after: result.startIndex)...]
310+
}
311+
return result
312+
}
313+
}

Tests/SourceKitLSPTests/DocumentSymbolTests.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,80 @@ final class DocumentSymbolTests: XCTestCase {
619619
]
620620
}
621621
}
622+
623+
func testIncludeMarkComments() async throws {
624+
try await assertDocumentSymbols(
625+
"""
626+
1️⃣// MARK: Marker2️⃣
627+
"""
628+
) { positions in
629+
[
630+
DocumentSymbol(
631+
name: "Marker",
632+
kind: .namespace,
633+
range: positions["1️⃣"]..<positions["2️⃣"],
634+
selectionRange: positions["1️⃣"]..<positions["2️⃣"]
635+
)
636+
]
637+
}
638+
639+
try await assertDocumentSymbols(
640+
"""
641+
1️⃣// MARK: - Marker2️⃣
642+
"""
643+
) { positions in
644+
[
645+
DocumentSymbol(
646+
name: "- Marker",
647+
kind: .namespace,
648+
range: positions["1️⃣"]..<positions["2️⃣"],
649+
selectionRange: positions["1️⃣"]..<positions["2️⃣"]
650+
)
651+
]
652+
}
653+
654+
try await assertDocumentSymbols(
655+
"""
656+
1️⃣/* MARK: Marker */2️⃣
657+
"""
658+
) { positions in
659+
[
660+
DocumentSymbol(
661+
name: "Marker",
662+
kind: .namespace,
663+
range: positions["1️⃣"]..<positions["2️⃣"],
664+
selectionRange: positions["1️⃣"]..<positions["2️⃣"]
665+
)
666+
]
667+
}
668+
}
669+
670+
func testIncludeNestedMarkComments() async throws {
671+
try await assertDocumentSymbols(
672+
"""
673+
1️⃣struct 2️⃣Foo3️⃣ {
674+
4️⃣// MARK: Marker5️⃣
675+
}6️⃣
676+
"""
677+
) { positions in
678+
[
679+
DocumentSymbol(
680+
name: "Foo",
681+
kind: .struct,
682+
range: positions["1️⃣"]..<positions["6️⃣"],
683+
selectionRange: positions["2️⃣"]..<positions["3️⃣"],
684+
children: [
685+
DocumentSymbol(
686+
name: "Marker",
687+
kind: .namespace,
688+
range: positions["4️⃣"]..<positions["5️⃣"],
689+
selectionRange: positions["4️⃣"]..<positions["5️⃣"]
690+
)
691+
]
692+
)
693+
]
694+
}
695+
}
622696
}
623697

624698
fileprivate func assertDocumentSymbols(

0 commit comments

Comments
 (0)