mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-06-16 10:34:34 +00:00
407 lines
17 KiB
Swift
407 lines
17 KiB
Swift
//
|
|
// FormattingHelpers.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Nick Lockwood on 16/08/2020.
|
|
// Copyright © 2020 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
// MARK: shared helper methods
|
|
|
|
extension Formatter {
|
|
// remove self if possible
|
|
func removeSelf(at i: Int, localNames: Set<String>) -> Bool {
|
|
assert(tokens[i] == .identifier("self"))
|
|
guard let dotIndex = index(of: .nonSpaceOrLinebreak, after: i, if: {
|
|
$0 == .operator(".", .infix)
|
|
}), let nextIndex = index(of: .nonSpaceOrLinebreak, after: dotIndex, if: {
|
|
$0.isIdentifier && !localNames.contains($0.unescaped())
|
|
}), !backticksRequired(at: nextIndex, ignoreLeadingDot: true) else {
|
|
return false
|
|
}
|
|
removeTokens(in: i ..< nextIndex)
|
|
return true
|
|
}
|
|
|
|
// Shared wrap implementation
|
|
func wrapCollectionsAndArguments(completePartialWrapping: Bool, wrapSingleArguments: Bool) {
|
|
let maxWidth = options.maxWidth
|
|
func removeLinebreakBeforeEndOfScope(at endOfScope: inout Int) {
|
|
guard let lastIndex = index(of: .nonSpace, before: endOfScope, if: {
|
|
$0.isLinebreak
|
|
}) else {
|
|
return
|
|
}
|
|
if case .commentBody? = last(.nonSpace, before: lastIndex) {
|
|
return
|
|
}
|
|
// Remove linebreak
|
|
removeTokens(in: lastIndex ..< endOfScope)
|
|
endOfScope = lastIndex
|
|
// Remove trailing comma
|
|
if let prevCommaIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: endOfScope, if: {
|
|
$0 == .delimiter(",")
|
|
}) {
|
|
removeToken(at: prevCommaIndex)
|
|
endOfScope -= 1
|
|
}
|
|
}
|
|
|
|
func keepParameterLabelsOnSameLine(startOfScope i: Int, endOfScope: inout Int) {
|
|
var endIndex = endOfScope
|
|
while let index = self.lastIndex(of: .linebreak, in: i + 1 ..< endIndex) {
|
|
endIndex = index
|
|
// Check if this linebreak sits between two identifiers
|
|
// (e.g. the external and internal argument labels)
|
|
guard let lastIndex = self.index(of: .nonSpaceOrLinebreak, before: index, if: {
|
|
$0.isIdentifier
|
|
}), let nextIndex = self.index(of: .nonSpaceOrLinebreak, after: index, if: {
|
|
$0.isIdentifier
|
|
}) else {
|
|
continue
|
|
}
|
|
// Remove linebreak
|
|
let range = lastIndex + 1 ..< nextIndex
|
|
let linebreakAndIndent = tokens[index ..< nextIndex]
|
|
replaceTokens(in: range, with: .space(" "))
|
|
endOfScope -= (range.count - 1)
|
|
// Insert replacement linebreak after next comma
|
|
if let nextComma = self.index(of: .delimiter(","), after: index) {
|
|
if token(at: nextComma + 1)?.isSpace == true {
|
|
replaceToken(at: nextComma + 1, with: linebreakAndIndent)
|
|
endOfScope += linebreakAndIndent.count - 1
|
|
} else {
|
|
insert(Array(linebreakAndIndent), at: nextComma + 1)
|
|
endOfScope += linebreakAndIndent.count
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func wrapArgumentsBeforeFirst(startOfScope i: Int,
|
|
endOfScope: Int,
|
|
allowGrouping: Bool,
|
|
endOfScopeOnSameLine: Bool)
|
|
{
|
|
// Get indent
|
|
let indent = indentForLine(at: i)
|
|
var endOfScope = endOfScope
|
|
|
|
keepParameterLabelsOnSameLine(startOfScope: i,
|
|
endOfScope: &endOfScope)
|
|
|
|
if endOfScopeOnSameLine {
|
|
removeLinebreakBeforeEndOfScope(at: &endOfScope)
|
|
} else {
|
|
// Insert linebreak before closing paren
|
|
if let lastIndex = self.index(of: .nonSpace, before: endOfScope) {
|
|
endOfScope += insertSpace(indent, at: lastIndex + 1)
|
|
if !tokens[lastIndex].isLinebreak {
|
|
insertLinebreak(at: lastIndex + 1)
|
|
endOfScope += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// Insert linebreak after each comma
|
|
var index = self.index(of: .nonSpaceOrCommentOrLinebreak, before: endOfScope)!
|
|
if tokens[index] != .delimiter(",") {
|
|
index += 1
|
|
}
|
|
while let commaIndex = self.lastIndex(of: .delimiter(","), in: i + 1 ..< index),
|
|
var linebreakIndex = self.index(of: .nonSpaceOrComment, after: commaIndex)
|
|
{
|
|
if let index = self.index(of: .nonSpace, before: linebreakIndex) {
|
|
linebreakIndex = index + 1
|
|
}
|
|
if !isCommentedCode(at: linebreakIndex + 1) {
|
|
if tokens[linebreakIndex].isLinebreak, !options.truncateBlankLines ||
|
|
next(.nonSpace, after: linebreakIndex).map({ !$0.isLinebreak }) ?? false
|
|
{
|
|
insertSpace(indent + options.indent, at: linebreakIndex + 1)
|
|
} else if !allowGrouping || (maxWidth > 0 &&
|
|
lineLength(at: linebreakIndex) > maxWidth &&
|
|
lineLength(upTo: linebreakIndex) <= maxWidth)
|
|
{
|
|
insertLinebreak(at: linebreakIndex)
|
|
insertSpace(indent + options.indent, at: linebreakIndex + 1)
|
|
}
|
|
}
|
|
index = commaIndex
|
|
}
|
|
// Insert linebreak and indent after opening paren
|
|
if let nextIndex = self.index(of: .nonSpaceOrComment, after: i) {
|
|
if !tokens[nextIndex].isLinebreak {
|
|
insertLinebreak(at: nextIndex)
|
|
}
|
|
if nextIndex + 1 < endOfScope {
|
|
var indent = indent
|
|
if (self.index(of: .nonSpace, after: nextIndex) ?? 0) < endOfScope {
|
|
indent += options.indent
|
|
}
|
|
insertSpace(indent, at: nextIndex + 1)
|
|
}
|
|
}
|
|
}
|
|
func wrapArgumentsAfterFirst(startOfScope i: Int, endOfScope: Int, allowGrouping: Bool) {
|
|
guard var firstArgumentIndex = self.index(of: .nonSpaceOrLinebreak, in: i + 1 ..< endOfScope) else {
|
|
return
|
|
}
|
|
|
|
var endOfScope = endOfScope
|
|
keepParameterLabelsOnSameLine(startOfScope: i,
|
|
endOfScope: &endOfScope)
|
|
|
|
// Remove linebreak after opening paren
|
|
removeTokens(in: i + 1 ..< firstArgumentIndex)
|
|
endOfScope -= (firstArgumentIndex - (i + 1))
|
|
firstArgumentIndex = i + 1
|
|
// Get indent
|
|
let start = startOfLine(at: i)
|
|
let indent = spaceEquivalentToTokens(from: start, upTo: firstArgumentIndex)
|
|
removeLinebreakBeforeEndOfScope(at: &endOfScope)
|
|
// Insert linebreak after each comma
|
|
var lastBreakIndex: Int?
|
|
var index = firstArgumentIndex
|
|
while let commaIndex = self.index(of: .delimiter(","), in: index ..< endOfScope),
|
|
var linebreakIndex = self.index(of: .nonSpaceOrComment, after: commaIndex)
|
|
{
|
|
if let index = self.index(of: .nonSpace, before: linebreakIndex) {
|
|
linebreakIndex = index + 1
|
|
}
|
|
if maxWidth > 0, lineLength(upTo: commaIndex) >= maxWidth, let breakIndex = lastBreakIndex {
|
|
endOfScope += 1 + insertSpace(indent, at: breakIndex)
|
|
insertLinebreak(at: breakIndex)
|
|
lastBreakIndex = nil
|
|
index = commaIndex + 1
|
|
continue
|
|
}
|
|
if tokens[linebreakIndex].isLinebreak {
|
|
if linebreakIndex + 1 != endOfScope, !isCommentedCode(at: linebreakIndex + 1) {
|
|
endOfScope += insertSpace(indent, at: linebreakIndex + 1)
|
|
}
|
|
} else if !allowGrouping {
|
|
insertLinebreak(at: linebreakIndex)
|
|
endOfScope += 1 + insertSpace(indent, at: linebreakIndex + 1)
|
|
} else {
|
|
lastBreakIndex = linebreakIndex
|
|
}
|
|
index = commaIndex + 1
|
|
}
|
|
if maxWidth > 0, let breakIndex = lastBreakIndex, lineLength(at: breakIndex) > maxWidth {
|
|
insertSpace(indent, at: breakIndex)
|
|
insertLinebreak(at: breakIndex)
|
|
}
|
|
}
|
|
|
|
var lastIndex = -1
|
|
forEachToken(onlyWhereEnabled: false) { i, token in
|
|
guard case let .startOfScope(string) = token else {
|
|
return
|
|
}
|
|
guard ["(", "[", "<"].contains(string) else {
|
|
lastIndex = i
|
|
return
|
|
}
|
|
|
|
if lastIndex < i, let i = (lastIndex + 1 ..< i).last(where: {
|
|
tokens[$0].isLinebreak
|
|
}) {
|
|
lastIndex = i
|
|
}
|
|
|
|
guard let endOfScope = endOfScope(at: i) else {
|
|
return
|
|
}
|
|
|
|
let mode: WrapMode
|
|
var endOfScopeOnSameLine = false
|
|
let hasMultipleArguments = index(of: .delimiter(","), in: i + 1 ..< endOfScope) != nil
|
|
var isParameters = false
|
|
switch string {
|
|
case "(":
|
|
/// Don't wrap color/image literals due to Xcode bug
|
|
guard let prevToken = self.token(at: i - 1),
|
|
prevToken != .keyword("#colorLiteral"),
|
|
prevToken != .keyword("#imageLiteral")
|
|
else {
|
|
return
|
|
}
|
|
guard hasMultipleArguments || wrapSingleArguments ||
|
|
index(in: i + 1 ..< endOfScope, where: { $0.isComment }) != nil
|
|
else {
|
|
// Not an argument list, or only one argument
|
|
lastIndex = i
|
|
return
|
|
}
|
|
|
|
endOfScopeOnSameLine = options.closingParenOnSameLine
|
|
isParameters = isParameterList(at: i)
|
|
if isParameters, options.wrapParameters != .default {
|
|
mode = options.wrapParameters
|
|
} else {
|
|
mode = options.wrapArguments
|
|
}
|
|
case "<":
|
|
mode = options.wrapArguments
|
|
case "[":
|
|
mode = options.wrapCollections
|
|
default:
|
|
return
|
|
}
|
|
guard mode != .disabled, let firstIdentifierIndex =
|
|
index(of: .nonSpaceOrCommentOrLinebreak, after: i),
|
|
!isStringLiteral(at: i)
|
|
else {
|
|
lastIndex = i
|
|
return
|
|
}
|
|
|
|
guard isEnabled else {
|
|
lastIndex = i
|
|
return
|
|
}
|
|
|
|
if completePartialWrapping,
|
|
let firstLinebreakIndex = index(of: .linebreak, in: i + 1 ..< endOfScope)
|
|
{
|
|
switch mode {
|
|
case .beforeFirst:
|
|
wrapArgumentsBeforeFirst(startOfScope: i,
|
|
endOfScope: endOfScope,
|
|
allowGrouping: firstIdentifierIndex > firstLinebreakIndex,
|
|
endOfScopeOnSameLine: endOfScopeOnSameLine)
|
|
case .preserve where firstIdentifierIndex > firstLinebreakIndex:
|
|
wrapArgumentsBeforeFirst(startOfScope: i,
|
|
endOfScope: endOfScope,
|
|
allowGrouping: true,
|
|
endOfScopeOnSameLine: endOfScopeOnSameLine)
|
|
case .afterFirst, .preserve:
|
|
wrapArgumentsAfterFirst(startOfScope: i,
|
|
endOfScope: endOfScope,
|
|
allowGrouping: true)
|
|
case .disabled, .default:
|
|
assertionFailure() // Shouldn't happen
|
|
}
|
|
|
|
} else if maxWidth > 0, hasMultipleArguments || wrapSingleArguments {
|
|
func willWrapAtStartOfReturnType(maxWidth: Int) -> Bool {
|
|
return isInReturnType(at: i) && maxWidth < lineLength(at: i)
|
|
}
|
|
|
|
func startOfNextScopeNotInReturnType() -> Int? {
|
|
let endOfLine = self.endOfLine(at: i)
|
|
guard endOfScope < endOfLine else { return nil }
|
|
|
|
var startOfLastScopeOnLine = endOfScope
|
|
|
|
repeat {
|
|
guard let startOfNextScope = index(
|
|
of: .startOfScope,
|
|
in: startOfLastScopeOnLine + 1 ..< endOfLine
|
|
) else {
|
|
return nil
|
|
}
|
|
|
|
startOfLastScopeOnLine = startOfNextScope
|
|
} while isInReturnType(at: startOfLastScopeOnLine)
|
|
|
|
return startOfLastScopeOnLine
|
|
}
|
|
|
|
func indexOfNextWrap() -> Int? {
|
|
let startOfNextScopeOnLine = startOfNextScopeNotInReturnType()
|
|
let nextNaturalWrap = indexWhereLineShouldWrap(from: endOfScope + 1)
|
|
|
|
switch (startOfNextScopeOnLine, nextNaturalWrap) {
|
|
case let (.some(startOfNextScopeOnLine), .some(nextNaturalWrap)):
|
|
return min(startOfNextScopeOnLine, nextNaturalWrap)
|
|
case let (nil, .some(nextNaturalWrap)):
|
|
return nextNaturalWrap
|
|
case let (.some(startOfNextScopeOnLine), nil):
|
|
return startOfNextScopeOnLine
|
|
case (nil, nil):
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func wrapArgumentsWithoutPartialWrapping() {
|
|
switch mode {
|
|
case .preserve, .beforeFirst:
|
|
wrapArgumentsBeforeFirst(startOfScope: i,
|
|
endOfScope: endOfScope,
|
|
allowGrouping: false,
|
|
endOfScopeOnSameLine: endOfScopeOnSameLine)
|
|
case .afterFirst:
|
|
wrapArgumentsAfterFirst(startOfScope: i,
|
|
endOfScope: endOfScope,
|
|
allowGrouping: true)
|
|
case .disabled, .default:
|
|
assertionFailure() // Shouldn't happen
|
|
}
|
|
}
|
|
|
|
if currentRule == FormatRules.wrap {
|
|
let nextWrapIndex = indexOfNextWrap() ?? endOfLine(at: i)
|
|
if nextWrapIndex > lastIndex,
|
|
maxWidth < lineLength(from: max(lastIndex, 0), upTo: nextWrapIndex),
|
|
!willWrapAtStartOfReturnType(maxWidth: maxWidth)
|
|
{
|
|
wrapArgumentsWithoutPartialWrapping()
|
|
lastIndex = nextWrapIndex
|
|
return
|
|
}
|
|
} else if maxWidth < lineLength(upTo: endOfScope) {
|
|
wrapArgumentsWithoutPartialWrapping()
|
|
}
|
|
}
|
|
|
|
lastIndex = i
|
|
}
|
|
}
|
|
|
|
func removeParen(at index: Int) {
|
|
func tokenOutsideParenRequiresSpacing(at index: Int) -> Bool {
|
|
guard let token = self.token(at: index) else { return false }
|
|
switch token {
|
|
case .identifier, .keyword, .number, .startOfScope("#if"):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func tokenInsideParenRequiresSpacing(at index: Int) -> Bool {
|
|
guard let token = self.token(at: index) else { return false }
|
|
switch token {
|
|
case .operator, .startOfScope("{"), .endOfScope("}"):
|
|
return true
|
|
default:
|
|
return tokenOutsideParenRequiresSpacing(at: index)
|
|
}
|
|
}
|
|
|
|
if token(at: index - 1)?.isSpace == true,
|
|
token(at: index + 1)?.isSpace == true
|
|
{
|
|
// Need to remove one
|
|
removeToken(at: index + 1)
|
|
} else if case .startOfScope = tokens[index] {
|
|
if tokenOutsideParenRequiresSpacing(at: index - 1),
|
|
tokenInsideParenRequiresSpacing(at: index + 1)
|
|
{
|
|
// Need to insert one
|
|
insert(.space(" "), at: index + 1)
|
|
}
|
|
} else if tokenInsideParenRequiresSpacing(at: index - 1),
|
|
tokenOutsideParenRequiresSpacing(at: index + 1)
|
|
{
|
|
// Need to insert one
|
|
insert(.space(" "), at: index + 1)
|
|
}
|
|
removeToken(at: index)
|
|
}
|
|
}
|