| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- import AppKit
- import Combine
- import Foundation
- @MainActor
- final class SpellCheckerViewModel: ObservableObject {
- static let maxCharacters = 10_000
- @Published var text: String = ""
- @Published private(set) var mistakes: [SpellingMistake] = []
- @Published private(set) var correctedText: String = ""
- @Published private(set) var hasResults = false
- private let historyStorage: HistoryStoring
- init(historyStorage: HistoryStoring = HistoryStorageService.shared) {
- self.historyStorage = historyStorage
- }
- var characterCount: Int { text.count }
- var characterCountLabel: String {
- "\(characterCount)/\(Self.maxCharacters)"
- }
- var mistakeCountLabel: String {
- let count = mistakes.count
- return "\(count) Mistake\(count == 1 ? "" : "s")"
- }
- func pasteText() {
- guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
- text = String(clipboard.prefix(Self.maxCharacters))
- resetResults()
- }
- func clearText() {
- text = ""
- resetResults()
- }
- func resetResults() {
- mistakes = []
- correctedText = ""
- hasResults = false
- }
- func checkSpelling() {
- let content = text.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !content.isEmpty else {
- resetResults()
- return
- }
- let checker = NSSpellChecker.shared
- let language = checker.language()
- let nsContent = content as NSString
- var location = 0
- var foundMistakes: [SpellingMistake] = []
- var replacements: [(range: NSRange, suggestion: String)] = []
- while location < nsContent.length {
- let misspelledRange = checker.checkSpelling(
- of: content,
- startingAt: location
- )
- guard misspelledRange.location != NSNotFound else { break }
- let original = nsContent.substring(with: misspelledRange)
- let guesses = checker.guesses(
- forWordRange: misspelledRange,
- in: content,
- language: language,
- inSpellDocumentWithTag: 0
- )
- let suggestion = guesses?.first ?? original
- foundMistakes.append(SpellingMistake(original: original, suggestion: suggestion))
- if original != suggestion {
- replacements.append((misspelledRange, suggestion))
- }
- let nextLocation = misspelledRange.location + max(misspelledRange.length, 1)
- if nextLocation <= location { break }
- location = nextLocation
- }
- let mutable = NSMutableString(string: content)
- for replacement in replacements.reversed() {
- mutable.replaceCharacters(in: replacement.range, with: replacement.suggestion)
- }
- mistakes = foundMistakes
- correctedText = mutable as String
- hasResults = true
- saveToHistory(text: content, mistakes: foundMistakes, correctedText: mutable as String)
- }
- func restore(from entry: HistoryEntry) {
- guard case .spellingChecker(let data) = entry.payload else { return }
- text = data.text
- mistakes = data.mistakes.map {
- SpellingMistake(original: $0.original, suggestion: $0.suggestion)
- }
- correctedText = data.correctedText
- hasResults = true
- }
- private func saveToHistory(text: String, mistakes: [SpellingMistake], correctedText: String) {
- let payload = HistoryPayload.spellingChecker(
- SpellCheckerHistoryData(
- text: text,
- mistakes: mistakes.map(\.stored),
- correctedText: correctedText
- )
- )
- let entry = HistoryEntry(
- tool: .spellingChecker,
- title: HistoryHelpers.title(from: text, fallback: "Spelling Check"),
- preview: HistoryHelpers.preview(from: correctedText),
- payload: payload
- )
- historyStorage.save(entry)
- }
- func copyCorrectedText() {
- guard !correctedText.isEmpty else { return }
- NSPasteboard.general.clearContents()
- NSPasteboard.general.setString(correctedText, forType: .string)
- }
- }
|