Skip to content

Commit fe919c4

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

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: documentSymbolName = markText.trimmingCharacters(in: .whitespaces)
132+
case .blockComment:
133+
documentSymbolName = markText.trimmingCharacters(in: CharacterSet(["/", "*"]).union(.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
@@ -620,6 +620,80 @@ final class DocumentSymbolTests: XCTestCase {
620620
]
621621
}
622622
}
623+
624+
func testIncludeMarkComments() async throws {
625+
try await assertDocumentSymbols(
626+
"""
627+
1️⃣// MARK: Marker2️⃣
628+
"""
629+
) { positions in
630+
[
631+
DocumentSymbol(
632+
name: "Marker",
633+
kind: .namespace,
634+
range: positions["1️⃣"]..<positions["2️⃣"],
635+
selectionRange: positions["1️⃣"]..<positions["2️⃣"]
636+
)
637+
]
638+
}
639+
640+
try await assertDocumentSymbols(
641+
"""
642+
1️⃣// MARK: - Marker2️⃣
643+
"""
644+
) { positions in
645+
[
646+
DocumentSymbol(
647+
name: "- Marker",
648+
kind: .namespace,
649+
range: positions["1️⃣"]..<positions["2️⃣"],
650+
selectionRange: positions["1️⃣"]..<positions["2️⃣"]
651+
)
652+
]
653+
}
654+
655+
try await assertDocumentSymbols(
656+
"""
657+
1️⃣/* MARK: Marker */2️⃣
658+
"""
659+
) { positions in
660+
[
661+
DocumentSymbol(
662+
name: "Marker",
663+
kind: .namespace,
664+
range: positions["1️⃣"]..<positions["2️⃣"],
665+
selectionRange: positions["1️⃣"]..<positions["2️⃣"]
666+
)
667+
]
668+
}
669+
}
670+
671+
func testIncludeNestedMarkComments() async throws {
672+
try await assertDocumentSymbols(
673+
"""
674+
1️⃣struct 2️⃣Foo3️⃣ {
675+
4️⃣// MARK: Marker5️⃣
676+
}6️⃣
677+
"""
678+
) { positions in
679+
[
680+
DocumentSymbol(
681+
name: "Foo",
682+
kind: .struct,
683+
range: positions["1️⃣"]..<positions["6️⃣"],
684+
selectionRange: positions["2️⃣"]..<positions["3️⃣"],
685+
children: [
686+
DocumentSymbol(
687+
name: "Marker",
688+
kind: .namespace,
689+
range: positions["4️⃣"]..<positions["5️⃣"],
690+
selectionRange: positions["4️⃣"]..<positions["5️⃣"]
691+
)
692+
]
693+
)
694+
]
695+
}
696+
}
623697
}
624698

625699
fileprivate func assertDocumentSymbols(

0 commit comments

Comments
 (0)