Skip to content
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

Early implementation of formatting comments with Markdown. #738

Closed
wants to merge 4 commits into from
Closed
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
2 changes: 2 additions & 0 deletions Sources/SwiftFormat/API/Configuration+Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ extension Configuration {
self.spacesAroundRangeFormationOperators = false
self.noAssignmentInExpressions = NoAssignmentInExpressionsConfiguration()
self.multiElementCollectionTrailingCommas = true
self.wrapComments = false
self.maximumCommentTextWidth = 72
}
}
17 changes: 17 additions & 0 deletions Sources/SwiftFormat/API/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public struct Configuration: Codable, Equatable {
case spacesAroundRangeFormationOperators
case noAssignmentInExpressions
case multiElementCollectionTrailingCommas
case wrapComments
case maximumCommentTextWidth
}

/// A dictionary containing the default enabled/disabled states of rules, keyed by the rules'
Expand Down Expand Up @@ -186,6 +188,13 @@ public struct Configuration: Codable, Equatable {
/// ```
public var multiElementCollectionTrailingCommas: Bool

/// Determines if comments should wrap onto multiple lines when they exceed the line length.
public var wrapComments: Bool

/// The maximum length of the text of a doc comment, after which the formatter will wrap
/// (if comment wrapping is enabled), even if the allowable line length is greater.
public var maximumCommentTextWidth: Int

/// Creates a new `Configuration` by loading it from a configuration file.
public init(contentsOf url: URL) throws {
let data = try Data(contentsOf: url)
Expand Down Expand Up @@ -272,6 +281,12 @@ public struct Configuration: Codable, Equatable {
try container.decodeIfPresent(
Bool.self, forKey: .multiElementCollectionTrailingCommas)
?? defaults.multiElementCollectionTrailingCommas
self.wrapComments =
try container.decodeIfPresent(Bool.self, forKey: .wrapComments)
?? defaults.wrapComments
self.maximumCommentTextWidth =
try container.decodeIfPresent(Int.self, forKey: .maximumCommentTextWidth)
?? defaults.maximumCommentTextWidth

// 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
Expand Down Expand Up @@ -305,6 +320,8 @@ public struct Configuration: Codable, Equatable {
try container.encode(indentSwitchCaseLabels, forKey: .indentSwitchCaseLabels)
try container.encode(noAssignmentInExpressions, forKey: .noAssignmentInExpressions)
try container.encode(multiElementCollectionTrailingCommas, forKey: .multiElementCollectionTrailingCommas)
try container.encode(wrapComments, forKey: .wrapComments)
try container.encode(maximumCommentTextWidth, forKey: .maximumCommentTextWidth)
try container.encode(rules, forKey: .rules)
}

Expand Down
46 changes: 45 additions & 1 deletion Sources/SwiftFormat/PrettyPrint/Comment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//===----------------------------------------------------------------------===//

import Foundation
import Markdown
import SwiftSyntax

extension StringProtocol {
Expand All @@ -31,6 +32,35 @@ extension StringProtocol {
}
}

fileprivate func markdownFormat(_ lines: [String], _ usableWidth: Int, linePrefix: String = "") -> [String] {
let linePrefix = linePrefix + " "
let document = Document(parsing: lines.joined(separator: "\n"), options: .disableSmartOpts)
let lineLimit = MarkupFormatter.Options.PreferredLineLimit(
maxLength: usableWidth,
breakWith: .softBreak
)
let formatterOptions = MarkupFormatter.Options(
orderedListNumerals: .incrementing(start: 1),
useCodeFence: .always,
condenseAutolinks: false,
preferredLineLimit: lineLimit,
customLinePrefix: linePrefix
)
let output = document.format(options: formatterOptions)
let lines = output.split(separator: "\n")
if lines.isEmpty {
return [linePrefix]
}
return lines.map {
// unfortunately we have to do a bit of post-processing...
if let last = $0.last, let secondLast = $0.dropLast().last, last.isWhitespace && secondLast.isWhitespace {
return $0.trimmingTrailingWhitespace() + " \\"
} else {
return $0.trimmingTrailingWhitespace()
}
}
}

struct Comment {
enum Kind {
case line, docLine, block, docBlock
Expand Down Expand Up @@ -85,10 +115,24 @@ struct Comment {
}
}

func print(indent: [Indent]) -> String {
func print(indent: [Indent], width: Int, textWidth: Int, wrap: Bool) -> String {
switch self.kind {
case .docLine where wrap:
let indentation = indent.indentation()
let usableWidth = width - indentation.count
let wrappedLines = markdownFormat(self.text, min(usableWidth - kind.prefixLength, textWidth))
let emptyLinesTrimmed = wrappedLines.map {
if $0.allSatisfy({ $0.isWhitespace }) {
return kind.prefix
} else {
return kind.prefix + $0
}
}
return emptyLinesTrimmed.joined(separator: "\n" + indentation)
case .line, .docLine:
let separator = "\n" + indent.indentation() + kind.prefix
// trailing whitespace is meaningful in Markdown, so we can't remove it
// when formatting comments, but we can here
let trimmedLines = self.text.map { $0.trimmingTrailingWhitespace() }
return kind.prefix + trimmedLines.joined(separator: separator)
case .block, .docBlock:
Expand Down
5 changes: 3 additions & 2 deletions Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,8 @@ public class PrettyPrinter {
case .comment(let comment, let wasEndOfLine):
lastBreak = false

write(comment.print(indent: currentIndentation))
write(comment.print(
indent: currentIndentation, width: configuration.lineLength, textWidth: configuration.maximumCommentTextWidth, wrap: configuration.wrapComments))
if wasEndOfLine {
if comment.length > spaceRemaining && !isBreakingSuppressed {
diagnose(.moveEndOfLineComment, category: .endOfLineComment)
Expand Down Expand Up @@ -803,7 +804,7 @@ public class PrettyPrinter {
print("[COMMENT DocBlock Length: \(length) EOL: \(wasEndOfLine) Idx: \(idx)]")
}
printDebugIndent()
print(comment.print(indent: debugIndent))
print(comment.print(indent: debugIndent, width: configuration.lineLength, textWidth: configuration.maximumCommentTextWidth, wrap: false))

case .verbatim(let verbatim):
printDebugIndent()
Expand Down
187 changes: 187 additions & 0 deletions Tests/SwiftFormatTests/PrettyPrint/CommentTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _SwiftFormatTestSupport
import SwiftFormat

final class CommentTests: PrettyPrintTestCase {
func testDocumentationComments() {
Expand Down Expand Up @@ -795,6 +796,9 @@ final class CommentTests: PrettyPrintTestCase {

"""
assertPrettyPrintEqual(input: input, expected: input, linelength: 80)
var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: input, linelength: 80, configuration: config)
}

func testNonmergeableComments() {
Expand All @@ -811,6 +815,9 @@ final class CommentTests: PrettyPrintTestCase {
"""

assertPrettyPrintEqual(input: input, expected: input, linelength: 80)
var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: input, linelength: 80, configuration: config)
}

func testMergeableComments() {
Expand All @@ -832,4 +839,184 @@ final class CommentTests: PrettyPrintTestCase {

assertPrettyPrintEqual(input: input, expected: input, linelength: 80)
}

func testDocLineFormattingWhitespace() {
let input = """
/// This has trailing whitespace \u{0020}\u{0020}\u{0020}\u{0020}\u{0020}
/// This has leading whitespace.
"""
let expected = #"""
/// This has trailing whitespace \
/// This has leading whitespace.

"""#

var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config)
}

func testDocLineFormattingShortLines() {
let input = """
/// Test imports with comments.
///
/// Comments that are short
/// should be merged.
"""

let expected = """
/// Test imports with comments.
///
/// Comments that are short should be merged.

"""

var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config)
}

func testDocLineFormattingMultipleBlocks() {
let input = """
/// This is one comment block.
/// These lines should merge.
///

/// This is another block. Since these lines are much longer, they should stay separate. Make
/// sure the lines wrap nicely.
"""

let expected = """
/// This is one comment block. These lines should merge.

/// This is another block. Since these lines are much
/// longer, they should stay separate. Make sure the lines
/// wrap nicely.

"""

var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: expected, linelength: 60, configuration: config)
}

func testDocLineFormattingForceNewline() {
let input = """
/// This is one comment block. \u{005c}
/// These lines should not merge.\u{0020}\u{0020}
/// None of them.\u{0020}\u{0020}\u{0020}\u{0020}
/// The end.
"""

let expected = #"""
/// This is one comment block. \
/// These lines should not merge. \
/// None of them. \
/// The end.

"""#

var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config)
}

func testDocLineFormattingBasics() {
let input = """
/// Here is some **bold text** and __another form__ followed
/// by *italic text* and again in _this form_ and then ~~strikethrough~~
/// and **nested bold *and italic* text** and ***bold and italic***
"""

let expected = """
/// Here is some **bold text** and **another form** followed by *italic
/// text* and again in *this form* and then ~strikethrough~ and **nested
/// bold *and italic* text** and ***bold and italic***

"""

var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config)
}

func testDocLineFormattingQuote() {
let input = """
/// > This is some quoted text that should still be wrapped properly.
"""

let expected = """
/// > This is some
/// > quoted text that
/// > should still be
/// > wrapped properly.

"""

var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: expected, linelength: 24, configuration: config)
}

func testDocLineFormattingOrderedList() {
let input = """
/// 4. This is a list.
/// 4. The list should be renumbered.
/// 8. But of course, not reordered.
/// 1. This is a subitem.
/// 1. Final item.
"""

let expected = """
/// 1. This is a list.
/// 2. The list should be renumbered.
/// 3. But of course, not reordered.
/// 1. This is a subitem.
/// 4. Final item.

"""

var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config)
}

func testDocLineFormattingUnorderedList() {
let input = """
/// - This is a list.
/// - The list is unordered.
/// - This is a subitem.
/// - Final item.
"""

let expected = """
/// - This is a list.
/// - The list is unordered.
/// - This is a subitem.
/// - Final item.

"""

var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: expected, linelength: 80, configuration: config)
}

func testDocLineFormattingCodeBlock() {
let input = """
/// ```
/// for (i=0; i<9; i++) {
/// a[i] = b[i];
/// }
/// ```

/// This is inline `*x = *y;` and should be left alone.

"""

var config = Configuration.forTesting
config.wrapComments = true
assertPrettyPrintEqual(input: input, expected: input, linelength: 80, configuration: config)
}

}