Skip to content

Commit abf9298

Browse files
committed
Add an indented(by:includeFirstLine:) method to SyntaxProtocol
1 parent 445486b commit abf9298

File tree

3 files changed

+273
-2
lines changed

3 files changed

+273
-2
lines changed

Sources/SwiftBasicFormat/Syntax+Extensions.swift

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,87 @@ extension TokenSyntax {
4242
return []
4343
}
4444
}
45+
46+
extension SyntaxProtocol {
47+
/// Indent this node’s lines by the provided amount.
48+
///
49+
/// - Parameter includeFirstLine: Whether the first token of this node should be indented.
50+
/// Pass `true` if you know that this node will be placed at the beginning of a line, even if its
51+
/// current leading trivia does not start with a newline (such as at the very start of a file).
52+
public func indented(by indentation: Trivia, indentFirstLine: Bool = false) -> Syntax {
53+
Indenter(indentation: indentation, indentFirstLine: indentFirstLine)
54+
.rewrite(self)
55+
}
56+
}
57+
58+
private class Indenter: SyntaxRewriter {
59+
private let indentation: Trivia
60+
private var shouldIndent: Bool
61+
62+
init(indentation: Trivia, indentFirstLine: Bool) {
63+
self.indentation = indentation
64+
self.shouldIndent = indentFirstLine
65+
}
66+
67+
private func indentIfNeeded() -> [TriviaPiece] {
68+
if shouldIndent {
69+
shouldIndent = false
70+
return indentation.pieces
71+
} else {
72+
return []
73+
}
74+
}
75+
76+
private func indentAfterNewlines(_ content: String) -> String {
77+
content.split(separator: "\n").joined(separator: "\n" + indentation.description)
78+
}
79+
80+
private func indent(_ trivia: Trivia, skipEmpty: Bool) -> Trivia {
81+
if skipEmpty, trivia.isEmpty { return trivia }
82+
83+
var result: [TriviaPiece] = []
84+
// most times, we won’t have anything to insert so this will
85+
// reserve enough space
86+
result.reserveCapacity(trivia.count)
87+
88+
for piece in trivia.pieces {
89+
result.append(contentsOf: indentIfNeeded())
90+
switch piece {
91+
case .newlines, .carriageReturns, .carriageReturnLineFeeds:
92+
shouldIndent = true
93+
// style decision: don’t indent totally blank lines
94+
result.append(piece)
95+
case .blockComment(let content):
96+
result.append(.blockComment(indentAfterNewlines(content)))
97+
case .docBlockComment(let content):
98+
result.append(.docBlockComment(indentAfterNewlines(content)))
99+
case .unexpectedText(let content):
100+
result.append(.unexpectedText(indentAfterNewlines(content)))
101+
default:
102+
result.append(piece)
103+
}
104+
}
105+
result.append(contentsOf: indentIfNeeded())
106+
return Trivia(pieces: result)
107+
}
108+
109+
override func visit(_ token: TokenSyntax) -> TokenSyntax {
110+
let indentedLeadingTrivia = indent(token.leadingTrivia, skipEmpty: false)
111+
112+
// compute this before indenting the trailing trivia since the
113+
// newline here is before the start of the trailing trivia (since
114+
// it is part of the string’s value)
115+
if case .stringSegment(let content) = token.tokenKind,
116+
let last = content.last,
117+
last.isNewline {
118+
shouldIndent = true
119+
}
120+
121+
return token
122+
.with(\.leadingTrivia, indentedLeadingTrivia)
123+
// source files as parsed can’t have anything requiring indentation
124+
// here, but it’s easy to do `.with(\.trailingTrivia, .newline)` so
125+
// we should still check if there’s something to indent.
126+
.with(\.trailingTrivia, indent(token.trailingTrivia, skipEmpty: true))
127+
}
128+
}

Sources/SwiftSyntax/generated/TriviaPieces.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ public enum TriviaPiece: Sendable {
3131
case carriageReturnLineFeeds(Int)
3232
/// A documentation block comment, starting with '/**' and ending with '*/'.
3333
case docBlockComment(String)
34-
/// A documentation line comment, starting with '///'.
34+
/// A documentation line comment, starting with '///' and excluding the trailing newline.
3535
case docLineComment(String)
3636
/// A form-feed 'f' character.
3737
case formfeeds(Int)
38-
/// A developer line comment, starting with '//'
38+
/// A developer line comment, starting with '//' and excluding the trailing newline.
3939
case lineComment(String)
4040
/// A newline '\n' character.
4141
case newlines(Int)
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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 SwiftBasicFormat
14+
import SwiftParser
15+
import SwiftSyntax
16+
@_spi(Testing) import SwiftSyntaxBuilder
17+
import XCTest
18+
import _SwiftSyntaxTestSupport
19+
20+
fileprivate func assertIndented(
21+
by indentation: Trivia = .tab,
22+
indentFirstLine: Bool = true,
23+
source: String,
24+
expected: String,
25+
file: StaticString = #filePath,
26+
line: UInt = #line
27+
) {
28+
assertStringsEqualWithDiff(Parser.parse(source: source).indented(by: indentation, indentFirstLine: indentFirstLine).description, expected, file: file, line: line)
29+
}
30+
31+
final class IndentTests: XCTestCase {
32+
func testNotIndented() {
33+
assertIndented(
34+
source: """
35+
func foo() {
36+
let bar = 2
37+
}
38+
""",
39+
expected: """
40+
\tfunc foo() {
41+
\t let bar = 2
42+
\t}
43+
"""
44+
)
45+
}
46+
47+
func testSingleLineComments() {
48+
assertIndented(
49+
source: """
50+
func foo() {
51+
// This is a comment
52+
// that extends onto
53+
// multiple lines \\
54+
let bar = 2
55+
// and another one
56+
}
57+
""",
58+
expected: """
59+
\tfunc foo() {
60+
\t // This is a comment
61+
\t // that extends onto
62+
\t // multiple lines \\
63+
\t let bar = 2
64+
\t // and another one
65+
\t}
66+
"""
67+
)
68+
}
69+
70+
func testMultiLineComments() {
71+
assertIndented(
72+
source: """
73+
func foo() {
74+
/* This is a multiline comment
75+
that extends onto
76+
multiple lines*/
77+
let bar = 2
78+
/* on a single line */
79+
let another = "Hello, world!" /* on a single line */
80+
}
81+
""",
82+
expected: """
83+
\tfunc foo() {
84+
\t /* This is a multiline comment
85+
\t that extends onto
86+
\tmultiple lines*/
87+
\t let bar = 2
88+
\t /* on a single line */
89+
\t let another = "Hello, world!" /* on a single line */
90+
\t}
91+
"""
92+
)
93+
}
94+
95+
func testMultiLineString() {
96+
assertIndented(
97+
source: #"""
98+
func foo() {
99+
let page = """
100+
<h1>Hello, world!</h1>
101+
<p>This is my web site</p>
102+
"""
103+
}
104+
"""#,
105+
expected: #"""
106+
\#tfunc foo() {
107+
\#t let page = """
108+
\#t <h1>Hello, world!</h1>
109+
\#t <p>This is my web site</p>
110+
\#t """
111+
\#t}
112+
"""#
113+
)
114+
}
115+
116+
func testIndented() {
117+
assertIndented(
118+
source: """
119+
func foo() {
120+
let bar = 2
121+
}
122+
""",
123+
expected: """
124+
\t func foo() {
125+
\t let bar = 2
126+
\t }
127+
"""
128+
)
129+
assertIndented(
130+
source: """
131+
\tfunc foo() {
132+
\t let bar = 2
133+
\t}
134+
""",
135+
expected: """
136+
\t\tfunc foo() {
137+
\t\t let bar = 2
138+
\t\t}
139+
"""
140+
)
141+
}
142+
143+
func testIndentBySpaces() {
144+
assertIndented(
145+
by: .spaces(4),
146+
source: """
147+
func foo() {
148+
let bar = 2
149+
}
150+
""",
151+
expected: """
152+
func foo() {
153+
let bar = 2
154+
}
155+
"""
156+
)
157+
}
158+
159+
func testSkipFirstLine() {
160+
assertIndented(
161+
indentFirstLine: false,
162+
source: """
163+
\nfunc foo() {
164+
let bar = 2
165+
}
166+
""",
167+
expected: """
168+
\n\tfunc foo() {
169+
\t let bar = 2
170+
\t}
171+
"""
172+
)
173+
assertIndented(
174+
indentFirstLine: false,
175+
source: """
176+
func foo() {
177+
let bar = 2
178+
}
179+
""",
180+
expected: """
181+
func foo() {
182+
\t let bar = 2
183+
\t}
184+
"""
185+
)
186+
}
187+
}

0 commit comments

Comments
 (0)