diff --git a/Rules.md b/Rules.md index c1a19b1f..173cf755 100644 --- a/Rules.md +++ b/Rules.md @@ -472,6 +472,7 @@ Option | Description --- | --- `--indent` | Number of spaces to indent, or "tab" to use tabs `--tabwidth` | The width of a tab character. Defaults to "unspecified" +`--smarttabs` | Align code independently of tab width. defaults to "enabled" `--indentcase` | Indent cases inside a switch: "true" or "false" (default) `--ifdef` | #if indenting: "indent" (default), "no-indent" or "outdent" `--xcodeindentation` | Xcode indent guard/enum: "enabled" or "disabled" (default) diff --git a/Sources/CommandLine.swift b/Sources/CommandLine.swift index a5b92b04..f209830b 100644 --- a/Sources/CommandLine.swift +++ b/Sources/CommandLine.swift @@ -816,8 +816,8 @@ func processInput(_ inputURLs: [URL], if formatOptions.swiftVersion == .undefined { print("warning: No Swift version was specified, so some formatting features were disabled. Specify the version of Swift you are using with the --swiftversion option, or by adding a \(swiftVersionFile) file to your project.", as: .warning) } - if formatOptions.useTabs, formatOptions.tabWidth <= 0 { - print("warning: The --indent option is set to tabs, but no --tabwidth was specified.", as: .warning) + if formatOptions.useTabs, formatOptions.tabWidth <= 0, !formatOptions.smartTabs { + print("warning: The --smarttabs option is disabled, but no --tabwidth was specified.", as: .warning) } showedConfigurationWarnings = true } @@ -838,11 +838,7 @@ func processInput(_ inputURLs: [URL], // Override options var options = options try options.addArguments(overrides, in: "") // No need for directory as overrides are formatOptions only - // Validate options let formatOptions = options.formatOptions ?? .default - if formatOptions.useTabs, formatOptions.tabWidth <= 0 { - throw FormatError.options("Indenting with tabs requires --tabwidth to also be set") - } // Check cache let rules = options.rules ?? allRules.subtracting(FormatRules.disabledByDefault) let configHash = computeHash("\(formatOptions)\(rules.sorted().joined(separator: ","))") diff --git a/Sources/Options.swift b/Sources/Options.swift index 727c84c5..c3e50053 100644 --- a/Sources/Options.swift +++ b/Sources/Options.swift @@ -285,6 +285,7 @@ public struct FormatOptions: CustomStringConvertible { public var xcodeIndentation: Bool public var tabWidth: Int public var maxWidth: Int + public var smartTabs: Bool public var noSpaceOperators: Set public var noWrapOperators: Set public var modifierOrder: [String] @@ -342,6 +343,7 @@ public struct FormatOptions: CustomStringConvertible { xcodeIndentation: Bool = false, tabWidth: Int = 0, maxWidth: Int = 0, + smartTabs: Bool = true, noSpaceOperators: Set = [], noWrapOperators: Set = [], modifierOrder: [String] = [], @@ -391,6 +393,7 @@ public struct FormatOptions: CustomStringConvertible { self.xcodeIndentation = xcodeIndentation self.tabWidth = tabWidth self.maxWidth = maxWidth + self.smartTabs = smartTabs self.noSpaceOperators = noSpaceOperators self.noWrapOperators = noWrapOperators self.modifierOrder = modifierOrder diff --git a/Sources/OptionsDescriptor.swift b/Sources/OptionsDescriptor.swift index c3515b6e..f44b31e6 100644 --- a/Sources/OptionsDescriptor.swift +++ b/Sources/OptionsDescriptor.swift @@ -279,6 +279,7 @@ extension FormatOptions.Descriptor { xcodeIndentation, tabWidth, maxWidth, + smartTabs, modifierOrder, noSpaceOperators, noWrapOperators, @@ -601,6 +602,15 @@ extension FormatOptions.Descriptor { fromArgument: { $0.lowercased() == "none" ? 0 : Int($0).map { max(0, $0) } }, toArgument: { $0 > 0 ? String($0) : "none" } ) + static let smartTabs = FormatOptions.Descriptor( + argumentName: "smarttabs", + propertyName: "smartTabs", + displayName: "Smart Tabs", + help: "Align code independently of tab width. defaults to \"enabled\"", + keyPath: \.smartTabs, + trueValues: ["enabled", "true"], + falseValues: ["disabled", "false"] + ) static let noSpaceOperators = FormatOptions.Descriptor( argumentName: "nospaceoperators", propertyName: "noSpaceOperators", diff --git a/Sources/ParsingHelpers.swift b/Sources/ParsingHelpers.swift index 5becce32..bcd9d8b3 100644 --- a/Sources/ParsingHelpers.swift +++ b/Sources/ParsingHelpers.swift @@ -76,7 +76,7 @@ extension Formatter { /// Returns white space made up of indent characters equvialent to the specified width func spaceEquivalentToWidth(_ width: Int) -> String { - if options.useTabs, options.tabWidth > 0 { + if !options.smartTabs, options.useTabs, options.tabWidth > 0 { let tabs = width / options.tabWidth let remainder = width % options.tabWidth return String(repeating: "\t", count: tabs) + String(repeating: " ", count: remainder) @@ -86,7 +86,7 @@ extension Formatter { /// Returns white space made up of indent characters equvialent to the specified token range func spaceEquivalentToTokens(from start: Int, upTo end: Int) -> String { - if options.useTabs, options.tabWidth > 0 { + if !options.smartTabs, options.useTabs, options.tabWidth > 0 { return spaceEquivalentToWidth(lineLength(from: start, upTo: end)) } return tokens[start ..< end].reduce(into: "") { result, token in diff --git a/Sources/Rules.swift b/Sources/Rules.swift index 3d96135b..88634bfd 100644 --- a/Sources/Rules.swift +++ b/Sources/Rules.swift @@ -911,7 +911,7 @@ public struct _FormatRules { public let indent = FormatRule( help: "Indent code in accordance with the scope level.", orderAfter: ["trailingSpace", "wrap", "wrapArguments", "wrapMultilineStatementBraces"], - options: ["indent", "tabwidth", "indentcase", "ifdef", "xcodeindentation"], + options: ["indent", "tabwidth", "smarttabs", "indentcase", "ifdef", "xcodeindentation"], sharedOptions: ["trimwhitespace", "closingparen"] ) { formatter in var scopeStack: [Token] = [] @@ -3142,7 +3142,7 @@ public struct _FormatRules { help: "Wrap lines that exceed the specified maximum width.", options: ["maxwidth", "nowrapoperators"], sharedOptions: ["wraparguments", "wrapparameters", "wrapcollections", "closingparen", "indent", - "trimwhitespace", "linebreaks", "tabwidth", "maxwidth"] + "trimwhitespace", "linebreaks", "tabwidth", "maxwidth", "smarttabs"] ) { formatter in let maxWidth = formatter.options.maxWidth guard maxWidth > 0 else { return } @@ -3195,7 +3195,7 @@ public struct _FormatRules { help: "Align wrapped function arguments or collection elements.", orderAfter: ["wrap"], options: ["wraparguments", "wrapparameters", "wrapcollections", "closingparen"], - sharedOptions: ["indent", "trimwhitespace", "linebreaks", "tabwidth", "maxwidth"] + sharedOptions: ["indent", "trimwhitespace", "linebreaks", "tabwidth", "maxwidth", "smarttabs"] ) { formatter in formatter.wrapCollectionsAndArguments(completePartialWrapping: true, wrapSingleArguments: false) diff --git a/Tests/ArgumentsTests.swift b/Tests/ArgumentsTests.swift index a70c41a1..f4540926 100644 --- a/Tests/ArgumentsTests.swift +++ b/Tests/ArgumentsTests.swift @@ -208,7 +208,7 @@ class ArgumentsTests: XCTestCase { } func testCommandLineArgumentsAreCorrect() { - let output = ["allman": "false", "wraparguments": "preserve", "wrapparameters": "preserve", "stripunusedargs": "always", "self": "remove", "header": "ignore", "importgrouping": "alphabetized", "fractiongrouping": "disabled", "binarygrouping": "4,8", "octalgrouping": "4,8", "indentcase": "false", "trimwhitespace": "always", "decimalgrouping": "3,6", "exponentgrouping": "disabled", "patternlet": "hoist", "commas": "always", "wrapcollections": "preserve", "semicolons": "inline", "indent": "4", "exponentcase": "lowercase", "operatorfunc": "spaced", "symlinks": "ignore", "elseposition": "same-line", "voidtype": "void", "hexliteralcase": "uppercase", "linebreaks": "lf", "hexgrouping": "4,8", "ifdef": "indent", "closingparen": "balanced", "selfrequired": "", "trailingclosures": "", "xcodeindentation": "disabled", "fragment": "false", "conflictmarkers": "reject", "tabwidth": "unspecified", "maxwidth": "none", "nospaceoperators": "", "nowrapoperators": "", "modifierorder": "", "minversion": "0", "shortoptionals": "always", "funcattributes": "preserve", "typeattributes": "preserve"] + let output = ["allman": "false", "wraparguments": "preserve", "wrapparameters": "preserve", "stripunusedargs": "always", "self": "remove", "header": "ignore", "importgrouping": "alphabetized", "fractiongrouping": "disabled", "binarygrouping": "4,8", "octalgrouping": "4,8", "indentcase": "false", "trimwhitespace": "always", "decimalgrouping": "3,6", "exponentgrouping": "disabled", "patternlet": "hoist", "commas": "always", "wrapcollections": "preserve", "semicolons": "inline", "indent": "4", "exponentcase": "lowercase", "operatorfunc": "spaced", "symlinks": "ignore", "elseposition": "same-line", "voidtype": "void", "hexliteralcase": "uppercase", "linebreaks": "lf", "hexgrouping": "4,8", "ifdef": "indent", "closingparen": "balanced", "selfrequired": "", "trailingclosures": "", "xcodeindentation": "disabled", "fragment": "false", "conflictmarkers": "reject", "tabwidth": "unspecified", "maxwidth": "none", "smarttabs": "enabled", "nospaceoperators": "", "nowrapoperators": "", "modifierorder": "", "minversion": "0", "shortoptionals": "always", "funcattributes": "preserve", "typeattributes": "preserve"] XCTAssertEqual(argumentsFor(.default), output) } diff --git a/Tests/MetadataTests.swift b/Tests/MetadataTests.swift index 9ca4cfb5..c132de52 100644 --- a/Tests/MetadataTests.swift +++ b/Tests/MetadataTests.swift @@ -121,12 +121,10 @@ class MetadataTests: XCTestCase { continue } switch formatter.tokens[index] { - case let .identifier(fn) where [ - "spaceEquivalentToWidth", - "spaceEquivalentToTokens", - "tokenLength", - "lineLength", - ].contains(fn): + case .identifier("spaceEquivalentToWidth"), + .identifier("spaceEquivalentToTokens"): + referencedOptions += [.indentation, .tabWidth, .smartTabs] + case .identifier("tokenLength"), .identifier("lineLength"): referencedOptions += [.indentation, .tabWidth] case .identifier("isCommentedCode"): referencedOptions.append(.indentation) @@ -134,8 +132,9 @@ class MetadataTests: XCTestCase { referencedOptions.append(.lineBreak) case .identifier("wrapCollectionsAndArguments"): referencedOptions += [ - .wrapArguments, .wrapParameters, .wrapCollections, .closingParen, - .indentation, .truncateBlankLines, .lineBreak, .tabWidth, .maxWidth, + .wrapArguments, .wrapParameters, .wrapCollections, + .closingParen, .lineBreak, .truncateBlankLines, + .indentation, .tabWidth, .maxWidth, .smartTabs, ] case .identifier("indexWhereLineShouldWrapInLine"): referencedOptions.append(.noWrapOperators) diff --git a/Tests/RulesTests.swift b/Tests/RulesTests.swift index bdb9cbd5..15b4139c 100644 --- a/Tests/RulesTests.swift +++ b/Tests/RulesTests.swift @@ -3251,7 +3251,16 @@ class RulesTests: XCTestCase { // indent with tabs - func testTabIndentWrappedTuple() { + func testTabIndentWrappedTupleWithSmartTabs() { + let input = """ + let foo = (bar: Int, + baz: Int) + """ + let options = FormatOptions(indent: "\t", tabWidth: 2, smartTabs: true) + testFormatting(for: input, rule: FormatRules.indent, options: options) + } + + func testTabIndentWrappedTupleWithoutSmartTabs() { let input = """ let foo = (bar: Int, baz: Int) @@ -3260,11 +3269,30 @@ class RulesTests: XCTestCase { let foo = (bar: Int, \t\t\t\t\t baz: Int) """ - let options = FormatOptions(indent: "\t", tabWidth: 2) + let options = FormatOptions(indent: "\t", tabWidth: 2, smartTabs: false) testFormatting(for: input, output, rule: FormatRules.indent, options: options) } - func testTabIndentCase() { + func testTabIndentCaseWithSmartTabs() { + let input = """ + switch x { + case .foo, + .bar: + break + } + """ + let output = """ + switch x { + case .foo, + .bar: + \tbreak + } + """ + let options = FormatOptions(indent: "\t", tabWidth: 2, smartTabs: true) + testFormatting(for: input, output, rule: FormatRules.indent, options: options) + } + + func testTabIndentCaseWithoutSmartTabs() { let input = """ switch x { case .foo, @@ -3279,11 +3307,11 @@ class RulesTests: XCTestCase { \tbreak } """ - let options = FormatOptions(indent: "\t", tabWidth: 2) + let options = FormatOptions(indent: "\t", tabWidth: 2, smartTabs: false) testFormatting(for: input, output, rule: FormatRules.indent, options: options) } - func testTabIndentCase2() { + func testTabIndentCaseWithoutSmartTabs2() { let input = """ switch x { case .foo, @@ -3298,7 +3326,8 @@ class RulesTests: XCTestCase { \t\tbreak } """ - let options = FormatOptions(indent: "\t", indentCase: true, tabWidth: 2) + let options = FormatOptions(indent: "\t", indentCase: true, + tabWidth: 2, smartTabs: false) testFormatting(for: input, output, rule: FormatRules.indent, options: options) } @@ -9153,7 +9182,17 @@ class RulesTests: XCTestCase { // MARK: indent with tabs - func testTabIndentWrappedFunction() { + func testTabIndentWrappedFunctionWithSmartTabs() { + let input = """ + func foo(bar: Int, + baz: Int) {} + """ + let options = FormatOptions(indent: "\t", wrapParameters: .afterFirst, tabWidth: 2) + testFormatting(for: input, rule: FormatRules.wrapArguments, options: options, + exclude: ["unusedArguments"]) + } + + func testTabIndentWrappedFunctionWithoutSmartTabs() { let input = """ func foo(bar: Int, baz: Int) {} @@ -9162,7 +9201,8 @@ class RulesTests: XCTestCase { func foo(bar: Int, \t\t\t\t baz: Int) {} """ - let options = FormatOptions(indent: "\t", wrapParameters: .afterFirst, tabWidth: 2) + let options = FormatOptions(indent: "\t", wrapParameters: .afterFirst, + tabWidth: 2, smartTabs: false) testFormatting(for: input, output, rule: FormatRules.wrapArguments, options: options, exclude: ["unusedArguments"]) } diff --git a/Tests/XCTestManifests.swift b/Tests/XCTestManifests.swift index 8b3a9f9b..381212e7 100644 --- a/Tests/XCTestManifests.swift +++ b/Tests/XCTestManifests.swift @@ -1680,10 +1680,13 @@ extension RulesTests { ("testSwitchWrappedEnumCaseIndentingVariant2", testSwitchWrappedEnumCaseIndentingVariant2), ("testSwitchWrappedEnumCaseIsIndenting", testSwitchWrappedEnumCaseIsIndenting), ("testSwitchWrappedEnumCaseWithIndentCaseTrue", testSwitchWrappedEnumCaseWithIndentCaseTrue), - ("testTabIndentCase", testTabIndentCase), - ("testTabIndentCase2", testTabIndentCase2), - ("testTabIndentWrappedFunction", testTabIndentWrappedFunction), - ("testTabIndentWrappedTuple", testTabIndentWrappedTuple), + ("testTabIndentCaseWithoutSmartTabs", testTabIndentCaseWithoutSmartTabs), + ("testTabIndentCaseWithoutSmartTabs2", testTabIndentCaseWithoutSmartTabs2), + ("testTabIndentCaseWithSmartTabs", testTabIndentCaseWithSmartTabs), + ("testTabIndentWrappedFunctionWithoutSmartTabs", testTabIndentWrappedFunctionWithoutSmartTabs), + ("testTabIndentWrappedFunctionWithSmartTabs", testTabIndentWrappedFunctionWithSmartTabs), + ("testTabIndentWrappedTupleWithoutSmartTabs", testTabIndentWrappedTupleWithoutSmartTabs), + ("testTabIndentWrappedTupleWithSmartTabs", testTabIndentWrappedTupleWithSmartTabs), ("testTernaryCountEqualsZero", testTernaryCountEqualsZero), ("testTernaryCountNotEqualToZero", testTernaryCountNotEqualToZero), ("testTestableImportIsNotWrapped", testTestableImportIsNotWrapped),