Skip to content

Commit 91444aa

Browse files
authored
Merge pull request #2843 from swiftlang/jed/indented
Add an 'indented' method to SyntaxProtocol
2 parents 2c271e5 + eb348e5 commit 91444aa

File tree

9 files changed

+313
-7
lines changed

9 files changed

+313
-7
lines changed

CodeGeneration/Sources/SyntaxSupport/Trivia.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public let TRIVIAS: [Trivia] = [
150150

151151
Trivia(
152152
name: "DocLineComment",
153-
comment: #"A documentation line comment, starting with '///'."#,
153+
comment: #"A documentation line comment, starting with '///' and excluding the trailing newline."#,
154154
isComment: true
155155
),
156156

@@ -168,7 +168,7 @@ public let TRIVIAS: [Trivia] = [
168168

169169
Trivia(
170170
name: "LineComment",
171-
comment: #"A developer line comment, starting with '//'"#,
171+
comment: #"A developer line comment, starting with '//' and excluding the trailing newline."#,
172172
isComment: true
173173
),
174174

Release Notes/601.md

+8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
- Added a new library `SwiftIfConfig`.
2323
- Description: This new library provides facilities for evaluating `#if` conditions and determining which regions of a syntax tree are active according to a given build configuration.
2424
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/1816
25+
26+
- `SwiftBasicFormat` adds a method `indented(by:)` to all syntax node types.
27+
- Description: This method indents a node’s contents using a provided piece of `Trivia`, optionally including the first line.
28+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2843
2529

2630
## API Behavior Changes
2731

@@ -44,6 +48,10 @@
4448
- Description: `ClosureCaptureSyntax` now has an `initializer` property instead of `equal` and `expression`. Additionally, the `name` property is no longer optional.
4549
- Pull request: https://github.com/swiftlang/swift-syntax/pull/2763
4650

51+
- `Indenter` in `SwiftSyntaxBuilder` has been deprecated in favor of the new `indented(by:)` in `SwiftBasicFormat`.
52+
- Description: Indenting is really more of a formatting operation than a syntax-building operation. Additionally, the `indented(by:)` method is more intuitive to use than a `SyntaxRewriter`. Aside from `BasicFormat`, there are no other public `SyntaxRewriter` classes in the package.
53+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2843
54+
4755
## API-Incompatible Changes
4856

4957
- Moved `Radix` and `IntegerLiteralExprSyntax.radix` from `SwiftRefactor` to `SwiftSyntax`.

Sources/SwiftBasicFormat/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
add_swift_syntax_library(SwiftBasicFormat
1010
BasicFormat.swift
11+
Indenter.swift
1112
InferIndentation.swift
1213
Syntax+Extensions.swift
1314
SyntaxProtocol+Formatted.swift
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 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+
#if swift(>=6)
14+
public import SwiftSyntax
15+
#else
16+
import SwiftSyntax
17+
#endif
18+
19+
extension SyntaxProtocol {
20+
/// Indent this node’s lines by the provided amount.
21+
///
22+
/// - Parameter indentFirstLine: Whether the first token of this node should be indented.
23+
/// Pass `true` if you know that this node will be placed at the beginning of a line, even if its
24+
/// current leading trivia does not start with a newline (such as at the very start of a file).
25+
public func indented(by indentation: Trivia, indentFirstLine: Bool = false) -> Self {
26+
Indenter(indentation: indentation, indentFirstLine: indentFirstLine)
27+
.rewrite(self)
28+
.cast(Self.self)
29+
}
30+
}
31+
32+
private class Indenter: SyntaxRewriter {
33+
private let indentation: Trivia
34+
private var shouldIndent: Bool
35+
36+
init(indentation: Trivia, indentFirstLine: Bool) {
37+
self.indentation = indentation
38+
self.shouldIndent = indentFirstLine
39+
}
40+
41+
private func indentationIfNeeded() -> [TriviaPiece] {
42+
if shouldIndent {
43+
shouldIndent = false
44+
return indentation.pieces
45+
} else {
46+
return []
47+
}
48+
}
49+
50+
private func indentAfterNewlines(_ content: String) -> String {
51+
content.split(separator: "\n").joined(separator: "\n" + indentation.description)
52+
}
53+
54+
private func indent(_ trivia: Trivia, skipEmpty: Bool) -> Trivia {
55+
if skipEmpty, trivia.isEmpty { return trivia }
56+
57+
var result: [TriviaPiece] = []
58+
// most times, we won’t have anything to insert so this will
59+
// reserve enough space
60+
result.reserveCapacity(trivia.count)
61+
62+
for piece in trivia.pieces {
63+
result.append(contentsOf: indentationIfNeeded())
64+
switch piece {
65+
case .newlines, .carriageReturns, .carriageReturnLineFeeds:
66+
shouldIndent = true
67+
// style decision: don’t indent totally blank lines
68+
result.append(piece)
69+
case .blockComment(let content):
70+
result.append(.blockComment(indentAfterNewlines(content)))
71+
case .docBlockComment(let content):
72+
result.append(.docBlockComment(indentAfterNewlines(content)))
73+
case .unexpectedText(let content):
74+
result.append(.unexpectedText(indentAfterNewlines(content)))
75+
default:
76+
result.append(piece)
77+
}
78+
}
79+
result.append(contentsOf: indentationIfNeeded())
80+
return Trivia(pieces: result)
81+
}
82+
83+
override func visit(_ token: TokenSyntax) -> TokenSyntax {
84+
let indentedLeadingTrivia = indent(token.leadingTrivia, skipEmpty: false)
85+
86+
// compute this before indenting the trailing trivia since the
87+
// newline here is before the start of the trailing trivia (since
88+
// it is part of the string’s value)
89+
if case .stringSegment(let content) = token.tokenKind,
90+
let last = content.last,
91+
last.isNewline
92+
{
93+
shouldIndent = true
94+
}
95+
96+
return
97+
token
98+
.with(\.leadingTrivia, indentedLeadingTrivia)
99+
// source files as parsed can’t have anything requiring indentation
100+
// here, but it’s easy to do `.with(\.trailingTrivia, .newline)` so
101+
// we should still check if there’s something to indent.
102+
.with(\.trailingTrivia, indent(token.trailingTrivia, skipEmpty: true))
103+
}
104+
}

Sources/SwiftSyntax/generated/TriviaPieces.swift

+2-2
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)

Sources/SwiftSyntaxBuilder/Indenter.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import SwiftSyntax
1717
#endif
1818

1919
extension Trivia {
20-
func indented(indentation: Trivia) -> Trivia {
20+
fileprivate func indented(indentation: Trivia) -> Trivia {
2121
let mappedPieces = self.flatMap { (piece) -> [TriviaPiece] in
2222
if piece.isNewline {
2323
return [piece] + indentation.pieces
@@ -30,6 +30,7 @@ extension Trivia {
3030
}
3131

3232
/// Adds a given amount of indentation after every newline in a syntax tree.
33+
@available(*, deprecated, message: "Use 'SyntaxProtocol.indented(by:)' from SwiftBasicFormat instead")
3334
public class Indenter: SyntaxRewriter {
3435
let indentation: Trivia
3536

Sources/SwiftSyntaxBuilder/Syntax+StringInterpolation.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ extension SyntaxStringInterpolation: StringInterpolationProtocol {
8282
let startIndex = sourceText.count
8383
let indentedNode: Node
8484
if let lastIndentation {
85-
indentedNode = Indenter.indent(node, indentation: lastIndentation)
85+
indentedNode = node.indented(by: lastIndentation)
8686
} else {
8787
indentedNode = node
8888
}

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1393,7 +1393,7 @@ private extension AccessorBlockSyntax {
13931393
accessorSpecifier: .keyword(.get, leadingTrivia: .newline + baseIndentation, trailingTrivia: .space),
13941394
body: CodeBlockSyntax(
13951395
leftBrace: .leftBraceToken(),
1396-
statements: Indenter.indent(getter, indentation: indentationWidth),
1396+
statements: getter.indented(by: indentationWidth),
13971397
rightBrace: .rightBraceToken(leadingTrivia: .newline + baseIndentation)
13981398
)
13991399
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 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(
29+
Parser.parse(source: source).indented(by: indentation, indentFirstLine: indentFirstLine).description,
30+
expected,
31+
file: file,
32+
line: line
33+
)
34+
}
35+
36+
final class IndentTests: XCTestCase {
37+
func testNotIndented() {
38+
assertIndented(
39+
source: """
40+
func foo() {
41+
let bar = 2
42+
}
43+
""",
44+
expected: """
45+
\tfunc foo() {
46+
\t let bar = 2
47+
\t}
48+
"""
49+
)
50+
}
51+
52+
func testSingleLineComments() {
53+
assertIndented(
54+
source: """
55+
func foo() {
56+
// This is a comment
57+
// that extends onto
58+
// multiple lines \\
59+
let bar = 2
60+
// and another one
61+
}
62+
""",
63+
expected: """
64+
\tfunc foo() {
65+
\t // This is a comment
66+
\t // that extends onto
67+
\t // multiple lines \\
68+
\t let bar = 2
69+
\t // and another one
70+
\t}
71+
"""
72+
)
73+
}
74+
75+
func testMultiLineComments() {
76+
assertIndented(
77+
source: """
78+
func foo() {
79+
/* This is a multiline comment
80+
that extends onto
81+
multiple lines*/
82+
let bar = 2
83+
/* on a single line */
84+
let another = "Hello, world!" /* on a single line */
85+
}
86+
""",
87+
expected: """
88+
\tfunc foo() {
89+
\t /* This is a multiline comment
90+
\t that extends onto
91+
\tmultiple lines*/
92+
\t let bar = 2
93+
\t /* on a single line */
94+
\t let another = "Hello, world!" /* on a single line */
95+
\t}
96+
"""
97+
)
98+
}
99+
100+
func testMultiLineString() {
101+
assertIndented(
102+
source: #"""
103+
func foo() {
104+
let page = """
105+
<h1>Hello, world!</h1>
106+
<p>This is my web site</p>
107+
"""
108+
}
109+
"""#,
110+
expected: #"""
111+
\#tfunc foo() {
112+
\#t let page = """
113+
\#t <h1>Hello, world!</h1>
114+
\#t <p>This is my web site</p>
115+
\#t """
116+
\#t}
117+
"""#
118+
)
119+
}
120+
121+
func testIndented() {
122+
assertIndented(
123+
source: """
124+
func foo() {
125+
let bar = 2
126+
}
127+
""",
128+
expected: """
129+
\t func foo() {
130+
\t let bar = 2
131+
\t }
132+
"""
133+
)
134+
assertIndented(
135+
source: """
136+
\tfunc foo() {
137+
\t let bar = 2
138+
\t}
139+
""",
140+
expected: """
141+
\t\tfunc foo() {
142+
\t\t let bar = 2
143+
\t\t}
144+
"""
145+
)
146+
}
147+
148+
func testIndentBySpaces() {
149+
assertIndented(
150+
by: .spaces(4),
151+
source: """
152+
func foo() {
153+
let bar = 2
154+
}
155+
""",
156+
expected: """
157+
func foo() {
158+
let bar = 2
159+
}
160+
"""
161+
)
162+
}
163+
164+
func testSkipFirstLine() {
165+
assertIndented(
166+
indentFirstLine: false,
167+
source: """
168+
\nfunc foo() {
169+
let bar = 2
170+
}
171+
""",
172+
expected: """
173+
\n\tfunc foo() {
174+
\t let bar = 2
175+
\t}
176+
"""
177+
)
178+
assertIndented(
179+
indentFirstLine: false,
180+
source: """
181+
func foo() {
182+
let bar = 2
183+
}
184+
""",
185+
expected: """
186+
func foo() {
187+
\t let bar = 2
188+
\t}
189+
"""
190+
)
191+
}
192+
}

0 commit comments

Comments
 (0)