SpellCheckerViewModel.swift 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import AppKit
  2. import Combine
  3. import Foundation
  4. @MainActor
  5. final class SpellCheckerViewModel: ObservableObject {
  6. static let maxCharacters = 10_000
  7. @Published var text: String = ""
  8. @Published private(set) var mistakes: [SpellingMistake] = []
  9. @Published private(set) var correctedText: String = ""
  10. @Published private(set) var hasResults = false
  11. private let historyStorage: HistoryStoring
  12. init(historyStorage: HistoryStoring = HistoryStorageService.shared) {
  13. self.historyStorage = historyStorage
  14. }
  15. var characterCount: Int { text.count }
  16. var characterCountLabel: String {
  17. "\(characterCount)/\(Self.maxCharacters)"
  18. }
  19. var mistakeCountLabel: String {
  20. let count = mistakes.count
  21. return "\(count) Mistake\(count == 1 ? "" : "s")"
  22. }
  23. func pasteText() {
  24. guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
  25. text = String(clipboard.prefix(Self.maxCharacters))
  26. resetResults()
  27. }
  28. func clearText() {
  29. text = ""
  30. resetResults()
  31. }
  32. func resetResults() {
  33. mistakes = []
  34. correctedText = ""
  35. hasResults = false
  36. }
  37. func checkSpelling() {
  38. let content = text.trimmingCharacters(in: .whitespacesAndNewlines)
  39. guard !content.isEmpty else {
  40. resetResults()
  41. return
  42. }
  43. let checker = NSSpellChecker.shared
  44. let language = checker.language()
  45. let nsContent = content as NSString
  46. var location = 0
  47. var foundMistakes: [SpellingMistake] = []
  48. var replacements: [(range: NSRange, suggestion: String)] = []
  49. while location < nsContent.length {
  50. let misspelledRange = checker.checkSpelling(
  51. of: content,
  52. startingAt: location
  53. )
  54. guard misspelledRange.location != NSNotFound else { break }
  55. let original = nsContent.substring(with: misspelledRange)
  56. let guesses = checker.guesses(
  57. forWordRange: misspelledRange,
  58. in: content,
  59. language: language,
  60. inSpellDocumentWithTag: 0
  61. )
  62. let suggestion = guesses?.first ?? original
  63. foundMistakes.append(SpellingMistake(original: original, suggestion: suggestion))
  64. if original != suggestion {
  65. replacements.append((misspelledRange, suggestion))
  66. }
  67. let nextLocation = misspelledRange.location + max(misspelledRange.length, 1)
  68. if nextLocation <= location { break }
  69. location = nextLocation
  70. }
  71. let mutable = NSMutableString(string: content)
  72. for replacement in replacements.reversed() {
  73. mutable.replaceCharacters(in: replacement.range, with: replacement.suggestion)
  74. }
  75. mistakes = foundMistakes
  76. correctedText = mutable as String
  77. hasResults = true
  78. saveToHistory(text: content, mistakes: foundMistakes, correctedText: mutable as String)
  79. }
  80. func restore(from entry: HistoryEntry) {
  81. guard case .spellingChecker(let data) = entry.payload else { return }
  82. text = data.text
  83. mistakes = data.mistakes.map {
  84. SpellingMistake(original: $0.original, suggestion: $0.suggestion)
  85. }
  86. correctedText = data.correctedText
  87. hasResults = true
  88. }
  89. private func saveToHistory(text: String, mistakes: [SpellingMistake], correctedText: String) {
  90. let payload = HistoryPayload.spellingChecker(
  91. SpellCheckerHistoryData(
  92. text: text,
  93. mistakes: mistakes.map(\.stored),
  94. correctedText: correctedText
  95. )
  96. )
  97. let entry = HistoryEntry(
  98. tool: .spellingChecker,
  99. title: HistoryHelpers.title(from: text, fallback: "Spelling Check"),
  100. preview: HistoryHelpers.preview(from: correctedText),
  101. payload: payload
  102. )
  103. historyStorage.save(entry)
  104. }
  105. func copyCorrectedText() {
  106. guard !correctedText.isEmpty else { return }
  107. NSPasteboard.general.clearContents()
  108. NSPasteboard.general.setString(correctedText, forType: .string)
  109. }
  110. }