Skip to content

Commit a8ff3ca

Browse files
committed
Split the PrettyPrint class into two pieces.
This change breaks up the PrettyPrint class into one piece that is responsible for tracking the state of the pretty printing algorithm - processing the tokens, tracking the break stack, comma delimited region stack, etc.; and another piece that is responsible for assembling the output and tracking the state of the output - current line, column position, and indentation.
1 parent d46e30f commit a8ff3ca

File tree

3 files changed

+194
-131
lines changed

3 files changed

+194
-131
lines changed

Sources/SwiftFormat/PrettyPrint/Indent+Length.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ extension Indent {
2222
return String(repeating: character, count: count)
2323
}
2424

25-
func length(in configuration: Configuration) -> Int {
25+
func length(tabWidth: Int) -> Int {
2626
switch self {
2727
case .spaces(let count): return count
28-
case .tabs(let count): return count * configuration.tabWidth
28+
case .tabs(let count): return count * tabWidth
2929
}
3030
}
3131
}
@@ -36,6 +36,10 @@ extension Array where Element == Indent {
3636
}
3737

3838
func length(in configuration: Configuration) -> Int {
39-
return reduce(into: 0) { $0 += $1.length(in: configuration) }
39+
return self.length(tabWidth: configuration.tabWidth)
40+
}
41+
42+
func length(tabWidth: Int) -> Int {
43+
return reduce(into: 0) { $0 += $1.length(tabWidth: tabWidth) }
4044
}
4145
}

Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift

Lines changed: 47 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,13 @@ public class PrettyPrinter {
7676
/// original source. When enabling formatting, we copy the text between `disabledPosition` and the
7777
/// current position to `outputBuffer`. From then on, we continue to format until the next
7878
/// `disableFormatting` token.
79-
private var disabledPosition: AbsolutePosition? = nil
80-
81-
private var outputBuffer: String = ""
79+
private var disabledPosition: AbsolutePosition? = nil {
80+
didSet {
81+
outputBuffer.isEnabled = disabledPosition == nil
82+
}
83+
}
8284

83-
/// The number of spaces remaining on the current line.
84-
private var spaceRemaining: Int
85+
private var outputBuffer: PrettyPrintBuffer
8586

8687
/// Keep track of the token lengths.
8788
private var lengths = [Int]()
@@ -103,19 +104,26 @@ public class PrettyPrinter {
103104

104105
/// Keeps track of the line numbers and indentation states of the open (and unclosed) breaks seen
105106
/// so far.
106-
private var activeOpenBreaks: [ActiveOpenBreak] = []
107+
private var activeOpenBreaks: [ActiveOpenBreak] = [] {
108+
didSet {
109+
outputBuffer.currentIndentation = currentIndentation
110+
}
111+
}
107112

108113
/// Stack of the active breaking contexts.
109114
private var activeBreakingContexts: [ActiveBreakingContext] = []
110115

111116
/// The most recently ended breaking context, used to force certain following `contextual` breaks.
112117
private var lastEndedBreakingContext: ActiveBreakingContext? = nil
113118

114-
/// Keeps track of the current line number being printed.
115-
private var lineNumber: Int = 1
116-
117119
/// Indicates whether or not the current line being printed is a continuation line.
118-
private var currentLineIsContinuation = false
120+
private var currentLineIsContinuation = false {
121+
didSet {
122+
if oldValue != currentLineIsContinuation {
123+
outputBuffer.currentIndentation = currentIndentation
124+
}
125+
}
126+
}
119127

120128
/// Keeps track of the continuation line state as you go into and out of open-close break groups.
121129
private var continuationStack: [Bool] = []
@@ -124,18 +132,6 @@ public class PrettyPrinter {
124132
/// corresponding end token are encountered.
125133
private var commaDelimitedRegionStack: [Int] = []
126134

127-
/// Keeps track of the most recent number of consecutive newlines that have been printed.
128-
///
129-
/// This value is reset to zero whenever non-newline content is printed.
130-
private var consecutiveNewlineCount = 0
131-
132-
/// Keeps track of the most recent number of spaces that should be printed before the next text
133-
/// token.
134-
private var pendingSpaces = 0
135-
136-
/// Indicates whether or not the printer is currently at the beginning of a line.
137-
private var isAtStartOfLine = true
138-
139135
/// Tracks how many printer control tokens to suppress firing breaks are active.
140136
private var activeBreakSuppressionCount = 0
141137

@@ -173,7 +169,7 @@ public class PrettyPrinter {
173169
/// line number to increase by one by the time we reach the break, when we really wish to consider
174170
/// the break as being located at the end of the previous line.
175171
private var openCloseBreakCompensatingLineNumber: Int {
176-
return isAtStartOfLine ? lineNumber - 1 : lineNumber
172+
return outputBuffer.lineNumber - (outputBuffer.isAtStartOfLine ? 1 : 0)
177173
}
178174

179175
/// Creates a new PrettyPrinter with the provided formatting configuration.
@@ -193,77 +189,9 @@ public class PrettyPrinter {
193189
selection: context.selection,
194190
operatorTable: context.operatorTable)
195191
self.maxLineLength = configuration.lineLength
196-
self.spaceRemaining = self.maxLineLength
197192
self.printTokenStream = printTokenStream
198193
self.whitespaceOnly = whitespaceOnly
199-
}
200-
201-
/// Append the given string to the output buffer.
202-
///
203-
/// No further processing is performed on the string.
204-
private func writeRaw<S: StringProtocol>(_ str: S) {
205-
if disabledPosition == nil {
206-
outputBuffer.append(String(str))
207-
}
208-
}
209-
210-
/// Writes newlines into the output stream, taking into account any preexisting consecutive
211-
/// newlines and the maximum allowed number of blank lines.
212-
///
213-
/// This function does some implicit collapsing of consecutive newlines to ensure that the
214-
/// results are consistent when breaks and explicit newlines coincide. For example, imagine a
215-
/// break token that fires (thus creating a single non-discretionary newline) because it is
216-
/// followed by a group that contains 2 discretionary newlines that were found in the user's
217-
/// source code at that location. In that case, the break "overlaps" with the discretionary
218-
/// newlines and it will write a newline before we get to the discretionaries. Thus, we have to
219-
/// subtract the previously written newlines during the second call so that we end up with the
220-
/// correct number overall.
221-
///
222-
/// - Parameter newlines: The number and type of newlines to write.
223-
private func writeNewlines(_ newlines: NewlineBehavior) {
224-
let numberToPrint: Int
225-
switch newlines {
226-
case .elective:
227-
numberToPrint = consecutiveNewlineCount == 0 ? 1 : 0
228-
case .soft(let count, _):
229-
// We add 1 to the max blank lines because it takes 2 newlines to create the first blank line.
230-
numberToPrint = min(count, configuration.maximumBlankLines + 1) - consecutiveNewlineCount
231-
case .hard(let count):
232-
numberToPrint = count
233-
}
234-
235-
guard numberToPrint > 0 else { return }
236-
writeRaw(String(repeating: "\n", count: numberToPrint))
237-
lineNumber += numberToPrint
238-
isAtStartOfLine = true
239-
consecutiveNewlineCount += numberToPrint
240-
pendingSpaces = 0
241-
}
242-
243-
/// Request that the given number of spaces be printed out before the next text token.
244-
///
245-
/// Spaces are printed only when the next text token is printed in order to prevent us from
246-
/// printing lines that are only whitespace or have trailing whitespace.
247-
private func enqueueSpaces(_ count: Int) {
248-
pendingSpaces += count
249-
spaceRemaining -= count
250-
}
251-
252-
/// Writes the given text to the output stream.
253-
///
254-
/// Before printing the text, this function will print any line-leading indentation or interior
255-
/// leading spaces that are required before the text itself.
256-
private func write(_ text: String) {
257-
if isAtStartOfLine {
258-
writeRaw(currentIndentation.indentation())
259-
spaceRemaining = maxLineLength - currentIndentation.length(in: configuration)
260-
isAtStartOfLine = false
261-
} else if pendingSpaces > 0 {
262-
writeRaw(String(repeating: " ", count: pendingSpaces))
263-
}
264-
writeRaw(text)
265-
consecutiveNewlineCount = 0
266-
pendingSpaces = 0
194+
self.outputBuffer = PrettyPrintBuffer(maximumBlankLines: configuration.maximumBlankLines, tabWidth: configuration.tabWidth)
267195
}
268196

269197
/// Print out the provided token, and apply line-wrapping and indentation as needed.
@@ -285,7 +213,7 @@ public class PrettyPrinter {
285213

286214
switch token {
287215
case .contextualBreakingStart:
288-
activeBreakingContexts.append(ActiveBreakingContext(lineNumber: lineNumber))
216+
activeBreakingContexts.append(ActiveBreakingContext(lineNumber: outputBuffer.lineNumber))
289217

290218
// Discard the last finished breaking context to keep it from effecting breaks inside of the
291219
// new context. The discarded context has already either had an impact on the contextual break
@@ -306,7 +234,7 @@ public class PrettyPrinter {
306234
// the group.
307235
case .open(let breaktype):
308236
// Determine if the break tokens in this group need to be forced.
309-
if (length > spaceRemaining || lastBreak), case .consistent = breaktype {
237+
if (shouldBreak(length) || lastBreak), case .consistent = breaktype {
310238
forceBreakStack.append(true)
311239
} else {
312240
forceBreakStack.append(false)
@@ -348,7 +276,7 @@ public class PrettyPrinter {
348276
// scope), so we need the continuation indentation to persist across all the lines in that
349277
// scope. Additionally, continuation open breaks must indent when the break fires.
350278
let continuationBreakWillFire = openKind == .continuation
351-
&& (isAtStartOfLine || length > spaceRemaining || mustBreak)
279+
&& (outputBuffer.isAtStartOfLine || shouldBreak(length) || mustBreak)
352280
let contributesContinuationIndent = currentLineIsContinuation || continuationBreakWillFire
353281

354282
activeOpenBreaks.append(
@@ -377,7 +305,7 @@ public class PrettyPrinter {
377305
if matchingOpenBreak.contributesBlockIndent {
378306
// The actual line number is used, instead of the compensating line number. When the close
379307
// break is at the start of a new line, the block indentation isn't carried to the new line.
380-
let currentLine = lineNumber
308+
let currentLine = outputBuffer.lineNumber
381309
// When two or more open breaks are encountered on the same line, only the final open
382310
// break is allowed to increase the block indent, avoiding multiple block indents. As the
383311
// open breaks on that line are closed, the new final open break must be enabled again to
@@ -395,7 +323,7 @@ public class PrettyPrinter {
395323
// If it's a mandatory breaking close, then we must break (regardless of line length) if
396324
// the break is on a different line than its corresponding open break.
397325
mustBreak = openedOnDifferentLine
398-
} else if spaceRemaining == 0 {
326+
} else if shouldBreak(1) {
399327
// If there is no room left on the line, then we must force this break to fire so that the
400328
// next token that comes along (typically a closing bracket of some kind) ends up on the
401329
// next line.
@@ -453,13 +381,13 @@ public class PrettyPrinter {
453381
// context includes a multiline trailing closure or multiline function argument list.
454382
if let lastBreakingContext = lastEndedBreakingContext {
455383
if configuration.lineBreakAroundMultilineExpressionChainComponents {
456-
mustBreak = lastBreakingContext.lineNumber != lineNumber
384+
mustBreak = lastBreakingContext.lineNumber != outputBuffer.lineNumber
457385
}
458386
}
459387

460388
// Wait for a contextual break to fire and then update the breaking behavior for the rest of
461389
// the contextual breaks in this scope to match the behavior of the one that fired.
462-
let willFire = (!isAtStartOfLine && length > spaceRemaining) || mustBreak
390+
let willFire = shouldBreak(length) || mustBreak
463391
if willFire {
464392
// Update the active breaking context according to the most recently finished breaking
465393
// context so all following contextual breaks in this scope to have matching behavior.
@@ -468,7 +396,7 @@ public class PrettyPrinter {
468396
case .unset = activeContext.contextualBreakingBehavior
469397
{
470398
activeBreakingContexts[activeBreakingContexts.count - 1].contextualBreakingBehavior =
471-
(closedContext.lineNumber == lineNumber) ? .continuation : .maintain
399+
(closedContext.lineNumber == outputBuffer.lineNumber) ? .continuation : .maintain
472400
}
473401
}
474402

@@ -499,52 +427,46 @@ public class PrettyPrinter {
499427
}
500428

501429
let suppressBreaking = isBreakingSuppressed && !overrideBreakingSuppressed
502-
if !suppressBreaking && ((!isAtStartOfLine && length > spaceRemaining) || mustBreak) {
430+
if !suppressBreaking && (shouldBreak(length) || mustBreak) {
503431
currentLineIsContinuation = isContinuationIfBreakFires
504-
writeNewlines(newline)
432+
outputBuffer.writeNewlines(newline)
505433
lastBreak = true
506434
} else {
507-
if isAtStartOfLine {
435+
if outputBuffer.isAtStartOfLine {
508436
// Make sure that the continuation status is correct even at the beginning of a line
509437
// (for example, after a newline token). This is necessary because a discretionary newline
510438
// might be inserted into the token stream before a continuation break, and the length of
511439
// that break might not be enough to satisfy the conditions above but we still need to
512440
// treat the line as a continuation.
513441
currentLineIsContinuation = isContinuationIfBreakFires
514442
}
515-
enqueueSpaces(size)
443+
outputBuffer.enqueueSpaces(size)
516444
lastBreak = false
517445
}
518446

519447
// Print out the number of spaces according to the size, and adjust spaceRemaining.
520448
case .space(let size, _):
521-
enqueueSpaces(size)
449+
outputBuffer.enqueueSpaces(size)
522450

523451
// Print any indentation required, followed by the text content of the syntax token.
524452
case .syntax(let text):
525453
guard !text.isEmpty else { break }
526454
lastBreak = false
527-
write(text)
528-
spaceRemaining -= text.count
455+
outputBuffer.write(text)
529456

530457
case .comment(let comment, let wasEndOfLine):
531458
lastBreak = false
532459

533-
write(comment.print(indent: currentIndentation))
534460
if wasEndOfLine {
535-
if comment.length > spaceRemaining && !isBreakingSuppressed {
461+
if shouldBreak(comment.length) && !isBreakingSuppressed {
536462
diagnose(.moveEndOfLineComment, category: .endOfLineComment)
537463
}
538-
} else {
539-
spaceRemaining -= comment.length
540464
}
465+
outputBuffer.write(comment.print(indent: currentIndentation))
541466

542467
case .verbatim(let verbatim):
543-
writeRaw(verbatim.print(indent: currentIndentation))
544-
consecutiveNewlineCount = 0
545-
pendingSpaces = 0
468+
outputBuffer.writeVerbatim(verbatim.print(indent: currentIndentation), length)
546469
lastBreak = false
547-
spaceRemaining -= length
548470

549471
case .printerControl(let kind):
550472
switch kind {
@@ -583,8 +505,7 @@ public class PrettyPrinter {
583505

584506
let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma
585507
if shouldWriteComma {
586-
write(",")
587-
spaceRemaining -= 1
508+
outputBuffer.write(",")
588509
}
589510

590511
case .enableFormatting(let enabledPosition):
@@ -607,21 +528,19 @@ public class PrettyPrinter {
607528
}
608529

609530
self.disabledPosition = nil
610-
writeRaw(text)
611-
if text.hasSuffix("\n") {
612-
isAtStartOfLine = true
613-
consecutiveNewlineCount = 1
614-
} else {
615-
isAtStartOfLine = false
616-
consecutiveNewlineCount = 0
617-
}
531+
outputBuffer.writeVerbatimAfterEnablingFormatting(text)
618532

619533
case .disableFormatting(let newPosition):
620534
assert(disabledPosition == nil)
621535
disabledPosition = newPosition
622536
}
623537
}
624538

539+
private func shouldBreak(_ length: Int) -> Bool {
540+
let spaceRemaining = configuration.lineLength - outputBuffer.column
541+
return !outputBuffer.isAtStartOfLine && length > spaceRemaining
542+
}
543+
625544
/// Scan over the array of Tokens and calculate their lengths.
626545
///
627546
/// This method is based on the `scan` function described in Derek Oppen's "Pretty Printing" paper
@@ -748,7 +667,7 @@ public class PrettyPrinter {
748667
fatalError("At least one .break(.open) was not matched by a .break(.close)")
749668
}
750669

751-
return outputBuffer
670+
return outputBuffer.output
752671
}
753672

754673
/// Used to track the indentation level for the debug token stream output.
@@ -843,11 +762,11 @@ public class PrettyPrinter {
843762
/// Emits a finding with the given message and category at the current location in `outputBuffer`.
844763
private func diagnose(_ message: Finding.Message, category: PrettyPrintFindingCategory) {
845764
// Add 1 since columns uses 1-based indices.
846-
let column = maxLineLength - spaceRemaining + 1
765+
let column = outputBuffer.column + 1
847766
context.findingEmitter.emit(
848767
message,
849768
category: category,
850-
location: Finding.Location(file: context.fileURL.path, line: lineNumber, column: column))
769+
location: Finding.Location(file: context.fileURL.path, line: outputBuffer.lineNumber, column: column))
851770
}
852771
}
853772

0 commit comments

Comments
 (0)