Skip to content

Commit c8b2f76

Browse files
committed
Move FoldingRange to its own file
No code change here.
1 parent d931806 commit c8b2f76

File tree

3 files changed

+247
-229
lines changed

3 files changed

+247
-229
lines changed

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ target_sources(SourceKitLSP PRIVATE
2323
Swift/Diagnostic.swift
2424
Swift/DocumentSymbols.swift
2525
Swift/EditorPlaceholder.swift
26+
Swift/FoldingRange.swift
2627
Swift/OpenInterface.swift
2728
Swift/RelatedIdentifiers.swift
2829
Swift/Rename.swift
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LSPLogging
14+
import LanguageServerProtocol
15+
import SwiftSyntax
16+
17+
fileprivate final class FoldingRangeFinder: SyntaxVisitor {
18+
private let snapshot: DocumentSnapshot
19+
/// Some ranges might occur multiple times.
20+
/// E.g. for `print("hi")`, `"hi"` is both the range of all call arguments and the range the first argument in the call.
21+
/// It doesn't make sense to report them multiple times, so use a `Set` here.
22+
private var ranges: Set<FoldingRange>
23+
/// The client-imposed limit on the number of folding ranges it would
24+
/// prefer to receive from the LSP server. If the value is `nil`, there
25+
/// is no preset limit.
26+
private var rangeLimit: Int?
27+
/// If `true`, the client is only capable of folding entire lines. If
28+
/// `false` the client can handle folding ranges.
29+
private var lineFoldingOnly: Bool
30+
31+
init(snapshot: DocumentSnapshot, rangeLimit: Int?, lineFoldingOnly: Bool) {
32+
self.snapshot = snapshot
33+
self.ranges = []
34+
self.rangeLimit = rangeLimit
35+
self.lineFoldingOnly = lineFoldingOnly
36+
super.init(viewMode: .sourceAccurate)
37+
}
38+
39+
override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind {
40+
// Index comments, so we need to see at least '/*', or '//'.
41+
if node.leadingTriviaLength.utf8Length > 2 {
42+
self.addTrivia(from: node, node.leadingTrivia)
43+
}
44+
45+
if node.trailingTriviaLength.utf8Length > 2 {
46+
self.addTrivia(from: node, node.trailingTrivia)
47+
}
48+
49+
return .visitChildren
50+
}
51+
52+
private func addTrivia(from node: TokenSyntax, _ trivia: Trivia) {
53+
let pieces = trivia.pieces
54+
var start = node.position.utf8Offset
55+
/// The index of the trivia piece we are currently inspecting.
56+
var index = 0
57+
58+
while index < pieces.count {
59+
let piece = pieces[index]
60+
defer {
61+
start += pieces[index].sourceLength.utf8Length
62+
index += 1
63+
}
64+
switch piece {
65+
case .blockComment:
66+
_ = self.addFoldingRange(
67+
start: start,
68+
end: start + piece.sourceLength.utf8Length,
69+
kind: .comment
70+
)
71+
case .docBlockComment:
72+
_ = self.addFoldingRange(
73+
start: start,
74+
end: start + piece.sourceLength.utf8Length,
75+
kind: .comment
76+
)
77+
case .lineComment, .docLineComment:
78+
let lineCommentBlockStart = start
79+
80+
// Keep scanning the upcoming trivia pieces to find the end of the
81+
// block of line comments.
82+
// As we find a new end of the block comment, we set `index` and
83+
// `start` to `lookaheadIndex` and `lookaheadStart` resp. to
84+
// commit the newly found end.
85+
var lookaheadIndex = index
86+
var lookaheadStart = start
87+
var hasSeenNewline = false
88+
LOOP: while lookaheadIndex < pieces.count {
89+
let piece = pieces[lookaheadIndex]
90+
defer {
91+
lookaheadIndex += 1
92+
lookaheadStart += piece.sourceLength.utf8Length
93+
}
94+
switch piece {
95+
case .newlines(let count), .carriageReturns(let count), .carriageReturnLineFeeds(let count):
96+
if count > 1 || hasSeenNewline {
97+
// More than one newline is separating the two line comment blocks.
98+
// We have reached the end of this block of line comments.
99+
break LOOP
100+
}
101+
hasSeenNewline = true
102+
case .spaces, .tabs:
103+
// We allow spaces and tabs because the comments might be indented
104+
continue
105+
case .lineComment, .docLineComment:
106+
// We have found a new line comment in this block. Commit it.
107+
index = lookaheadIndex
108+
start = lookaheadStart
109+
hasSeenNewline = false
110+
default:
111+
// We assume that any other trivia piece terminates the block
112+
// of line comments.
113+
break LOOP
114+
}
115+
}
116+
_ = self.addFoldingRange(
117+
start: lineCommentBlockStart,
118+
end: start + pieces[index].sourceLength.utf8Length,
119+
kind: .comment
120+
)
121+
default:
122+
break
123+
}
124+
}
125+
}
126+
127+
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
128+
return self.addFoldingRange(
129+
start: node.statements.position.utf8Offset,
130+
end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset
131+
)
132+
}
133+
134+
override func visit(_ node: MemberBlockSyntax) -> SyntaxVisitorContinueKind {
135+
return self.addFoldingRange(
136+
start: node.members.position.utf8Offset,
137+
end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset
138+
)
139+
}
140+
141+
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
142+
return self.addFoldingRange(
143+
start: node.statements.position.utf8Offset,
144+
end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset
145+
)
146+
}
147+
148+
override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind {
149+
return self.addFoldingRange(
150+
start: node.accessors.position.utf8Offset,
151+
end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset
152+
)
153+
}
154+
155+
override func visit(_ node: SwitchExprSyntax) -> SyntaxVisitorContinueKind {
156+
return self.addFoldingRange(
157+
start: node.cases.position.utf8Offset,
158+
end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset
159+
)
160+
}
161+
162+
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
163+
return self.addFoldingRange(
164+
start: node.arguments.position.utf8Offset,
165+
end: node.arguments.endPosition.utf8Offset
166+
)
167+
}
168+
169+
override func visit(_ node: SubscriptCallExprSyntax) -> SyntaxVisitorContinueKind {
170+
return self.addFoldingRange(
171+
start: node.arguments.position.utf8Offset,
172+
end: node.arguments.endPosition.utf8Offset
173+
)
174+
}
175+
176+
__consuming func finalize() -> Set<FoldingRange> {
177+
return self.ranges
178+
}
179+
180+
private func addFoldingRange(start: Int, end: Int, kind: FoldingRangeKind? = nil) -> SyntaxVisitorContinueKind {
181+
if let limit = self.rangeLimit, self.ranges.count >= limit {
182+
return .skipChildren
183+
}
184+
185+
guard let start: Position = snapshot.positionOf(utf8Offset: start),
186+
let end: Position = snapshot.positionOf(utf8Offset: end)
187+
else {
188+
logger.error("folding range failed to retrieve position of \(self.snapshot.uri.forLogging): \(start)-\(end)")
189+
return .visitChildren
190+
}
191+
let range: FoldingRange
192+
if lineFoldingOnly {
193+
// Since the client cannot fold less than a single line, if the
194+
// fold would span 1 line there's no point in reporting it.
195+
guard end.line > start.line else {
196+
return .visitChildren
197+
}
198+
199+
// If the client only supports folding full lines, don't report
200+
// the end of the range since there's nothing they could do with it.
201+
range = FoldingRange(
202+
startLine: start.line,
203+
startUTF16Index: nil,
204+
endLine: end.line,
205+
endUTF16Index: nil,
206+
kind: kind
207+
)
208+
} else {
209+
range = FoldingRange(
210+
startLine: start.line,
211+
startUTF16Index: start.utf16index,
212+
endLine: end.line,
213+
endUTF16Index: end.utf16index,
214+
kind: kind
215+
)
216+
}
217+
ranges.insert(range)
218+
return .visitChildren
219+
}
220+
}
221+
222+
extension SwiftLanguageServer {
223+
public func foldingRange(_ req: FoldingRangeRequest) async throws -> [FoldingRange]? {
224+
let foldingRangeCapabilities = capabilityRegistry.clientCapabilities.textDocument?.foldingRange
225+
let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri)
226+
227+
let sourceFile = await syntaxTreeManager.syntaxTree(for: snapshot)
228+
229+
try Task.checkCancellation()
230+
231+
// If the limit is less than one, do nothing.
232+
if let limit = foldingRangeCapabilities?.rangeLimit, limit <= 0 {
233+
return []
234+
}
235+
236+
let rangeFinder = FoldingRangeFinder(
237+
snapshot: snapshot,
238+
rangeLimit: foldingRangeCapabilities?.rangeLimit,
239+
lineFoldingOnly: foldingRangeCapabilities?.lineFoldingOnly ?? false
240+
)
241+
rangeFinder.walk(sourceFile)
242+
let ranges = rangeFinder.finalize()
243+
244+
return ranges.sorted()
245+
}
246+
}

0 commit comments

Comments
 (0)