Skip to content

Commit d8b446b

Browse files
authored
Merge pull request swiftlang#759 from shawnhyam/pretty-print-code-reorg
Split the PrettyPrint class into two pieces.
2 parents 8be2ab2 + 415b8ea commit d8b446b

File tree

3 files changed

+206
-131
lines changed

3 files changed

+206
-131
lines changed

Sources/SwiftFormat/PrettyPrint/Indent+Length.swift

+7-3
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

+49-128
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 (!canFit(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 || !canFit(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 !canFit() {
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 = !canFit(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 && (!canFit(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 !(canFit(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,21 @@ 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+
/// Indicates whether the current line can fit a string of the given length. If no length
540+
/// is given, it indicates whether the current line can accomodate *any* text.
541+
private func canFit(_ length: Int = 1) -> Bool {
542+
let spaceRemaining = configuration.lineLength - outputBuffer.column
543+
return outputBuffer.isAtStartOfLine || length <= spaceRemaining
544+
}
545+
625546
/// Scan over the array of Tokens and calculate their lengths.
626547
///
627548
/// This method is based on the `scan` function described in Derek Oppen's "Pretty Printing" paper
@@ -748,7 +669,7 @@ public class PrettyPrinter {
748669
fatalError("At least one .break(.open) was not matched by a .break(.close)")
749670
}
750671

751-
return outputBuffer
672+
return outputBuffer.output
752673
}
753674

754675
/// Used to track the indentation level for the debug token stream output.
@@ -843,11 +764,11 @@ public class PrettyPrinter {
843764
/// Emits a finding with the given message and category at the current location in `outputBuffer`.
844765
private func diagnose(_ message: Finding.Message, category: PrettyPrintFindingCategory) {
845766
// Add 1 since columns uses 1-based indices.
846-
let column = maxLineLength - spaceRemaining + 1
767+
let column = outputBuffer.column + 1
847768
context.findingEmitter.emit(
848769
message,
849770
category: category,
850-
location: Finding.Location(file: context.fileURL.path, line: lineNumber, column: column))
771+
location: Finding.Location(file: context.fileURL.path, line: outputBuffer.lineNumber, column: column))
851772
}
852773
}
853774

0 commit comments

Comments
 (0)