Skip to content

Commit f32e7dd

Browse files
authored
Merge pull request #999 from ahoppen/ahoppen/folding-range-improvements
Improve code folding
2 parents 4f5186e + be0dd73 commit f32e7dd

File tree

4 files changed

+573
-384
lines changed

4 files changed

+573
-384
lines changed

Sources/SourceKitLSP/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ target_sources(SourceKitLSP PRIVATE
2424
Swift/Diagnostic.swift
2525
Swift/DocumentSymbols.swift
2626
Swift/EditorPlaceholder.swift
27+
Swift/FoldingRange.swift
2728
Swift/OpenInterface.swift
2829
Swift/RelatedIdentifiers.swift
2930
Swift/SemanticRefactorCommand.swift
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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: SyntaxAnyVisitor {
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
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 = start.advanced(by: pieces[index].sourceLength.utf8Length)
62+
index += 1
63+
}
64+
switch piece {
65+
case .blockComment:
66+
_ = self.addFoldingRange(
67+
start: start,
68+
end: start.advanced(by: piece.sourceLength.utf8Length),
69+
kind: .comment
70+
)
71+
case .docBlockComment:
72+
_ = self.addFoldingRange(
73+
start: start,
74+
end: start.advanced(by: 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 = lookaheadStart.advanced(by: 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.advanced(by: pieces[index].sourceLength.utf8Length),
119+
kind: .comment
120+
)
121+
default:
122+
break
123+
}
124+
}
125+
}
126+
127+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
128+
if let braced = node.asProtocol(BracedSyntax.self) {
129+
return self.addFoldingRange(
130+
start: braced.leftBrace.endPositionBeforeTrailingTrivia,
131+
end: braced.rightBrace.positionAfterSkippingLeadingTrivia
132+
)
133+
}
134+
if let parenthesized = node.asProtocol(ParenthesizedSyntax.self) {
135+
return self.addFoldingRange(
136+
start: parenthesized.leftParen.endPositionBeforeTrailingTrivia,
137+
end: parenthesized.rightParen.positionAfterSkippingLeadingTrivia
138+
)
139+
}
140+
return .visitChildren
141+
}
142+
143+
override func visit(_ node: ArrayExprSyntax) -> SyntaxVisitorContinueKind {
144+
return self.addFoldingRange(
145+
start: node.leftSquare.endPositionBeforeTrailingTrivia,
146+
end: node.rightSquare.positionAfterSkippingLeadingTrivia
147+
)
148+
}
149+
150+
override func visit(_ node: DictionaryExprSyntax) -> SyntaxVisitorContinueKind {
151+
return self.addFoldingRange(
152+
start: node.leftSquare.endPositionBeforeTrailingTrivia,
153+
end: node.rightSquare.positionAfterSkippingLeadingTrivia
154+
)
155+
}
156+
157+
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
158+
if let leftParen = node.leftParen, let rightParen = node.rightParen {
159+
return self.addFoldingRange(
160+
start: leftParen.endPositionBeforeTrailingTrivia,
161+
end: rightParen.positionAfterSkippingLeadingTrivia
162+
)
163+
}
164+
return .visitChildren
165+
}
166+
167+
override func visit(_ node: SubscriptCallExprSyntax) -> SyntaxVisitorContinueKind {
168+
return self.addFoldingRange(
169+
start: node.leftSquare.endPositionBeforeTrailingTrivia,
170+
end: node.rightSquare.positionAfterSkippingLeadingTrivia
171+
)
172+
}
173+
174+
override func visit(_ node: SwitchCaseSyntax) -> SyntaxVisitorContinueKind {
175+
return self.addFoldingRange(
176+
start: node.label.endPositionBeforeTrailingTrivia,
177+
end: node.statements.endPosition
178+
)
179+
}
180+
181+
__consuming func finalize() -> Set<FoldingRange> {
182+
return self.ranges
183+
}
184+
185+
private func addFoldingRange(
186+
start: AbsolutePosition,
187+
end: AbsolutePosition,
188+
kind: FoldingRangeKind? = nil
189+
) -> SyntaxVisitorContinueKind {
190+
if let limit = self.rangeLimit, self.ranges.count >= limit {
191+
return .skipChildren
192+
}
193+
if start == end {
194+
// Don't report empty ranges
195+
return .visitChildren
196+
}
197+
198+
guard let start: Position = snapshot.positionOf(utf8Offset: start.utf8Offset),
199+
let end: Position = snapshot.positionOf(utf8Offset: end.utf8Offset)
200+
else {
201+
logger.error(
202+
"folding range failed to retrieve position of \(self.snapshot.uri.forLogging): \(start.utf8Offset)-\(end.utf8Offset)"
203+
)
204+
return .visitChildren
205+
}
206+
let range: FoldingRange
207+
if lineFoldingOnly {
208+
// Since the client cannot fold less than a single line, if the
209+
// fold would span 1 line there's no point in reporting it.
210+
guard end.line > start.line else {
211+
return .visitChildren
212+
}
213+
214+
// If the client only supports folding full lines, don't report
215+
// the end of the range since there's nothing they could do with it.
216+
range = FoldingRange(
217+
startLine: start.line,
218+
startUTF16Index: nil,
219+
endLine: end.line,
220+
endUTF16Index: nil,
221+
kind: kind
222+
)
223+
} else {
224+
range = FoldingRange(
225+
startLine: start.line,
226+
startUTF16Index: start.utf16index,
227+
endLine: end.line,
228+
endUTF16Index: end.utf16index,
229+
kind: kind
230+
)
231+
}
232+
ranges.insert(range)
233+
return .visitChildren
234+
}
235+
}
236+
237+
extension SwiftLanguageServer {
238+
public func foldingRange(_ req: FoldingRangeRequest) async throws -> [FoldingRange]? {
239+
let foldingRangeCapabilities = capabilityRegistry.clientCapabilities.textDocument?.foldingRange
240+
let snapshot = try self.documentManager.latestSnapshot(req.textDocument.uri)
241+
242+
let sourceFile = await syntaxTreeManager.syntaxTree(for: snapshot)
243+
244+
try Task.checkCancellation()
245+
246+
// If the limit is less than one, do nothing.
247+
if let limit = foldingRangeCapabilities?.rangeLimit, limit <= 0 {
248+
return []
249+
}
250+
251+
let rangeFinder = FoldingRangeFinder(
252+
snapshot: snapshot,
253+
rangeLimit: foldingRangeCapabilities?.rangeLimit,
254+
lineFoldingOnly: foldingRangeCapabilities?.lineFoldingOnly ?? false
255+
)
256+
rangeFinder.walk(sourceFile)
257+
let ranges = rangeFinder.finalize()
258+
259+
return ranges.sorted()
260+
}
261+
}

0 commit comments

Comments
 (0)