Files
2025-07-13 11:32:06 -07:00

220 lines
6.4 KiB
Swift

//
// RulesStore.swift
// SwiftFormat
//
// Created by Vincent Bernier on 28-01-18.
// Copyright 2018 Nick Lockwood
//
// Distributed under the permissive MIT license
// Get the latest version from here:
//
// https://github.com/nicklockwood/SwiftFormat
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
import NaturalLanguage
extension UserDefaults {
static let groupDomain = "com.charcoaldesign.SwiftFormat"
func clearAll(in domainName: String) {
persistentDomain(forName: domainName)?.forEach {
removeObject(forKey: $0.key)
}
}
}
struct Rule {
let name: String
var isEnabled: Bool
}
extension Rule: Comparable {
/// Looks up and returns a format rule, if found.
var formatRule: FormatRule? {
FormatRules.byName[name]
}
var isDeprecated: Bool {
formatRule?.isDeprecated == true
}
/// Space-separated, lowercased text terms that this rule might by found by.
var searchableText: String {
var items = [name]
if let formatRule {
items.append(formatRule.help.keywords.joined(separator: " "))
items.append(formatRule.options.joined(separator: " "))
items.append(formatRule.sharedOptions.joined(separator: " "))
}
return items.joined(separator: " ").lowercased()
}
static func < (lhs: Rule, rhs: Rule) -> Bool {
if lhs.name == rhs.name {
return lhs.isEnabled
}
return lhs.name < rhs.name
}
}
private extension Rule {
init(_ ruleRep: (String, Bool)) {
self.init(name: ruleRep.0, isEnabled: ruleRep.1)
}
}
struct RulesStore {
private typealias RuleName = String
private typealias RuleIsEnabled = Bool
private typealias RulesRepresentation = [RuleName: RuleIsEnabled]
private let rulesKey = "rules"
private let store: UserDefaults
private static var defaultStore: UserDefaults = {
guard let defaults = UserDefaults(suiteName: UserDefaults.groupDomain) else {
fatalError("The UserDefaults Store is invalid")
}
return defaults
}()
init(_ store: UserDefaults = RulesStore.defaultStore) {
self.store = store
setupDefaultValuesIfNeeded()
}
var rules: [Rule] {
load()
.map { Rule($0) }
.filter { !$0.isDeprecated }
}
func save(_ rule: Rule) {
save([rule])
}
func save(_ rules: [Rule]) {
var active = Set<String>()
var disabled = Set<String>()
for rule in rules {
if rule.isEnabled {
active.insert(rule.name)
} else {
disabled.insert(rule.name)
}
}
save(active: active, disabled: disabled)
}
func restore(_ rules: [Rule]) {
clear()
save(rules)
addNewRulesIfNeeded()
}
func resetRulesToDefaults() {
let allRuleNames = Set(FormatRules.all.map(\.name))
let disabledRules = Set(FormatRules.disabledByDefault.map(\.name))
let activeRules = allRuleNames.subtracting(disabledRules)
clear()
save(active: activeRules, disabled: disabledRules)
}
}
// MARK: - Business Rules
extension RulesStore {
private func setupDefaultValuesIfNeeded() {
// check if first time
if store.value(forKey: rulesKey) == nil {
resetRulesToDefaults()
} else {
addNewRulesIfNeeded()
}
}
private func addNewRulesIfNeeded() {
let currentRules = load()
let currentRuleNames = Set(currentRules.keys)
let allRuleNames = Set(FormatRules.all.map(\.name))
let newRuleNames = allRuleNames.subtracting(currentRuleNames)
if newRuleNames.isEmpty {
return
}
let disabledRules = Set(FormatRules.disabledByDefault.map(\.name))
var rules = currentRules
for newRuleName in newRuleNames {
rules[newRuleName] = !disabledRules.contains(newRuleName)
}
save(rules)
}
}
// MARK: - Store Interactions
extension RulesStore {
private func clear() {
store.set(nil, forKey: rulesKey)
}
private func load() -> RulesRepresentation {
guard let rules = store.value(forKey: rulesKey) as? RulesRepresentation else {
return RulesRepresentation()
}
return rules
}
/// Save the provided rules
/// Will only override the rules in the params
private func save(active: Set<String>, disabled: Set<String>) {
var rules = load()
active.forEach { rules[$0] = true }
disabled.forEach { rules[$0] = false }
save(rules)
}
/// Will replace the rules with the param
private func save(_ rules: RulesRepresentation) {
store.set(rules, forKey: rulesKey)
}
}
private extension String {
/// Returns search-relevant keywords, ignoring prepositions, conjunctions, etc.
var keywords: [String] {
let tagger = NLTagger(tagSchemes: [.lexicalClass])
tagger.string = self
var results = [String]()
tagger.enumerateTags(in: startIndex ..< endIndex, unit: .word, scheme: .lexicalClass) { tag, range in
if [.verb, .noun, .adjective, .adverb, .otherWord].contains(tag) {
results.append(String(self[range]))
}
return true
}
return results
}
}