From d0240cdc4afe8a034b996cec1233a5b1b9dcd1bd Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Tue, 28 Jul 2020 18:35:43 +0100 Subject: [PATCH] Add Swift version dropdown to Xcode extension app --- .../Application/Base.lproj/Main.storyboard | 62 ++++++++++++++----- .../Application/Source/AppDelegate.swift | 12 ++-- .../Source/RulesViewController.swift | 23 +++++++ .../Extension/FormatFileCommand.swift | 5 +- .../Extension/FormatSelectionCommand.swift | 8 +-- .../Extension/LintFileCommand.swift | 1 + EditorExtension/Shared/OptionsStore.swift | 22 ++++--- Sources/Arguments.swift | 29 ++++++--- Sources/SwiftFormat.swift | 3 + 9 files changed, 122 insertions(+), 43 deletions(-) diff --git a/EditorExtension/Application/Base.lproj/Main.storyboard b/EditorExtension/Application/Base.lproj/Main.storyboard index 7e5d2113..39176f56 100644 --- a/EditorExtension/Application/Base.lproj/Main.storyboard +++ b/EditorExtension/Application/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -359,7 +359,7 @@ - + @@ -380,7 +380,7 @@ - + @@ -407,7 +407,7 @@ - + @@ -420,13 +420,13 @@ - + - + @@ -450,7 +450,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + @@ -598,6 +627,7 @@ + diff --git a/EditorExtension/Application/Source/AppDelegate.swift b/EditorExtension/Application/Source/AppDelegate.swift index 4e8cdfea..ef2604a9 100644 --- a/EditorExtension/Application/Source/AppDelegate.swift +++ b/EditorExtension/Application/Source/AppDelegate.swift @@ -50,6 +50,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { do { let args = try parseConfigFile(data) options = try Options(args, in: url.deletingLastPathComponent().path) + OptionsStore().inferOptions = Set(args.keys) + .intersection(formattingArguments) + .subtracting([FormatOptions.Descriptor.swiftVersion.argumentName]) + .isEmpty } catch { showError(error) return false @@ -60,10 +64,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { Rule(name: $0, isEnabled: rules.contains($0)) }) if let formatOptions = options.formatOptions { - OptionsStore().inferOptions = false OptionsStore().restore(formatOptions) - } else { - OptionsStore().inferOptions = true } return true } @@ -123,7 +124,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { let optionsStore = OptionsStore() let formatOptions = optionsStore.inferOptions ? nil : optionsStore.formatOptions let rules = RulesStore().rules.compactMap { $0.isEnabled ? $0.name : nil } - let config = serialize(options: Options(formatOptions: formatOptions, rules: Set(rules))) + "\n" + let config = serialize( + options: Options(formatOptions: formatOptions, rules: Set(rules)), + swiftVersion: optionsStore.formatOptions.swiftVersion + ) + "\n" do { try config.write(to: url, atomically: true, encoding: .utf8) } catch { diff --git a/EditorExtension/Application/Source/RulesViewController.swift b/EditorExtension/Application/Source/RulesViewController.swift index 0e39518f..9c905f83 100644 --- a/EditorExtension/Application/Source/RulesViewController.swift +++ b/EditorExtension/Application/Source/RulesViewController.swift @@ -54,10 +54,14 @@ final class RulesViewController: NSViewController { @IBOutlet var tableView: NSTableView! @IBOutlet var inferOptionsButton: NSButton! + @IBOutlet var swiftVersionDropDown: NSPopUpButton! override func viewDidLoad() { super.viewDidLoad() inferOptionsButton.state = optionStore.inferOptions ? .on : .off + swiftVersionDropDown.removeAllItems() + swiftVersionDropDown.addItems(withTitles: ["auto"] + swiftVersions) + updateSelectedVersion() viewModels = buildRules() NotificationCenter.default.addObserver(self, selector: #selector(didLoadNewConfiguration), name: .applicationDidLoadNewConfiguration, object: nil) @@ -67,6 +71,7 @@ final class RulesViewController: NSViewController { viewModels = buildRules() tableView?.reloadData() inferOptionsButton?.state = (optionStore.inferOptions ? .on : .off) + updateSelectedVersion() } @IBAction private func toggleInferOptions(_ sender: NSButton) { @@ -75,6 +80,24 @@ final class RulesViewController: NSViewController { tableView?.reloadData() } + @IBAction func selectVersion(_ sender: NSPopUpButton) { + var formatOptions = optionStore.formatOptions + let version = Version(rawValue: sender.selectedItem?.title ?? "0") ?? .undefined + formatOptions.swiftVersion = version + optionStore.save(formatOptions) + } + + private func updateSelectedVersion() { + let currentVersion = optionStore.formatOptions.swiftVersion + var selectedIndex = 0 + for (i, versionString) in (["0"] + swiftVersions).enumerated() { + if currentVersion >= Version(rawValue: versionString) ?? .undefined { + selectedIndex = i + } + } + swiftVersionDropDown.selectItem(at: selectedIndex) + } + private func buildRules() -> [UserSelectionType] { let optionsByName = Dictionary(uniqueKeysWithValues: optionStore .options diff --git a/EditorExtension/Extension/FormatFileCommand.swift b/EditorExtension/Extension/FormatFileCommand.swift index 3b88fe14..b0600bd2 100644 --- a/EditorExtension/Extension/FormatFileCommand.swift +++ b/EditorExtension/Extension/FormatFileCommand.swift @@ -38,7 +38,7 @@ class FormatFileCommand: NSObject, XCSourceEditorCommand { return completionHandler(FormatCommandError.notSwiftLanguage) } - // Grab the selected source to format + // Grab the file source to format let sourceToFormat = invocation.buffer.completeBuffer let input = tokenize(sourceToFormat) @@ -50,6 +50,7 @@ class FormatFileCommand: NSObject, XCSourceEditorCommand { var formatOptions = store.inferOptions ? inferFormatOptions(from: input) : store.formatOptions formatOptions.indent = invocation.buffer.indentationString formatOptions.tabWidth = invocation.buffer.tabWidth + formatOptions.swiftVersion = store.formatOptions.swiftVersion let output: [Token] do { @@ -63,7 +64,7 @@ class FormatFileCommand: NSObject, XCSourceEditorCommand { } // Remove all selections to avoid a crash when changing the contents of the buffer. - let selections = invocation.buffer.selections.copy() as! [XCSourceTextRange] + let selections = invocation.buffer.selections.copy() as? [XCSourceTextRange] ?? [] invocation.buffer.selections.removeAllObjects() // Update buffer diff --git a/EditorExtension/Extension/FormatSelectionCommand.swift b/EditorExtension/Extension/FormatSelectionCommand.swift index bc25b28a..af72ce58 100644 --- a/EditorExtension/Extension/FormatSelectionCommand.swift +++ b/EditorExtension/Extension/FormatSelectionCommand.swift @@ -42,7 +42,7 @@ class FormatSelectionCommand: NSObject, XCSourceEditorCommand { return completionHandler(FormatCommandError.noSelection) } - // Grab the selected source to format + // Grab the file source to format let sourceToFormat = invocation.buffer.completeBuffer let input = tokenize(sourceToFormat) @@ -54,14 +54,14 @@ class FormatSelectionCommand: NSObject, XCSourceEditorCommand { var formatOptions = store.inferOptions ? inferFormatOptions(from: input) : store.formatOptions formatOptions.indent = invocation.buffer.indentationString formatOptions.tabWidth = invocation.buffer.tabWidth + formatOptions.swiftVersion = store.formatOptions.swiftVersion // Apply formatting for each range var output = input - let tabWidth = invocation.buffer.tabWidth for selection in selections { let startOffset = SourceOffset(selection.start), endOffset = SourceOffset(selection.end) - let start = tokenIndexForOffset(startOffset, in: output, tabWidth: tabWidth) - let end = tokenIndexForOffset(endOffset, in: output, tabWidth: tabWidth) + let start = tokenIndexForOffset(startOffset, in: output, tabWidth: formatOptions.tabWidth) + let end = tokenIndexForOffset(endOffset, in: output, tabWidth: formatOptions.tabWidth) do { output = try format(output, rules: rules, options: formatOptions, range: start ..< end) } catch { diff --git a/EditorExtension/Extension/LintFileCommand.swift b/EditorExtension/Extension/LintFileCommand.swift index 617881e9..295671a1 100644 --- a/EditorExtension/Extension/LintFileCommand.swift +++ b/EditorExtension/Extension/LintFileCommand.swift @@ -29,6 +29,7 @@ class LintFileCommand: NSObject, XCSourceEditorCommand { var formatOptions = store.inferOptions ? inferFormatOptions(from: input) : store.formatOptions formatOptions.indent = invocation.buffer.indentationString formatOptions.tabWidth = invocation.buffer.tabWidth + formatOptions.swiftVersion = store.formatOptions.swiftVersion // Apply linting do { diff --git a/EditorExtension/Shared/OptionsStore.swift b/EditorExtension/Shared/OptionsStore.swift index c8feeacb..17f7a929 100644 --- a/EditorExtension/Shared/OptionsStore.swift +++ b/EditorExtension/Shared/OptionsStore.swift @@ -56,11 +56,11 @@ extension SavedOption { extension FormatOptions { fileprivate init(_ rep: OptionsStore.OptionStoreRepresentation) throws { var formatOptions = FormatOptions.default - for d in Descriptor.formatting.reversed() { + for descriptor in Descriptor.all.reversed() { // By loading formatting options in reverse, we ensure that // non-deprecated/renamed values will overwrite legacy values - if let value = rep[d.argumentName] { - try d.toOptions(value, &formatOptions) + if let value = rep[descriptor.argumentName] { + try descriptor.toOptions(value, &formatOptions) } } self = formatOptions @@ -95,7 +95,9 @@ struct OptionsStore { var inferOptions: Bool { get { return (store.object(forKey: inferOptionsKey) as? NSNumber)?.boolValue ?? true } - nonmutating set { store.set(NSNumber(booleanLiteral: newValue), forKey: inferOptionsKey) } + nonmutating set { + store.set(NSNumber(booleanLiteral: newValue), forKey: inferOptionsKey) + } } var options: [SavedOption] { @@ -107,8 +109,7 @@ struct OptionsStore { } func save(_ options: [SavedOption]) { - let optRepresentations = options.map { (id: $0.descriptor.argumentName, arg: $0.argumentValue) } - save(optRepresentations) + save(options.map { (id: $0.descriptor.argumentName, arg: $0.argumentValue) }) } func save(_ options: FormatOptions) { @@ -132,7 +133,9 @@ struct OptionsStore { func resetOptionsToDefaults() { inferOptions = true - let options = FormatOptions.Descriptor.formatting.map { (id: $0.argumentName, arg: $0.defaultArgument) } + let options = FormatOptions.Descriptor.all.map { + (id: $0.argumentName, arg: $0.defaultArgument) + } clear() save(options) } @@ -150,7 +153,7 @@ extension OptionsStore { } private func addNewOptionsIfNeeded() { - let allDescriptors = FormatOptions.Descriptor.formatting + let allDescriptors = FormatOptions.Descriptor.all var options = load() var idsToRemove = Set(options.keys) @@ -178,7 +181,8 @@ extension OptionsStore { } private func load() -> OptionStoreRepresentation { - guard let options = store.value(forKey: optionsKey) as? OptionStoreRepresentation else { + guard let options = store + .value(forKey: optionsKey) as? OptionStoreRepresentation else { return OptionStoreRepresentation() } return options diff --git a/Sources/Arguments.swift b/Sources/Arguments.swift index a3e5cbad..11d9f612 100644 --- a/Sources/Arguments.swift +++ b/Sources/Arguments.swift @@ -285,22 +285,35 @@ func parseConfigFile(_ data: Data) throws -> [String: String] { // Serialize a set of options into either an arguments string or a file func serialize(options: Options, + swiftVersion: Version = .undefined, excludingDefaults: Bool = false, separator: String = "\n") -> String { - var optionSets = [Options]() + var arguments = [[String: String]]() if let fileOptions = options.fileOptions { - optionSets.append(Options(fileOptions: fileOptions)) + arguments.append(argumentsFor( + Options(fileOptions: fileOptions), + excludingDefaults: excludingDefaults + )) } if let formatOptions = options.formatOptions { - optionSets.append(Options(formatOptions: formatOptions)) + arguments.append(argumentsFor( + Options(formatOptions: formatOptions), + excludingDefaults: excludingDefaults + )) + } else if swiftVersion != .undefined { + let descriptor = FormatOptions.Descriptor.swiftVersion + arguments.append([descriptor.argumentName: swiftVersion.rawValue]) } if let rules = options.rules { - optionSets.append(Options(rules: rules)) + arguments.append(argumentsFor( + Options(rules: rules), + excludingDefaults: excludingDefaults + )) } - return optionSets.map { - let arguments = argumentsFor($0, excludingDefaults: excludingDefaults) - return serialize(arguments: arguments, separator: separator) - }.filter { !$0.isEmpty }.joined(separator: separator) + return arguments + .map { serialize(arguments: $0, separator: separator) } + .filter { !$0.isEmpty } + .joined(separator: separator) } // Serialize arguments diff --git a/Sources/SwiftFormat.swift b/Sources/SwiftFormat.swift index f1dae9c1..24ef7116 100644 --- a/Sources/SwiftFormat.swift +++ b/Sources/SwiftFormat.swift @@ -41,6 +41,9 @@ public let swiftFormatConfigurationFile = ".swiftformat" /// The standard Swift version file name public let swiftVersionFile = ".swift-version" +/// Supported Swift versions +public let swiftVersions = ["3.x", "4.0", "4.1", "4.2", "5.0", "5.1", "5.2", "5.3"] + /// An enumeration of the types of error that may be thrown by SwiftFormat public enum FormatError: Error, CustomStringConvertible, LocalizedError, CustomNSError { case reading(String)