mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
Add preserve ifdef indent mode (#2269)
This commit is contained in:
committed by
Cal Stephens
parent
d02ba73bb5
commit
d3b707d7cc
@@ -1392,7 +1392,7 @@ Option | Description
|
||||
`--tab-width` | The width of a tab character. Defaults to "unspecified"
|
||||
`--smart-tabs` | Align code independently of tab-width: "enabled" (default) or "disabled"
|
||||
`--indent-case` | Indent cases inside a switch statement: "true" or "false" (default)
|
||||
`--ifdef` | #if statement indenting: "indent" (default), "no-indent" or "outdent"
|
||||
`--ifdef` | #if statement indenting: "indent" (default), "no-indent", "preserve" or "outdent"
|
||||
`--xcode-indentation` | Match Xcode indenting: "enabled" or "disabled" (default)
|
||||
`--indent-strings` | Indent multiline strings: "true" or "false" (default)
|
||||
|
||||
|
||||
+14
-2
@@ -302,7 +302,8 @@ private struct Inference {
|
||||
}
|
||||
|
||||
let ifdefIndent = OptionInferrer { formatter, options in
|
||||
var indented = 0, notIndented = 0, outdented = 0
|
||||
var indented = 0, notIndented = 0, outdented = 0, preserveCandidates = 0
|
||||
|
||||
formatter.forEach(.startOfScope("#if")) { i, _ in
|
||||
if let indent = formatter.token(at: i - 1), case let .space(string) = indent,
|
||||
!string.isEmpty
|
||||
@@ -318,6 +319,11 @@ private struct Inference {
|
||||
return
|
||||
} else if innerString == string {
|
||||
notIndented += 1
|
||||
if let token = formatter.next(.nonSpaceOrCommentOrLinebreak, after: nextLineIndex),
|
||||
case .operator(".", _) = token
|
||||
{
|
||||
preserveCandidates += 1
|
||||
}
|
||||
} else {
|
||||
// Assume more indented, as less would be a mistake
|
||||
indented += 1
|
||||
@@ -360,7 +366,13 @@ private struct Inference {
|
||||
// Error?
|
||||
}
|
||||
if notIndented > indented {
|
||||
options.ifdefIndent = outdented > notIndented ? .outdent : .noIndent
|
||||
if outdented > notIndented {
|
||||
options.ifdefIndent = .outdent
|
||||
} else if preserveCandidates > 0 {
|
||||
options.ifdefIndent = .preserve
|
||||
} else {
|
||||
options.ifdefIndent = .noIndent
|
||||
}
|
||||
} else {
|
||||
options.ifdefIndent = outdented > indented ? .outdent : .indent
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import Foundation
|
||||
public enum IndentMode: String, CaseIterable {
|
||||
case indent
|
||||
case noIndent = "no-indent"
|
||||
case preserve
|
||||
case outdent
|
||||
|
||||
public init?(rawValue: String) {
|
||||
@@ -43,6 +44,8 @@ public enum IndentMode: String, CaseIterable {
|
||||
self = .indent
|
||||
case "no-indent", "noindent":
|
||||
self = .noIndent
|
||||
case "preserve":
|
||||
self = .preserve
|
||||
case "outdent":
|
||||
self = .outdent
|
||||
default:
|
||||
|
||||
+47
-17
@@ -27,6 +27,15 @@ public extension FormatRule {
|
||||
var indentCounts = [1]
|
||||
var linewrapStack = [false]
|
||||
var lineIndex = 0
|
||||
var preserveIfdefDepth = 0
|
||||
|
||||
@discardableResult
|
||||
func applyIndent(_ indent: String, at index: Int) -> Int {
|
||||
if formatter.options.ifdefIndent == .preserve, preserveIfdefDepth > 0 {
|
||||
return 0
|
||||
}
|
||||
return formatter.insertSpaceIfEnabled(indent, at: index)
|
||||
}
|
||||
|
||||
if formatter.options.fragment,
|
||||
let firstIndex = formatter.index(of: .nonSpaceOrLinebreak, after: -1),
|
||||
@@ -103,12 +112,17 @@ public extension FormatRule {
|
||||
}
|
||||
switch formatter.options.ifdefIndent {
|
||||
case .indent:
|
||||
i += formatter.insertSpaceIfEnabled(indent, at: formatter.startOfLine(at: i))
|
||||
i += applyIndent(indent, at: formatter.startOfLine(at: i))
|
||||
indent += formatter.options.indent
|
||||
case .noIndent:
|
||||
i += formatter.insertSpaceIfEnabled(indent, at: formatter.startOfLine(at: i))
|
||||
i += applyIndent(indent, at: formatter.startOfLine(at: i))
|
||||
case .preserve:
|
||||
indent = formatter.currentIndentForLine(at: i)
|
||||
case .outdent:
|
||||
i += formatter.insertSpaceIfEnabled("", at: formatter.startOfLine(at: i))
|
||||
i += applyIndent("", at: formatter.startOfLine(at: i))
|
||||
}
|
||||
if formatter.options.ifdefIndent == .preserve {
|
||||
preserveIfdefDepth += 1
|
||||
}
|
||||
case "{" where formatter.isFirstStackedClosureArgument(at: i):
|
||||
guard var prevIndex = formatter.index(of: .nonSpace, before: i) else {
|
||||
@@ -213,9 +227,11 @@ public extension FormatRule {
|
||||
let start = formatter.startOfLine(at: i)
|
||||
switch formatter.options.ifdefIndent {
|
||||
case .indent, .noIndent:
|
||||
i += formatter.insertSpaceIfEnabled(indent, at: start)
|
||||
i += applyIndent(indent, at: start)
|
||||
case .outdent:
|
||||
i += formatter.insertSpaceIfEnabled("", at: start)
|
||||
i += applyIndent("", at: start)
|
||||
case .preserve:
|
||||
break
|
||||
}
|
||||
case .keyword("@unknown") where scopeStack.last != .startOfScope("#if"):
|
||||
var indent = indentStack[indentStack.count - 2]
|
||||
@@ -224,7 +240,7 @@ public extension FormatRule {
|
||||
}
|
||||
let start = formatter.startOfLine(at: i)
|
||||
let stringIndent = stringBodyIndentStack.last!
|
||||
i += formatter.insertSpaceIfEnabled(stringIndent + indent, at: start)
|
||||
i += applyIndent(stringIndent + indent, at: start)
|
||||
case .keyword("in") where scopeStack.last == .startOfScope("{"):
|
||||
if let startIndex = formatter.index(of: .startOfScope("{"), before: i),
|
||||
formatter.index(of: .keyword("for"), in: startIndex + 1 ..< i) == nil,
|
||||
@@ -330,7 +346,7 @@ public extension FormatRule {
|
||||
}
|
||||
|
||||
if token == .endOfScope("#endif"), formatter.options.ifdefIndent == .outdent {
|
||||
i += formatter.insertSpaceIfEnabled("", at: start)
|
||||
i += applyIndent("", at: start)
|
||||
} else {
|
||||
var indent = indentStack.last ?? ""
|
||||
if token.isSwitchCaseOrDefault,
|
||||
@@ -339,7 +355,7 @@ public extension FormatRule {
|
||||
indent += formatter.options.indent
|
||||
}
|
||||
let stringIndent = stringBodyIndentStack.last!
|
||||
i += formatter.insertSpaceIfEnabled(stringIndent + indent, at: start)
|
||||
i += applyIndent(stringIndent + indent, at: start)
|
||||
}
|
||||
} else if token == .endOfScope("#endif"), indentStack.count > 1 {
|
||||
var indent = indentStack[indentStack.count - 2]
|
||||
@@ -352,14 +368,19 @@ public extension FormatRule {
|
||||
}
|
||||
switch formatter.options.ifdefIndent {
|
||||
case .indent, .noIndent:
|
||||
i += formatter.insertSpaceIfEnabled(indent, at: formatter.startOfLine(at: i))
|
||||
i += applyIndent(indent, at: formatter.startOfLine(at: i))
|
||||
case .outdent:
|
||||
i += formatter.insertSpaceIfEnabled("", at: formatter.startOfLine(at: i))
|
||||
i += applyIndent("", at: formatter.startOfLine(at: i))
|
||||
case .preserve:
|
||||
break
|
||||
}
|
||||
if scopeStack.last == .startOfScope("#if") {
|
||||
popScope()
|
||||
}
|
||||
}
|
||||
if token == .endOfScope("#endif"), formatter.options.ifdefIndent == .preserve {
|
||||
preserveIfdefDepth = max(preserveIfdefDepth - 1, 0)
|
||||
}
|
||||
}
|
||||
switch token {
|
||||
case .endOfScope("case"):
|
||||
@@ -397,13 +418,13 @@ public extension FormatRule {
|
||||
{
|
||||
// Set indent for comment immediately before this line to match this line
|
||||
if !formatter.isCommentedCode(at: startIndex + 1) {
|
||||
formatter.insertSpaceIfEnabled(indent, at: startIndex + 1)
|
||||
applyIndent(indent, at: startIndex + 1)
|
||||
}
|
||||
if case .endOfScope("*/") = prevToken,
|
||||
var index = formatter.index(of: .startOfScope("/*"), after: startIndex)
|
||||
{
|
||||
while let linebreakIndex = formatter.index(of: .linebreak, after: index) {
|
||||
formatter.insertSpaceIfEnabled(indent + " ", at: linebreakIndex + 1)
|
||||
applyIndent(indent + " ", at: linebreakIndex + 1)
|
||||
index = linebreakIndex
|
||||
}
|
||||
}
|
||||
@@ -580,7 +601,9 @@ public extension FormatRule {
|
||||
if lastToken.isEndOfScope {
|
||||
indent = formatter.currentIndentForLine(at: lastNonSpaceOrLinebreakIndex)
|
||||
}
|
||||
if !lastToken.isEndOfScope || lastToken == .endOfScope("case") ||
|
||||
if formatter.options.ifdefIndent == .preserve, preserveIfdefDepth > 0 {
|
||||
// keep relative indentation unchanged
|
||||
} else if !lastToken.isEndOfScope || lastToken == .endOfScope("case") ||
|
||||
formatter.options.xcodeIndentation, ![
|
||||
.endOfScope("}"), .endOfScope(")"),
|
||||
].contains(lastToken)
|
||||
@@ -602,6 +625,9 @@ public extension FormatRule {
|
||||
guard !formatter.isCommentedCode(at: nextNonSpaceIndex) else {
|
||||
break
|
||||
}
|
||||
if formatter.options.ifdefIndent == .preserve, preserveIfdefDepth > 0 {
|
||||
break
|
||||
}
|
||||
// Apply indent
|
||||
switch nextToken {
|
||||
case .linebreak, .error, .keyword("#else"), .keyword("#elseif"), .endOfScope("#endif"),
|
||||
@@ -630,19 +656,22 @@ public extension FormatRule {
|
||||
{
|
||||
break
|
||||
}
|
||||
formatter.insertSpaceIfEnabled(indent, at: i + 1)
|
||||
applyIndent(indent, at: i + 1)
|
||||
case .endOfScope, .keyword("@unknown"):
|
||||
if let scope = scopeStack.last {
|
||||
switch scope {
|
||||
case .startOfScope("/*"), .startOfScope("#if"),
|
||||
.keyword("#else"), .keyword("#elseif"),
|
||||
.startOfScope where scope.isStringDelimiter:
|
||||
formatter.insertSpaceIfEnabled(indent, at: i + 1)
|
||||
applyIndent(indent, at: i + 1)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
if formatter.options.ifdefIndent == .preserve, preserveIfdefDepth > 0 {
|
||||
break
|
||||
}
|
||||
var lastIndex = lastNonSpaceOrLinebreakIndex > -1 ? lastNonSpaceOrLinebreakIndex : i
|
||||
while formatter.token(at: lastIndex) == .endOfScope("#endif"),
|
||||
let index = formatter.index(of: .startOfScope, before: lastIndex, if: {
|
||||
@@ -682,7 +711,7 @@ public extension FormatRule {
|
||||
break
|
||||
}
|
||||
}
|
||||
formatter.insertSpaceIfEnabled(indent, at: i + 1)
|
||||
applyIndent(indent, at: i + 1)
|
||||
}
|
||||
|
||||
if linewrapped, formatter.shouldIndentNextLine(at: i) {
|
||||
@@ -837,7 +866,7 @@ extension Formatter {
|
||||
}
|
||||
|
||||
func isInIfdef(at i: Int, scopeStack: [Token]) -> Bool {
|
||||
guard scopeStack.last == .startOfScope("#if") else {
|
||||
guard scopeStack.contains(.startOfScope("#if")) else {
|
||||
return false
|
||||
}
|
||||
var index = i - 1
|
||||
@@ -854,6 +883,7 @@ extension Formatter {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func isWrappedDeclaration(at i: Int) -> Bool {
|
||||
guard let keywordIndex = indexOfLastSignificantKeyword(at: i, excluding: [
|
||||
"where", "throws", "rethrows",
|
||||
|
||||
@@ -204,6 +204,24 @@ final class InferenceTests: XCTestCase {
|
||||
XCTAssertEqual(options.ifdefIndent, output)
|
||||
}
|
||||
|
||||
func testInferIfdefPreserve() {
|
||||
let input = """
|
||||
struct ContentView {
|
||||
var body: some View {
|
||||
Text("Example")
|
||||
.frame(maxWidth: 200)
|
||||
#if DEBUG
|
||||
.font(.body)
|
||||
#endif
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
"""
|
||||
let output = IndentMode.preserve
|
||||
let options = inferFormatOptions(from: tokenize(input))
|
||||
XCTAssertEqual(options.ifdefIndent, output)
|
||||
}
|
||||
|
||||
func testInferIndentedIfdefOutdent() {
|
||||
let input = "{\n {\n#if foo\n //foo\n#endif\n }\n}"
|
||||
let output = IndentMode.outdent
|
||||
|
||||
@@ -4645,6 +4645,275 @@ final class IndentTests: XCTestCase {
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
func testIfDefPostfixMemberSyntaxPreserveKeepsAlignment() {
|
||||
let input = """
|
||||
struct Example: View {
|
||||
var body: some View {
|
||||
Text("Example")
|
||||
.frame(maxWidth: 500, alignment: .leading)
|
||||
#if !os(tvOS)
|
||||
.font(.system(size: 14, design: .monospaced))
|
||||
#endif
|
||||
.padding(10)
|
||||
}
|
||||
}
|
||||
"""
|
||||
let options = FormatOptions(ifdefIndent: .preserve)
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
func testIfDefPreserveWithinIndentedChain() {
|
||||
let input = """
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Hello World")
|
||||
}
|
||||
.foregroundStyle(Color.white)
|
||||
#if os(iOS)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
"""
|
||||
let options = FormatOptions(ifdefIndent: .preserve)
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
func testIfDefPreserveWithinNestedChainBlock() {
|
||||
let input = """
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Hello World")
|
||||
}
|
||||
.foregroundStyle(Color.white)
|
||||
#if os(iOS)
|
||||
.background {
|
||||
Color.black
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
"""
|
||||
let options = FormatOptions(ifdefIndent: .preserve)
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
func testIfDefPreserveWithinNestedChainBlock2() {
|
||||
let input = """
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Hello World")
|
||||
}
|
||||
.foregroundStyle(Color.white)
|
||||
#if os(iOS)
|
||||
.background {
|
||||
Color.black
|
||||
.overlay {
|
||||
Color.white
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
"""
|
||||
let options = FormatOptions(ifdefIndent: .preserve)
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
func testIfDefPreserveWithinNestedChainBlock3() {
|
||||
let input = """
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Hello World")
|
||||
}
|
||||
.foregroundStyle(Color.white)
|
||||
#if os(iOS)
|
||||
.background {
|
||||
Color.black
|
||||
.overlay {
|
||||
Color.white
|
||||
.mask {
|
||||
Circle()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
"""
|
||||
let options = FormatOptions(ifdefIndent: .preserve)
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
func testIfDefPreserveWithinNestedChainBlock4() {
|
||||
let input = """
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Hello World")
|
||||
}
|
||||
.foregroundStyle(Color.white)
|
||||
#if os(iOS)
|
||||
.background {
|
||||
Color.black
|
||||
.overlay {
|
||||
Color.white
|
||||
.mask {
|
||||
Circle()
|
||||
.overlay {
|
||||
Rectangle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
"""
|
||||
let options = FormatOptions(ifdefIndent: .preserve)
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
func testIfDefPreserveWithCommentBeforeModifier() {
|
||||
let input = """
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Text("Hello")
|
||||
.frame(maxWidth: 200)
|
||||
#if os(iOS)
|
||||
// comment about padding
|
||||
.padding(4)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
"""
|
||||
let options = FormatOptions(ifdefIndent: .preserve)
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
func testIfDefPreserveWithMultiplePlatformBranches() {
|
||||
let input = """
|
||||
import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
@Suite
|
||||
struct NavigationViewWithColumnsStyleTests {
|
||||
#if canImport(UIKit) && (os(iOS) || os(visionOS))
|
||||
typealias PlatformNavigationViewWithColumnsStyle = UISplitViewController
|
||||
#elseif canImport(UIKit) && os(tvOS)
|
||||
typealias PlatformNavigationViewWithColumnsStyle = UINavigationController
|
||||
#elseif canImport(AppKit)
|
||||
typealias PlatformNavigationViewWithColumnsStyle = NSSplitView
|
||||
#endif
|
||||
|
||||
@Test func introspect() async throws {
|
||||
try await introspection(of: PlatformNavigationViewWithColumnsStyle.self) { spy in
|
||||
NavigationView {
|
||||
ZStack {
|
||||
Color.red
|
||||
Text("Something")
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(DoubleColumnNavigationViewStyle())
|
||||
#if os(iOS) || os(visionOS)
|
||||
.introspect(.navigationView(style: .columns), on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26), .visionOS(.v1, .v2, .v26), customize: spy)
|
||||
#elseif os(tvOS)
|
||||
.introspect(.navigationView(style: .columns), on: .tvOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26), customize: spy)
|
||||
#elseif os(macOS)
|
||||
.introspect(.navigationView(style: .columns), on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15, .v26), customize: spy)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@Test func introspectAsAncestor() async throws {
|
||||
try await introspection(of: PlatformNavigationViewWithColumnsStyle.self) { spy in
|
||||
NavigationView {
|
||||
ZStack {
|
||||
Color.red
|
||||
Text("Something")
|
||||
#if os(iOS) || os(visionOS)
|
||||
.introspect(.navigationView(style: .columns), on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26), .visionOS(.v1, .v2, .v26), scope: .ancestor, customize: spy)
|
||||
#elseif os(tvOS)
|
||||
.introspect(.navigationView(style: .columns), on: .tvOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26), scope: .ancestor, customize: spy)
|
||||
#elseif os(macOS)
|
||||
.introspect(.navigationView(style: .columns), on: .macOS(.v10_15, .v11, .v12, .v13, .v14, .v15, .v26), scope: .ancestor, customize: spy)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(DoubleColumnNavigationViewStyle())
|
||||
#if os(iOS)
|
||||
// NB: this is necessary for ancestor introspection to work, because initially on iPad the "Customized" text isn't shown as it's hidden in the sidebar. This is why ancestor introspection is discouraged for most situations and it's opt-in.
|
||||
.introspect(.navigationView(style: .columns), on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18, .v26)) {
|
||||
$0.preferredDisplayMode = .oneOverSecondary
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
let options = FormatOptions(ifdefIndent: .preserve)
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
func testIfDefPreserveMultipleModifiersInChain() {
|
||||
let input = """
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Text("Example")
|
||||
.frame(maxWidth: 200)
|
||||
#if os(iOS)
|
||||
.padding(4)
|
||||
.background {
|
||||
Color.red
|
||||
.overlay {
|
||||
Text("Inner")
|
||||
}
|
||||
}
|
||||
.cornerRadius(8)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
"""
|
||||
let options = FormatOptions(ifdefIndent: .preserve)
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
func testIfDefPreserveWithElseIfBranches() {
|
||||
let input = """
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Text("Example")
|
||||
.frame(maxWidth: 200)
|
||||
#if os(iOS)
|
||||
.padding(4)
|
||||
.background {
|
||||
Color.red
|
||||
}
|
||||
#elseif os(macOS)
|
||||
.padding(10)
|
||||
.background {
|
||||
Color.blue
|
||||
.overlay {
|
||||
Circle()
|
||||
}
|
||||
}
|
||||
#else
|
||||
.foregroundColor(.gray)
|
||||
.shadow(radius: 2)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
"""
|
||||
let options = FormatOptions(ifdefIndent: .preserve)
|
||||
testFormatting(for: input, rule: .indent, options: options)
|
||||
}
|
||||
|
||||
// indent #if/#else/#elseif/#endif (mode: outdent)
|
||||
|
||||
func testIfEndifOutdenting() {
|
||||
|
||||
Reference in New Issue
Block a user