diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index d31f98a0..14072d84 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -94,6 +94,11 @@ top-level keys and values: * `multiElementCollectionTrailingCommas` _(boolean)_: Determines whether multi-element collection literals should have trailing commas. Defaults to `true`. + +* `indentBlankLines` _(boolean)_: Determines whether blank lines should be modified + to match the current indentation. When this setting is true, blank lines will be modified + to match the indentation level, adding indentation whether or not there is existing whitespace. + When false (the default), all whitespace in blank lines will be completely removed. > TODO: Add support for enabling/disabling specific syntax transformations in > the pipeline. diff --git a/Sources/SwiftFormat/API/Configuration+Default.swift b/Sources/SwiftFormat/API/Configuration+Default.swift index d18164f3..1af06a12 100644 --- a/Sources/SwiftFormat/API/Configuration+Default.swift +++ b/Sources/SwiftFormat/API/Configuration+Default.swift @@ -41,5 +41,6 @@ extension Configuration { self.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration() self.multiElementCollectionTrailingCommas = true self.reflowMultilineStringLiterals = .never + self.indentBlankLines = false } } diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index c6836ab8..00bc3b5f 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -46,6 +46,7 @@ public struct Configuration: Codable, Equatable { case noAssignmentInExpressions case multiElementCollectionTrailingCommas case reflowMultilineStringLiterals + case indentBlankLines } /// A dictionary containing the default enabled/disabled states of rules, keyed by the rules' @@ -259,6 +260,13 @@ public struct Configuration: Codable, Equatable { } public var reflowMultilineStringLiterals: MultilineStringReflowBehavior + + /// Determines whether to add indentation whitespace to blank lines or remove it entirely. + /// + /// If true, blank lines will be modified to match the current indentation level: + /// if they contain whitespace, the existing whitespace will be adjusted, and if they are empty, spaces will be added to match the indentation. + /// If false (the default), the whitespace in blank lines will be removed entirely. + public var indentBlankLines: Bool /// Creates a new `Configuration` by loading it from a configuration file. public init(contentsOf url: URL) throws { @@ -352,10 +360,13 @@ public struct Configuration: Codable, Equatable { try container.decodeIfPresent( Bool.self, forKey: .multiElementCollectionTrailingCommas) ?? defaults.multiElementCollectionTrailingCommas - self.reflowMultilineStringLiterals = try container.decodeIfPresent(MultilineStringReflowBehavior.self, forKey: .reflowMultilineStringLiterals) ?? defaults.reflowMultilineStringLiterals + self.indentBlankLines = + try container.decodeIfPresent( + Bool.self, forKey: .indentBlankLines) + ?? defaults.indentBlankLines // If the `rules` key is not present at all, default it to the built-in set // so that the behavior is the same as if the configuration had been diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index 8ad1c04c..705b3dcd 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -432,7 +432,7 @@ public class PrettyPrinter { outputBuffer.enqueueSpaces(size) outputBuffer.write("\\") } - outputBuffer.writeNewlines(newline) + outputBuffer.writeNewlines(newline, shouldIndentBlankLines: configuration.indentBlankLines) lastBreak = true } else { if outputBuffer.isAtStartOfLine { @@ -449,7 +449,12 @@ public class PrettyPrinter { // Print out the number of spaces according to the size, and adjust spaceRemaining. case .space(let size, _): - outputBuffer.enqueueSpaces(size) + if configuration.indentBlankLines, outputBuffer.isAtStartOfLine { + // An empty string write is needed to add line-leading indentation that matches the current indentation on a line that contains only whitespaces. + outputBuffer.write("") + } else { + outputBuffer.enqueueSpaces(size) + } // Print any indentation required, followed by the text content of the syntax token. case .syntax(let text): diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift index 6c3402a0..d6d6c817 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift @@ -70,8 +70,11 @@ struct PrettyPrintBuffer { /// 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. - mutating func writeNewlines(_ newlines: NewlineBehavior) { + /// - Parameters: + /// - newlines: The number and type of newlines to write. + /// - shouldIndentBlankLines: A Boolean value indicating whether to insert spaces + /// for blank lines based on the current indentation level. + mutating func writeNewlines(_ newlines: NewlineBehavior, shouldIndentBlankLines: Bool) { let numberToPrint: Int switch newlines { case .elective: @@ -86,7 +89,15 @@ struct PrettyPrintBuffer { } guard numberToPrint > 0 else { return } - writeRaw(String(repeating: "\n", count: numberToPrint)) + (0..= 1 { + writeRaw(currentIndentation.indentation()) + writeRaw("\n") + } else { + writeRaw("\n") + } + } + lineNumber += numberToPrint isAtStartOfLine = true consecutiveNewlineCount += numberToPrint diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index c3709c60..a8070711 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -3465,7 +3465,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { case .spaces(let n): guard leadingIndent == .spaces(0) else { break } - leadingIndent = .spaces(n) + if config.indentBlankLines, trivia.count > index + 1, trivia[index + 1].isNewline { + appendToken(.space(size: n)) + requiresNextNewline = true + } else { + leadingIndent = .spaces(n) + } case .tabs(let n): guard leadingIndent == .spaces(0) else { break } leadingIndent = .tabs(n) diff --git a/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift b/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift index 8d095767..a3c59372 100644 --- a/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift +++ b/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift @@ -41,6 +41,7 @@ extension Configuration { config.spacesAroundRangeFormationOperators = false config.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration() config.multiElementCollectionTrailingCommas = true + config.indentBlankLines = false return config } } diff --git a/Tests/SwiftFormatTests/PrettyPrint/IndentBlankLinesTests.swift b/Tests/SwiftFormatTests/PrettyPrint/IndentBlankLinesTests.swift new file mode 100644 index 00000000..805d2d35 --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/IndentBlankLinesTests.swift @@ -0,0 +1,196 @@ +import SwiftFormat + +final class IndentBlankLinesTests: PrettyPrintTestCase { + func testIndentBlankLinesEnabled() { + let input = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020}\u{0020} + func bar() -> Int { + return 2 + } + } + """ + + let expected = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020}\u{0020} + func bar() -> Int { + return 2 + } + } + + """ + var config = Configuration.forTesting + config.indentBlankLines = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config) + } + + func testIndentBlankLinesDisabled() { + let input = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020}\u{0020} + func bar() -> Int { + return 2 + } + } + """ + + let expected = + """ + class A { + func foo() -> Int { + return 1 + } + + func bar() -> Int { + return 2 + } + } + + """ + var config = Configuration.forTesting + config.indentBlankLines = false + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config) + } + + func testLineWithMoreWhitespacesThanIndentation() { + let input = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020}\u{0020}\u{0020}\u{0020}\u{0020} + func bar() -> Int { + return 2 + } + } + """ + + let expected = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020}\u{0020} + func bar() -> Int { + return 2 + } + } + + """ + var config = Configuration.forTesting + config.indentBlankLines = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config) + } + + func testLineWithFewerWhitespacesThanIndentation() { + let input = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020} + func bar() -> Int { + return 2 + } + } + """ + + let expected = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020}\u{0020} + func bar() -> Int { + return 2 + } + } + + """ + var config = Configuration.forTesting + config.indentBlankLines = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config) + } + + func testLineWithoutWhitespace() { + let input = + """ + class A { + func foo() -> Int { + return 1 + } + + func bar() -> Int { + return 2 + } + } + """ + + let expected = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020}\u{0020} + func bar() -> Int { + return 2 + } + } + + """ + var config = Configuration.forTesting + config.indentBlankLines = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config) + } + + + func testExpressionsWithUnnecessaryWhitespaces() { + let input = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020}\u{0020} + func bar() -> Int { + return 2 + } + } + """ + + let expected = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020}\u{0020} + func bar() -> Int { + return 2 + } + } + + """ + var config = Configuration.forTesting + config.indentBlankLines = true + assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config) + } +}