Skip to content

Commit 6154447

Browse files
authored
Add option to disable line wrapping within string interpolation (#2059)
1 parent 21a019d commit 6154447

11 files changed

+71
-21
lines changed

Rules.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3289,7 +3289,8 @@ Option | Description
32893289
`--maxwidth` | Maximum length of a line before wrapping. defaults to "none"
32903290
`--nowrapoperators` | Comma-delimited list of operators that shouldn't be wrapped
32913291
`--assetliterals` | Color/image literal width. "actual-width" or "visual-width"
3292-
`--wrapternary` | Wrap ternary operators: "default", "before-operators"
3292+
`--wrapternary` | Wrap ternary operators: "default" (wrap if needed), "before-operators"
3293+
`--wrapstringinterpolation` | Wrap string interpolation: "default" (wrap if needed), "preserve"
32933294

32943295
## wrapArguments
32953296

@@ -3306,6 +3307,7 @@ Option | Description
33063307
`--wrapconditions` | Wrap conditions: "before-first", "after-first", "preserve"
33073308
`--wraptypealiases` | Wrap typealiases: "before-first", "after-first", "preserve"
33083309
`--wrapeffects` | Wrap effects: "if-multiline", "never", "preserve"
3310+
`--wrapstringinterpolation` | Wrap string interpolation: "default" (wrap if needed), "preserve"
33093311

33103312
<details>
33113313
<summary>Examples</summary>

Sources/Arguments.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
import Foundation
3333

3434
extension Options {
35-
static let maxArgumentNameLength = 16
35+
// TODO: Consider removing max option name lengths
36+
static let maxArgumentNameLength = 30
3637

3738
init(_ args: [String: String], in directory: String) throws {
3839
fileOptions = try fileOptionsFor(args, in: directory)

Sources/FormattingHelpers.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ extension Formatter {
590590
}
591591
guard mode != .disabled, let firstIdentifierIndex =
592592
index(of: .nonSpaceOrCommentOrLinebreak, after: i),
593-
!isInSingleLineStringLiteral(at: i)
593+
!isInStringLiteralWithWrappingDisabled(at: i)
594594
else {
595595
lastIndex = i
596596
return
@@ -858,7 +858,7 @@ extension Formatter {
858858
forEach(.operator("?", .infix)) { conditionIndex, _ in
859859
guard options.wrapTernaryOperators != .default,
860860
let expressionStartIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: conditionIndex),
861-
!isInSingleLineStringLiteral(at: conditionIndex)
861+
!isInStringLiteralWithWrappingDisabled(at: conditionIndex)
862862
else { return }
863863

864864
// Find the : operator that separates the true and false branches
@@ -1026,7 +1026,7 @@ extension Formatter {
10261026
func wrapStatementBody(at i: Int) {
10271027
assert(token(at: i) == .startOfScope("{"))
10281028

1029-
guard !isInSingleLineStringLiteral(at: i) else {
1029+
guard !isInStringLiteralWithWrappingDisabled(at: i) else {
10301030
return
10311031
}
10321032

@@ -1088,6 +1088,26 @@ extension Formatter {
10881088
insertSpace(currentIndentForLine(at: i), at: closingBraceIndex)
10891089
}
10901090

1091+
/// Returns true if the token at the specified index is inside a single-line string literal (including inside an interpolation),
1092+
/// which should never be wrapped, or in any string literal when string interpolation wrapping is disabled.
1093+
func isInStringLiteralWithWrappingDisabled(at i: Int) -> Bool {
1094+
var i = i
1095+
while let startOfScope = startOfScope(at: i) {
1096+
i = startOfScope
1097+
1098+
if tokens[startOfScope].isStringDelimiter {
1099+
if options.wrapStringInterpolation == .preserve {
1100+
return true
1101+
} else if !tokens[startOfScope].isMultilineStringDelimiter {
1102+
// Single line strings can never have line break
1103+
return true
1104+
}
1105+
}
1106+
}
1107+
1108+
return false
1109+
}
1110+
10911111
func removeParen(at index: Int) {
10921112
func tokenOutsideParenRequiresSpacing(at index: Int) -> Bool {
10931113
guard let token = token(at: index) else { return false }

Sources/OptionDescriptor.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,9 +604,15 @@ struct _Descriptors {
604604
let wrapTernaryOperators = OptionDescriptor(
605605
argumentName: "wrapternary",
606606
displayName: "Wrap Ternary Operators",
607-
help: "Wrap ternary operators: \"default\", \"before-operators\"",
607+
help: "Wrap ternary operators: \"default\" (wrap if needed), \"before-operators\"",
608608
keyPath: \.wrapTernaryOperators
609609
)
610+
let wrapStringInterpolation = OptionDescriptor(
611+
argumentName: "wrapstringinterpolation",
612+
displayName: "Wrap String Interpolation",
613+
help: "Wrap string interpolation: \"default\" (wrap if needed), \"preserve\"",
614+
keyPath: \.wrapStringInterpolation
615+
)
610616
let closingParenPosition = OptionDescriptor(
611617
argumentName: "closingparen",
612618
displayName: "Closing Paren Position",

Sources/Options.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,14 @@ public enum TernaryOperatorWrapMode: String, CaseIterable {
164164
case beforeOperators = "before-operators"
165165
}
166166

167+
public enum StringInterpolationWrapMode: String, CaseIterable {
168+
/// Wraps string interpolation if necessary based on the max line length
169+
case `default`
170+
/// Preserve existing wrapping for string interpolations,
171+
/// and don't insert line breaks.
172+
case preserve
173+
}
174+
167175
/// Whether or not to remove `-> Void` from closures
168176
public enum ClosureVoidReturn: String, CaseIterable {
169177
case remove
@@ -653,6 +661,7 @@ public struct FormatOptions: CustomStringConvertible {
653661
public var wrapReturnType: WrapReturnType
654662
public var wrapConditions: WrapMode
655663
public var wrapTernaryOperators: TernaryOperatorWrapMode
664+
public var wrapStringInterpolation: StringInterpolationWrapMode
656665
public var uppercaseHex: Bool
657666
public var uppercaseExponent: Bool
658667
public var decimalGrouping: Grouping
@@ -783,6 +792,7 @@ public struct FormatOptions: CustomStringConvertible {
783792
wrapReturnType: WrapReturnType = .preserve,
784793
wrapConditions: WrapMode = .preserve,
785794
wrapTernaryOperators: TernaryOperatorWrapMode = .default,
795+
wrapStringInterpolation: StringInterpolationWrapMode = .default,
786796
uppercaseHex: Bool = true,
787797
uppercaseExponent: Bool = false,
788798
decimalGrouping: Grouping = .group(3, 6),
@@ -903,6 +913,7 @@ public struct FormatOptions: CustomStringConvertible {
903913
self.wrapReturnType = wrapReturnType
904914
self.wrapConditions = wrapConditions
905915
self.wrapTernaryOperators = wrapTernaryOperators
916+
self.wrapStringInterpolation = wrapStringInterpolation
906917
self.uppercaseHex = uppercaseHex
907918
self.uppercaseExponent = uppercaseExponent
908919
self.decimalGrouping = decimalGrouping

Sources/ParsingHelpers.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,18 +1078,6 @@ extension Formatter {
10781078
}
10791079
}
10801080

1081-
/// Returns true if the token at the specified index is inside a single-line string literal (including inside an interpolation)
1082-
func isInSingleLineStringLiteral(at i: Int) -> Bool {
1083-
var i = i
1084-
while let token = token(at: i), !token.isLinebreak {
1085-
if token.isStringDelimiter {
1086-
return !token.isMultilineStringDelimiter
1087-
}
1088-
i -= 1
1089-
}
1090-
return false
1091-
}
1092-
10931081
/// Crude check to detect if code is inside a Result Builder
10941082
/// Note: this will produce false positives for any init that takes a closure
10951083
func isInResultBuilder(at i: Int) -> Bool {

Sources/Rules/Wrap.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Foundation
1111
public extension FormatRule {
1212
static let wrap = FormatRule(
1313
help: "Wrap lines that exceed the specified maximum width.",
14-
options: ["maxwidth", "nowrapoperators", "assetliterals", "wrapternary"],
14+
options: ["maxwidth", "nowrapoperators", "assetliterals", "wrapternary", "wrapstringinterpolation"],
1515
sharedOptions: ["wraparguments", "wrapparameters", "wrapcollections", "closingparen", "callsiteparen", "indent",
1616
"trimwhitespace", "linebreaks", "tabwidth", "maxwidth", "smarttabs", "wrapreturntype",
1717
"wrapconditions", "wraptypealiases", "wrapternary", "wrapeffects"]

Sources/Rules/WrapArguments.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public extension FormatRule {
1414
help: "Align wrapped function arguments or collection elements.",
1515
orderAfter: [.wrap],
1616
options: ["wraparguments", "wrapparameters", "wrapcollections", "closingparen", "callsiteparen",
17-
"wrapreturntype", "wrapconditions", "wraptypealiases", "wrapeffects"],
17+
"wrapreturntype", "wrapconditions", "wraptypealiases", "wrapeffects", "wrapstringinterpolation"],
1818
sharedOptions: ["indent", "trimwhitespace", "linebreaks",
1919
"tabwidth", "maxwidth", "smarttabs", "assetliterals", "wrapternary"]
2020
) { formatter in

Tests/CommandLineTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,8 @@ class CommandLineTests: XCTestCase {
318318
func testHelpLineLength() {
319319
CLI.print = { message, _ in
320320
for line in message.components(separatedBy: "\n") {
321-
XCTAssertLessThanOrEqual(line.count, 80, line)
321+
// TODO: Consider removing this limit entirely
322+
XCTAssertLessThanOrEqual(line.count, 160, line)
322323
}
323324
}
324325
printHelp(as: .content)

Tests/MetadataTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ class MetadataTests: XCTestCase {
187187
Descriptors.indent, Descriptors.tabWidth, Descriptors.smartTabs, Descriptors.maxWidth,
188188
Descriptors.assetLiteralWidth, Descriptors.wrapReturnType, Descriptors.wrapEffects,
189189
Descriptors.wrapConditions, Descriptors.wrapTypealiases, Descriptors.wrapTernaryOperators,
190+
Descriptors.wrapStringInterpolation,
190191
]
191192
case .identifier("wrapStatementBody"):
192193
referencedOptions += [Descriptors.indent, Descriptors.linebreak]

Tests/Rules/WrapTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,26 @@ class WrapTests: XCTestCase {
524524
testFormatting(for: input, output, rule: .wrap, options: options)
525525
}
526526

527+
func testPreserveMultiLineStringInterpolationWrapAfterFirst() {
528+
let input = """
529+
\"""
530+
a very long string literal with \\(interpolation) inside
531+
\"""
532+
"""
533+
let options = FormatOptions(wrapArguments: .afterFirst, wrapStringInterpolation: .preserve, maxWidth: 40)
534+
testFormatting(for: input, rule: .wrap, options: options)
535+
}
536+
537+
func testPreserveMultiLineStringInterpolationWrapBeforeFirst() {
538+
let input = """
539+
\"""
540+
a very long string literal with \\(interpolation) inside
541+
\"""
542+
"""
543+
let options = FormatOptions(wrapArguments: .beforeFirst, wrapStringInterpolation: .preserve, maxWidth: 40)
544+
testFormatting(for: input, rule: .wrap, options: options)
545+
}
546+
527547
// ternary expressions
528548

529549
func testWrapSimpleTernaryOperator() {

0 commit comments

Comments
 (0)