@@ -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