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 a32eea51..70ac916a 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' @@ -260,6 +261,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 { let data = try Data(contentsOf: url) @@ -368,6 +376,12 @@ public struct Configuration: Codable, Equatable { 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 f9e9246e..607364ee 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -441,7 +441,7 @@ public class PrettyPrinter { outputBuffer.enqueueSpaces(size) outputBuffer.write("\\") } - outputBuffer.writeNewlines(newline) + outputBuffer.writeNewlines(newline, shouldIndentBlankLines: configuration.indentBlankLines) lastBreak = true } else { if outputBuffer.isAtStartOfLine { @@ -458,6 +458,10 @@ public class PrettyPrinter { // Print out the number of spaces according to the size, and adjust spaceRemaining. case .space(let 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("") + } outputBuffer.enqueueSpaces(size) // Print any indentation required, followed by the text content of the syntax token. diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrintBuffer.swift index 6c3402a0..af9d02f1 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,13 @@ struct PrettyPrintBuffer { } guard numberToPrint > 0 else { return } - writeRaw(String(repeating: "\n", count: numberToPrint)) + for number in 0..= 1 { + writeRaw(currentIndentation.indentation()) + } + writeRaw("\n") + } + lineNumber += numberToPrint isAtStartOfLine = true consecutiveNewlineCount += numberToPrint diff --git a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift index 3832d687..03da7d4b 100644 --- a/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift +++ b/Sources/SwiftFormat/PrettyPrint/TokenStreamCreator.swift @@ -3521,6 +3521,12 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { leadingIndent = nil case .newlines(let count), .carriageReturns(let count), .carriageReturnLineFeeds(let count): + if config.indentBlankLines, + let leadingIndent, leadingIndent.count > 0 + { + requiresNextNewline = true + } + leadingIndent = .spaces(0) guard !isStartOfFile else { break } @@ -3557,6 +3563,7 @@ fileprivate final class TokenStreamCreator: SyntaxVisitor { case .spaces(let n): guard leadingIndent == .spaces(0) else { break } 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..31f8a119 --- /dev/null +++ b/Tests/SwiftFormatTests/PrettyPrint/IndentBlankLinesTests.swift @@ -0,0 +1,294 @@ +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 testConsecutiveLinesWithMoreWhitespacesThanIndentation() { + let input = + """ + class A { + func foo() -> Int { + return 1 + } + \u{0020}\u{0020}\u{0020}\u{0020}\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 testConsecutiveLinesWithFewerWhitespacesThanIndentation() { + 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 testConsecutiveLinesWithoutWhitespace() { + 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) + } +}