Skip to content

Commit 63e6d2d

Browse files
committed
Merge newline token into break token.
The handling of separate newlines and breaks in the PrettyPrinter resulted in a fairly complex implementation. In particular, it required storing the most recent break, and mutating the indentation stack to retroactively "fire" a break. This has been a source of bugs. This updates the TokenStreamCreator and PrettyPrinter to a model without an explicit newline token. Instead, newlines are encapsulated by breaks. The same types of newlines exist ("flexible", "discretionary", and "mandatory" newlines have equivalent counterparts), but they exist as associated data with a break. This means newlines always exist where breaks are by definition. This refactor "broke" some cases where comments were working with the right indentation "by accident". In cases where a comment existed outside of any scoping tokens (i.e. open/close breaks, continue breaks, etc.), the PrettyPrinter carried over indentation of the last break even if that break wasn't really relevant. To fix this, I added `splitScopingBeforeTokens(of:)` which splits the before-tokens so that those that start a scope can be placed before comments when visiting a token. This fixed several existing comment indentation issues, and fixed comments whose indentation was only "accidentally" working thanks to the PrettyPrinter's break carry-over behavior.
1 parent 684a3cd commit 63e6d2d

File tree

5 files changed

+267
-236
lines changed

5 files changed

+267
-236
lines changed

Sources/SwiftFormatPrettyPrint/PrettyPrint.swift

Lines changed: 41 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,6 @@ public class PrettyPrinter {
9191
/// Indicates whether or not the printer is currently at the beginning of a line.
9292
private var isAtStartOfLine = true
9393

94-
/// Indicates whether the kind of the last break was one that triggers a continuation line (i.e.,
95-
/// a `.continue`, an `.open(.continuation)`, or a `.close` break that causes
96-
/// `currentLineIsContinuation` to become true).
97-
private var wasLastBreakKindContinue = false
98-
9994
/// Tracks how many printer control tokens to suppress firing breaks are active.
10095
private var activeBreakSuppressionCount = 0
10196

@@ -159,8 +154,8 @@ public class PrettyPrinter {
159154
outputBuffer.append(String(str))
160155
}
161156

162-
/// Ensures that the given number of newlines to the output stream (taking into account any
163-
/// pre-existing consecutive newlines).
157+
/// Writes newlines into the output stream, taking into account any pre-existing consecutive
158+
/// newlines and the maximum allowed number of blank lines.
164159
///
165160
/// This function does some implicit collapsing of consecutive newlines to ensure that the
166161
/// results are consistent when breaks and explicit newlines coincide. For example, imagine a
@@ -171,28 +166,20 @@ public class PrettyPrinter {
171166
/// subtract the previously written newlines during the second call so that we end up with the
172167
/// correct number overall.
173168
///
174-
/// - Parameters:
175-
/// - count: The number of newlines to write.
176-
/// - kind: Indicates whether the newlines are flexible, discretionary, or mandatory newlines.
177-
/// Refer to the documentation of `NewlineKind` for details on how each of these are printed.
178-
private func writeNewlines(_ count: Int, kind: NewlineKind) {
179-
// We add 1 because it takes 2 newlines to create a blank line.
169+
/// - Parameter newlines: The number and type of newlines to write.
170+
private func writeNewlines(_ newlines: NewlineBehavior) {
180171
let numberToPrint: Int
181-
if kind == .mandatory {
172+
switch newlines {
173+
case .elective:
174+
numberToPrint = consecutiveNewlineCount == 0 ? 1 : 0
175+
case .soft(let count, _):
176+
// We add 1 to the max blank lines because it takes 2 newlines to create the first blank line.
177+
numberToPrint = min(count - consecutiveNewlineCount, configuration.maximumBlankLines + 1)
178+
case .hard(let count):
182179
numberToPrint = count
183-
} else {
184-
let maximumNewlines = configuration.maximumBlankLines + 1
185-
if count <= maximumNewlines {
186-
numberToPrint = count - consecutiveNewlineCount
187-
} else {
188-
numberToPrint = maximumNewlines - consecutiveNewlineCount
189-
}
190-
191-
guard (kind == .discretionary && numberToPrint > 0) || consecutiveNewlineCount == 0 else {
192-
return
193-
}
194180
}
195181

182+
guard numberToPrint > 0 else { return }
196183
writeRaw(String(repeating: "\n", count: numberToPrint))
197184
lineNumber += numberToPrint
198185
isAtStartOfLine = true
@@ -259,8 +246,7 @@ public class PrettyPrinter {
259246

260247
// Create a line break if needed. Calculate the indentation required and adjust spaceRemaining
261248
// accordingly.
262-
case .break(let kind, let size, _):
263-
wasLastBreakKindContinue = false
249+
case .break(let kind, let size, let newline):
264250
var mustBreak = forceBreakStack.last ?? false
265251

266252
// Tracks whether the current line should be considered a continuation line, *if and only if
@@ -304,12 +290,6 @@ public class PrettyPrinter {
304290

305291
continuationStack.append(currentLineIsContinuation)
306292

307-
// If the open break kind is a continuation and it fired, then we don't want to set this
308-
// flag because the active open break will provide the continuation indentation for the
309-
// remaining lines. If the break doesn't fire, we need to set it so that the remaining lines
310-
// get the appropriate indentation.
311-
wasLastBreakKindContinue = openKind == .continuation && !continuationBreakWillFire
312-
313293
// Once we've reached an open break and preserved the continuation state, the "scope" we now
314294
// enter is *not* a continuation scope. If it was one, we'll re-enter it when we reach the
315295
// corresponding close.
@@ -382,11 +362,9 @@ public class PrettyPrinter {
382362

383363
// Restore the continuation state of the scope we were in before the open break occurred.
384364
currentLineIsContinuation = currentLineIsContinuation || wasContinuationWhenOpened
385-
wasLastBreakKindContinue = wasContinuationWhenOpened
386365
isContinuationIfBreakFires = wasContinuationWhenOpened
387366

388367
case .continue:
389-
wasLastBreakKindContinue = true
390368
isContinuationIfBreakFires = true
391369

392370
case .same:
@@ -396,9 +374,24 @@ public class PrettyPrinter {
396374
mustBreak = currentLineIsContinuation
397375
}
398376

399-
if !isBreakingSupressed && ((!isAtStartOfLine && length > spaceRemaining) || mustBreak) {
377+
var overrideBreakingSuppressed = false
378+
switch newline {
379+
case .elective: break
380+
case .soft(_, let discretionary):
381+
// A discretionary newline (i.e. from the source) should create a line break even if the
382+
// rules for breaking are disabled.
383+
overrideBreakingSuppressed = discretionary
384+
mustBreak = true
385+
case .hard:
386+
// A hard newline must always create a line break, regardless of the context.
387+
overrideBreakingSuppressed = true
388+
mustBreak = true
389+
}
390+
391+
let suppressBreaking = isBreakingSupressed && !overrideBreakingSuppressed
392+
if !suppressBreaking && ((!isAtStartOfLine && length > spaceRemaining) || mustBreak) {
400393
currentLineIsContinuation = isContinuationIfBreakFires
401-
writeNewlines(1, kind: .flexible)
394+
writeNewlines(newline)
402395
lastBreak = true
403396
} else {
404397
if isAtStartOfLine {
@@ -417,24 +410,6 @@ public class PrettyPrinter {
417410
case .space(let size, _):
418411
enqueueSpaces(size)
419412

420-
// Apply `count` line breaks, calculate the indentation required, and adjust spaceRemaining.
421-
case .newlines(let count, let kind):
422-
// If a newline immediately followed an open-continue break, then this is effectively the
423-
// same as if it had fired. Activate it, and reset the last-break-kind flag so that the
424-
// indentation of subsequent lines is contributed by that break and not by inherited
425-
// continuation state.
426-
if let lastActiveOpenBreak = activeOpenBreaks.last,
427-
lastActiveOpenBreak.index == idx - 1,
428-
lastActiveOpenBreak.kind == .continuation
429-
{
430-
activeOpenBreaks[activeOpenBreaks.count - 1].contributesContinuationIndent = true
431-
wasLastBreakKindContinue = false
432-
}
433-
434-
currentLineIsContinuation = wasLastBreakKindContinue
435-
writeNewlines(count, kind: kind)
436-
lastBreak = true
437-
438413
// Print any indentation required, followed by the text content of the syntax token.
439414
case .syntax(let text):
440415
guard !text.isEmpty else { break }
@@ -515,42 +490,27 @@ public class PrettyPrinter {
515490

516491
// Break lengths are equal to its size plus the token or group following it. Calculate the
517492
// length of any prior break tokens.
518-
case .break(_, let size, _):
493+
case .break(_, let size, let newline):
519494
if let index = delimIndexStack.last, case .break = tokens[index] {
520495
lengths[index] += total
521496
delimIndexStack.removeLast()
522497
}
523-
524498
lengths.append(-total)
525499
delimIndexStack.append(i)
526-
total += size
500+
501+
if case .elective = newline {
502+
total += size
503+
} else {
504+
// `size` is never used in this case, because the break always fires. Use `maxLineLength`
505+
// to ensure enclosing groups are large enough to force preceding breaks to fire.
506+
total += maxLineLength
507+
}
527508

528509
// Space tokens have a length equal to its size.
529510
case .space(let size, _):
530511
lengths.append(size)
531512
total += size
532513

533-
// The length of newlines are equal to the maximum allowed line length. Calculate the length
534-
// of any prior break tokens.
535-
case .newlines:
536-
if let index = delimIndexStack.last, case .break = tokens[index] {
537-
if index == i - 1 {
538-
// A break immediately preceding a newline should have a length of zero, so that it
539-
// doesn't fire.
540-
lengths[index] = 0
541-
} else {
542-
lengths[index] += total
543-
}
544-
delimIndexStack.removeLast()
545-
}
546-
547-
// Since newlines must always cause a line-break, we set their length as the full allowed
548-
// width of the line. This causes any enclosing groups to have a length exceeding the line
549-
// limit, and so the group must break and indent. e.g. single-line versus multi-line
550-
// function bodies.
551-
lengths.append(maxLineLength)
552-
total += maxLineLength
553-
554514
// Syntax tokens have a length equal to the number of columns needed to print its contents.
555515
case .syntax(let text):
556516
lengths.append(text.count)
@@ -615,11 +575,9 @@ public class PrettyPrinter {
615575
printDebugIndent()
616576
print("[SYNTAX \"\(syntax)\" Length: \(length) Idx: \(idx)]")
617577

618-
case .break(let kind, let size, let ignoresDiscretionary):
578+
case .break(let kind, let size, let newline):
619579
printDebugIndent()
620-
print(
621-
"[BREAK Kind: \(kind) Size: \(size) Length: \(length) "
622-
+ "Ignores Discretionary NL: \(ignoresDiscretionary) Idx: \(idx)]")
580+
print("[BREAK Kind: \(kind) Size: \(size) Length: \(length) NL: \(newline) Idx: \(idx)]")
623581

624582
case .open(let breakstyle):
625583
printDebugIndent()
@@ -636,10 +594,6 @@ public class PrettyPrinter {
636594
printDebugIndent()
637595
print("[CLOSE Idx: \(idx)]")
638596

639-
case .newlines(let N, let required):
640-
printDebugIndent()
641-
print("[NEWLINES N: \(N) Required: \(required) Length: \(length) Idx: \(idx)]")
642-
643597
case .space(let size, let flexible):
644598
printDebugIndent()
645599
print("[SPACE Size: \(size) Flexible: \(flexible) Length: \(length) Idx: \(idx)]")

Sources/SwiftFormatPrettyPrint/Token.swift

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -118,27 +118,32 @@ enum BreakKind: Equatable {
118118
static let open = BreakKind.open(kind: .block)
119119
}
120120

121-
enum NewlineKind {
122-
/// A newline that has been inserted by the formatter independent of the source code given by the
123-
/// user (for example, between the getter and setter blocks of a computed property).
124-
///
125-
/// Flexible newlines are only printed if a discretionary or mandatory newline has not yet been
126-
/// printed at the same location, and only up to the maximum allowed by the formatter
127-
/// configuration.
128-
case flexible
129-
130-
/// A newline that was present in the source code given by the user (that is, at the user's
131-
/// discretion).
132-
///
133-
/// Discretionary newlines are printed after excluding any other consecutive newlines printed thus
134-
/// far at the same location, and only up to the maximum allowed by the formatter configuration.
135-
case discretionary
136-
137-
/// A mandatory newline that must always be printed (for example, in a multiline string literal).
138-
///
139-
/// Mandatory newlines are never omitted by the pretty printer, even if it would result in a
140-
/// number of consecutive newlines that exceeds that allowed by the formatter configuration.
141-
case mandatory
121+
/// Behaviors for creating newlines as part of a break, i.e. where breaking onto a newline is
122+
/// allowed.
123+
enum NewlineBehavior {
124+
/// Breaking onto a newline is allowed if necessary, but is not required. `ignoresDiscretionary`
125+
/// specifies whether a user-entered discretionary newline should be respected.
126+
case elective(ignoresDiscretionary: Bool)
127+
128+
/// Breaking onto a newline `count` times is required, unless it would create more blank lines
129+
/// than are allowed by the current configuration. Any blank lines over the configured limit are
130+
/// discarded. `discretionary` tracks whether these newlines were created based on user-entered
131+
/// discretionary newlines, from the source, or were inserted by the formatter.
132+
case soft(count: Int, discretionary: Bool)
133+
134+
/// Breaking onto a newline `count` times is required and any limits on blank lines are
135+
/// **ignored**. Exactly `count` newlines are always printed, regardless of existing consecutive
136+
/// newlines and the configured maximum number of blank lines.
137+
case hard(count: Int)
138+
139+
/// An elective newline that respects discretionary newlines from the user-entered text.
140+
static let elective = NewlineBehavior.elective(ignoresDiscretionary: false)
141+
142+
/// A single soft newline that is created by the formatter, i.e. *not* discretionary.
143+
static let soft = NewlineBehavior.soft(count: 1, discretionary: false)
144+
145+
/// A single hard newline.
146+
static let hard = NewlineBehavior.hard(count: 1)
142147
}
143148

144149
/// Kinds of printer control tokens that can be used to customize the pretty printer's behavior in
@@ -160,9 +165,8 @@ enum Token {
160165
case syntax(String)
161166
case open(GroupBreakStyle)
162167
case close
163-
case `break`(BreakKind, size: Int, ignoresDiscretionary: Bool)
168+
case `break`(BreakKind, size: Int, newlines: NewlineBehavior)
164169
case space(size: Int, flexible: Bool)
165-
case newlines(Int, kind: NewlineKind)
166170
case comment(Comment, wasEndOfLine: Bool)
167171
case verbatim(Verbatim)
168172
case printerControl(kind: PrinterControlKind)
@@ -174,24 +178,20 @@ enum Token {
174178
return Token.open(breakStyle)
175179
}
176180

177-
/// A single, flexible newline.
178-
static let newline = Token.newlines(1, kind: .flexible)
179-
180-
/// Returns a single newline with the given kind.
181-
static func newline(kind: NewlineKind) -> Token {
182-
return Token.newlines(1, kind: kind)
183-
}
184-
185181
static let space = Token.space(size: 1, flexible: false)
186182

187183
static func space(size: Int) -> Token {
188184
return .space(size: size, flexible: false)
189185
}
190186

191-
static let `break` = Token.break(.continue, size: 1, ignoresDiscretionary: false)
187+
static let `break` = Token.break(.continue, size: 1, newlines: .elective)
192188

193189
static func `break`(_ kind: BreakKind, size: Int = 1) -> Token {
194-
return .break(kind, size: size, ignoresDiscretionary: false)
190+
return .break(kind, size: size, newlines: .elective)
191+
}
192+
193+
static func `break`(_ kind: BreakKind, newlines: NewlineBehavior) -> Token {
194+
return .break(kind, size: 1, newlines: newlines)
195195
}
196196

197197
static func verbatim(text: String) -> Token {

0 commit comments

Comments
 (0)