Skip to content

Split the PrettyPrint class into two pieces. #759

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Sources/SwiftFormat/PrettyPrint/Indent+Length.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ extension Indent {
return String(repeating: character, count: count)
}

func length(in configuration: Configuration) -> Int {
func length(tabWidth: Int) -> Int {
switch self {
case .spaces(let count): return count
case .tabs(let count): return count * configuration.tabWidth
case .tabs(let count): return count * tabWidth
}
}
}
Expand All @@ -36,6 +36,10 @@ extension Array where Element == Indent {
}

func length(in configuration: Configuration) -> Int {
return reduce(into: 0) { $0 += $1.length(in: configuration) }
return self.length(tabWidth: configuration.tabWidth)
}

func length(tabWidth: Int) -> Int {
return reduce(into: 0) { $0 += $1.length(tabWidth: tabWidth) }
}
}
177 changes: 49 additions & 128 deletions Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@ public class PrettyPrinter {
/// original source. When enabling formatting, we copy the text between `disabledPosition` and the
/// current position to `outputBuffer`. From then on, we continue to format until the next
/// `disableFormatting` token.
private var disabledPosition: AbsolutePosition? = nil

private var outputBuffer: String = ""
private var disabledPosition: AbsolutePosition? = nil {
didSet {
outputBuffer.isEnabled = disabledPosition == nil
}
}

/// The number of spaces remaining on the current line.
private var spaceRemaining: Int
private var outputBuffer: PrettyPrintBuffer

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

/// Keeps track of the line numbers and indentation states of the open (and unclosed) breaks seen
/// so far.
private var activeOpenBreaks: [ActiveOpenBreak] = []
private var activeOpenBreaks: [ActiveOpenBreak] = [] {
didSet {
outputBuffer.currentIndentation = currentIndentation
}
}

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

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

/// Keeps track of the current line number being printed.
private var lineNumber: Int = 1

/// Indicates whether or not the current line being printed is a continuation line.
private var currentLineIsContinuation = false
private var currentLineIsContinuation = false {
didSet {
if oldValue != currentLineIsContinuation {
outputBuffer.currentIndentation = currentIndentation
}
}
}

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

/// Keeps track of the most recent number of consecutive newlines that have been printed.
///
/// This value is reset to zero whenever non-newline content is printed.
private var consecutiveNewlineCount = 0

/// Keeps track of the most recent number of spaces that should be printed before the next text
/// token.
private var pendingSpaces = 0

/// Indicates whether or not the printer is currently at the beginning of a line.
private var isAtStartOfLine = true

/// Tracks how many printer control tokens to suppress firing breaks are active.
private var activeBreakSuppressionCount = 0

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

/// Creates a new PrettyPrinter with the provided formatting configuration.
Expand All @@ -193,77 +189,9 @@ public class PrettyPrinter {
selection: context.selection,
operatorTable: context.operatorTable)
self.maxLineLength = configuration.lineLength
self.spaceRemaining = self.maxLineLength
self.printTokenStream = printTokenStream
self.whitespaceOnly = whitespaceOnly
}

/// Append the given string to the output buffer.
///
/// No further processing is performed on the string.
private func writeRaw<S: StringProtocol>(_ str: S) {
if disabledPosition == nil {
outputBuffer.append(String(str))
}
}

/// Writes newlines into the output stream, taking into account any preexisting consecutive
/// newlines and the maximum allowed number of blank lines.
///
/// This function does some implicit collapsing of consecutive newlines to ensure that the
/// results are consistent when breaks and explicit newlines coincide. For example, imagine a
/// break token that fires (thus creating a single non-discretionary newline) because it is
/// followed by a group that contains 2 discretionary newlines that were found in the user's
/// source code at that location. In that case, the break "overlaps" with the discretionary
/// newlines and it will write a newline before we get to the discretionaries. Thus, we have to
/// subtract the previously written newlines during the second call so that we end up with the
/// correct number overall.
///
/// - Parameter newlines: The number and type of newlines to write.
private func writeNewlines(_ newlines: NewlineBehavior) {
let numberToPrint: Int
switch newlines {
case .elective:
numberToPrint = consecutiveNewlineCount == 0 ? 1 : 0
case .soft(let count, _):
// We add 1 to the max blank lines because it takes 2 newlines to create the first blank line.
numberToPrint = min(count, configuration.maximumBlankLines + 1) - consecutiveNewlineCount
case .hard(let count):
numberToPrint = count
}

guard numberToPrint > 0 else { return }
writeRaw(String(repeating: "\n", count: numberToPrint))
lineNumber += numberToPrint
isAtStartOfLine = true
consecutiveNewlineCount += numberToPrint
pendingSpaces = 0
}

/// Request that the given number of spaces be printed out before the next text token.
///
/// Spaces are printed only when the next text token is printed in order to prevent us from
/// printing lines that are only whitespace or have trailing whitespace.
private func enqueueSpaces(_ count: Int) {
pendingSpaces += count
spaceRemaining -= count
}

/// Writes the given text to the output stream.
///
/// Before printing the text, this function will print any line-leading indentation or interior
/// leading spaces that are required before the text itself.
private func write(_ text: String) {
if isAtStartOfLine {
writeRaw(currentIndentation.indentation())
spaceRemaining = maxLineLength - currentIndentation.length(in: configuration)
isAtStartOfLine = false
} else if pendingSpaces > 0 {
writeRaw(String(repeating: " ", count: pendingSpaces))
}
writeRaw(text)
consecutiveNewlineCount = 0
pendingSpaces = 0
self.outputBuffer = PrettyPrintBuffer(maximumBlankLines: configuration.maximumBlankLines, tabWidth: configuration.tabWidth)
}

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

switch token {
case .contextualBreakingStart:
activeBreakingContexts.append(ActiveBreakingContext(lineNumber: lineNumber))
activeBreakingContexts.append(ActiveBreakingContext(lineNumber: outputBuffer.lineNumber))

// Discard the last finished breaking context to keep it from effecting breaks inside of the
// new context. The discarded context has already either had an impact on the contextual break
Expand All @@ -306,7 +234,7 @@ public class PrettyPrinter {
// the group.
case .open(let breaktype):
// Determine if the break tokens in this group need to be forced.
if (length > spaceRemaining || lastBreak), case .consistent = breaktype {
if (!canFit(length) || lastBreak), case .consistent = breaktype {
forceBreakStack.append(true)
} else {
forceBreakStack.append(false)
Expand Down Expand Up @@ -348,7 +276,7 @@ public class PrettyPrinter {
// scope), so we need the continuation indentation to persist across all the lines in that
// scope. Additionally, continuation open breaks must indent when the break fires.
let continuationBreakWillFire = openKind == .continuation
&& (isAtStartOfLine || length > spaceRemaining || mustBreak)
&& (outputBuffer.isAtStartOfLine || !canFit(length) || mustBreak)
let contributesContinuationIndent = currentLineIsContinuation || continuationBreakWillFire

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

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

Expand Down Expand Up @@ -499,52 +427,46 @@ public class PrettyPrinter {
}

let suppressBreaking = isBreakingSuppressed && !overrideBreakingSuppressed
if !suppressBreaking && ((!isAtStartOfLine && length > spaceRemaining) || mustBreak) {
if !suppressBreaking && (!canFit(length) || mustBreak) {
currentLineIsContinuation = isContinuationIfBreakFires
writeNewlines(newline)
outputBuffer.writeNewlines(newline)
lastBreak = true
} else {
if isAtStartOfLine {
if outputBuffer.isAtStartOfLine {
// Make sure that the continuation status is correct even at the beginning of a line
// (for example, after a newline token). This is necessary because a discretionary newline
// might be inserted into the token stream before a continuation break, and the length of
// that break might not be enough to satisfy the conditions above but we still need to
// treat the line as a continuation.
currentLineIsContinuation = isContinuationIfBreakFires
}
enqueueSpaces(size)
outputBuffer.enqueueSpaces(size)
lastBreak = false
}

// Print out the number of spaces according to the size, and adjust spaceRemaining.
case .space(let size, _):
enqueueSpaces(size)
outputBuffer.enqueueSpaces(size)

// Print any indentation required, followed by the text content of the syntax token.
case .syntax(let text):
guard !text.isEmpty else { break }
lastBreak = false
write(text)
spaceRemaining -= text.count
outputBuffer.write(text)

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

write(comment.print(indent: currentIndentation))
if wasEndOfLine {
if comment.length > spaceRemaining && !isBreakingSuppressed {
if !(canFit(comment.length) || isBreakingSuppressed) {
diagnose(.moveEndOfLineComment, category: .endOfLineComment)
}
} else {
spaceRemaining -= comment.length
}
outputBuffer.write(comment.print(indent: currentIndentation))

case .verbatim(let verbatim):
writeRaw(verbatim.print(indent: currentIndentation))
consecutiveNewlineCount = 0
pendingSpaces = 0
outputBuffer.writeVerbatim(verbatim.print(indent: currentIndentation), length)
lastBreak = false
spaceRemaining -= length

case .printerControl(let kind):
switch kind {
Expand Down Expand Up @@ -583,8 +505,7 @@ public class PrettyPrinter {

let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma
if shouldWriteComma {
write(",")
spaceRemaining -= 1
outputBuffer.write(",")
}

case .enableFormatting(let enabledPosition):
Expand All @@ -607,21 +528,21 @@ public class PrettyPrinter {
}

self.disabledPosition = nil
writeRaw(text)
if text.hasSuffix("\n") {
isAtStartOfLine = true
consecutiveNewlineCount = 1
} else {
isAtStartOfLine = false
consecutiveNewlineCount = 0
}
outputBuffer.writeVerbatimAfterEnablingFormatting(text)

case .disableFormatting(let newPosition):
assert(disabledPosition == nil)
disabledPosition = newPosition
}
}

/// Indicates whether the current line can fit a string of the given length. If no length
/// is given, it indicates whether the current line can accomodate *any* text.
private func canFit(_ length: Int = 1) -> Bool {
let spaceRemaining = configuration.lineLength - outputBuffer.column
return outputBuffer.isAtStartOfLine || length <= spaceRemaining
}

/// Scan over the array of Tokens and calculate their lengths.
///
/// This method is based on the `scan` function described in Derek Oppen's "Pretty Printing" paper
Expand Down Expand Up @@ -748,7 +669,7 @@ public class PrettyPrinter {
fatalError("At least one .break(.open) was not matched by a .break(.close)")
}

return outputBuffer
return outputBuffer.output
}

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

Expand Down
Loading