Skip to content

Commit 89ccc79

Browse files
authored
Merge pull request #792 from thunderseethe/multiline
Wrap multiline string literals to line length.
2 parents c658a00 + 705af09 commit 89ccc79

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)