Skip to content

Commit 1139c22

Browse files
committed
Redo the Folding Range Request on top of the Syntax Tree
Building upon the infrastructure that requests the lexical structure of the document from SwiftSyntax, redo the folding ranges request using the tree directly. This corrects a number of inconsistencies in the tests mostly due to incorrect SourceLocations in the semantic ASTs and the line comment merging logic.
1 parent 43c072d commit 1139c22

File tree

2 files changed

+174
-117
lines changed

2 files changed

+174
-117
lines changed

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 161 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -978,143 +978,200 @@ extension SwiftLanguageServer {
978978
}
979979

980980
public func foldingRange(_ req: Request<FoldingRangeRequest>) {
981-
let keys = self.keys
982-
983981
queue.async {
984982
guard let snapshot = self.documentManager.latestSnapshot(req.params.textDocument.uri) else {
985983
log("failed to find snapshot for url \(req.params.textDocument.uri)")
986984
req.reply(nil)
987985
return
988986
}
989987

990-
let helperDocumentName = "FoldingRanges:" + snapshot.document.uri.pseudoPath
991-
let skreq = SKDRequestDictionary(sourcekitd: self.sourcekitd)
992-
skreq[keys.request] = self.requests.editor_open
993-
skreq[keys.name] = helperDocumentName
994-
skreq[keys.sourcetext] = snapshot.text
995-
skreq[keys.syntactic_only] = 1
988+
guard let sourceFile = snapshot.tokens.syntaxTree else {
989+
log("no lexical structure available for url \(req.params.textDocument.uri)")
990+
req.reply(nil)
991+
return
992+
}
996993

997-
let handle = self.sourcekitd.send(skreq, self.queue) { [weak self] result in
998-
guard let self = self else { return }
994+
final class FoldingRangeFinder: SyntaxVisitor {
995+
private let snapshot: DocumentSnapshot
996+
/// Some ranges might occur multiple times.
997+
/// E.g. for `print("hi")`, `"hi"` is both the range of all call arguments and the range the first argument in the call.
998+
/// It doesn't make sense to report them multiple times, so use a `Set` here.
999+
private var ranges: Set<FoldingRange>
1000+
/// The client-imposed limit on the number of folding ranges it would
1001+
/// prefer to recieve from the LSP server. If the value is `nil`, there
1002+
/// is no preset limit.
1003+
private var rangeLimit: Int?
1004+
/// If `true`, the client is only capable of folding entire lines. If
1005+
/// `false` the client can handle folding ranges.
1006+
private var lineFoldingOnly: Bool
1007+
1008+
init(snapshot: DocumentSnapshot, rangeLimit: Int?, lineFoldingOnly: Bool) {
1009+
self.snapshot = snapshot
1010+
self.ranges = []
1011+
self.rangeLimit = rangeLimit
1012+
self.lineFoldingOnly = lineFoldingOnly
1013+
super.init(viewMode: .sourceAccurate)
1014+
}
9991015

1000-
defer {
1001-
let closeHelperReq = SKDRequestDictionary(sourcekitd: self.sourcekitd)
1002-
closeHelperReq[keys.request] = self.requests.editor_close
1003-
closeHelperReq[keys.name] = helperDocumentName
1004-
_ = self.sourcekitd.send(closeHelperReq, .global(qos: .utility), reply: { _ in })
1016+
override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind {
1017+
// Index comments, so we need to see at least '/*', or '//'.
1018+
if node.leadingTriviaLength.utf8Length > 2 {
1019+
self.addTrivia(from: node, node.leadingTrivia)
1020+
}
1021+
1022+
if node.trailingTriviaLength.utf8Length > 2 {
1023+
self.addTrivia(from: node, node.trailingTrivia)
1024+
}
1025+
1026+
return .visitChildren
10051027
}
10061028

1007-
guard let dict = result.success else {
1008-
req.reply(.failure(ResponseError(result.failure!)))
1009-
return
1029+
private func addTrivia(from node: TokenSyntax, _ trivia: Trivia) {
1030+
var start = node.position.utf8Offset
1031+
var lineCommentStart: Int? = nil
1032+
func flushLineComment(_ offset: Int = 0) {
1033+
if let lineCommentStart = lineCommentStart {
1034+
_ = self.addFoldingRange(
1035+
start: lineCommentStart,
1036+
end: start + offset,
1037+
kind: .comment)
1038+
}
1039+
lineCommentStart = nil
1040+
}
1041+
1042+
for piece in node.leadingTrivia {
1043+
defer { start += piece.sourceLength.utf8Length }
1044+
switch piece {
1045+
case .blockComment(_):
1046+
flushLineComment()
1047+
_ = self.addFoldingRange(
1048+
start: start,
1049+
end: start + piece.sourceLength.utf8Length,
1050+
kind: .comment)
1051+
case .docBlockComment(_):
1052+
flushLineComment()
1053+
_ = self.addFoldingRange(
1054+
start: start,
1055+
end: start + piece.sourceLength.utf8Length,
1056+
kind: .comment)
1057+
case .lineComment(_), .docLineComment(_):
1058+
if lineCommentStart == nil {
1059+
lineCommentStart = start
1060+
}
1061+
case .newlines(1), .carriageReturns(1), .spaces(_), .tabs(_):
1062+
if lineCommentStart != nil {
1063+
continue
1064+
} else {
1065+
flushLineComment()
1066+
}
1067+
default:
1068+
flushLineComment()
1069+
continue
1070+
}
1071+
}
1072+
1073+
flushLineComment()
10101074
}
10111075

1012-
guard let syntaxMap: SKDResponseArray = dict[self.keys.syntaxmap],
1013-
let substructure: SKDResponseArray = dict[self.keys.substructure] else {
1014-
return req.reply([])
1076+
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
1077+
return self.addFoldingRange(
1078+
start: node.statements.position.utf8Offset,
1079+
end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset)
10151080
}
10161081

1017-
/// Some ranges might occur multiple times.
1018-
/// E.g. for `print("hi")`, `"hi"` is both the range of all call arguments and the range the first argument in the call.
1019-
/// It doesn't make sense to report them multiple times, so use a `Set` here.
1020-
var ranges: Set<FoldingRange> = []
1082+
override func visit(_ node: MemberDeclBlockSyntax) -> SyntaxVisitorContinueKind {
1083+
return self.addFoldingRange(
1084+
start: node.members.position.utf8Offset,
1085+
end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset)
1086+
}
10211087

1022-
var hasReachedLimit: Bool {
1023-
let capabilities = self.clientCapabilities.textDocument?.foldingRange
1024-
guard let rangeLimit = capabilities?.rangeLimit else {
1025-
return false
1026-
}
1027-
return ranges.count >= rangeLimit
1088+
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
1089+
return self.addFoldingRange(
1090+
start: node.statements.position.utf8Offset,
1091+
end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset)
10281092
}
10291093

1030-
// If the limit is less than one, do nothing.
1031-
guard hasReachedLimit == false else {
1032-
req.reply([])
1033-
return
1094+
override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind {
1095+
return self.addFoldingRange(
1096+
start: node.accessors.position.utf8Offset,
1097+
end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset)
10341098
}
10351099

1036-
// Merge successive comments into one big comment by adding their lengths.
1037-
var currentComment: (offset: Int, length: Int)? = nil
1100+
override func visit(_ node: SwitchStmtSyntax) -> SyntaxVisitorContinueKind {
1101+
return self.addFoldingRange(
1102+
start: node.cases.position.utf8Offset,
1103+
end: node.rightBrace.positionAfterSkippingLeadingTrivia.utf8Offset)
1104+
}
10381105

1039-
syntaxMap.forEach { _, value in
1040-
if let kind: sourcekitd_uid_t = value[self.keys.kind],
1041-
kind.isCommentKind(self.values),
1042-
let offset: Int = value[self.keys.offset],
1043-
let length: Int = value[self.keys.length]
1044-
{
1045-
if let comment = currentComment, comment.offset + comment.length == offset {
1046-
currentComment!.length += length
1047-
return true
1048-
}
1049-
if let comment = currentComment {
1050-
self.addFoldingRange(offset: comment.offset, length: comment.length, kind: .comment, in: snapshot, toSet: &ranges)
1051-
}
1052-
currentComment = (offset: offset, length: length)
1053-
}
1054-
return hasReachedLimit == false
1106+
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
1107+
return self.addFoldingRange(
1108+
start: node.argumentList.position.utf8Offset,
1109+
end: node.argumentList.endPosition.utf8Offset)
10551110
}
10561111

1057-
// Add the last stored comment.
1058-
if let comment = currentComment, hasReachedLimit == false {
1059-
self.addFoldingRange(offset: comment.offset, length: comment.length, kind: .comment, in: snapshot, toSet: &ranges)
1060-
currentComment = nil
1112+
override func visit(_ node: SubscriptExprSyntax) -> SyntaxVisitorContinueKind {
1113+
return self.addFoldingRange(
1114+
start: node.argumentList.position.utf8Offset,
1115+
end: node.argumentList.endPosition.utf8Offset)
10611116
}
10621117

1063-
var structureStack: [SKDResponseArray] = [substructure]
1064-
while !hasReachedLimit, let substructure = structureStack.popLast() {
1065-
substructure.forEach { _, value in
1066-
if let offset: Int = value[self.keys.bodyoffset],
1067-
let length: Int = value[self.keys.bodylength],
1068-
length > 0
1069-
{
1070-
self.addFoldingRange(offset: offset, length: length, in: snapshot, toSet: &ranges)
1071-
if hasReachedLimit {
1072-
return false
1073-
}
1074-
}
1075-
if let substructure: SKDResponseArray = value[self.keys.substructure] {
1076-
structureStack.append(substructure)
1118+
__consuming func finalize() -> Set<FoldingRange> {
1119+
return self.ranges
1120+
}
1121+
1122+
private func addFoldingRange(start: Int, end: Int, kind: FoldingRangeKind? = nil) -> SyntaxVisitorContinueKind {
1123+
if let limit = self.rangeLimit, self.ranges.count >= limit {
1124+
return .skipChildren
1125+
}
1126+
1127+
guard let start: Position = snapshot.positionOf(utf8Offset: start),
1128+
let end: Position = snapshot.positionOf(utf8Offset: end) else {
1129+
log("folding range failed to retrieve position of \(snapshot.document.uri): \(start)-\(end)", level: .warning)
1130+
return .visitChildren
1131+
}
1132+
let range: FoldingRange
1133+
if lineFoldingOnly {
1134+
// Since the client cannot fold less than a single line, if the
1135+
// fold would span 1 line there's no point in reporting it.
1136+
guard end.line > start.line else {
1137+
return .visitChildren
10771138
}
1078-
return true
1139+
1140+
// If the client only supports folding full lines, don't report
1141+
// the end of the range since there's nothing they could do with it.
1142+
range = FoldingRange(startLine: start.line,
1143+
startUTF16Index: nil,
1144+
endLine: end.line,
1145+
endUTF16Index: nil,
1146+
kind: kind)
1147+
} else {
1148+
range = FoldingRange(startLine: start.line,
1149+
startUTF16Index: start.utf16index,
1150+
endLine: end.line,
1151+
endUTF16Index: end.utf16index,
1152+
kind: kind)
10791153
}
1154+
ranges.insert(range)
1155+
return .visitChildren
10801156
}
1081-
1082-
req.reply(ranges.sorted())
10831157
}
10841158

1085-
// FIXME: cancellation
1086-
_ = handle
1087-
}
1088-
}
1089-
1090-
func addFoldingRange(offset: Int, length: Int, kind: FoldingRangeKind? = nil, in snapshot: DocumentSnapshot, toSet ranges: inout Set<FoldingRange>) {
1091-
guard let start: Position = snapshot.positionOf(utf8Offset: offset),
1092-
let end: Position = snapshot.positionOf(utf8Offset: offset + length) else {
1093-
log("folding range failed to retrieve position of \(snapshot.document.uri): \(offset)-\(offset + length)", level: .warning)
1094-
return
1095-
}
1096-
let capabilities = clientCapabilities.textDocument?.foldingRange
1097-
let range: FoldingRange
1098-
// If the client only supports folding full lines, ignore the end character's line.
1099-
if capabilities?.lineFoldingOnly == true {
1100-
let lastLineToFold = end.line - 1
1101-
if lastLineToFold <= start.line {
1159+
let capabilities = self.clientCapabilities.textDocument?.foldingRange
1160+
// If the limit is less than one, do nothing.
1161+
if let limit = capabilities?.rangeLimit, limit <= 0 {
1162+
req.reply([])
11021163
return
1103-
} else {
1104-
range = FoldingRange(startLine: start.line,
1105-
startUTF16Index: nil,
1106-
endLine: lastLineToFold,
1107-
endUTF16Index: nil,
1108-
kind: kind)
11091164
}
1110-
} else {
1111-
range = FoldingRange(startLine: start.line,
1112-
startUTF16Index: start.utf16index,
1113-
endLine: end.line,
1114-
endUTF16Index: end.utf16index,
1115-
kind: kind)
1165+
1166+
let rangeFinder = FoldingRangeFinder(
1167+
snapshot: snapshot,
1168+
rangeLimit: capabilities?.rangeLimit,
1169+
lineFoldingOnly: capabilities?.lineFoldingOnly ?? false)
1170+
rangeFinder.walk(sourceFile)
1171+
let ranges = rangeFinder.finalize()
1172+
1173+
req.reply(ranges.sorted())
11161174
}
1117-
ranges.insert(range)
11181175
}
11191176

11201177
public func codeAction(_ req: Request<CodeActionRequest>) {

Tests/SourceKitLSPTests/FoldingRangeTests.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,17 @@ final class FoldingRangeTests: XCTestCase {
3939
let ranges = try withExtendedLifetime(ws) { try ws.sk.sendSync(request) }
4040

4141
XCTAssertEqual(ranges, [
42-
FoldingRange(startLine: 0, startUTF16Index: 0, endLine: 2, endUTF16Index: 0, kind: .comment),
42+
FoldingRange(startLine: 0, startUTF16Index: 0, endLine: 1, endUTF16Index: 18, kind: .comment),
4343
FoldingRange(startLine: 3, startUTF16Index: 0, endLine: 13, endUTF16Index: 2, kind: .comment),
4444
FoldingRange(startLine: 14, startUTF16Index: 10, endLine: 27, endUTF16Index: 0, kind: nil),
45-
FoldingRange(startLine: 15, startUTF16Index: 2, endLine: 16, endUTF16Index: 0, kind: .comment),
46-
FoldingRange(startLine: 16, startUTF16Index: 2, endLine: 17, endUTF16Index: 0, kind: .comment),
45+
FoldingRange(startLine: 15, startUTF16Index: 2, endLine: 17, endUTF16Index: 2, kind: .comment),
4746
FoldingRange(startLine: 17, startUTF16Index: 2, endLine: 19, endUTF16Index: 4, kind: .comment),
4847
FoldingRange(startLine: 22, startUTF16Index: 21, endLine: 25, endUTF16Index: 2, kind: nil),
49-
FoldingRange(startLine: 23, startUTF16Index: 22, endLine: 23, endUTF16Index: 30, kind: nil),
48+
FoldingRange(startLine: 23, startUTF16Index: 23, endLine: 23, endUTF16Index: 30, kind: nil),
5049
FoldingRange(startLine: 26, startUTF16Index: 2, endLine: 26, endUTF16Index: 10, kind: .comment),
51-
FoldingRange(startLine: 29, startUTF16Index: 0, endLine: 32, endUTF16Index: 0, kind: .comment),
52-
FoldingRange(startLine: 33, startUTF16Index: 0, endLine: 36, endUTF16Index: 0, kind: .comment),
53-
FoldingRange(startLine: 37, startUTF16Index: 0, endLine: 38, endUTF16Index: 0, kind: .comment),
50+
FoldingRange(startLine: 29, startUTF16Index: 0, endLine: 31, endUTF16Index: 2, kind: .comment),
51+
FoldingRange(startLine: 33, startUTF16Index: 0, endLine: 35, endUTF16Index: 2, kind: .comment),
52+
FoldingRange(startLine: 37, startUTF16Index: 0, endLine: 37, endUTF16Index: 32, kind: .comment),
5453
FoldingRange(startLine: 39, startUTF16Index: 0, endLine: 39, endUTF16Index: 11, kind: .comment),
5554
])
5655
}
@@ -66,10 +65,11 @@ final class FoldingRangeTests: XCTestCase {
6665

6766
XCTAssertEqual(ranges, [
6867
FoldingRange(startLine: 0, endLine: 1, kind: .comment),
69-
FoldingRange(startLine: 3, endLine: 12, kind: .comment),
70-
FoldingRange(startLine: 14, endLine: 26, kind: nil),
71-
FoldingRange(startLine: 17, endLine: 18, kind: .comment),
72-
FoldingRange(startLine: 22, endLine: 24, kind: nil),
68+
FoldingRange(startLine: 3, endLine: 13, kind: .comment),
69+
FoldingRange(startLine: 14, endLine: 27, kind: nil),
70+
FoldingRange(startLine: 15, endLine: 17, kind: .comment),
71+
FoldingRange(startLine: 17, endLine: 19, kind: .comment),
72+
FoldingRange(startLine: 22, endLine: 25, kind: nil),
7373
FoldingRange(startLine: 29, endLine: 31, kind: .comment),
7474
FoldingRange(startLine: 33, endLine: 35, kind: .comment),
7575
])
@@ -90,8 +90,8 @@ final class FoldingRangeTests: XCTestCase {
9090
try performTest(withRangeLimit: -100, expecting: 0)
9191
try performTest(withRangeLimit: 0, expecting: 0)
9292
try performTest(withRangeLimit: 4, expecting: 4)
93-
try performTest(withRangeLimit: 5000, expecting: 13)
94-
try performTest(withRangeLimit: nil, expecting: 13)
93+
try performTest(withRangeLimit: 5000, expecting: 12)
94+
try performTest(withRangeLimit: nil, expecting: 12)
9595
}
9696

9797
func testNoRanges() throws {

0 commit comments

Comments
 (0)