Skip to content

Commit 705af09

Browse files
committed
Wrap multiline string literals to line length.
Change how PrettyPrinter emits StringSegmentSyntax's so that they can be broken up over multiple lines within multiline string literals. This does not change the behavior of single line string literals, which cannot contain newlines. This also does not change interpolations which are still emitted as verbatims and are never line broken by the pretty printer. Line breaking is done exclusively through escaped newlines. A literal containing escaped newlines will remove all escaped newlines and then reinsert them based on line length. Hard newlines are respected by the formatter and will not be moved, even if it causes short lines. Escaped newlines will be reformatted by the pretty printer so that lines ending in an escaped newline are at line length. Wrapping is implemented by introducing a new newlinebreak behavior `.escaped`. `.escaped` acts very similarly to `.elective` but has slightly different length calculation logic and printing behavior. An escaped newline is printed including it's preceeding whitespace followed by "\\\n". So a break of `.break(_, 2, .escaped)` will print as " \\\n". Because an escaped line break takes up characters when broken (unlike other breaks), length calculation must be handled differently for `.escaped` breaks.
1 parent 021a5ab commit 705af09

File tree

7 files changed

+452
-37
lines changed

7 files changed

+452
-37
lines changed

Sources/SwiftFormat/API/Configuration+Default.swift

+1
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ extension Configuration {
4040
self.spacesAroundRangeFormationOperators = false
4141
self.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration()
4242
self.multiElementCollectionTrailingCommas = true
43+
self.reflowMultilineStringLiterals = .never
4344
}
4445
}

Sources/SwiftFormat/API/Configuration.swift

+71
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public struct Configuration: Codable, Equatable {
4545
case spacesAroundRangeFormationOperators
4646
case noAssignmentInExpressions
4747
case multiElementCollectionTrailingCommas
48+
case reflowMultilineStringLiterals
4849
}
4950

5051
/// A dictionary containing the default enabled/disabled states of rules, keyed by the rules'
@@ -194,6 +195,71 @@ public struct Configuration: Codable, Equatable {
194195
/// ```
195196
public var multiElementCollectionTrailingCommas: Bool
196197

198+
/// Determines how multiline string literals should reflow when formatted.
199+
public enum MultilineStringReflowBehavior: Codable {
200+
/// Never reflow multiline string literals.
201+
case never
202+
/// Reflow lines in string literal that exceed the maximum line length. For example with a line length of 10:
203+
/// ```swift
204+
/// """
205+
/// an escape\
206+
/// line break
207+
/// a hard line break
208+
/// """
209+
/// ```
210+
/// will be formatted as:
211+
/// ```swift
212+
/// """
213+
/// an esacpe\
214+
/// line break
215+
/// a hard \
216+
/// line break
217+
/// """
218+
/// ```
219+
/// The existing `\` is left in place, but the line over line length is broken.
220+
case onlyLinesOverLength
221+
/// Always reflow multiline string literals, this will ignore existing escaped newlines in the literal and reflow each line. Hard linebreaks are still respected.
222+
/// For example, with a line length of 10:
223+
/// ```swift
224+
/// """
225+
/// one \
226+
/// word \
227+
/// a line.
228+
/// this is too long.
229+
/// """
230+
/// ```
231+
/// will be formatted as:
232+
/// ```swift
233+
/// """
234+
/// one word \
235+
/// a line.
236+
/// this is \
237+
/// too long.
238+
/// """
239+
/// ```
240+
case always
241+
242+
var isNever: Bool {
243+
switch self {
244+
case .never:
245+
return true
246+
default:
247+
return false
248+
}
249+
}
250+
251+
var isAlways: Bool {
252+
switch self {
253+
case .always:
254+
return true
255+
default:
256+
return false
257+
}
258+
}
259+
}
260+
261+
public var reflowMultilineStringLiterals: MultilineStringReflowBehavior
262+
197263
/// Creates a new `Configuration` by loading it from a configuration file.
198264
public init(contentsOf url: URL) throws {
199265
let data = try Data(contentsOf: url)
@@ -287,6 +353,10 @@ public struct Configuration: Codable, Equatable {
287353
Bool.self, forKey: .multiElementCollectionTrailingCommas)
288354
?? defaults.multiElementCollectionTrailingCommas
289355

356+
self.reflowMultilineStringLiterals =
357+
try container.decodeIfPresent(MultilineStringReflowBehavior.self, forKey: .reflowMultilineStringLiterals)
358+
?? defaults.reflowMultilineStringLiterals
359+
290360
// If the `rules` key is not present at all, default it to the built-in set
291361
// so that the behavior is the same as if the configuration had been
292362
// default-initialized. To get an empty rules dictionary, one can explicitly
@@ -321,6 +391,7 @@ public struct Configuration: Codable, Equatable {
321391
try container.encode(indentSwitchCaseLabels, forKey: .indentSwitchCaseLabels)
322392
try container.encode(noAssignmentInExpressions, forKey: .noAssignmentInExpressions)
323393
try container.encode(multiElementCollectionTrailingCommas, forKey: .multiElementCollectionTrailingCommas)
394+
try container.encode(reflowMultilineStringLiterals, forKey: .reflowMultilineStringLiterals)
324395
try container.encode(rules, forKey: .rules)
325396
}
326397

Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift

+45-10
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@ public class PrettyPrinter {
214214
switch token {
215215
case .contextualBreakingStart:
216216
activeBreakingContexts.append(ActiveBreakingContext(lineNumber: outputBuffer.lineNumber))
217-
218217
// Discard the last finished breaking context to keep it from effecting breaks inside of the
219218
// new context. The discarded context has already either had an impact on the contextual break
220219
// after it or there was no relevant contextual break, so it's safe to discard.
@@ -414,7 +413,7 @@ public class PrettyPrinter {
414413

415414
var overrideBreakingSuppressed = false
416415
switch newline {
417-
case .elective: break
416+
case .elective, .escaped: break
418417
case .soft(_, let discretionary):
419418
// A discretionary newline (i.e. from the source) should create a line break even if the
420419
// rules for breaking are disabled.
@@ -429,6 +428,10 @@ public class PrettyPrinter {
429428
let suppressBreaking = isBreakingSuppressed && !overrideBreakingSuppressed
430429
if !suppressBreaking && (!canFit(length) || mustBreak) {
431430
currentLineIsContinuation = isContinuationIfBreakFires
431+
if case .escaped = newline {
432+
outputBuffer.enqueueSpaces(size)
433+
outputBuffer.write("\\")
434+
}
432435
outputBuffer.writeNewlines(newline)
433436
lastBreak = true
434437
} else {
@@ -594,19 +597,51 @@ public class PrettyPrinter {
594597
// Break lengths are equal to its size plus the token or group following it. Calculate the
595598
// length of any prior break tokens.
596599
case .break(_, let size, let newline):
597-
if let index = delimIndexStack.last, case .break = tokens[index] {
598-
lengths[index] += total
600+
if let index = delimIndexStack.last, case .break(_, _, let lastNewline) = tokens[index] {
601+
/// If the last break and this break are both `.escaped` we add an extra 1 to the total for the last `.escaped` break.
602+
/// This is to handle situations where adding the `\` for an escaped line break would put us over the line length.
603+
/// For example, consider the token sequence:
604+
/// `[.syntax("this fits"), .break(.escaped), .syntax("this fits in line length"), .break(.escaped)]`
605+
/// The naive layout of these tokens will incorrectly print as:
606+
/// """
607+
/// this fits this fits in line length \
608+
/// """
609+
/// which will be too long because of the '\' character. Instead we have to print it as:
610+
/// """
611+
/// this fits \
612+
/// this fits in line length
613+
/// """
614+
///
615+
/// While not prematurely inserting a line in situations where a hard line break is occurring, such as:
616+
///
617+
/// `[.syntax("some text"), .break(.escaped), .syntax("this is exactly the right length"), .break(.hard)]`
618+
///
619+
/// We want this to print as:
620+
/// """
621+
/// some text this is exactly the right length
622+
/// """
623+
/// and not:
624+
/// """
625+
/// some text \
626+
/// this is exactly the right length
627+
/// """
628+
if case .escaped = newline, case .escaped = lastNewline {
629+
lengths[index] += total + 1
630+
} else {
631+
lengths[index] += total
632+
}
599633
delimIndexStack.removeLast()
600634
}
601635
lengths.append(-total)
602636
delimIndexStack.append(i)
603637

604-
if case .elective = newline {
605-
total += size
606-
} else {
607-
// `size` is never used in this case, because the break always fires. Use `maxLineLength`
608-
// to ensure enclosing groups are large enough to force preceding breaks to fire.
609-
total += maxLineLength
638+
switch newline {
639+
case .elective, .escaped:
640+
total += size
641+
default:
642+
// `size` is never used in this case, because the break always fires. Use `maxLineLength`
643+
// to ensure enclosing groups are large enough to force preceding breaks to fire.
644+
total += maxLineLength
610645
}
611646

612647
// Space tokens have a length equal to its size.

Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift

+2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ struct PrettyPrintBuffer {
8181
numberToPrint = min(count, maximumBlankLines + 1) - consecutiveNewlineCount
8282
case .hard(let count):
8383
numberToPrint = count
84+
case .escaped:
85+
numberToPrint = 1
8486
}
8587

8688
guard numberToPrint > 0 else { return }

Sources/SwiftFormat/PrettyPrint/Token.swift

+4
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ enum NewlineBehavior {
147147
/// newlines and the configured maximum number of blank lines.
148148
case hard(count: Int)
149149

150+
/// Break onto a new line is allowed if neccessary. If a line break is emitted, it will be escaped with a '\', and this breaks whitespace will be printed prior to the
151+
/// escaped line break. This is useful in multiline strings where we don't want newlines printed in syntax to appear in the literal.
152+
case escaped
153+
150154
/// An elective newline that respects discretionary newlines from the user-entered text.
151155
static let elective = NewlineBehavior.elective(ignoresDiscretionary: false)
152156

0 commit comments

Comments
 (0)